mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-18 23:05:26 +02:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
e7961c436b
@ -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
|
||||
|
@ -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<String>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,9 @@ abstract class IntervalRecorderService<I, B : BatchesFolder> :
|
||||
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<I, B : BatchesFolder> :
|
||||
|
||||
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..<lockedIndex!!)
|
||||
}
|
||||
|
||||
lockedIndex = null
|
||||
}
|
||||
|
||||
// Make overrideable
|
||||
open fun startNewCycle() {
|
||||
counter += 1
|
||||
@ -72,12 +92,12 @@ abstract class IntervalRecorderService<I, B : BatchesFolder> :
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -50,7 +50,7 @@ class VideoRecorderService :
|
||||
|
||||
// Used to listen and check if the camera is available
|
||||
private var _cameraAvailableListener = CompletableDeferred<Unit>()
|
||||
private var _videoFinalizerListener = CompletableDeferred<Unit>()
|
||||
private lateinit var _videoFinalizerListener: CompletableDeferred<Unit>;
|
||||
|
||||
// Absolute last completer that can be awaited to ensure that the camera is closed
|
||||
private var _cameraCloserListener = CompletableDeferred<Unit>()
|
||||
@ -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 {
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
@ -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 = {}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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<I, B : BatchesFolder, T : IntervalRecorderService<I, B>> :
|
||||
@ -31,6 +32,9 @@ abstract class BaseRecorderModel<I, B : BatchesFolder, T : IntervalRecorderServi
|
||||
open val isInRecording: Boolean
|
||||
get() = recorderService != null
|
||||
|
||||
open val isCurrentlyActivelyRecording
|
||||
get() = recorderState === RecorderState.RECORDING
|
||||
|
||||
val isPaused: Boolean
|
||||
get() = recorderState === RecorderState.PAUSED
|
||||
|
||||
@ -45,7 +49,9 @@ abstract class BaseRecorderModel<I, B : BatchesFolder, T : IntervalRecorderServi
|
||||
|
||||
// If `isSavingAsOldRecording` is true, the user is saving an old recording,
|
||||
// thus the service is not running and thus doesn't need to be stopped or destroyed
|
||||
var onRecordingSave: (isSavingAsOldRecording: Boolean) -> Unit = {}
|
||||
var onRecordingSave: (cleanupOldFiles: Boolean) -> Job = {
|
||||
throw NotImplementedError("onRecordingSave not implemented")
|
||||
}
|
||||
var onRecordingStart: () -> Unit = {}
|
||||
var onError: () -> Unit = {}
|
||||
var onBatchesFolderNotAccessible: () -> Unit = {}
|
||||
|
@ -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,
|
||||
|
@ -191,4 +191,6 @@
|
||||
<string name="ui_settings_option_saveFolder_explainInternalFolder_explanation">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.</string>
|
||||
<string name="ui_rotateDevice_portrait_label">Please rotate your device to portait mode</string>
|
||||
<string name="goBack">Back</string>
|
||||
<string name="ui_recorder_action_saveCurrent">Save now?</string>
|
||||
<string name="ui_recorder_action_saveCurrent_explanation">You can save the current ongoing recording by pressing and holding down on the save button. The recording will continue in the background.</string>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user