fix: Fix BatchesFolder export

This commit is contained in:
Myzel394 2023-11-19 17:41:37 +01:00
parent 18195c9893
commit 0364a79dcb
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
6 changed files with 141 additions and 78 deletions

View File

@ -16,25 +16,21 @@ import java.time.format.DateTimeFormatter
data class AudioRecorderExporter(
val recording: RecordingInformation,
) {
private fun getInternalFilePaths(context: Context): List<File> =
getFolder(context)
.listFiles()
?.filter {
val name = it.nameWithoutExtension
name.toIntOrNull() != null
}
?.toList()
?: emptyList()
suspend fun concatenateFiles(
context: Context,
batchesFolder: BatchesFolder,
outputFilePath: String,
forceConcatenation: Boolean = false,
) {
val filePaths = batchesFolder.getBatchesForFFmpeg().joinToString("|")
val outputFile =
batchesFolder.getOutputFileForFFmpeg(recording.recordingStart, recording.fileExtension)
if (batchesFolder.checkIfOutputAlreadyExists(
recording.recordingStart,
recording.fileExtension
) && !forceConcatenation
) {
return
}
val command =
"-protocol_whitelist saf,concat,content,file,subfile" +
@ -44,7 +40,7 @@ data class AudioRecorderExporter(
" -metadata batch_count='${filePaths.length}'" +
" -metadata batch_duration='${recording.intervalDuration}'" +
" -metadata max_duration='${recording.maxDuration}'" +
" $outputFile"
" $outputFilePath"
val session = FFmpegKit.execute(command)
@ -68,34 +64,6 @@ data class AudioRecorderExporter(
companion object {
fun getFolder(context: Context) = File(context.filesDir, RECORDER_SUBFOLDER_NAME)
fun clearAllRecordings(context: Context) {
getFolder(context).deleteRecursively()
}
fun hasRecordingsAvailable(context: Context) =
getFolder(context).listFiles()?.isNotEmpty() ?: false
fun linkBatches(context: Context, batchesFolder: Uri, destinationFolder: File) {
val folder =
DocumentFile.fromTreeUri(
context,
batchesFolder,
)!!
destinationFolder.mkdirs()
folder.listFiles().forEach {
if (it.name?.substringBeforeLast(".")?.toIntOrNull() == null) {
return@forEach
}
Os.symlink(
"${folder.uri}/${it.name}",
"${destinationFolder.absolutePath}/${it.name}",
)
}
}
}
}

View File

@ -68,10 +68,47 @@ data class BatchesFolder(
}
}
fun getName(date: LocalDateTime, extension: String): String {
val name = date
.format(DateTimeFormatter.ISO_DATE_TIME)
.toString()
.replace(":", "-")
.replace(".", "_")
return "$name.$extension"
}
fun asInternalGetOutputFile(date: LocalDateTime, extension: String): File {
return File(getInternalFolder(), getName(date, extension))
}
fun asCustomGetOutputFile(
date: LocalDateTime,
extension: String,
): DocumentFile {
return getCustomDefinedFolder().createFile("audio/$extension", getName(date, extension))!!
}
fun getOutputFileForFFmpeg(
date: LocalDateTime,
extension: String,
): String {
return when (type) {
BatchType.INTERNAL -> asInternalGetOutputFile(date, extension).absolutePath
BatchType.CUSTOM -> FFmpegKitConfig.getSafParameterForWrite(
context,
customFolder!!.createFile(
"audio/${extension}",
getName(date, extension),
)!!.uri
)!!
}
}
fun checkIfOutputAlreadyExists(
date: LocalDateTime,
extension: String
): Boolean {
val name = date
.format(DateTimeFormatter.ISO_DATE_TIME)
.toString()
@ -79,14 +116,9 @@ data class BatchesFolder(
.replace(".", "_")
return when (type) {
BatchType.INTERNAL -> File(getInternalFolder(), "$name.$extension").absolutePath
BatchType.CUSTOM -> FFmpegKitConfig.getSafParameterForWrite(
context,
getCustomDefinedFolder().createFile(
"audio/${extension}",
"${name}.${extension}"
)!!.uri
)!!
BatchType.INTERNAL -> File(getInternalFolder(), "$name.$extension").exists()
BatchType.CUSTOM ->
getCustomDefinedFolder().findFile("${name}.${extension}")?.exists() ?: false
}
}
@ -104,6 +136,13 @@ data class BatchesFolder(
}
}
fun hasRecordingsAvailable(): Boolean {
return when (type) {
BatchType.INTERNAL -> getInternalFolder().listFiles()?.isNotEmpty() ?: false
BatchType.CUSTOM -> getCustomDefinedFolder().listFiles().isNotEmpty()
}
}
fun deleteOldRecordings(earliestCounter: Long) {
when (type) {
BatchType.INTERNAL -> getInternalFolder().listFiles()?.forEach {

View File

@ -1,7 +1,6 @@
package app.myzel394.alibi.ui.components.AudioRecorder.molecules
import android.Manifest
import android.net.Uri
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@ -21,13 +20,11 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.rememberStandardBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@ -39,22 +36,12 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.documentfile.provider.DocumentFile
import app.myzel394.alibi.NotificationHelper
import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.helpers.AudioRecorderExporter
import app.myzel394.alibi.helpers.AudioRecorderExporter.Companion.clearAllRecordings
import app.myzel394.alibi.helpers.BatchesFolder
import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.lastOrNull
import kotlinx.coroutines.launch
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle

View File

@ -107,8 +107,7 @@ fun RecordingStatus(
DeleteButton(
onDelete = {
audioRecorder.stopRecording(context)
AudioRecorderExporter.clearAllRecordings(context)
audioRecorder.batchesFolder!!.deleteRecordings();
}
)
}

View File

@ -1,5 +1,7 @@
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
@ -18,7 +20,13 @@ 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
@ -28,8 +36,7 @@ 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.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.core.content.ContextCompat.startActivity
import androidx.navigation.NavController
import app.myzel394.alibi.ui.components.AudioRecorder.organisms.RecordingStatus
import app.myzel394.alibi.ui.components.AudioRecorder.molecules.StartRecording
@ -38,7 +45,6 @@ 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.db.RecordingInformation
import app.myzel394.alibi.helpers.AudioRecorderExporter
import app.myzel394.alibi.helpers.BatchesFolder
import app.myzel394.alibi.ui.effects.rememberSettings
@ -52,6 +58,7 @@ fun AudioRecorderScreen(
navController: NavController,
audioRecorder: AudioRecorderModel,
) {
val snackbarHostState = remember { SnackbarHostState() }
val context = LocalContext.current
val dataStore = context.dataStore
@ -62,10 +69,10 @@ fun AudioRecorderScreen(
settings.audioRecorderSettings.getMimeType()
) {
if (settings.audioRecorderSettings.deleteRecordingsImmediately) {
AudioRecorderExporter.clearAllRecordings(context)
audioRecorder.batchesFolder!!.deleteRecordings()
}
if (!AudioRecorderExporter.hasRecordingsAvailable(context)) {
if (!audioRecorder.batchesFolder!!.hasRecordingsAvailable()) {
scope.launch {
dataStore.updateData {
it.setLastRecording(null)
@ -89,6 +96,29 @@ fun AudioRecorderScreen(
}
}
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 {
isProcessingAudio = true
@ -97,16 +127,39 @@ fun AudioRecorderScreen(
delay(100)
try {
AudioRecorderExporter(
audioRecorder.recorderService?.getRecordingInformation()
?: settings.lastRecording
?: throw Exception("No recording information available"),
).concatenateFiles(
context,
audioRecorder.recorderService!!.batchesFolder
val recording = audioRecorder.recorderService?.getRecordingInformation()
?: settings.lastRecording
?: throw Exception("No recording information available")
val outputFile = audioRecorder.batchesFolder!!.getOutputFileForFFmpeg(
recording.recordingStart,
recording.fileExtension
)
// saveFile(file, file.name)
AudioRecorderExporter(recording).concatenateFiles(
context,
audioRecorder.recorderService!!.batchesFolder,
outputFile,
)
val name = audioRecorder.batchesFolder!!.getName(
recording.recordingStart,
recording.fileExtension,
)
when (audioRecorder.batchesFolder!!.type) {
BatchesFolder.BatchType.INTERNAL -> {
saveFile(
audioRecorder.batchesFolder!!.asInternalGetOutputFile(
recording.recordingStart,
recording.fileExtension,
), name
)
}
BatchesFolder.BatchType.CUSTOM -> {
showSnackbar(audioRecorder.batchesFolder!!.customFolder!!.uri)
}
}
} catch (error: Exception) {
Log.getStackTraceString(error)
} finally {
@ -198,6 +251,21 @@ fun AudioRecorderScreen(
}
)
Scaffold(
snackbarHost = {
SnackbarHost(
hostState = snackbarHostState,
snackbar = {
Snackbar(
snackbarData = it,
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
actionColor = MaterialTheme.colorScheme.onPrimaryContainer,
actionContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
dismissActionContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
)
}
)
},
topBar = {
TopAppBar(
title = {

View File

@ -118,4 +118,6 @@
<string name="ui_settings_option_saveFolder_warning_text">By default, Alibi will save the recording batches into its private, encrypted file storage. You can change this and specify an external, unencrypted folder. This will allow you to access the batches manually. ONLY DO THIS IF YOU KNOW WHAT YOU ARE DOING!</string>
<string name="ui_settings_option_saveFolder_warning_action_confirm">Yes, change folder</string>
<string name="ui_settings_option_saveFolder_action_default_label">Use private, encrypted storage</string>
<string name="ui_audioRecorder_action_save_success">Recording has been saved successfully!</string>
<string name="ui_audioRecorder_action_save_openFolder">Open</string>
</resources>