feat: Add save recording button

This commit is contained in:
Myzel394 2023-08-08 19:50:45 +02:00
parent 0bfed18314
commit 09f9be8414
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
6 changed files with 135 additions and 25 deletions

View File

@ -5,6 +5,7 @@ import android.os.Build
import android.util.Log import android.util.Log
import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.ReturnCode import com.arthenica.ffmpegkit.ReturnCode
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.io.File import java.io.File
import java.time.LocalDateTime import java.time.LocalDateTime

View File

@ -9,6 +9,10 @@ import androidx.compose.foundation.background
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState 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.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@ -16,6 +20,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import app.myzel394.alibi.dataStore import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.LastRecording
import app.myzel394.alibi.ui.enums.Screen import app.myzel394.alibi.ui.enums.Screen
import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.screens.AudioRecorder import app.myzel394.alibi.ui.screens.AudioRecorder

View File

@ -13,12 +13,15 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -43,6 +46,7 @@ import androidx.compose.ui.semantics.semantics
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.services.RecorderService 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.ConfirmDeletionDialog
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RealtimeAudioVisualizer import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RealtimeAudioVisualizer
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveRecordingButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveRecordingButton
@ -135,7 +139,7 @@ fun RecordingStatus(
}, },
onConfirm = { onConfirm = {
showDeleteDialog = false showDeleteDialog = false
audioRecorder.stopRecording(context) audioRecorder.stopRecording(context, saveAsLastRecording = false)
}, },
) )
} }
@ -159,6 +163,31 @@ fun RecordingStatus(
Text(label) Text(label)
} }
} }
val alpha by animateFloatAsState(if (progressVisible) 1f else 0f, tween(1000)) 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))
}
} }
} }

View File

@ -1,14 +1,7 @@
package app.myzel394.alibi.ui.components.AudioRecorder.molecules package app.myzel394.alibi.ui.components.AudioRecorder.molecules
import android.Manifest 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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -28,14 +21,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.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.services.RecorderService
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE 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.components.atoms.PermissionRequester
import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.utils.rememberFileSaverDialog import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
@ -130,13 +113,39 @@ fun StartRecording(
.fillMaxWidth(), .fillMaxWidth(),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
if (false) if (audioRecorder.lastRecording != null && audioRecorder.lastRecording!!.hasRecordingAvailable) {
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom, 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 else
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
} }

View File

@ -12,6 +12,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import app.myzel394.alibi.db.LastRecording
import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.enums.RecorderState
import app.myzel394.alibi.services.AudioRecorderService import app.myzel394.alibi.services.AudioRecorderService
import app.myzel394.alibi.services.RecorderService import app.myzel394.alibi.services.RecorderService
@ -36,6 +37,11 @@ class AudioRecorderModel: ViewModel() {
var recorderService: AudioRecorderService? = null var recorderService: AudioRecorderService? = null
private set private set
var lastRecording: LastRecording? by mutableStateOf<LastRecording?>(null)
private set
var onRecordingSave: () -> Unit = {}
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
recorderService = ((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also {recorder -> 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) 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 { runCatching {
context.unbindService(connection) context.unbindService(connection)
context.stopService(intent) context.stopService(intent)

View File

@ -1,21 +1,30 @@
package app.myzel394.alibi.ui.screens package app.myzel394.alibi.ui.screens
import android.util.Log
import androidx.compose.foundation.layout.Box 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.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons 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.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
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.*
import androidx.compose.ui.Alignment
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.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import app.myzel394.alibi.ui.components.AudioRecorder.molecules.RecordingStatus 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.enums.Screen
import app.myzel394.alibi.ui.utils.rememberFileSaverDialog import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
import app.myzel394.alibi.R import app.myzel394.alibi.R
import app.myzel394.alibi.db.LastRecording
import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.models.AudioRecorderModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AudioRecorder( fun AudioRecorder(
navController: NavController, navController: NavController,
audioRecorder: AudioRecorderModel audioRecorder: AudioRecorderModel,
) { ) {
val context = LocalContext.current
val saveFile = rememberFileSaverDialog("audio/aac") 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( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -61,9 +119,7 @@ fun AudioRecorder(
.padding(padding), .padding(padding),
) { ) {
if (audioRecorder.isInRecording) if (audioRecorder.isInRecording)
RecordingStatus( RecordingStatus(audioRecorder = audioRecorder)
audioRecorder = audioRecorder,
)
else else
StartRecording(audioRecorder = audioRecorder) StartRecording(audioRecorder = audioRecorder)
} }