mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-19 07:15:25 +02:00
Merge pull request #94 from Myzel394/add-auto-save-83
Add save current status functionality
This commit is contained in:
commit
24383a7bd8
@ -11,6 +11,7 @@ import app.myzel394.alibi.helpers.AudioBatchesFolder
|
|||||||
import app.myzel394.alibi.helpers.VideoBatchesFolder
|
import app.myzel394.alibi.helpers.VideoBatchesFolder
|
||||||
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
|
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
|
||||||
import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE
|
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 app.myzel394.alibi.ui.utils.PermissionHelper
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@ -102,6 +103,16 @@ data class AppSettings(
|
|||||||
return copy(appLockSettings = appLockSettings)
|
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.
|
// If the object is present, biometric authentication is enabled.
|
||||||
// To disable biometric authentication, set the instance to null.
|
// To disable biometric authentication, set the instance to null.
|
||||||
fun isAppLockEnabled() = appLockSettings != null
|
fun isAppLockEnabled() = appLockSettings != null
|
||||||
|
@ -3,28 +3,26 @@ package app.myzel394.alibi.helpers
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import app.myzel394.alibi.ui.MEDIA_RECORDINGS_PREFIX
|
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.Video.Media
|
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 android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.net.toUri
|
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_INTERNAL_SELECTED_VALUE
|
||||||
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
|
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
|
||||||
import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE
|
import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE
|
||||||
import app.myzel394.alibi.ui.utils.PermissionHelper
|
import app.myzel394.alibi.ui.utils.PermissionHelper
|
||||||
import com.arthenica.ffmpegkit.FFprobeKit
|
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import java.io.File
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
import kotlin.reflect.KFunction4
|
import kotlin.reflect.KFunction4
|
||||||
|
|
||||||
abstract class BatchesFolder(
|
abstract class BatchesFolder(
|
||||||
@ -197,7 +195,6 @@ abstract class BatchesFolder(
|
|||||||
createNewFile()
|
createNewFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun checkIfOutputAlreadyExists(
|
fun checkIfOutputAlreadyExists(
|
||||||
date: LocalDateTime,
|
date: LocalDateTime,
|
||||||
extension: String
|
extension: String
|
||||||
@ -388,12 +385,12 @@ abstract class BatchesFolder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteOldRecordings(earliestCounter: Long) {
|
fun deleteRecordings(range: LongRange) {
|
||||||
when (type) {
|
when (type) {
|
||||||
BatchType.INTERNAL -> getInternalFolder().listFiles()?.forEach {
|
BatchType.INTERNAL -> getInternalFolder().listFiles()?.forEach {
|
||||||
val fileCounter = it.nameWithoutExtension.toIntOrNull() ?: return@forEach
|
val fileCounter = it.nameWithoutExtension.toIntOrNull() ?: return@forEach
|
||||||
|
|
||||||
if (fileCounter < earliestCounter) {
|
if (fileCounter in range) {
|
||||||
it.delete()
|
it.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -401,7 +398,7 @@ abstract class BatchesFolder(
|
|||||||
BatchType.CUSTOM -> getCustomDefinedFolder().listFiles().forEach {
|
BatchType.CUSTOM -> getCustomDefinedFolder().listFiles().forEach {
|
||||||
val fileCounter = it.name?.substringBeforeLast(".")?.toIntOrNull() ?: return@forEach
|
val fileCounter = it.name?.substringBeforeLast(".")?.toIntOrNull() ?: return@forEach
|
||||||
|
|
||||||
if (fileCounter < earliestCounter) {
|
if (fileCounter in range) {
|
||||||
it.delete()
|
it.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -411,7 +408,7 @@ abstract class BatchesFolder(
|
|||||||
val deletableNames = mutableListOf<String>()
|
val deletableNames = mutableListOf<String>()
|
||||||
|
|
||||||
queryMediaContent { rawName, counter, _, _ ->
|
queryMediaContent { rawName, counter, _, _ ->
|
||||||
if (counter < earliestCounter) {
|
if (counter in range) {
|
||||||
deletableNames.add(rawName)
|
deletableNames.add(rawName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -428,7 +425,7 @@ abstract class BatchesFolder(
|
|||||||
it.nameWithoutExtension.substring(mediaPrefix.length).toIntOrNull()
|
it.nameWithoutExtension.substring(mediaPrefix.length).toIntOrNull()
|
||||||
?: return@forEach
|
?: return@forEach
|
||||||
|
|
||||||
if (fileCounter < earliestCounter) {
|
if (fileCounter in range) {
|
||||||
it.delete()
|
it.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,9 @@ abstract class IntervalRecorderService<I, B : BatchesFolder> :
|
|||||||
protected var counter = 0L
|
protected var counter = 0L
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
// Tracks the index of the currently locked file
|
||||||
|
private var lockedIndex: Long? = null
|
||||||
|
|
||||||
lateinit var settings: AppSettings
|
lateinit var settings: AppSettings
|
||||||
|
|
||||||
private lateinit var cycleTimer: ScheduledExecutorService
|
private lateinit var cycleTimer: ScheduledExecutorService
|
||||||
@ -21,6 +24,23 @@ abstract class IntervalRecorderService<I, B : BatchesFolder> :
|
|||||||
|
|
||||||
abstract fun getRecordingInformation(): I
|
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
|
// Make overrideable
|
||||||
open fun startNewCycle() {
|
open fun startNewCycle() {
|
||||||
counter += 1
|
counter += 1
|
||||||
@ -72,12 +92,12 @@ abstract class IntervalRecorderService<I, B : BatchesFolder> :
|
|||||||
|
|
||||||
private fun deleteOldRecordings() {
|
private fun deleteOldRecordings() {
|
||||||
val timeMultiplier = settings.maxDuration / settings.intervalDuration
|
val timeMultiplier = settings.maxDuration / settings.intervalDuration
|
||||||
val earliestCounter = counter - timeMultiplier
|
val earliestCounter = Math.max(counter - timeMultiplier, lockedIndex ?: 0)
|
||||||
|
|
||||||
if (earliestCounter <= 0) {
|
if (earliestCounter <= 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
batchesFolder.deleteOldRecordings(earliestCounter)
|
batchesFolder.deleteRecordings(0..earliestCounter)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -139,7 +139,7 @@ class VideoRecorderService :
|
|||||||
if (_cameraAvailableListener.isCompleted) {
|
if (_cameraAvailableListener.isCompleted) {
|
||||||
action()
|
action()
|
||||||
} else {
|
} 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
|
// 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.
|
// and the interval can't be set shorter than 10 seconds.
|
||||||
_cameraAvailableListener.invokeOnCompletion {
|
_cameraAvailableListener.invokeOnCompletion {
|
||||||
|
@ -1,35 +1,51 @@
|
|||||||
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
|
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.ButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import app.myzel394.alibi.R
|
import app.myzel394.alibi.R
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SaveButton(
|
fun SaveButton(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onSave: () -> Unit,
|
onSave: () -> Unit,
|
||||||
|
onLongClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val label = stringResource(R.string.ui_recorder_action_save_label)
|
val label = stringResource(R.string.ui_recorder_action_save_label)
|
||||||
|
|
||||||
TextButton(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.clip(ButtonDefaults.textShape)
|
||||||
.semantics {
|
.semantics {
|
||||||
contentDescription = label
|
contentDescription = label
|
||||||
}
|
}
|
||||||
.then(modifier),
|
.combinedClickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
|
||||||
onClick = onSave,
|
onClick = onSave,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
)
|
||||||
|
.padding(ButtonDefaults.TextButtonContentPadding)
|
||||||
|
.then(modifier)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
label,
|
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,
|
recordingTime: Long,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
onPauseResume: () -> Unit,
|
onPauseResume: () -> Unit,
|
||||||
onSave: () -> Unit,
|
onSaveAndStop: () -> Unit,
|
||||||
|
onSaveCurrent: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val animateIn = rememberInitialRecordingAnimation(recordingTime)
|
val animateIn = rememberInitialRecordingAnimation(recordingTime)
|
||||||
|
|
||||||
@ -106,7 +107,8 @@ fun RecordingControl(
|
|||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
SaveButton(
|
SaveButton(
|
||||||
onSave = onSave,
|
onSave = onSaveAndStop,
|
||||||
|
onLongClick = onSaveCurrent,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -170,7 +172,10 @@ fun RecordingControl(
|
|||||||
.alpha(saveButtonAlpha),
|
.alpha(saveButtonAlpha),
|
||||||
contentAlignment = Alignment.Center,
|
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.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
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.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.MicrophoneStatus
|
||||||
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl
|
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl
|
||||||
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus
|
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus
|
||||||
@ -39,8 +41,6 @@ fun AudioRecordingStatus(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val configuration = LocalConfiguration.current.orientation
|
val configuration = LocalConfiguration.current.orientation
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
var now by remember { mutableStateOf(LocalDateTime.now()) }
|
var now by remember { mutableStateOf(LocalDateTime.now()) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@ -90,34 +90,7 @@ fun AudioRecordingStatus(
|
|||||||
MicrophoneStatus(audioRecorder)
|
MicrophoneStatus(audioRecorder)
|
||||||
}
|
}
|
||||||
|
|
||||||
RecordingControl(
|
_PrimitiveControls(audioRecorder)
|
||||||
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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +111,38 @@ fun AudioRecordingStatus(
|
|||||||
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
||||||
|
_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(
|
RecordingControl(
|
||||||
isPaused = audioRecorder.isPaused,
|
isPaused = audioRecorder.isPaused,
|
||||||
recordingTime = audioRecorder.recordingTime,
|
recordingTime = audioRecorder.recordingTime,
|
||||||
@ -159,12 +164,23 @@ fun AudioRecordingStatus(
|
|||||||
audioRecorder.pauseRecording()
|
audioRecorder.pauseRecording()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSave = {
|
onSaveAndStop = {
|
||||||
audioRecorder.onRecordingSave(false)
|
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
|
package app.myzel394.alibi.ui.components.RecorderScreen.organisms
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
@ -9,8 +8,6 @@ import androidx.compose.material3.SnackbarResult
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableDoubleStateOf
|
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
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
|
isProcessing = true
|
||||||
|
|
||||||
// Give the user some time to see the processing dialog
|
// Give the user some time to see the processing dialog
|
||||||
delay(100)
|
delay(100)
|
||||||
|
|
||||||
thread {
|
return thread {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
try {
|
try {
|
||||||
|
if (recorder.isCurrentlyActivelyRecording) {
|
||||||
|
recorder.recorderService?.lockFiles()
|
||||||
|
}
|
||||||
|
|
||||||
val recording =
|
val recording =
|
||||||
// When new recording created
|
// When new recording created
|
||||||
recorder.recorderService?.getRecordingInformation()
|
recorder.recorderService?.getRecordingInformation()
|
||||||
@ -218,6 +219,9 @@ fun RecorderEventsHandler(
|
|||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
Log.getStackTraceString(error)
|
Log.getStackTraceString(error)
|
||||||
} finally {
|
} finally {
|
||||||
|
if (recorder.isCurrentlyActivelyRecording) {
|
||||||
|
recorder.recorderService?.unlockFiles(cleanupOldFiles)
|
||||||
|
}
|
||||||
isProcessing = false
|
isProcessing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,19 +230,14 @@ fun RecorderEventsHandler(
|
|||||||
|
|
||||||
// Register audio recorder events
|
// Register audio recorder events
|
||||||
DisposableEffect(key1 = audioRecorder, key2 = settings) {
|
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 {
|
scope.launch {
|
||||||
if (justSave) {
|
saveRecording(audioRecorder as RecorderModel, cleanupOldFiles).join()
|
||||||
saveRecording(audioRecorder as RecorderModel)
|
|
||||||
} else {
|
|
||||||
audioRecorder.stopRecording(context)
|
|
||||||
|
|
||||||
saveAsLastRecording(audioRecorder as RecorderModel)
|
|
||||||
|
|
||||||
saveRecording(audioRecorder)
|
|
||||||
|
|
||||||
audioRecorder.destroyService(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
audioRecorder.onRecordingStart = {
|
audioRecorder.onRecordingStart = {
|
||||||
@ -265,26 +264,23 @@ fun RecorderEventsHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDispose {
|
onDispose {
|
||||||
audioRecorder.onRecordingSave = {}
|
audioRecorder.onRecordingSave = {
|
||||||
|
throw NotImplementedError("onRecordingSave should not be called now")
|
||||||
|
}
|
||||||
audioRecorder.onError = {}
|
audioRecorder.onError = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register video recorder events
|
// Register video recorder events
|
||||||
DisposableEffect(key1 = videoRecorder, key2 = settings) {
|
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 {
|
scope.launch {
|
||||||
if (justSave) {
|
saveRecording(videoRecorder as RecorderModel, cleanupOldFiles).join()
|
||||||
saveRecording(videoRecorder as RecorderModel)
|
|
||||||
} else {
|
|
||||||
videoRecorder.stopRecording(context)
|
|
||||||
|
|
||||||
saveAsLastRecording(videoRecorder as RecorderModel)
|
|
||||||
|
|
||||||
saveRecording(videoRecorder)
|
|
||||||
|
|
||||||
videoRecorder.destroyService(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
videoRecorder.onRecordingStart = {
|
videoRecorder.onRecordingStart = {
|
||||||
@ -311,7 +307,9 @@ fun RecorderEventsHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDispose {
|
onDispose {
|
||||||
videoRecorder.onRecordingSave = {}
|
videoRecorder.onRecordingSave = {
|
||||||
|
throw NotImplementedError("onRecordingSave should not be called now")
|
||||||
|
}
|
||||||
videoRecorder.onError = {}
|
videoRecorder.onError = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
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.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.myzel394.alibi.R
|
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.atoms.TorchStatus
|
||||||
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl
|
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl
|
||||||
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus
|
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus
|
||||||
@ -189,8 +192,28 @@ fun _VideoRecordingStatus(videoRecorder: VideoRecorderModel) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun _PrimitiveControls(videoRecorder: VideoRecorderModel) {
|
fun _PrimitiveControls(videoRecorder: VideoRecorderModel) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val dataStore = context.dataStore
|
||||||
val scope = rememberCoroutineScope()
|
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(
|
RecordingControl(
|
||||||
orientation = Configuration.ORIENTATION_PORTRAIT,
|
orientation = Configuration.ORIENTATION_PORTRAIT,
|
||||||
// There may be some edge cases where the app may crash if the
|
// There may be some edge cases where the app may crash if the
|
||||||
@ -217,8 +240,23 @@ fun _PrimitiveControls(videoRecorder: VideoRecorderModel) {
|
|||||||
videoRecorder.pauseRecording()
|
videoRecorder.pauseRecording()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSave = {
|
onSaveAndStop = {
|
||||||
videoRecorder.onRecordingSave(false)
|
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.IntervalRecorderService
|
||||||
import app.myzel394.alibi.services.RecorderNotificationHelper
|
import app.myzel394.alibi.services.RecorderNotificationHelper
|
||||||
import app.myzel394.alibi.services.RecorderService
|
import app.myzel394.alibi.services.RecorderService
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
abstract class BaseRecorderModel<I, B : BatchesFolder, T : IntervalRecorderService<I, B>> :
|
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
|
open val isInRecording: Boolean
|
||||||
get() = recorderService != null
|
get() = recorderService != null
|
||||||
|
|
||||||
|
open val isCurrentlyActivelyRecording
|
||||||
|
get() = recorderState === RecorderState.RECORDING
|
||||||
|
|
||||||
val isPaused: Boolean
|
val isPaused: Boolean
|
||||||
get() = recorderState === RecorderState.PAUSED
|
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,
|
// 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
|
// 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 onRecordingStart: () -> Unit = {}
|
||||||
var onError: () -> Unit = {}
|
var onError: () -> Unit = {}
|
||||||
var onBatchesFolderNotAccessible: () -> Unit = {}
|
var onBatchesFolderNotAccessible: () -> Unit = {}
|
||||||
|
@ -16,22 +16,26 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
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.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
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.R
|
||||||
import app.myzel394.alibi.dataStore
|
import app.myzel394.alibi.dataStore
|
||||||
import app.myzel394.alibi.db.AppSettings
|
import app.myzel394.alibi.db.AppSettings
|
||||||
import app.myzel394.alibi.db.RecordingInformation
|
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.RecorderEventsHandler
|
||||||
|
import app.myzel394.alibi.ui.components.RecorderScreen.organisms.StartRecording
|
||||||
import app.myzel394.alibi.ui.components.RecorderScreen.organisms.VideoRecordingStatus
|
import app.myzel394.alibi.ui.components.RecorderScreen.organisms.VideoRecordingStatus
|
||||||
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
||||||
import app.myzel394.alibi.ui.models.VideoRecorderModel
|
import app.myzel394.alibi.ui.models.VideoRecorderModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -43,6 +47,7 @@ fun RecorderScreen(
|
|||||||
) {
|
) {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
RecorderEventsHandler(
|
RecorderEventsHandler(
|
||||||
settings = settings,
|
settings = settings,
|
||||||
@ -112,12 +117,14 @@ fun RecorderScreen(
|
|||||||
videoRecorder = videoRecorder,
|
videoRecorder = videoRecorder,
|
||||||
appSettings = appSettings,
|
appSettings = appSettings,
|
||||||
onSaveLastRecording = {
|
onSaveLastRecording = {
|
||||||
|
scope.launch {
|
||||||
when (settings.lastRecording!!.type) {
|
when (settings.lastRecording!!.type) {
|
||||||
RecordingInformation.Type.AUDIO ->
|
RecordingInformation.Type.AUDIO ->
|
||||||
audioRecorder.onRecordingSave(true)
|
audioRecorder.onRecordingSave(false)
|
||||||
|
|
||||||
RecordingInformation.Type.VIDEO ->
|
RecordingInformation.Type.VIDEO ->
|
||||||
videoRecorder.onRecordingSave(true)
|
videoRecorder.onRecordingSave(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showAudioRecorder = topBarVisible,
|
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_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="ui_rotateDevice_portrait_label">Please rotate your device to portait mode</string>
|
||||||
<string name="goBack">Back</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>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user