From 09f9be8414f13bfc12138046e9d7bf5e4aa88646 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 8 Aug 2023 19:50:45 +0200 Subject: [PATCH] feat: Add save recording button --- .../java/app/myzel394/alibi/db/AppSettings.kt | 1 + .../java/app/myzel394/alibi/ui/Navigation.kt | 5 ++ .../molecules/RecordingStatus.kt | 31 ++++++++- .../AudioRecorder/molecules/StartRecording.kt | 45 ++++++++----- .../alibi/ui/models/AudioRecorderModel.kt | 12 +++- .../alibi/ui/screens/AudioRecorder.kt | 66 +++++++++++++++++-- 6 files changed, 135 insertions(+), 25 deletions(-) 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 7cfd621..3a5d7c5 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -5,6 +5,7 @@ import android.os.Build import android.util.Log import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.ReturnCode +import kotlinx.coroutines.delay import kotlinx.serialization.Serializable import java.io.File import java.time.LocalDateTime diff --git a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt index 7dc6ffb..8617b52 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt @@ -9,6 +9,10 @@ import androidx.compose.foundation.background import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel @@ -16,6 +20,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import app.myzel394.alibi.dataStore +import app.myzel394.alibi.db.LastRecording import app.myzel394.alibi.ui.enums.Screen import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.screens.AudioRecorder diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/RecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/RecordingStatus.kt index 19aa303..e382876 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/RecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/RecordingStatus.kt @@ -13,12 +13,15 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Save import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon @@ -43,6 +46,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import app.myzel394.alibi.R import app.myzel394.alibi.services.RecorderService +import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE import app.myzel394.alibi.ui.components.AudioRecorder.atoms.ConfirmDeletionDialog import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RealtimeAudioVisualizer import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveRecordingButton @@ -135,7 +139,7 @@ fun RecordingStatus( }, onConfirm = { showDeleteDialog = false - audioRecorder.stopRecording(context) + audioRecorder.stopRecording(context, saveAsLastRecording = false) }, ) } @@ -159,6 +163,31 @@ fun RecordingStatus( Text(label) } } + val alpha by animateFloatAsState(if (progressVisible) 1f else 0f, tween(1000)) + val label = stringResource(R.string.ui_audioRecorder_action_save_label) + + Button( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .height(BIG_PRIMARY_BUTTON_SIZE) + .alpha(alpha) + .semantics { + contentDescription = label + }, + onClick = { + audioRecorder.stopRecording(context) + audioRecorder.onRecordingSave() + }, + ) { + Icon( + Icons.Default.Save, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.ui_audioRecorder_action_save_label)) + } } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt index b8cd59d..49a5948 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt @@ -1,14 +1,7 @@ package app.myzel394.alibi.ui.components.AudioRecorder.molecules import android.Manifest -import android.content.ServiceConnection -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.animation.expandHorizontally import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -28,14 +21,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.autoSaver -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -48,10 +34,7 @@ import androidx.compose.ui.unit.dp import app.myzel394.alibi.R import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings -import app.myzel394.alibi.services.RecorderService import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE -import app.myzel394.alibi.ui.components.AudioRecorder.atoms.AudioVisualizer -import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveRecordingButton import app.myzel394.alibi.ui.components.atoms.PermissionRequester import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.utils.rememberFileSaverDialog @@ -130,13 +113,39 @@ fun StartRecording( .fillMaxWidth(), textAlign = TextAlign.Center, ) - if (false) + if (audioRecorder.lastRecording != null && audioRecorder.lastRecording!!.hasRecordingAvailable) { Column( modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Bottom, ) { + val label = stringResource( + R.string.ui_audioRecorder_action_saveOldRecording_label, + DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).format(audioRecorder.lastRecording!!.recordingStart), + ) + Button( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .height(BIG_PRIMARY_BUTTON_SIZE) + .semantics { + contentDescription = label + }, + onClick = { + audioRecorder.stopRecording(context) + audioRecorder.onRecordingSave() + }, + ) { + Icon( + Icons.Default.Save, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text(label) + } } + } else Spacer(modifier = Modifier.weight(1f)) } diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt index 17c3763..9757662 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModel +import app.myzel394.alibi.db.LastRecording import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.services.AudioRecorderService import app.myzel394.alibi.services.RecorderService @@ -36,6 +37,11 @@ class AudioRecorderModel: ViewModel() { var recorderService: AudioRecorderService? = null private set + var lastRecording: LastRecording? by mutableStateOf(null) + private set + + var onRecordingSave: () -> Unit = {} + private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { recorderService = ((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also {recorder -> @@ -77,7 +83,11 @@ class AudioRecorderModel: ViewModel() { context.bindService(intent!!, connection, Context.BIND_AUTO_CREATE) } - fun stopRecording(context: Context) { + fun stopRecording(context: Context, saveAsLastRecording: Boolean = true) { + if (saveAsLastRecording) { + lastRecording = recorderService!!.createLastRecording() + } + runCatching { context.unbindService(connection) context.stopService(intent) diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt index 3736f0c..0e20792 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt @@ -1,21 +1,30 @@ package app.myzel394.alibi.ui.screens +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.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold 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.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import app.myzel394.alibi.ui.components.AudioRecorder.molecules.RecordingStatus @@ -23,17 +32,66 @@ import app.myzel394.alibi.ui.components.AudioRecorder.molecules.StartRecording import app.myzel394.alibi.ui.enums.Screen import app.myzel394.alibi.ui.utils.rememberFileSaverDialog import app.myzel394.alibi.R +import app.myzel394.alibi.db.LastRecording import app.myzel394.alibi.ui.models.AudioRecorderModel +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun AudioRecorder( navController: NavController, - audioRecorder: AudioRecorderModel + audioRecorder: AudioRecorderModel, ) { - val context = LocalContext.current val saveFile = rememberFileSaverDialog("audio/aac") + val scope = rememberCoroutineScope() + var isProcessingAudio by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + audioRecorder.onRecordingSave = { + scope.launch { + isProcessingAudio = true + + try { + val file = audioRecorder.lastRecording!!.concatenateFiles() + + saveFile(file) + } catch (error: Exception) { + Log.getStackTraceString(error) + } finally { + isProcessingAudio = false + } + } + } + } + + if (isProcessingAudio) + 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 = {} + ) Scaffold( topBar = { TopAppBar( @@ -61,9 +119,7 @@ fun AudioRecorder( .padding(padding), ) { if (audioRecorder.isInRecording) - RecordingStatus( - audioRecorder = audioRecorder, - ) + RecordingStatus(audioRecorder = audioRecorder) else StartRecording(audioRecorder = audioRecorder) }