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( data class AudioRecorderExporter(
val recording: RecordingInformation, 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( suspend fun concatenateFiles(
context: Context, context: Context,
batchesFolder: BatchesFolder, batchesFolder: BatchesFolder,
outputFilePath: String,
forceConcatenation: Boolean = false, forceConcatenation: Boolean = false,
) { ) {
val filePaths = batchesFolder.getBatchesForFFmpeg().joinToString("|") val filePaths = batchesFolder.getBatchesForFFmpeg().joinToString("|")
val outputFile =
batchesFolder.getOutputFileForFFmpeg(recording.recordingStart, recording.fileExtension) if (batchesFolder.checkIfOutputAlreadyExists(
recording.recordingStart,
recording.fileExtension
) && !forceConcatenation
) {
return
}
val command = val command =
"-protocol_whitelist saf,concat,content,file,subfile" + "-protocol_whitelist saf,concat,content,file,subfile" +
@ -44,7 +40,7 @@ data class AudioRecorderExporter(
" -metadata batch_count='${filePaths.length}'" + " -metadata batch_count='${filePaths.length}'" +
" -metadata batch_duration='${recording.intervalDuration}'" + " -metadata batch_duration='${recording.intervalDuration}'" +
" -metadata max_duration='${recording.maxDuration}'" + " -metadata max_duration='${recording.maxDuration}'" +
" $outputFile" " $outputFilePath"
val session = FFmpegKit.execute(command) val session = FFmpegKit.execute(command)
@ -68,34 +64,6 @@ data class AudioRecorderExporter(
companion object { companion object {
fun getFolder(context: Context) = File(context.filesDir, RECORDER_SUBFOLDER_NAME) 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( fun getOutputFileForFFmpeg(
date: LocalDateTime, date: LocalDateTime,
extension: String, extension: String,
): 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 val name = date
.format(DateTimeFormatter.ISO_DATE_TIME) .format(DateTimeFormatter.ISO_DATE_TIME)
.toString() .toString()
@ -79,14 +116,9 @@ data class BatchesFolder(
.replace(".", "_") .replace(".", "_")
return when (type) { return when (type) {
BatchType.INTERNAL -> File(getInternalFolder(), "$name.$extension").absolutePath BatchType.INTERNAL -> File(getInternalFolder(), "$name.$extension").exists()
BatchType.CUSTOM -> FFmpegKitConfig.getSafParameterForWrite( BatchType.CUSTOM ->
context, getCustomDefinedFolder().findFile("${name}.${extension}")?.exists() ?: false
getCustomDefinedFolder().createFile(
"audio/${extension}",
"${name}.${extension}"
)!!.uri
)!!
} }
} }
@ -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) { fun deleteOldRecordings(earliestCounter: Long) {
when (type) { when (type) {
BatchType.INTERNAL -> getInternalFolder().listFiles()?.forEach { BatchType.INTERNAL -> getInternalFolder().listFiles()?.forEach {

View File

@ -1,7 +1,6 @@
package app.myzel394.alibi.ui.components.AudioRecorder.molecules package app.myzel394.alibi.ui.components.AudioRecorder.molecules
import android.Manifest import android.Manifest
import android.net.Uri
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -21,13 +20,11 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon 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.material3.rememberStandardBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
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
import androidx.compose.ui.Alignment 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.semantics.semantics
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp 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.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.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.BIG_PRIMARY_BUTTON_SIZE
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 kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.lastOrNull
import kotlinx.coroutines.launch
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle

View File

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

View File

@ -1,5 +1,7 @@
package app.myzel394.alibi.ui.screens package app.myzel394.alibi.ui.screens
import android.content.Intent
import android.net.Uri
import android.util.Log 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.Column
@ -18,7 +20,13 @@ 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.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold 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.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -28,8 +36,7 @@ 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.compose.ui.unit.dp
import androidx.core.net.toUri import androidx.core.content.ContextCompat.startActivity
import androidx.documentfile.provider.DocumentFile
import androidx.navigation.NavController import androidx.navigation.NavController
import app.myzel394.alibi.ui.components.AudioRecorder.organisms.RecordingStatus import app.myzel394.alibi.ui.components.AudioRecorder.organisms.RecordingStatus
import app.myzel394.alibi.ui.components.AudioRecorder.molecules.StartRecording 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.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.helpers.AudioRecorderExporter import app.myzel394.alibi.helpers.AudioRecorderExporter
import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.helpers.BatchesFolder
import app.myzel394.alibi.ui.effects.rememberSettings import app.myzel394.alibi.ui.effects.rememberSettings
@ -52,6 +58,7 @@ fun AudioRecorderScreen(
navController: NavController, navController: NavController,
audioRecorder: AudioRecorderModel, audioRecorder: AudioRecorderModel,
) { ) {
val snackbarHostState = remember { SnackbarHostState() }
val context = LocalContext.current val context = LocalContext.current
val dataStore = context.dataStore val dataStore = context.dataStore
@ -62,10 +69,10 @@ fun AudioRecorderScreen(
settings.audioRecorderSettings.getMimeType() settings.audioRecorderSettings.getMimeType()
) { ) {
if (settings.audioRecorderSettings.deleteRecordingsImmediately) { if (settings.audioRecorderSettings.deleteRecordingsImmediately) {
AudioRecorderExporter.clearAllRecordings(context) audioRecorder.batchesFolder!!.deleteRecordings()
} }
if (!AudioRecorderExporter.hasRecordingsAvailable(context)) { if (!audioRecorder.batchesFolder!!.hasRecordingsAvailable()) {
scope.launch { scope.launch {
dataStore.updateData { dataStore.updateData {
it.setLastRecording(null) 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() { fun saveRecording() {
scope.launch { scope.launch {
isProcessingAudio = true isProcessingAudio = true
@ -97,16 +127,39 @@ fun AudioRecorderScreen(
delay(100) delay(100)
try { try {
AudioRecorderExporter( val recording = audioRecorder.recorderService?.getRecordingInformation()
audioRecorder.recorderService?.getRecordingInformation() ?: settings.lastRecording
?: settings.lastRecording ?: throw Exception("No recording information available")
?: throw Exception("No recording information available"), val outputFile = audioRecorder.batchesFolder!!.getOutputFileForFFmpeg(
).concatenateFiles( recording.recordingStart,
context, recording.fileExtension
audioRecorder.recorderService!!.batchesFolder
) )
// 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) { } catch (error: Exception) {
Log.getStackTraceString(error) Log.getStackTraceString(error)
} finally { } finally {
@ -198,6 +251,21 @@ fun AudioRecorderScreen(
} }
) )
Scaffold( 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 = { topBar = {
TopAppBar( TopAppBar(
title = { 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_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_warning_action_confirm">Yes, change folder</string>
<string name="ui_settings_option_saveFolder_action_default_label">Use private, encrypted storage</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> </resources>