From 9dc1c05d6965d878a4c9d663a7baa7ef908cf3ff Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:27:10 +0200 Subject: [PATCH 1/6] feat: Use internal directory for saving files --- .../java/app/myzel394/alibi/db/AppSettings.kt | 90 +------------ .../alibi/helpers/AudioRecorderExporter.kt | 119 ++++++++++++++++++ .../alibi/services/AudioRecorderService.kt | 4 +- .../alibi/services/IntervalRecorderService.kt | 34 ++--- .../java/app/myzel394/alibi/ui/Constants.kt | 2 + .../java/app/myzel394/alibi/ui/Navigation.kt | 6 - .../AudioRecorder/molecules/StartRecording.kt | 6 +- .../alibi/ui/models/AudioRecorderModel.kt | 9 +- .../alibi/ui/screens/AudioRecorder.kt | 7 +- 9 files changed, 148 insertions(+), 129 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt index 236d2e6be..465ad19c5 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -2,7 +2,6 @@ package app.myzel394.alibi.db import android.media.MediaRecorder import android.os.Build -import android.util.Log import app.myzel394.alibi.R import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.ReturnCode @@ -63,7 +62,7 @@ data class AppSettings( } @Serializable -data class LastRecording( +data class RecordingInformation( val folderPath: String, @Serializable(with = LocalDateTimeSerializer::class) val recordingStart: LocalDateTime, @@ -72,91 +71,8 @@ data class LastRecording( val fileExtension: String, val forceExactMaxDuration: Boolean, ) { - val fileFolder: File - get() = File(folderPath) - - val filePaths: List - get() = - File(folderPath).listFiles()?.filter { - val name = it.nameWithoutExtension - - name.toIntOrNull() != null - }?.toList() ?: emptyList() - - val hasRecordingAvailable: Boolean - get() = filePaths.isNotEmpty() - - private fun stripConcatenatedFileToExactDuration( - outputFile: File - ) { - // Move the concatenated file to a temporary file - val rawFile = File("$folderPath/${outputFile.nameWithoutExtension}-raw.${fileExtension}") - outputFile.renameTo(rawFile) - - val command = "-sseof ${maxDuration / -1000} -i $rawFile -y $outputFile" - - val session = FFmpegKit.execute(command) - - if (!ReturnCode.isSuccess(session.returnCode)) { - Log.d( - "Audio Concatenation", - String.format( - "Command failed with state %s and rc %s.%s", - session.getState(), - session.getReturnCode(), - session.getFailStackTrace() - ) - ) - - throw Exception("Failed to strip concatenated audio") - } - } - - suspend fun concatenateFiles(forceConcatenation: Boolean = false): File { - val paths = filePaths.joinToString("|") - val fileName = recordingStart - .format(ISO_DATE_TIME) - .toString() - .replace(":", "-") - .replace(".", "_") - val outputFile = File("$fileFolder/$fileName.${fileExtension}") - - if (outputFile.exists() && !forceConcatenation) { - return outputFile - } - - val command = "-i 'concat:$paths' -y" + - " -acodec copy" + - " -metadata title='$fileName' " + - " -metadata date='${recordingStart.format(ISO_DATE_TIME)}'" + - " -metadata batch_count='${filePaths.size}'" + - " -metadata batch_duration='${intervalDuration}'" + - " -metadata max_duration='${maxDuration}'" + - " $outputFile" - - val session = FFmpegKit.execute(command) - - if (!ReturnCode.isSuccess(session.returnCode)) { - Log.d( - "Audio Concatenation", - String.format( - "Command failed with state %s and rc %s.%s", - session.getState(), - session.getReturnCode(), - session.getFailStackTrace() - ) - ) - - throw Exception("Failed to concatenate audios") - } - - val minRequiredForPossibleInExactMaxDuration = maxDuration / intervalDuration - if (forceExactMaxDuration && filePaths.size > minRequiredForPossibleInExactMaxDuration) { - stripConcatenatedFileToExactDuration(outputFile) - } - - return outputFile - } + val hasRecordingsAvailable + get() = File(folderPath).listFiles()?.isNotEmpty() ?: false } @Serializable diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt new file mode 100644 index 000000000..6be1f67f3 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -0,0 +1,119 @@ +package app.myzel394.alibi.helpers + +import android.content.Context +import android.util.Log +import app.myzel394.alibi.db.RecordingInformation +import app.myzel394.alibi.ui.RECORDER_SUBFOLDER_NAME +import com.arthenica.ffmpegkit.FFmpegKit +import com.arthenica.ffmpegkit.ReturnCode +import java.io.File +import java.time.format.DateTimeFormatter + +data class AudioRecorderExporter( + val recording: RecordingInformation, +) { + val filePaths: List + get() = + File(recording.folderPath).listFiles()?.filter { + val name = it.nameWithoutExtension + + name.toIntOrNull() != null + }?.toList() ?: emptyList() + + val hasRecordingAvailable: Boolean + get() = filePaths.isNotEmpty() + + private fun stripConcatenatedFileToExactDuration( + outputFile: File + ) { + // Move the concatenated file to a temporary file + val rawFile = + File("${recording.folderPath}/${outputFile.nameWithoutExtension}-raw.${recording.fileExtension}") + outputFile.renameTo(rawFile) + + val command = "-sseof ${recording.maxDuration / -1000} -i $rawFile -y $outputFile" + + val session = FFmpegKit.execute(command) + + if (!ReturnCode.isSuccess(session.returnCode)) { + Log.d( + "Audio Concatenation", + String.format( + "Command failed with state %s and rc %s.%s", + session.state, + session.returnCode, + session.failStackTrace, + ) + ) + + throw Exception("Failed to strip concatenated audio") + } + } + + suspend fun concatenateFiles(forceConcatenation: Boolean = false): File { + val paths = filePaths.joinToString("|") + val fileName = recording.recordingStart + .format(DateTimeFormatter.ISO_DATE_TIME) + .toString() + .replace(":", "-") + .replace(".", "_") + val outputFile = File("${recording.folderPath}/$fileName.${recording.fileExtension}") + + if (outputFile.exists() && !forceConcatenation) { + return outputFile + } + + val command = "-i 'concat:$paths' -y" + + " -acodec copy" + + " -metadata title='$fileName' " + + " -metadata date='${recording.recordingStart.format(DateTimeFormatter.ISO_DATE_TIME)}'" + + " -metadata batch_count='${filePaths.size}'" + + " -metadata batch_duration='${recording.intervalDuration}'" + + " -metadata max_duration='${recording.maxDuration}'" + + " $outputFile" + + val session = FFmpegKit.execute(command) + + if (!ReturnCode.isSuccess(session.returnCode)) { + Log.d( + "Audio Concatenation", + String.format( + "Command failed with state %s and rc %s.%s", + session.state, + session.returnCode, + session.failStackTrace, + ) + ) + + throw Exception("Failed to concatenate audios") + } + + val minRequiredForPossibleInExactMaxDuration = + recording.maxDuration / recording.intervalDuration + if (recording.forceExactMaxDuration && filePaths.size > minRequiredForPossibleInExactMaxDuration) { + stripConcatenatedFileToExactDuration(outputFile) + } + + return outputFile + } + + suspend fun cleanupFiles() { + filePaths.forEach { + runCatching { + it.delete() + } + } + } + + companion object { + fun getFolder(context: Context) = File(context.filesDir, RECORDER_SUBFOLDER_NAME) + + fun clearAllRecordings(context: Context) { + getFolder(context).deleteRecursively() + } + + fun hasRecordingsAvailable(context: Context) { + getFolder(context).listFiles()?.isNotEmpty() ?: false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt index 0d8617a59..62b089a6b 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -5,7 +5,7 @@ import android.media.MediaRecorder.OnErrorListener import android.os.Build import java.lang.IllegalStateException -class AudioRecorderService: IntervalRecorderService() { +class AudioRecorderService : IntervalRecorderService() { var amplitudesAmount = 1000 var recorder: MediaRecorder? = null @@ -13,7 +13,7 @@ class AudioRecorderService: IntervalRecorderService() { var onError: () -> Unit = {} val filePath: String - get() = "$folder/$counter.${settings!!.fileExtension}" + get() = "${outputFolder}/$counter.${settings!!.fileExtension}" private fun createRecorder(): MediaRecorder { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { diff --git a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt index b2f6fa494..262aceb92 100644 --- a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt @@ -1,39 +1,37 @@ package app.myzel394.alibi.services -import android.content.Context import android.media.MediaRecorder import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AudioRecorderSettings -import app.myzel394.alibi.db.LastRecording +import app.myzel394.alibi.db.RecordingInformation +import app.myzel394.alibi.helpers.AudioRecorderExporter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.io.File -import java.time.LocalDateTime -import java.util.Timer -import java.util.TimerTask -import java.util.UUID -import java.util.concurrent.Executor import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit -abstract class IntervalRecorderService: ExtraRecorderInformationService() { +abstract class IntervalRecorderService : ExtraRecorderInformationService() { private var job = SupervisorJob() private var scope = CoroutineScope(Dispatchers.IO + job) protected var counter = 0 private set - protected lateinit var folder: File + var settings: Settings? = null protected set private lateinit var cycleTimer: ScheduledExecutorService - fun createLastRecording(): LastRecording = LastRecording( - folderPath = folder.absolutePath, + protected val outputFolder: File + get() = AudioRecorderExporter.getFolder(this) + + fun createLastRecording(): RecordingInformation = RecordingInformation( + folderPath = outputFolder.absolutePath, recordingStart = recordingStart, maxDuration = settings!!.maxDuration, fileExtension = settings!!.fileExtension, @@ -60,18 +58,10 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() { } } - private fun getRandomFileFolder(): String { - // uuid - val folder = UUID.randomUUID().toString() - - return "${externalCacheDir!!.absolutePath}/$folder" - } - override fun start() { super.start() - folder = File(getRandomFileFolder()) - folder.mkdirs() + outputFolder.mkdirs() scope.launch { dataStore.data.collectLatest { preferenceSettings -> @@ -104,7 +94,7 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() { val timeMultiplier = settings!!.maxDuration / settings!!.intervalDuration val earliestCounter = counter - timeMultiplier - folder.listFiles()?.forEach { file -> + outputFolder.listFiles()?.forEach { file -> val fileCounter = file.nameWithoutExtension.toIntOrNull() ?: return if (fileCounter < earliestCounter) { @@ -123,7 +113,7 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() { val encoder: Int, ) { val fileExtension: String - get() = when(outputFormat) { + get() = when (outputFormat) { MediaRecorder.OutputFormat.AAC_ADTS -> "aac" MediaRecorder.OutputFormat.THREE_GPP -> "3gp" MediaRecorder.OutputFormat.MPEG_4 -> "mp4" diff --git a/app/src/main/java/app/myzel394/alibi/ui/Constants.kt b/app/src/main/java/app/myzel394/alibi/ui/Constants.kt index bf3dce158..28a440413 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/Constants.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/Constants.kt @@ -2,10 +2,12 @@ package app.myzel394.alibi.ui import android.os.Build import androidx.compose.ui.unit.dp +import java.io.File val BIG_PRIMARY_BUTTON_SIZE = 64.dp val MAX_AMPLITUDE = 20000 val SUPPORTS_DARK_MODE_NATIVELY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q +val RECORDER_SUBFOLDER_NAME = ".recordings" // You are not allowed to change the constants below. // If you do so, you will be blocked on GitHub. diff --git a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt index efef5d4af..07c57104b 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt @@ -1,6 +1,5 @@ package app.myzel394.alibi.ui -import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -13,10 +12,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel @@ -24,7 +19,6 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import app.myzel394.alibi.dataStore -import app.myzel394.alibi.db.LastRecording import app.myzel394.alibi.ui.enums.Screen import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.screens.AboutScreen diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt index 0c3c0464d..2c7580bdd 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt @@ -42,6 +42,7 @@ import app.myzel394.alibi.NotificationHelper import app.myzel394.alibi.R import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.helpers.AudioRecorderExporter import app.myzel394.alibi.services.RecorderNotificationHelper import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE import app.myzel394.alibi.ui.components.atoms.PermissionRequester @@ -79,6 +80,9 @@ fun StartRecording( it ) } + + AudioRecorderExporter.clearAllRecordings(context) + audioRecorder.startRecording(context) } } @@ -144,7 +148,7 @@ fun StartRecording( .fillMaxWidth(), textAlign = TextAlign.Center, ) - if (audioRecorder.lastRecording != null && audioRecorder.lastRecording!!.hasRecordingAvailable) { + if (audioRecorder.lastRecording != null && audioRecorder.lastRecording!!.hasRecordingsAvailable) { Column( modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt index 7e278ee37..b754a81da 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt @@ -4,22 +4,17 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection -import android.media.MediaRecorder import android.os.IBinder -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModel -import app.myzel394.alibi.dataStore -import app.myzel394.alibi.db.LastRecording +import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.services.AudioRecorderService import app.myzel394.alibi.services.RecorderNotificationHelper import app.myzel394.alibi.services.RecorderService -import kotlinx.coroutines.flow.last import kotlinx.serialization.json.Json class AudioRecorderModel : ViewModel() { @@ -44,7 +39,7 @@ class AudioRecorderModel : ViewModel() { var recorderService: AudioRecorderService? = null private set - var lastRecording: LastRecording? by mutableStateOf(null) + var lastRecording: RecordingInformation? by mutableStateOf(null) private set var onRecordingSave: () -> Unit = {} diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt index e5124b253..ec1004949 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import app.myzel394.alibi.ui.components.AudioRecorder.molecules.RecordingStatus import app.myzel394.alibi.ui.components.AudioRecorder.molecules.StartRecording @@ -37,8 +36,7 @@ import app.myzel394.alibi.ui.utils.rememberFileSaverDialog import app.myzel394.alibi.R import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings -import app.myzel394.alibi.db.LastRecording -import app.myzel394.alibi.services.RecorderNotificationHelper +import app.myzel394.alibi.helpers.AudioRecorderExporter import app.myzel394.alibi.ui.effects.rememberSettings import app.myzel394.alibi.ui.models.AudioRecorderModel import kotlinx.coroutines.delay @@ -67,7 +65,8 @@ fun AudioRecorder( delay(100) try { - val file = audioRecorder.lastRecording!!.concatenateFiles() + val file = + AudioRecorderExporter(audioRecorder.lastRecording!!).concatenateFiles() saveFile(file, file.name) } catch (error: Exception) { From 6b6a19ead36abc5391bb7590c3c5b91cabb80020 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:50:15 +0200 Subject: [PATCH 2/6] feat: Add DeleteRecordingsImmediately to settings --- .../java/app/myzel394/alibi/db/AppSettings.kt | 5 ++ .../organisms/RecordingStatus.kt | 2 + .../atoms/DeleteRecordingsImmediatelyTile.kt | 52 +++++++++++++++++++ .../alibi/ui/screens/SettingsScreen.kt | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 63 insertions(+) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/DeleteRecordingsImmediatelyTile.kt diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt index 62e99ed3f..ea048025a 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -88,6 +88,7 @@ data class AudioRecorderSettings( val outputFormat: Int? = null, val encoder: Int? = null, val showAllMicrophones: Boolean = false, + val deleteRecordingsImmediately: Boolean = false, ) { fun getOutputFormat(): Int { if (outputFormat != null) { @@ -219,6 +220,10 @@ data class AudioRecorderSettings( return copy(showAllMicrophones = showAllMicrophones) } + fun setDeleteRecordingsImmediately(deleteRecordingsImmediately: Boolean): AudioRecorderSettings { + return copy(deleteRecordingsImmediately = deleteRecordingsImmediately) + } + fun isEncoderCompatible(encoder: Int): Boolean { if (outputFormat == null || outputFormat == MediaRecorder.OutputFormat.DEFAULT) { return true diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt index ba4d3303f..878c51c07 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.helpers.AudioRecorderExporter import app.myzel394.alibi.ui.components.AudioRecorder.atoms.DeleteButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneDisconnectedDialog import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneReconnectedDialog @@ -106,6 +107,7 @@ fun RecordingStatus( DeleteButton( onDelete = { audioRecorder.stopRecording(context, saveAsLastRecording = false) + AudioRecorderExporter.clearAllRecordings(context) } ) } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/DeleteRecordingsImmediatelyTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/DeleteRecordingsImmediatelyTile.kt new file mode 100644 index 000000000..453f9b662 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/DeleteRecordingsImmediatelyTile.kt @@ -0,0 +1,52 @@ +package app.myzel394.alibi.ui.components.SettingsScreen.atoms + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DeleteSweep +import androidx.compose.material3.Icon +import androidx.compose.material3.Switch +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import app.myzel394.alibi.R +import app.myzel394.alibi.dataStore +import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.ui.components.atoms.SettingsTile +import kotlinx.coroutines.launch + +@Composable +fun DeleteRecordingsImmediatelyTile() { + val scope = rememberCoroutineScope() + + val dataStore = LocalContext.current.dataStore + val settings = dataStore + .data + .collectAsState(initial = AppSettings.getDefaultInstance()) + .value + + SettingsTile( + title = stringResource(R.string.ui_settings_option_deleteRecordingsImmediately_title), + description = stringResource(R.string.ui_settings_option_deleteRecordingsImmediately_description), + leading = { + Icon( + Icons.Default.DeleteSweep, + contentDescription = null, + ) + }, + trailing = { + Switch( + checked = settings.audioRecorderSettings.deleteRecordingsImmediately, + onCheckedChange = { + scope.launch { + dataStore.updateData { + it.setAudioRecorderSettings( + it.audioRecorderSettings.setDeleteRecordingsImmediately(it.audioRecorderSettings.deleteRecordingsImmediately.not()) + ) + } + } + } + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt index abdac0af1..f9e8ab844 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt @@ -42,6 +42,7 @@ import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY import app.myzel394.alibi.ui.components.SettingsScreen.atoms.AboutTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.BitrateTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.CustomNotificationTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.DeleteRecordingsImmediatelyTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.EncoderTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ForceExactMaxDurationTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ImportExport @@ -148,6 +149,7 @@ fun SettingsScreen( IntervalDurationTile() ForceExactMaxDurationTile() InAppLanguagePicker() + DeleteRecordingsImmediatelyTile() CustomNotificationTile(navController = navController) AboutTile(navController = navController) AnimatedVisibility(visible = settings.showAdvancedSettings) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bfa6fb658..76c30ab1b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -107,4 +107,6 @@ You can copy my GPG key here. This key only exists once and I can use it to prove to you that I\'m really who I am. Please save it now so that you can verify my signature later. Copy GPG Key Become a GitHub Sponsor + Delete Recordings Immediately + If enabled, Alibi will immediately delete recordings after you have saved the file. \ No newline at end of file From 369050e94e09e32b41bf837f0866ac88419f83e1 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 26 Oct 2023 19:12:32 +0200 Subject: [PATCH 3/6] fix: Properly clear recordings when deleteRecordingsImmediately is set --- .../app/myzel394/alibi/ui/screens/AudioRecorder.kt | 12 ++++++++++-- .../main/java/app/myzel394/alibi/ui/utils/file.kt | 9 ++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt index 4c0edddcc..8346c0da6 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt @@ -49,14 +49,22 @@ fun AudioRecorder( audioRecorder: AudioRecorderModel, ) { val context = LocalContext.current + val settings = rememberSettings() - val saveFile = rememberFileSaverDialog(settings.audioRecorderSettings.getMimeType()) val scope = rememberCoroutineScope() + val saveFile = rememberFileSaverDialog( + settings.audioRecorderSettings.getMimeType() + ) { + if (settings.audioRecorderSettings.deleteRecordingsImmediately) { + AudioRecorderExporter.clearAllRecordings(context) + } + } + var isProcessingAudio by remember { mutableStateOf(false) } var showRecorderError by remember { mutableStateOf(false) } - DisposableEffect(Unit) { + DisposableEffect(key1 = audioRecorder, key2 = settings.audioRecorderSettings) { audioRecorder.onRecordingSave = { scope.launch { isProcessingAudio = true diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt index cd50b8081..db61daf2b 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt @@ -12,7 +12,10 @@ import androidx.compose.ui.platform.LocalContext import java.io.File @Composable -fun rememberFileSaverDialog(mimeType: String): ((File, String) -> Unit) { +fun rememberFileSaverDialog( + mimeType: String, + callback: (Uri) -> Unit = {}, +): ((File, String) -> Unit) { val context = LocalContext.current var file = remember { mutableStateOf(null) } @@ -28,6 +31,10 @@ fun rememberFileSaverDialog(mimeType: String): ((File, String) -> Unit) { } file.value = null + + if (it != null) { + callback(it) + } } return { it, name -> From eacf3cc2f13fc028f2fc79370fd7b12baae135d2 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 26 Oct 2023 19:41:13 +0200 Subject: [PATCH 4/6] fix: Move lastRecording to AppSettings --- .../java/app/myzel394/alibi/db/AppSettings.kt | 5 ++++ .../alibi/services/IntervalRecorderService.kt | 2 +- .../AudioRecorder/molecules/StartRecording.kt | 4 +-- .../organisms/RecordingStatus.kt | 3 +- .../alibi/ui/models/AudioRecorderModel.kt | 10 +------ .../alibi/ui/screens/AudioRecorder.kt | 29 +++++++++++++++---- 6 files changed, 34 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt index ea048025a..505f1a7b3 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -18,6 +18,7 @@ data class AppSettings( val hasSeenOnboarding: Boolean = false, val showAdvancedSettings: Boolean = false, val theme: Theme = Theme.SYSTEM, + val lastRecording: RecordingInformation? = null, ) { fun setShowAdvancedSettings(showAdvancedSettings: Boolean): AppSettings { return copy(showAdvancedSettings = showAdvancedSettings) @@ -39,6 +40,10 @@ data class AppSettings( return copy(theme = theme) } + fun setLastRecording(lastRecording: RecordingInformation?): AppSettings { + return copy(lastRecording = lastRecording) + } + enum class Theme { SYSTEM, LIGHT, diff --git a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt index 262aceb92..88d2bca0a 100644 --- a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt @@ -30,7 +30,7 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { protected val outputFolder: File get() = AudioRecorderExporter.getFolder(this) - fun createLastRecording(): RecordingInformation = RecordingInformation( + fun getRecordingInformation(): RecordingInformation = RecordingInformation( folderPath = outputFolder.absolutePath, recordingStart = recordingStart, maxDuration = settings!!.maxDuration, diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt index 2c7580bdd..ef761282e 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt @@ -148,7 +148,7 @@ fun StartRecording( .fillMaxWidth(), textAlign = TextAlign.Center, ) - if (audioRecorder.lastRecording != null && audioRecorder.lastRecording!!.hasRecordingsAvailable) { + if (appSettings.lastRecording?.hasRecordingsAvailable == true) { Column( modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally, @@ -157,7 +157,7 @@ fun StartRecording( val label = stringResource( R.string.ui_audioRecorder_action_saveOldRecording_label, DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL) - .format(audioRecorder.lastRecording!!.recordingStart), + .format(appSettings.lastRecording.recordingStart), ) Button( modifier = Modifier diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt index 878c51c07..7aabe6e3a 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt @@ -106,7 +106,8 @@ fun RecordingStatus( ) { DeleteButton( onDelete = { - audioRecorder.stopRecording(context, saveAsLastRecording = false) + audioRecorder.stopRecording(context) + AudioRecorderExporter.clearAllRecordings(context) } ) diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt index 2df981f4a..7c9724af8 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt @@ -42,9 +42,6 @@ class AudioRecorderModel : ViewModel() { var recorderService: AudioRecorderService? = null private set - var lastRecording: RecordingInformation? by mutableStateOf(null) - private set - var onRecordingSave: () -> Unit = {} var onError: () -> Unit = {} var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null @@ -73,7 +70,6 @@ class AudioRecorderModel : ViewModel() { onAmplitudeChange() } recorder.onError = { - recorderService!!.createLastRecording() onError() } recorder.onSelectedMicrophoneChange = { microphone -> @@ -132,11 +128,7 @@ class AudioRecorderModel : ViewModel() { context.bindService(intent, connection, Context.BIND_AUTO_CREATE) } - fun stopRecording(context: Context, saveAsLastRecording: Boolean = true) { - if (saveAsLastRecording) { - lastRecording = recorderService!!.createLastRecording() - } - + fun stopRecording(context: Context) { runCatching { context.unbindService(connection) } diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt index 8346c0da6..6c9c0dbe5 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt @@ -50,6 +50,7 @@ fun AudioRecorder( ) { val context = LocalContext.current + val dataStore = context.dataStore val settings = rememberSettings() val scope = rememberCoroutineScope() @@ -64,8 +65,22 @@ fun AudioRecorder( var isProcessingAudio by remember { mutableStateOf(false) } var showRecorderError by remember { mutableStateOf(false) } - DisposableEffect(key1 = audioRecorder, key2 = settings.audioRecorderSettings) { - audioRecorder.onRecordingSave = { + fun saveAsLastRecording() { + if (!settings.audioRecorderSettings.deleteRecordingsImmediately) { + scope.launch { + dataStore.updateData { + it.setLastRecording( + audioRecorder.recorderService!!.getRecordingInformation() + ) + } + } + } + } + + DisposableEffect(key1 = audioRecorder, key2 = settings) { + audioRecorder.onRecordingSave = onRecordingSave@{ + val recordingInformation = audioRecorder.recorderService!!.getRecordingInformation() + scope.launch { isProcessingAudio = true @@ -73,10 +88,11 @@ fun AudioRecorder( delay(100) try { - val file = - AudioRecorderExporter(audioRecorder.lastRecording!!).concatenateFiles() + val file = AudioRecorderExporter(recordingInformation).concatenateFiles() saveFile(file, file.name) + + saveAsLastRecording() } catch (error: Exception) { Log.getStackTraceString(error) } finally { @@ -85,8 +101,9 @@ fun AudioRecorder( } } audioRecorder.onError = { - // No need to save last recording as it's done automatically on error - audioRecorder.stopRecording(context, saveAsLastRecording = false) + saveAsLastRecording() + + audioRecorder.stopRecording(context) showRecorderError = true } From b2a413f6094351bfb0a8782fb8d0c78d5c74ccdd Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 26 Oct 2023 19:42:18 +0200 Subject: [PATCH 5/6] refactor: Rename AudioRecorder.kt -> AudioRecorderScreen --- app/src/main/java/app/myzel394/alibi/ui/Navigation.kt | 4 ++-- .../ui/screens/{AudioRecorder.kt => AudioRecorderScreen.kt} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename app/src/main/java/app/myzel394/alibi/ui/screens/{AudioRecorder.kt => AudioRecorderScreen.kt} (99%) diff --git a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt index 07c57104b..985e79d56 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt @@ -22,7 +22,7 @@ import app.myzel394.alibi.dataStore import app.myzel394.alibi.ui.enums.Screen import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.screens.AboutScreen -import app.myzel394.alibi.ui.screens.AudioRecorder +import app.myzel394.alibi.ui.screens.AudioRecorderScreen import app.myzel394.alibi.ui.screens.CustomRecordingNotificationsScreen import app.myzel394.alibi.ui.screens.SettingsScreen import app.myzel394.alibi.ui.screens.WelcomeScreen @@ -70,7 +70,7 @@ fun Navigation( scaleOut(targetScale = SCALE_IN) + fadeOut(tween(durationMillis = 150)) } ) { - AudioRecorder( + AudioRecorderScreen( navController = navController, audioRecorder = audioRecorder, ) diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt similarity index 99% rename from app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt rename to app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt index 6c9c0dbe5..9a434d9e4 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt @@ -44,7 +44,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AudioRecorder( +fun AudioRecorderScreen( navController: NavController, audioRecorder: AudioRecorderModel, ) { From 558aa2c86f127e64efb8812fcd9962ac5c9e63b6 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 26 Oct 2023 19:56:52 +0200 Subject: [PATCH 6/6] fix: Fix recording deletions --- .../alibi/helpers/AudioRecorderExporter.kt | 3 +- .../AudioRecorder/molecules/StartRecording.kt | 8 +-- .../alibi/ui/screens/AudioRecorderScreen.kt | 62 ++++++++++++------- .../java/app/myzel394/alibi/ui/utils/file.kt | 6 +- 4 files changed, 47 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt index 6be1f67f3..5c1d54ceb 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -112,8 +112,7 @@ data class AudioRecorderExporter( getFolder(context).deleteRecursively() } - fun hasRecordingsAvailable(context: Context) { + fun hasRecordingsAvailable(context: Context) = getFolder(context).listFiles()?.isNotEmpty() ?: false - } } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt index ef761282e..72f2f42e5 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt @@ -60,7 +60,8 @@ fun StartRecording( // Loading this from parent, because if we load it ourselves // and permissions have already been granted, initial // settings will be used, instead of the actual settings. - appSettings: AppSettings + appSettings: AppSettings, + onSaveLastRecording: () -> Unit, ) { val context = LocalContext.current @@ -168,10 +169,7 @@ fun StartRecording( contentDescription = label }, colors = ButtonDefaults.textButtonColors(), - onClick = { - audioRecorder.stopRecording(context) - audioRecorder.onRecordingSave() - }, + onClick = onSaveLastRecording, ) { Icon( Icons.Default.Save, diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt index 9a434d9e4..ce3cff6d5 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt @@ -36,6 +36,7 @@ import app.myzel394.alibi.ui.utils.rememberFileSaverDialog import app.myzel394.alibi.R import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.helpers.AudioRecorderExporter import app.myzel394.alibi.ui.effects.rememberSettings import app.myzel394.alibi.ui.models.AudioRecorderModel @@ -60,6 +61,14 @@ fun AudioRecorderScreen( if (settings.audioRecorderSettings.deleteRecordingsImmediately) { AudioRecorderExporter.clearAllRecordings(context) } + + if (!AudioRecorderExporter.hasRecordingsAvailable(context)) { + scope.launch { + dataStore.updateData { + it.setLastRecording(null) + } + } + } } var isProcessingAudio by remember { mutableStateOf(false) } @@ -77,28 +86,34 @@ fun AudioRecorderScreen( } } + fun saveRecording() { + scope.launch { + isProcessingAudio = true + + // Give the user some time to see the processing dialog + delay(100) + + try { + val file = AudioRecorderExporter( + audioRecorder.recorderService?.getRecordingInformation() + ?: settings.lastRecording + ?: throw Exception("No recording information available"), + ).concatenateFiles() + + saveFile(file, file.name) + } catch (error: Exception) { + Log.getStackTraceString(error) + } finally { + isProcessingAudio = false + } + } + } + DisposableEffect(key1 = audioRecorder, key2 = settings) { audioRecorder.onRecordingSave = onRecordingSave@{ - val recordingInformation = audioRecorder.recorderService!!.getRecordingInformation() - - scope.launch { - isProcessingAudio = true - - // Give the user some time to see the processing dialog - delay(100) - - try { - val file = AudioRecorderExporter(recordingInformation).concatenateFiles() - - saveFile(file, file.name) + saveAsLastRecording() - saveAsLastRecording() - } catch (error: Exception) { - Log.getStackTraceString(error) - } finally { - isProcessingAudio = false - } - } + saveRecording() } audioRecorder.onError = { saveAsLastRecording() @@ -166,7 +181,9 @@ fun AudioRecorderScreen( confirmButton = { Button( onClick = { - audioRecorder.onRecordingSave() + showRecorderError = false + + saveRecording() }, colors = ButtonDefaults.textButtonColors(), ) { @@ -206,7 +223,10 @@ fun AudioRecorderScreen( if (audioRecorder.isInRecording) RecordingStatus(audioRecorder = audioRecorder) else - StartRecording(audioRecorder = audioRecorder, appSettings = appSettings) + StartRecording( + audioRecorder = audioRecorder, appSettings = appSettings, + onSaveLastRecording = ::saveRecording, + ) } } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt index db61daf2b..f90d01bd6 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt @@ -14,7 +14,7 @@ import java.io.File @Composable fun rememberFileSaverDialog( mimeType: String, - callback: (Uri) -> Unit = {}, + callback: (Uri?) -> Unit = {}, ): ((File, String) -> Unit) { val context = LocalContext.current @@ -32,9 +32,7 @@ fun rememberFileSaverDialog( file.value = null - if (it != null) { - callback(it) - } + callback(it) } return { it, name ->