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 1f45583..3b00d06 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -11,6 +11,7 @@ import app.myzel394.alibi.helpers.AudioBatchesFolder import app.myzel394.alibi.helpers.VideoBatchesFolder import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE +import app.myzel394.alibi.ui.components.RecorderScreen.organisms.RecorderModel import app.myzel394.alibi.ui.utils.PermissionHelper import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -102,6 +103,16 @@ data class AppSettings( return copy(appLockSettings = appLockSettings) } + fun saveLastRecording(recorder: RecorderModel): AppSettings { + return if (deleteRecordingsImmediately) { + this + } else { + setLastRecording( + recorder.recorderService!!.getRecordingInformation() + ) + } + } + // If the object is present, biometric authentication is enabled. // To disable biometric authentication, set the instance to null. fun isAppLockEnabled() = appLockSettings != null diff --git a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt index 70c12f5..ac48807 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -3,28 +3,26 @@ package app.myzel394.alibi.helpers import android.Manifest import android.content.ContentUris import android.content.ContentValues -import app.myzel394.alibi.ui.MEDIA_RECORDINGS_PREFIX - import android.content.Context import android.database.Cursor import android.net.Uri import android.os.Build import android.provider.MediaStore import android.provider.MediaStore.Video.Media -import androidx.documentfile.provider.DocumentFile -import java.io.File -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import com.arthenica.ffmpegkit.FFmpegKitConfig import android.util.Log import androidx.annotation.RequiresApi import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import app.myzel394.alibi.ui.MEDIA_RECORDINGS_PREFIX import app.myzel394.alibi.ui.RECORDER_INTERNAL_SELECTED_VALUE import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE import app.myzel394.alibi.ui.utils.PermissionHelper -import com.arthenica.ffmpegkit.FFprobeKit +import com.arthenica.ffmpegkit.FFmpegKitConfig import kotlinx.coroutines.CompletableDeferred +import java.io.File +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import kotlin.reflect.KFunction4 abstract class BatchesFolder( @@ -197,7 +195,6 @@ abstract class BatchesFolder( createNewFile() } - fun checkIfOutputAlreadyExists( date: LocalDateTime, extension: String @@ -388,12 +385,12 @@ abstract class BatchesFolder( } } - fun deleteOldRecordings(earliestCounter: Long) { + fun deleteRecordings(range: LongRange) { when (type) { BatchType.INTERNAL -> getInternalFolder().listFiles()?.forEach { val fileCounter = it.nameWithoutExtension.toIntOrNull() ?: return@forEach - if (fileCounter < earliestCounter) { + if (fileCounter in range) { it.delete() } } @@ -401,7 +398,7 @@ abstract class BatchesFolder( BatchType.CUSTOM -> getCustomDefinedFolder().listFiles().forEach { val fileCounter = it.name?.substringBeforeLast(".")?.toIntOrNull() ?: return@forEach - if (fileCounter < earliestCounter) { + if (fileCounter in range) { it.delete() } } @@ -411,7 +408,7 @@ abstract class BatchesFolder( val deletableNames = mutableListOf() queryMediaContent { rawName, counter, _, _ -> - if (counter < earliestCounter) { + if (counter in range) { deletableNames.add(rawName) } } @@ -428,7 +425,7 @@ abstract class BatchesFolder( it.nameWithoutExtension.substring(mediaPrefix.length).toIntOrNull() ?: return@forEach - if (fileCounter < earliestCounter) { + if (fileCounter in range) { it.delete() } } 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 ed8f9c7..6783ef2 100644 --- a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt @@ -11,6 +11,9 @@ abstract class IntervalRecorderService : protected var counter = 0L private set + // Tracks the index of the currently locked file + private var lockedIndex: Long? = null + lateinit var settings: AppSettings private lateinit var cycleTimer: ScheduledExecutorService @@ -21,6 +24,23 @@ abstract class IntervalRecorderService : abstract fun getRecordingInformation(): I + // When saving the recording, the files should be locked. + // This prevents the service from deleting the currently available files, so that + // they can be safely used to save the recording. + // Once finished, make sure to unlock the files using `unlockFiles`. + fun lockFiles() { + lockedIndex = counter + } + + // Unlocks and deletes the files that were locked using `lockFiles`. + fun unlockFiles(cleanupFiles: Boolean = false) { + if (cleanupFiles) { + batchesFolder.deleteRecordings(0.. : private fun deleteOldRecordings() { val timeMultiplier = settings.maxDuration / settings.intervalDuration - val earliestCounter = counter - timeMultiplier + val earliestCounter = Math.max(counter - timeMultiplier, lockedIndex ?: 0) if (earliestCounter <= 0) { return } - batchesFolder.deleteOldRecordings(earliestCounter) + batchesFolder.deleteRecordings(0..earliestCounter) } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt index 5aaf273..aa74867 100644 --- a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt @@ -50,7 +50,7 @@ class VideoRecorderService : // Used to listen and check if the camera is available private var _cameraAvailableListener = CompletableDeferred() - private var _videoFinalizerListener = CompletableDeferred() + private lateinit var _videoFinalizerListener: CompletableDeferred; // Absolute last completer that can be awaited to ensure that the camera is closed private var _cameraCloserListener = CompletableDeferred() @@ -129,8 +129,10 @@ class VideoRecorderService : stopActiveRecording() val newRecording = prepareVideoRecording() + _videoFinalizerListener = CompletableDeferred() + activeRecording = newRecording.start(ContextCompat.getMainExecutor(this)) { event -> - if (event is VideoRecordEvent.Finalize && this@VideoRecorderService.state == RecorderState.STOPPED) { + if (event is VideoRecordEvent.Finalize && this@VideoRecorderService.state == RecorderState.STOPPED || this@VideoRecorderService.state == RecorderState.PAUSED) { _videoFinalizerListener.complete(Unit) } } @@ -139,7 +141,7 @@ class VideoRecorderService : if (_cameraAvailableListener.isCompleted) { action() } else { - // Race condition of `startNewCycle` being called before `invpkeOnCompletion` + // Race condition of `startNewCycle` being called before `invokeOnCompletion` // has been called can be ignored, as the camera usually opens within 5 seconds // and the interval can't be set shorter than 10 seconds. _cameraAvailableListener.invokeOnCompletion { diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt index 28d960d..f8719c1 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt @@ -1,35 +1,51 @@ package app.myzel394.alibi.ui.components.RecorderScreen.atoms -import androidx.compose.material3.Button +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import app.myzel394.alibi.R +@OptIn(ExperimentalFoundationApi::class) @Composable fun SaveButton( modifier: Modifier = Modifier, onSave: () -> Unit, + onLongClick: () -> Unit = {}, ) { val label = stringResource(R.string.ui_recorder_action_save_label) - TextButton( + Box( modifier = Modifier + .clip(ButtonDefaults.textShape) .semantics { contentDescription = label } - .then(modifier), - onClick = onSave, + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(color = MaterialTheme.colorScheme.primary), + onClick = onSave, + onLongClick = onLongClick, + ) + .padding(ButtonDefaults.TextButtonContentPadding) + .then(modifier) ) { Text( label, - fontSize = MaterialTheme.typography.bodySmall.fontSize, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary, ) } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveCurrentNowModal.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveCurrentNowModal.kt new file mode 100644 index 0000000..8480f55 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveCurrentNowModal.kt @@ -0,0 +1,58 @@ +package app.myzel394.alibi.ui.components.RecorderScreen.atoms + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.ui.SHEET_BOTTOM_OFFSET + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SaveCurrentNowModal( + onDismiss: () -> Unit, + onConfirm: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(true) + + // Auto save on specific events + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = SHEET_BOTTOM_OFFSET) + .padding(16.dp) + ) { + Text( + stringResource(R.string.ui_recorder_action_saveCurrent), + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center, + ) + + Text( + stringResource(R.string.ui_recorder_action_saveCurrent_explanation), + ) + + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.ui_recorder_action_save_label)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt index 8e1bdde..6e36a86 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt @@ -35,7 +35,8 @@ fun RecordingControl( recordingTime: Long, onDelete: () -> Unit, onPauseResume: () -> Unit, - onSave: () -> Unit, + onSaveAndStop: () -> Unit, + onSaveCurrent: () -> Unit, ) { val animateIn = rememberInitialRecordingAnimation(recordingTime) @@ -106,7 +107,8 @@ fun RecordingControl( contentAlignment = Alignment.Center, ) { SaveButton( - onSave = onSave, + onSave = onSaveAndStop, + onLongClick = onSaveCurrent, modifier = Modifier.fillMaxWidth(), ) } @@ -170,7 +172,10 @@ fun RecordingControl( .alpha(saveButtonAlpha), contentAlignment = Alignment.Center, ) { - SaveButton(onSave = onSave) + SaveButton( + onSave = onSaveAndStop, + onLongClick = onSaveCurrent, + ) } } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt index 2a639a0..0e6d983 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt @@ -22,7 +22,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import app.myzel394.alibi.dataStore import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RealtimeAudioVisualizer +import app.myzel394.alibi.ui.components.RecorderScreen.atoms.SaveCurrentNowModal import app.myzel394.alibi.ui.components.RecorderScreen.molecules.MicrophoneStatus import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus @@ -39,8 +41,6 @@ fun AudioRecordingStatus( val context = LocalContext.current val configuration = LocalConfiguration.current.orientation - val scope = rememberCoroutineScope() - var now by remember { mutableStateOf(LocalDateTime.now()) } LaunchedEffect(Unit) { @@ -90,34 +90,7 @@ fun AudioRecordingStatus( MicrophoneStatus(audioRecorder) } - RecordingControl( - modifier = Modifier - .weight(1f) - .fillMaxWidth(), - isPaused = audioRecorder.isPaused, - recordingTime = audioRecorder.recordingTime, - onDelete = { - scope.launch { - runCatching { - audioRecorder.stopRecording(context) - } - runCatching { - audioRecorder.destroyService(context) - } - audioRecorder.batchesFolder!!.deleteRecordings() - } - }, - onPauseResume = { - if (audioRecorder.isPaused) { - audioRecorder.resumeRecording() - } else { - audioRecorder.pauseRecording() - } - }, - onSave = { - audioRecorder.onRecordingSave(false) - } - ) + _PrimitiveControls(audioRecorder) } } @@ -138,33 +111,76 @@ fun AudioRecordingStatus( HorizontalDivider() - RecordingControl( - isPaused = audioRecorder.isPaused, - recordingTime = audioRecorder.recordingTime, - onDelete = { - scope.launch { - runCatching { - audioRecorder.stopRecording(context) - } - runCatching { - audioRecorder.destroyService(context) - } - audioRecorder.batchesFolder!!.deleteRecordings() - } - }, - onPauseResume = { - if (audioRecorder.isPaused) { - audioRecorder.resumeRecording() - } else { - audioRecorder.pauseRecording() - } - }, - onSave = { - audioRecorder.onRecordingSave(false) - } - ) + _PrimitiveControls(audioRecorder) } } } } +} + +@Composable +fun _PrimitiveControls(audioRecorder: AudioRecorderModel) { + val context = LocalContext.current + val dataStore = context.dataStore + val scope = rememberCoroutineScope() + + var showConfirmSaveNow by remember { mutableStateOf(false) } + + if (showConfirmSaveNow) { + SaveCurrentNowModal( + onDismiss = { + showConfirmSaveNow = false + }, + onConfirm = { + showConfirmSaveNow = false + + scope.launch { + audioRecorder.recorderService!!.startNewCycle() + + audioRecorder.onRecordingSave(false).join() + } + }, + ) + } + + RecordingControl( + isPaused = audioRecorder.isPaused, + recordingTime = audioRecorder.recordingTime, + onDelete = { + scope.launch { + runCatching { + audioRecorder.stopRecording(context) + } + runCatching { + audioRecorder.destroyService(context) + } + audioRecorder.batchesFolder!!.deleteRecordings() + } + }, + onPauseResume = { + if (audioRecorder.isPaused) { + audioRecorder.resumeRecording() + } else { + audioRecorder.pauseRecording() + } + }, + onSaveAndStop = { + scope.launch { + audioRecorder.stopRecording(context) + + dataStore.updateData { + it.saveLastRecording(audioRecorder as RecorderModel) + } + + audioRecorder.onRecordingSave(false).join() + + runCatching { + audioRecorder.destroyService(context) + } + } + }, + onSaveCurrent = { + showConfirmSaveNow = true + }, + ) } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/RecorderEventsHandler.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/RecorderEventsHandler.kt index d01c5ad..6db673d 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/RecorderEventsHandler.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/RecorderEventsHandler.kt @@ -1,6 +1,5 @@ package app.myzel394.alibi.ui.components.RecorderScreen.organisms -import android.content.Intent import android.net.Uri import android.util.Log import androidx.compose.material3.SnackbarDuration @@ -9,8 +8,6 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableDoubleStateOf -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -132,15 +129,19 @@ fun RecorderEventsHandler( } } - suspend fun saveRecording(recorder: RecorderModel) { + suspend fun saveRecording(recorder: RecorderModel, cleanupOldFiles: Boolean = false): Thread { isProcessing = true // Give the user some time to see the processing dialog delay(100) - thread { + return thread { runBlocking { try { + if (recorder.isCurrentlyActivelyRecording) { + recorder.recorderService?.lockFiles() + } + val recording = // When new recording created recorder.recorderService?.getRecordingInformation() @@ -218,6 +219,9 @@ fun RecorderEventsHandler( } catch (error: Exception) { Log.getStackTraceString(error) } finally { + if (recorder.isCurrentlyActivelyRecording) { + recorder.recorderService?.unlockFiles(cleanupOldFiles) + } isProcessing = false } } @@ -226,19 +230,14 @@ fun RecorderEventsHandler( // Register audio recorder events DisposableEffect(key1 = audioRecorder, key2 = settings) { - audioRecorder.onRecordingSave = { justSave -> + audioRecorder.onRecordingSave = { cleanupOldFiles -> + // We create our own coroutine because we show our own dialog and we want to + // keep saving until it's finished. + // So it's smarter to take things into our own hands and use our local coroutine, + // instead of hoping that the coroutine from where this will be called will be alive + // until the end of the saving process scope.launch { - if (justSave) { - saveRecording(audioRecorder as RecorderModel) - } else { - audioRecorder.stopRecording(context) - - saveAsLastRecording(audioRecorder as RecorderModel) - - saveRecording(audioRecorder) - - audioRecorder.destroyService(context) - } + saveRecording(audioRecorder as RecorderModel, cleanupOldFiles).join() } } audioRecorder.onRecordingStart = { @@ -265,26 +264,23 @@ fun RecorderEventsHandler( } onDispose { - audioRecorder.onRecordingSave = {} + audioRecorder.onRecordingSave = { + throw NotImplementedError("onRecordingSave should not be called now") + } audioRecorder.onError = {} } } // Register video recorder events DisposableEffect(key1 = videoRecorder, key2 = settings) { - videoRecorder.onRecordingSave = { justSave -> + videoRecorder.onRecordingSave = { cleanupOldFiles -> + // We create our own coroutine because we show our own dialog and we want to + // keep saving until it's finished. + // So it's smarter to take things into our own hands and use our local coroutine, + // instead of hoping that the coroutine from where this will be called will be alive + // until the end of the saving process scope.launch { - if (justSave) { - saveRecording(videoRecorder as RecorderModel) - } else { - videoRecorder.stopRecording(context) - - saveAsLastRecording(videoRecorder as RecorderModel) - - saveRecording(videoRecorder) - - videoRecorder.destroyService(context) - } + saveRecording(videoRecorder as RecorderModel, cleanupOldFiles).join() } } videoRecorder.onRecordingStart = { @@ -311,7 +307,9 @@ fun RecorderEventsHandler( } onDispose { - videoRecorder.onRecordingSave = {} + videoRecorder.onRecordingSave = { + throw NotImplementedError("onRecordingSave should not be called now") + } videoRecorder.onError = {} } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt index 47479d9..9009a99 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -32,6 +33,8 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.myzel394.alibi.R +import app.myzel394.alibi.dataStore +import app.myzel394.alibi.ui.components.RecorderScreen.atoms.SaveCurrentNowModal import app.myzel394.alibi.ui.components.RecorderScreen.atoms.TorchStatus import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus @@ -189,8 +192,28 @@ fun _VideoRecordingStatus(videoRecorder: VideoRecorderModel) { @Composable fun _PrimitiveControls(videoRecorder: VideoRecorderModel) { val context = LocalContext.current + val dataStore = context.dataStore val scope = rememberCoroutineScope() + var showConfirmSaveNow by remember { mutableStateOf(false) } + + if (showConfirmSaveNow) { + SaveCurrentNowModal( + onDismiss = { + showConfirmSaveNow = false + }, + onConfirm = { + showConfirmSaveNow = false + + scope.launch { + videoRecorder.recorderService!!.startNewCycle() + + videoRecorder.onRecordingSave(false).join() + } + }, + ) + } + RecordingControl( orientation = Configuration.ORIENTATION_PORTRAIT, // There may be some edge cases where the app may crash if the @@ -217,8 +240,23 @@ fun _PrimitiveControls(videoRecorder: VideoRecorderModel) { videoRecorder.pauseRecording() } }, - onSave = { - videoRecorder.onRecordingSave(false) + onSaveAndStop = { + scope.launch { + videoRecorder.stopRecording(context) + + dataStore.updateData { + it.saveLastRecording(videoRecorder as RecorderModel) + } + + videoRecorder.onRecordingSave(false).join() + + runCatching { + videoRecorder.destroyService(context) + } + } + }, + onSaveCurrent = { + showConfirmSaveNow = true } ) } diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt index 452419b..7796849 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt @@ -17,6 +17,7 @@ import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.services.IntervalRecorderService import app.myzel394.alibi.services.RecorderNotificationHelper import app.myzel394.alibi.services.RecorderService +import kotlinx.coroutines.Job import kotlinx.serialization.json.Json abstract class BaseRecorderModel> : @@ -31,6 +32,9 @@ abstract class BaseRecorderModel Unit = {} + var onRecordingSave: (cleanupOldFiles: Boolean) -> Job = { + throw NotImplementedError("onRecordingSave not implemented") + } var onRecordingStart: () -> Unit = {} var onError: () -> Unit = {} var onBatchesFolderNotAccessible: () -> Unit = {} diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt index 68752b3..a086840 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt @@ -16,22 +16,26 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.* +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.navigation.NavController -import app.myzel394.alibi.ui.components.RecorderScreen.organisms.AudioRecordingStatus -import app.myzel394.alibi.ui.components.RecorderScreen.organisms.StartRecording -import app.myzel394.alibi.ui.enums.Screen 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.ui.components.RecorderScreen.organisms.AudioRecordingStatus import app.myzel394.alibi.ui.components.RecorderScreen.organisms.RecorderEventsHandler +import app.myzel394.alibi.ui.components.RecorderScreen.organisms.StartRecording import app.myzel394.alibi.ui.components.RecorderScreen.organisms.VideoRecordingStatus import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.models.VideoRecorderModel +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -43,6 +47,7 @@ fun RecorderScreen( ) { val snackbarHostState = remember { SnackbarHostState() } val context = LocalContext.current + val scope = rememberCoroutineScope() RecorderEventsHandler( settings = settings, @@ -112,12 +117,14 @@ fun RecorderScreen( videoRecorder = videoRecorder, appSettings = appSettings, onSaveLastRecording = { - when (settings.lastRecording!!.type) { - RecordingInformation.Type.AUDIO -> - audioRecorder.onRecordingSave(true) + scope.launch { + when (settings.lastRecording!!.type) { + RecordingInformation.Type.AUDIO -> + audioRecorder.onRecordingSave(false) - RecordingInformation.Type.VIDEO -> - videoRecorder.onRecordingSave(true) + RecordingInformation.Type.VIDEO -> + videoRecorder.onRecordingSave(false) + } } }, showAudioRecorder = topBarVisible, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 15b1d26..edeb3f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -191,4 +191,6 @@ To protect your privacy, Alibi stores its batches into its own private, encrypted storage. This storage is only accessible by Alibi and can\'t be accessed by other apps or by a possible intruder. Once you save the recording, you will be asked where you want to save the recording to. Please rotate your device to portait mode Back + Save now? + You can save the current ongoing recording by pressing and holding down on the save button. The recording will continue in the background. \ No newline at end of file