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 c7ede5c93..505f1a7b3 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 @@ -19,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) @@ -40,6 +40,10 @@ data class AppSettings( return copy(theme = theme) } + fun setLastRecording(lastRecording: RecordingInformation?): AppSettings { + return copy(lastRecording = lastRecording) + } + enum class Theme { SYSTEM, LIGHT, @@ -63,7 +67,7 @@ data class AppSettings( } @Serializable -data class LastRecording( +data class RecordingInformation( val folderPath: String, @Serializable(with = LocalDateTimeSerializer::class) val recordingStart: LocalDateTime, @@ -72,91 +76,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 @@ -172,6 +93,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) { @@ -303,6 +225,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/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt new file mode 100644 index 000000000..5c1d54ceb --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -0,0 +1,118 @@ +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 2c5fcf33c..e871138c7 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -28,7 +28,7 @@ class AudioRecorderService : IntervalRecorderService() { var onMicrophoneReconnected: () -> Unit = {} val filePath: String - get() = "$folder/$counter.${settings!!.fileExtension}" + get() = "${outputFolder}/$counter.${settings!!.fileExtension}" /// Tell Android to use the correct bluetooth microphone, if any selected private fun startAudioDevice() { 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..88d2bca0a 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 getRecordingInformation(): 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..985e79d56 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,11 +19,10 @@ 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 -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 @@ -76,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/components/AudioRecorder/molecules/StartRecording.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt index 0c3c0464d..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 @@ -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 @@ -59,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 @@ -79,6 +81,9 @@ fun StartRecording( it ) } + + AudioRecorderExporter.clearAllRecordings(context) + audioRecorder.startRecording(context) } } @@ -144,7 +149,7 @@ fun StartRecording( .fillMaxWidth(), textAlign = TextAlign.Center, ) - if (audioRecorder.lastRecording != null && audioRecorder.lastRecording!!.hasRecordingAvailable) { + if (appSettings.lastRecording?.hasRecordingsAvailable == true) { Column( modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally, @@ -153,7 +158,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 @@ -164,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/components/AudioRecorder/organisms/RecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt index ba4d3303f..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 @@ -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 @@ -105,7 +106,9 @@ 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/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/models/AudioRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt index 8048501e8..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 @@ -10,13 +10,11 @@ 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 import app.myzel394.alibi.ui.utils.MicrophoneInfo @@ -44,9 +42,6 @@ class AudioRecorderModel : ViewModel() { var recorderService: AudioRecorderService? = null private set - var lastRecording: LastRecording? by mutableStateOf(null) - private set - var onRecordingSave: () -> Unit = {} var onError: () -> Unit = {} var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null @@ -75,7 +70,6 @@ class AudioRecorderModel : ViewModel() { onAmplitudeChange() } recorder.onError = { - recorderService!!.createLastRecording() onError() } recorder.onSelectedMicrophoneChange = { microphone -> @@ -134,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/AudioRecorderScreen.kt similarity index 71% 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 25f80e3a7..ce3cff6d5 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 @@ -36,8 +36,8 @@ 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.db.RecordingInformation +import app.myzel394.alibi.helpers.AudioRecorderExporter import app.myzel394.alibi.ui.effects.rememberSettings import app.myzel394.alibi.ui.models.AudioRecorderModel import kotlinx.coroutines.delay @@ -45,40 +45,80 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AudioRecorder( +fun AudioRecorderScreen( navController: NavController, audioRecorder: AudioRecorderModel, ) { val context = LocalContext.current + + val dataStore = context.dataStore 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) + } + + if (!AudioRecorderExporter.hasRecordingsAvailable(context)) { + scope.launch { + dataStore.updateData { + it.setLastRecording(null) + } + } + } + } + var isProcessingAudio by remember { mutableStateOf(false) } var showRecorderError by remember { mutableStateOf(false) } - DisposableEffect(Unit) { - audioRecorder.onRecordingSave = { + fun saveAsLastRecording() { + if (!settings.audioRecorderSettings.deleteRecordingsImmediately) { scope.launch { - isProcessingAudio = true + dataStore.updateData { + it.setLastRecording( + audioRecorder.recorderService!!.getRecordingInformation() + ) + } + } + } + } - // Give the user some time to see the processing dialog - delay(100) + fun saveRecording() { + scope.launch { + isProcessingAudio = true - try { - val file = audioRecorder.lastRecording!!.concatenateFiles() + // Give the user some time to see the processing dialog + delay(100) - saveFile(file, file.name) - } catch (error: Exception) { - Log.getStackTraceString(error) - } finally { - isProcessingAudio = false - } + 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@{ + saveAsLastRecording() + + saveRecording() + } 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 } @@ -141,7 +181,9 @@ fun AudioRecorder( confirmButton = { Button( onClick = { - audioRecorder.onRecordingSave() + showRecorderError = false + + saveRecording() }, colors = ButtonDefaults.textButtonColors(), ) { @@ -181,7 +223,10 @@ fun AudioRecorder( 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/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/java/app/myzel394/alibi/ui/utils/file.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt index cd50b8081..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 @@ -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,8 @@ fun rememberFileSaverDialog(mimeType: String): ((File, String) -> Unit) { } file.value = null + + callback(it) } return { it, name -> 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