refactor: Improving Recorder events handling

This commit is contained in:
Myzel394 2023-12-06 21:57:04 +01:00
parent ce50ed1d68
commit 3cba3382f3
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
8 changed files with 328 additions and 229 deletions

View File

@ -253,9 +253,7 @@ class VideoRecorderService :
targetVideoBitRate = appSettings.videoRecorderSettings.targetedVideoBitRate,
targetFrameRate = appSettings.videoRecorderSettings.targetFrameRate,
quality = appSettings.videoRecorderSettings.getQualitySelector()
?: QualitySelector.from(
Quality.HIGHEST
),
?: QualitySelector.from(Quality.HIGHEST),
)
}
}

View File

@ -0,0 +1,50 @@
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import app.myzel394.alibi.R
@Composable
fun RecorderErrorDialog(
onClose: () -> Unit,
onSave: () -> Unit,
) {
AlertDialog(
onDismissRequest = onClose,
icon = {
Icon(
Icons.Default.Warning,
contentDescription = null,
)
},
title = {
Text(stringResource(R.string.ui_audioRecorder_error_recording_title))
},
text = {
Text(stringResource(R.string.ui_audioRecorder_error_recording_description))
},
dismissButton = {
Button(
onClick = onClose,
colors = ButtonDefaults.textButtonColors(),
) {
Text(stringResource(R.string.dialog_close_cancel_label))
}
},
confirmButton = {
Button(
onClick = onSave,
colors = ButtonDefaults.textButtonColors(),
) {
Text(stringResource(R.string.ui_audioRecorder_action_save_label))
}
}
)
}

View File

@ -0,0 +1,213 @@
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.helpers.AudioBatchesFolder
import app.myzel394.alibi.helpers.BatchesFolder
import app.myzel394.alibi.services.IntervalRecorderService
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.models.BaseRecorderModel
import app.myzel394.alibi.ui.models.VideoRecorderModel
import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
typealias RecorderModel = BaseRecorderModel<
IntervalRecorderService.Settings,
RecordingInformation,
IntervalRecorderService<IntervalRecorderService.Settings, RecordingInformation>,
BatchesFolder?
>
@Composable
fun RecorderEventsHandler(
settings: AppSettings,
snackbarHostState: SnackbarHostState,
audioRecorder: AudioRecorderModel,
videoRecorder: VideoRecorderModel,
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val dataStore = context.dataStore
var isProcessing by remember { mutableStateOf(false) }
var showRecorderError by remember { mutableStateOf(false) }
val saveFile = rememberFileSaverDialog(
settings.audioRecorderSettings.getMimeType()
) {
if (settings.deleteRecordingsImmediately) {
audioRecorder.batchesFolder!!.deleteRecordings()
videoRecorder.batchesFolder!!.deleteRecordings()
}
if (!audioRecorder.batchesFolder!!.hasRecordingsAvailable()
|| !videoRecorder.batchesFolder!!.hasRecordingsAvailable()
) {
scope.launch {
dataStore.updateData {
it.setLastRecording(null)
}
}
}
}
fun saveAsLastRecording(
recorder: RecorderModel
) {
if (!settings.deleteRecordingsImmediately) {
scope.launch {
dataStore.updateData {
it.setLastRecording(
recorder.recorderService!!.getRecordingInformation()
)
}
}
}
}
val successMessage = stringResource(R.string.ui_audioRecorder_action_save_success)
val openMessage = stringResource(R.string.ui_audioRecorder_action_save_openFolder)
fun openFolder(uri: Uri) {
val intent = Intent(Intent.ACTION_VIEW, uri)
context.startActivity(intent)
}
fun showSnackbar(uri: Uri) {
scope.launch {
val result = snackbarHostState.showSnackbar(
message = successMessage,
actionLabel = openMessage,
duration = SnackbarDuration.Short,
)
if (result == SnackbarResult.ActionPerformed) {
openFolder(uri)
}
}
}
fun saveRecording(recorder: RecorderModel) {
scope.launch {
isProcessing = true
// Give the user some time to see the processing dialog
delay(100)
try {
val recording =
// When new recording created
recorder.recorderService?.getRecordingInformation()
// When recording is loaded from lastRecording
?: settings.lastRecording
?: throw Exception("No recording information available")
val batchesFolder =
AudioBatchesFolder.importFromFolder(recording.folderPath, context)
batchesFolder.concatenate(
recording.recordingStart,
recording.fileExtension,
)
// Save file
val name = batchesFolder.getName(
recording.recordingStart,
recording.fileExtension,
)
when (batchesFolder.type) {
BatchesFolder.BatchType.INTERNAL -> {
saveFile(
batchesFolder.asInternalGetOutputFile(
recording.recordingStart,
recording.fileExtension,
), name
)
}
BatchesFolder.BatchType.CUSTOM -> {
showSnackbar(batchesFolder.customFolder!!.uri)
if (settings.deleteRecordingsImmediately) {
batchesFolder.deleteRecordings()
}
}
}
} catch (error: Exception) {
Log.getStackTraceString(error)
} finally {
isProcessing = false
}
}
}
// Register audio recorder events
DisposableEffect(key1 = audioRecorder, key2 = settings) {
audioRecorder.onRecordingSave = {
saveAsLastRecording(audioRecorder as RecorderModel)
saveRecording(audioRecorder)
}
audioRecorder.onError = {
saveAsLastRecording(audioRecorder as RecorderModel)
showRecorderError = true
}
onDispose {
audioRecorder.onRecordingSave = {}
audioRecorder.onError = {}
}
}
// Register video recorder events
DisposableEffect(key1 = videoRecorder, key2 = settings) {
videoRecorder.onRecordingSave = {
saveAsLastRecording(videoRecorder as RecorderModel)
saveRecording(videoRecorder)
}
videoRecorder.onError = {
saveAsLastRecording(videoRecorder as RecorderModel)
showRecorderError = true
}
onDispose {
videoRecorder.onRecordingSave = {}
videoRecorder.onError = {}
}
}
if (isProcessing)
RecorderProcessingDialog()
if (showRecorderError)
RecorderErrorDialog(
onClose = {
showRecorderError = false
},
onSave = {
},
)
}

View File

@ -0,0 +1,47 @@
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Memory
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text
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.unit.dp
import app.myzel394.alibi.R
@Composable
fun RecorderProcessingDialog() {
AlertDialog(
onDismissRequest = { },
icon = {
Icon(
Icons.Default.Memory,
contentDescription = null,
)
},
title = {
Text(
stringResource(R.string.ui_audioRecorder_action_save_processing_dialog_title),
)
},
text = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
stringResource(R.string.ui_audioRecorder_action_save_processing_dialog_description),
)
Spacer(modifier = Modifier.height(32.dp))
LinearProgressIndicator()
}
},
confirmButton = {}
)
}

View File

@ -75,7 +75,9 @@ fun AudioRecordingStatus(
isPaused = audioRecorder.isPaused,
onDelete = {
scope.launch {
audioRecorder.stopRecording(context)
runCatching {
audioRecorder.stopRecording(context)
}
audioRecorder.batchesFolder!!.deleteRecordings()
}
},
@ -88,9 +90,7 @@ fun AudioRecordingStatus(
},
onSave = {
scope.launch {
runCatching {
audioRecorder.stopRecording(context)
}
audioRecorder.stopRecording(context)
audioRecorder.onRecordingSave()
}
}

View File

@ -78,8 +78,6 @@ fun VideoRecordingStatus(
modifier = Modifier.padding(bottom = 32.dp),
) {
val cameraControl = videoRecorder.recorderService!!.cameraControl!!
println("cameraControl.hasTorchAvailable(): ${cameraControl.hasTorchAvailable()}")
println("videoRecorder: ${videoRecorder.recorderService?.cameraControl}")
if (cameraControl.hasTorchAvailable()) {
val isTorchEnabled = cameraControl.isTorchEnabled()
@ -101,7 +99,9 @@ fun VideoRecordingStatus(
isPaused = videoRecorder.isPaused,
onDelete = {
scope.launch {
videoRecorder.stopRecording(context)
runCatching {
videoRecorder.stopRecording(context)
}
videoRecorder.batchesFolder!!.deleteRecordings()
}
},
@ -114,9 +114,7 @@ fun VideoRecordingStatus(
},
onSave = {
scope.launch {
runCatching {
videoRecorder.stopRecording(context)
}
videoRecorder.stopRecording(context)
videoRecorder.onRecordingSave()
}
}

View File

@ -50,9 +50,4 @@ class VideoRecorderModel :
putExtra("cameraID", cameraID)
putExtra("enableAudio", enableAudio)
}
override fun startRecording(context: Context, settings: AppSettings) {
super.startRecording(context, settings)
}
}

View File

@ -1,57 +1,37 @@
package app.myzel394.alibi.ui.screens
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Memory
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.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.ui.utils.rememberFileSaverDialog
import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.helpers.AudioBatchesFolder
import app.myzel394.alibi.helpers.BatchesFolder
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RecorderEventsHandler
import app.myzel394.alibi.ui.components.RecorderScreen.organisms.VideoRecordingStatus
import app.myzel394.alibi.ui.effects.rememberSettings
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.models.VideoRecorderModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -63,197 +43,14 @@ fun RecorderScreen(
val snackbarHostState = remember { SnackbarHostState() }
val context = LocalContext.current
val dataStore = context.dataStore
val settings = rememberSettings()
val scope = rememberCoroutineScope()
val saveFile = rememberFileSaverDialog(
settings.audioRecorderSettings.getMimeType()
) {
if (settings.deleteRecordingsImmediately) {
audioRecorder.batchesFolder!!.deleteRecordings()
}
if (!audioRecorder.batchesFolder!!.hasRecordingsAvailable()) {
scope.launch {
dataStore.updateData {
it.setLastRecording(null)
}
}
}
}
var isProcessing by remember { mutableStateOf(false) }
var showRecorderError by remember { mutableStateOf(false) }
fun saveAsLastRecording() {
if (!settings.deleteRecordingsImmediately) {
scope.launch {
dataStore.updateData {
it.setLastRecording(
audioRecorder.recorderService!!.getRecordingInformation()
)
}
}
}
}
val successMessage = stringResource(R.string.ui_audioRecorder_action_save_success)
val openMessage = stringResource(R.string.ui_audioRecorder_action_save_openFolder)
fun openFolder(uri: Uri) {
val intent = Intent(Intent.ACTION_VIEW, uri)
context.startActivity(intent)
}
fun showSnackbar(uri: Uri) {
scope.launch {
val result = snackbarHostState.showSnackbar(
message = successMessage,
actionLabel = openMessage,
duration = SnackbarDuration.Short,
)
if (result == SnackbarResult.ActionPerformed) {
openFolder(uri)
}
}
}
fun saveRecording() {
scope.launch {
isProcessing = true
// Give the user some time to see the processing dialog
delay(100)
try {
val recording = audioRecorder.recorderService?.getRecordingInformation()
?: settings.lastRecording
?: throw Exception("No recording information available")
val batchesFolder =
AudioBatchesFolder.importFromFolder(recording.folderPath, context)
batchesFolder.concatenate(
recording.recordingStart,
recording.fileExtension,
)
// Save file
val name = batchesFolder.getName(
recording.recordingStart,
recording.fileExtension,
)
when (batchesFolder.type) {
BatchesFolder.BatchType.INTERNAL -> {
saveFile(
batchesFolder.asInternalGetOutputFile(
recording.recordingStart,
recording.fileExtension,
), name
)
}
BatchesFolder.BatchType.CUSTOM -> {
showSnackbar(batchesFolder.customFolder!!.uri)
if (settings.deleteRecordingsImmediately) {
batchesFolder.deleteRecordings()
}
}
}
} catch (error: Exception) {
Log.getStackTraceString(error)
} finally {
isProcessing = false
}
}
}
DisposableEffect(key1 = audioRecorder, key2 = settings) {
audioRecorder.onRecordingSave = onRecordingSave@{
saveAsLastRecording()
saveRecording()
}
audioRecorder.onError = {
saveAsLastRecording()
showRecorderError = true
}
onDispose {
audioRecorder.onRecordingSave = {}
audioRecorder.onError = {}
}
}
if (isProcessing)
AlertDialog(
onDismissRequest = { },
icon = {
Icon(
Icons.Default.Memory,
contentDescription = null,
)
},
title = {
Text(
stringResource(R.string.ui_audioRecorder_action_save_processing_dialog_title),
)
},
text = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
stringResource(R.string.ui_audioRecorder_action_save_processing_dialog_description),
)
Spacer(modifier = Modifier.height(32.dp))
LinearProgressIndicator()
}
},
confirmButton = {}
)
if (showRecorderError)
AlertDialog(
onDismissRequest = { showRecorderError = false },
icon = {
Icon(
Icons.Default.Warning,
contentDescription = null,
)
},
title = {
Text(stringResource(R.string.ui_audioRecorder_error_recording_title))
},
text = {
Text(stringResource(R.string.ui_audioRecorder_error_recording_description))
},
dismissButton = {
Button(
onClick = { showRecorderError = false },
colors = ButtonDefaults.textButtonColors(),
) {
Text(stringResource(R.string.dialog_close_cancel_label))
}
},
confirmButton = {
Button(
onClick = {
showRecorderError = false
saveRecording()
},
colors = ButtonDefaults.textButtonColors(),
) {
Text(stringResource(R.string.ui_audioRecorder_action_save_label))
}
}
)
RecorderEventsHandler(
settings = settings,
snackbarHostState = snackbarHostState,
audioRecorder = audioRecorder,
videoRecorder = videoRecorder,
)
// TopAppBar and AudioRecordingStart should be hidden when
// the video preview is visible.
@ -315,7 +112,8 @@ fun RecorderScreen(
audioRecorder = audioRecorder,
videoRecorder = videoRecorder,
appSettings = appSettings,
onSaveLastRecording = ::saveRecording,
onSaveLastRecording = {
},
showAudioRecorder = topBarVisible,
onHideTopBar = {
topBarVisible = false