From 6605f44eecbcd9f408919d3da67c28240b05dbe1 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 30 Oct 2023 12:38:39 +0100 Subject: [PATCH 01/20] feat: Add custom save folder to AppSettings --- .../java/app/myzel394/alibi/db/AppSettings.kt | 29 +++++++++++++++++++ .../alibi/helpers/AudioRecorderExporter.kt | 4 +++ 2 files changed, 33 insertions(+) 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 505f1a7..8c612e7 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -1,8 +1,10 @@ package app.myzel394.alibi.db +import android.content.Context import android.media.MediaRecorder import android.os.Build import app.myzel394.alibi.R +import app.myzel394.alibi.helpers.AudioRecorderExporter import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.ReturnCode import kotlinx.serialization.Serializable @@ -94,6 +96,7 @@ data class AudioRecorderSettings( val encoder: Int? = null, val showAllMicrophones: Boolean = false, val deleteRecordingsImmediately: Boolean = false, + val saveFolder: String? = null, ) { fun getOutputFormat(): Int { if (outputFormat != null) { @@ -161,6 +164,28 @@ data class AudioRecorderSettings( else MediaRecorder.AudioEncoder.AMR_NB + fun getSaveFolder(context: Context): File { + val defaultFolder = AudioRecorderExporter.getFolder(context) + + if (saveFolder == null) { + return defaultFolder + } + + runCatching { + return File(saveFolder!!).apply { + if (!AudioRecorderExporter.canFolderBeUsed(this)) { + throw SecurityException("Can't write to folder") + } + + if (!exists()) { + mkdirs() + } + } + } + + return defaultFolder + } + fun setIntervalDuration(duration: Long): AudioRecorderSettings { if (duration < 10 * 1000L || duration > 60 * 60 * 1000L) { throw Exception("Interval duration must be between 10 seconds and 1 hour") @@ -229,6 +254,10 @@ data class AudioRecorderSettings( return copy(deleteRecordingsImmediately = deleteRecordingsImmediately) } + fun setSaveFolder(saveFolder: String?): AudioRecorderSettings { + return copy(saveFolder = saveFolder) + } + fun isEncoderCompatible(encoder: Int): Boolean { if (outputFormat == null || outputFormat == MediaRecorder.OutputFormat.DEFAULT) { return true diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt index 5c1d54c..c2f2fe8 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -114,5 +114,9 @@ data class AudioRecorderExporter( fun hasRecordingsAvailable(context: Context) = getFolder(context).listFiles()?.isNotEmpty() ?: false + + // Write required for saving the audio files + // Read required for concatenating the audio files + fun canFolderBeUsed(file: File) = file.canRead() && file.canWrite() } } \ No newline at end of file From aa8fd2a37f49dd96aaf86f4b4501244c05f8fc9b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 30 Oct 2023 14:02:02 +0100 Subject: [PATCH 02/20] feat: Add SaveFolderTile --- .../java/app/myzel394/alibi/db/AppSettings.kt | 4 - .../alibi/helpers/AudioRecorderExporter.kt | 4 - .../SettingsScreen/atoms/SaveFolderTile.kt | 196 ++++++++++++++++++ .../alibi/ui/screens/SettingsScreen.kt | 2 + .../java/app/myzel394/alibi/ui/utils/file.kt | 15 ++ app/src/main/res/values/strings.xml | 9 + 6 files changed, 222 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt 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 8c612e7..a86ea23 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -173,10 +173,6 @@ data class AudioRecorderSettings( runCatching { return File(saveFolder!!).apply { - if (!AudioRecorderExporter.canFolderBeUsed(this)) { - throw SecurityException("Can't write to folder") - } - if (!exists()) { mkdirs() } diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt index c2f2fe8..5c1d54c 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -114,9 +114,5 @@ data class AudioRecorderExporter( fun hasRecordingsAvailable(context: Context) = getFolder(context).listFiles()?.isNotEmpty() ?: false - - // Write required for saving the audio files - // Read required for concatenating the audio files - fun canFolderBeUsed(file: File) = file.canRead() && file.canWrite() } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt new file mode 100644 index 0000000..113b549 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt @@ -0,0 +1,196 @@ +package app.myzel394.alibi.ui.components.SettingsScreen.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AudioFile +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material.icons.filled.Lock +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.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.net.toFile +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.ui.components.atoms.SettingsTile +import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog +import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState +import kotlinx.coroutines.launch +import java.io.File + +@Composable +fun SaveFolderTile( + settings: AppSettings, +) { + val scope = rememberCoroutineScope() + val context = LocalContext.current + val dataStore = context.dataStore + + fun updateValue(path: String?) { + scope.launch { + dataStore.updateData { + it.setAudioRecorderSettings( + it.audioRecorderSettings.setSaveFolder(path) + ) + } + } + } + + val selectFolder = rememberFolderSelectorDialog { folder -> + if (folder == null) { + return@rememberFolderSelectorDialog + } + + updateValue(folder.path) + } + + var showWarning by remember { mutableStateOf(false) } + + if (showWarning) { + val title = stringResource(R.string.ui_settings_option_saveFolder_warning_title) + val text = stringResource(R.string.ui_settings_option_saveFolder_warning_text) + + AlertDialog( + icon = { + Icon( + Icons.Default.Warning, + contentDescription = null, + ) + }, + onDismissRequest = { + showWarning = false + }, + title = { + Text(text = title) + }, + text = { + Text(text = text) + }, + confirmButton = { + Button( + onClick = { + showWarning = false + selectFolder() + }, + ) { + Text( + text = stringResource(R.string.ui_settings_option_saveFolder_warning_action_confirm), + ) + } + }, + dismissButton = { + Button( + onClick = { + showWarning = false + }, + colors = ButtonDefaults.textButtonColors(), + ) { + Icon( + Icons.Default.Cancel, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.dialog_close_cancel_label)) + } + } + ) + } + + SettingsTile( + title = stringResource(R.string.ui_settings_option_saveFolder_title), + description = stringResource(R.string.ui_settings_option_saveFolder_explanation), + leading = { + Icon( + Icons.Default.AudioFile, + contentDescription = null, + ) + }, + trailing = { + Button( + onClick = { + showWarning = true + }, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ), + shape = MaterialTheme.shapes.medium, + ) { + Icon( + Icons.Default.Folder, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer( + modifier = Modifier.size(ButtonDefaults.IconSpacing) + ) + Text( + text = stringResource(R.string.ui_settings_option_saveFolder_action_select_label), + ) + } + }, + extra = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (settings.audioRecorderSettings.saveFolder != null) { + Button( + colors = ButtonDefaults.filledTonalButtonColors(), + onClick = { + updateValue(null) + } + ) { + Icon( + Icons.Default.Lock, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer( + modifier = Modifier.size(ButtonDefaults.IconSpacing) + ) + Text( + text = stringResource(R.string.ui_settings_option_saveFolder_action_default_label), + ) + } + } + Text( + text = stringResource( + R.string.form_value_selected, + settings.audioRecorderSettings.saveFolder + ?: stringResource(R.string.ui_settings_option_saveFolder_defaultValue) + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + } + } + ) +} diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt index 0a1b4f9..b4f5247 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt @@ -51,6 +51,7 @@ import app.myzel394.alibi.ui.components.SettingsScreen.atoms.IntervalDurationTil import app.myzel394.alibi.ui.components.SettingsScreen.atoms.MaxDurationTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.OutputFormatTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.SamplingRateTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.SaveFolderTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ShowAllMicrophonesTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ThemeSelector import app.myzel394.alibi.ui.components.atoms.GlobalSwitch @@ -161,6 +162,7 @@ fun SettingsScreen( .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 32.dp) ) + SaveFolderTile(settings = settings) ShowAllMicrophonesTile(settings = settings) BitrateTile(settings = settings) SamplingRateTile(settings = settings) diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt index f90d01b..1dc5580 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt @@ -56,3 +56,18 @@ fun rememberFileSelectorDialog( launcher.launch(arrayOf(mimeType)) } } + +@Composable +fun rememberFolderSelectorDialog( + callback: (Uri?) -> Unit +): (() -> Unit) { + val launcher = + rememberLauncherForActivityResult( + ActivityResultContracts.OpenDocumentTree(), + callback, + ) + + return { + launcher.launch(null) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a1328ef..52b5e83 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,6 +9,7 @@ Please enter a valid number Please enter a number between %s and %s Please enter a number greater than %s + Selected: %s Recorder Shows the current recording status @@ -109,4 +110,12 @@ Become a GitHub Sponsor Delete Recordings Immediately If enabled, Alibi will immediately delete recordings after you have saved the file. + Batches folder + Where Alibi should store the temporary batches of your recordings. + Select + Encrypted Internal Storage + Are you sure you want to change the folder? + 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! + Yes, change folder + Use private, encrypted storage \ No newline at end of file From 542170f189b80604563d8f01bf4dad3d7c39adc9 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 30 Oct 2023 14:20:41 +0100 Subject: [PATCH 03/20] feat: Improve SaveFolderTile folder preview --- .../SettingsScreen/atoms/FolderBreadcrumbs.kt | 48 +++++++++++++++++++ .../SettingsScreen/atoms/SaveFolderTile.kt | 39 ++++++++++----- 2 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/FolderBreadcrumbs.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/FolderBreadcrumbs.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/FolderBreadcrumbs.kt new file mode 100644 index 0000000..6681db6 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/FolderBreadcrumbs.kt @@ -0,0 +1,48 @@ +package app.myzel394.alibi.ui.components.SettingsScreen.atoms + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +@Composable +fun FolderBreadcrumbs( + modifier: Modifier = Modifier, + textStyle: TextStyle? = null, + folders: Iterable, +) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + folders.forEachIndexed { index, folder -> + if (index != 0) { + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null, + ) + } + Text( + text = folder, + modifier = Modifier + .then(modifier), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = textStyle ?: MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt index 113b549..9676ecc 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -156,12 +157,28 @@ fun SaveFolderTile( }, extra = { Column( + modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), ) { if (settings.audioRecorderSettings.saveFolder != null) { + Text( + text = stringResource( + R.string.form_value_selected, + settings + .audioRecorderSettings + .saveFolder + .split(":")[1] + .replace("/", " > ") + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) Button( colors = ButtonDefaults.filledTonalButtonColors(), + contentPadding = ButtonDefaults.ButtonWithIconContentPadding, onClick = { updateValue(null) } @@ -178,18 +195,18 @@ fun SaveFolderTile( text = stringResource(R.string.ui_settings_option_saveFolder_action_default_label), ) } + } else { + Text( + text = stringResource( + R.string.form_value_selected, + stringResource(R.string.ui_settings_option_saveFolder_defaultValue) + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) } - Text( - text = stringResource( - R.string.form_value_selected, - settings.audioRecorderSettings.saveFolder - ?: stringResource(R.string.ui_settings_option_saveFolder_defaultValue) - ), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) } } ) From 47b85e74d2244db22ace171a88d6a2a90da1c23a Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 31 Oct 2023 20:43:19 +0100 Subject: [PATCH 04/20] current stand --- app/build.gradle | 1 + .../alibi/services/AudioRecorderService.kt | 31 +++++++--- .../alibi/services/IntervalRecorderService.kt | 60 ++++++++++++++++--- .../AudioRecorder/molecules/StartRecording.kt | 34 +++++++---- .../SettingsScreen/atoms/SaveFolderTile.kt | 4 +- .../alibi/ui/models/AudioRecorderModel.kt | 6 ++ 6 files changed, 106 insertions(+), 30 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d3c9db3..1d605c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,6 +102,7 @@ dependencies { implementation 'androidx.compose.material3:material3' implementation "androidx.compose.material:material-icons-extended:1.5.1" implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.documentfile:documentfile:1.0.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt index e871138..57b1dbc 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -1,20 +1,19 @@ package app.myzel394.alibi.services -import android.annotation.SuppressLint import android.content.Context import android.media.AudioDeviceCallback import android.media.AudioDeviceInfo import android.media.AudioManager import android.media.MediaRecorder import android.media.MediaRecorder.OnErrorListener +import android.net.Uri import android.os.Build import android.os.Handler import android.os.Looper -import androidx.core.content.ContextCompat.getSystemService +import androidx.documentfile.provider.DocumentFile import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.ui.utils.MicrophoneInfo import java.lang.IllegalStateException -import java.util.concurrent.Executor class AudioRecorderService : IntervalRecorderService() { var amplitudesAmount = 1000 @@ -27,9 +26,6 @@ class AudioRecorderService : IntervalRecorderService() { var onMicrophoneDisconnected: () -> Unit = {} var onMicrophoneReconnected: () -> Unit = {} - val filePath: String - get() = "${outputFolder}/$counter.${settings!!.fileExtension}" - /// Tell Android to use the correct bluetooth microphone, if any selected private fun startAudioDevice() { if (selectedMicrophone == null) { @@ -61,6 +57,26 @@ class AudioRecorderService : IntervalRecorderService() { } else { MediaRecorder() }.apply { + setOutputFormat(settings!!.outputFormat) + + // Setting file path + if (customOutputFolder == null) { + val newFilePath = "${defaultOutputFolder}/$counter.${settings!!.fileExtension}" + + println("newfile path: ${newFilePath}") + + setOutputFile(newFilePath) + } else { + customOutputFolder!!.createFile( + "audio/${settings!!.fileExtension}", + "${counter}.${settings!!.fileExtension}" + )!!.let { + val fileDescriptor = + contentResolver.openFileDescriptor(it.uri, "w")!!.fileDescriptor + setOutputFile(fileDescriptor) + } + } + // Audio Source is kinda strange, here are my experimental findings using a Pixel 7 Pro // and Redmi Buds 3 Pro: // - MIC: Uses the bottom microphone of the phone (17) @@ -68,8 +84,7 @@ class AudioRecorderService : IntervalRecorderService() { // - VOICE_COMMUNICATION: Uses the bottom microphone of the phone (17) // - DEFAULT: Uses the bottom microphone of the phone (17) setAudioSource(MediaRecorder.AudioSource.MIC) - setOutputFile(filePath) - setOutputFormat(settings!!.outputFormat) + setAudioEncoder(settings!!.encoder) setAudioEncodingBitRate(settings!!.bitRate) setAudioSamplingRate(settings!!.samplingRate) diff --git a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt index 88d2bca..e7ac4d0 100644 --- a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt @@ -1,6 +1,8 @@ package app.myzel394.alibi.services import android.media.MediaRecorder +import android.net.Uri +import androidx.documentfile.provider.DocumentFile import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AudioRecorderSettings import app.myzel394.alibi.db.RecordingInformation @@ -10,6 +12,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import org.w3c.dom.DocumentFragment import java.io.File import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService @@ -27,11 +30,15 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { private lateinit var cycleTimer: ScheduledExecutorService - protected val outputFolder: File + protected val defaultOutputFolder: File get() = AudioRecorderExporter.getFolder(this) + var customOutputFolder: DocumentFile? = null + + var onCustomOutputFolderNotAccessible: () -> Unit = {} + fun getRecordingInformation(): RecordingInformation = RecordingInformation( - folderPath = outputFolder.absolutePath, + folderPath = customOutputFolder?.uri?.toString() ?: defaultOutputFolder.absolutePath, recordingStart = recordingStart, maxDuration = settings!!.maxDuration, fileExtension = settings!!.fileExtension, @@ -61,15 +68,29 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { override fun start() { super.start() - outputFolder.mkdirs() - scope.launch { dataStore.data.collectLatest { preferenceSettings -> if (settings == null) { settings = Settings.from(preferenceSettings.audioRecorderSettings) + if (settings!!.folder != null) { + customOutputFolder = DocumentFile.fromTreeUri( + this@IntervalRecorderService, + Uri.parse(settings!!.folder) + ) + + if (!customOutputFolder!!.canRead() || !customOutputFolder!!.canWrite()) { + customOutputFolder = null + onCustomOutputFolderNotAccessible() + } + } + createTimer() } + + if (customOutputFolder == null) { + defaultOutputFolder.mkdirs() + } } } } @@ -90,15 +111,37 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { cycleTimer.shutdown() } + fun clearAllRecordings() { + if (customOutputFolder != null) { + customOutputFolder!!.listFiles().forEach { + it.delete() + } + } else { + defaultOutputFolder.listFiles()?.forEach { + it.delete() + } + } + } + private fun deleteOldRecordings() { val timeMultiplier = settings!!.maxDuration / settings!!.intervalDuration val earliestCounter = counter - timeMultiplier - outputFolder.listFiles()?.forEach { file -> - val fileCounter = file.nameWithoutExtension.toIntOrNull() ?: return + if (customOutputFolder != null) { + customOutputFolder!!.listFiles().forEach { + val fileCounter = it.name?.substringBeforeLast(".")?.toIntOrNull() ?: return@forEach - if (fileCounter < earliestCounter) { - file.delete() + if (fileCounter < earliestCounter) { + it.delete() + } + } + } else { + defaultOutputFolder.listFiles()?.forEach { + val fileCounter = it.nameWithoutExtension.toIntOrNull() ?: return + + if (fileCounter < earliestCounter) { + it.delete() + } } } } @@ -111,6 +154,7 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { val samplingRate: Int, val outputFormat: Int, val encoder: Int, + val folder: String? = null, ) { val fileExtension: String get() = when (outputFormat) { 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 72f2f42..21d9b86 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,6 +1,7 @@ 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 @@ -38,11 +39,13 @@ 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.services.RecorderNotificationHelper import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE import app.myzel394.alibi.ui.components.atoms.PermissionRequester @@ -72,19 +75,26 @@ fun StartRecording( LaunchedEffect(startRecording) { if (startRecording) { startRecording = false - audioRecorder.notificationDetails = appSettings.notificationSettings.let { - if (it == null) - null - else - RecorderNotificationHelper.NotificationDetails.fromNotificationSettings( - context, - it - ) + + audioRecorder.let { recorder -> + recorder.notificationDetails = appSettings.notificationSettings.let { + if (it == null) + null + else + RecorderNotificationHelper.NotificationDetails.fromNotificationSettings( + context, + it + ) + } + recorder.customOutputFolder = appSettings.audioRecorderSettings.saveFolder.let { + if (it == null) + null + else + DocumentFile.fromTreeUri(context, Uri.parse(it)) + } + + recorder.startRecording(context) } - - AudioRecorderExporter.clearAllRecordings(context) - - audioRecorder.startRecording(context) } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt index 9676ecc..ce158d6 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt @@ -1,5 +1,6 @@ package app.myzel394.alibi.ui.components.SettingsScreen.atoms +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -42,7 +43,6 @@ import app.myzel394.alibi.ui.components.atoms.SettingsTile import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import kotlinx.coroutines.launch -import java.io.File @Composable fun SaveFolderTile( @@ -67,7 +67,7 @@ fun SaveFolderTile( return@rememberFolderSelectorDialog } - updateValue(folder.path) + updateValue(folder.toString()) } var showWarning by remember { mutableStateOf(false) } 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 7c9724a..671ca02 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 @@ -9,9 +9,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.content.ContextCompat +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModel import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.enums.RecorderState +import app.myzel394.alibi.helpers.AudioRecorderExporter import app.myzel394.alibi.services.AudioRecorderService import app.myzel394.alibi.services.RecorderNotificationHelper import app.myzel394.alibi.services.RecorderService @@ -45,6 +47,7 @@ class AudioRecorderModel : ViewModel() { var onRecordingSave: () -> Unit = {} var onError: () -> Unit = {} var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null + var customOutputFolder: DocumentFile? = null var microphoneStatus: MicrophoneConnectivityStatus = MicrophoneConnectivityStatus.CONNECTED private set @@ -58,6 +61,8 @@ class AudioRecorderModel : ViewModel() { override fun onServiceConnected(className: ComponentName, service: IBinder) { recorderService = ((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also { recorder -> + recorder.clearAllRecordings() + // Update UI when the service changes recorder.onStateChange = { state -> recorderState = state @@ -81,6 +86,7 @@ class AudioRecorderModel : ViewModel() { recorder.onMicrophoneReconnected = { microphoneStatus = MicrophoneConnectivityStatus.CONNECTED } + recorder.customOutputFolder = customOutputFolder }.also { // Init UI from the service it.startRecording() From 8f8376cd16a7960673f057d4ec3aadd510daf701 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 3 Nov 2023 16:04:08 +0100 Subject: [PATCH 05/20] current stand --- .../alibi/services/AudioRecorderService.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt index 57b1dbc..79450af 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -57,15 +57,9 @@ class AudioRecorderService : IntervalRecorderService() { } else { MediaRecorder() }.apply { - setOutputFormat(settings!!.outputFormat) - // Setting file path + /* if (customOutputFolder == null) { - val newFilePath = "${defaultOutputFolder}/$counter.${settings!!.fileExtension}" - - println("newfile path: ${newFilePath}") - - setOutputFile(newFilePath) } else { customOutputFolder!!.createFile( "audio/${settings!!.fileExtension}", @@ -76,6 +70,15 @@ class AudioRecorderService : IntervalRecorderService() { setOutputFile(fileDescriptor) } } + */ + + val newFilePath = "${defaultOutputFolder}/$counter.${settings!!.fileExtension}" + + println("newfile path: ${newFilePath}") + + setOutputFile(newFilePath) + println("outputformat eta: ${settings!!.outputFormat}") + setOutputFormat(settings!!.outputFormat) // Audio Source is kinda strange, here are my experimental findings using a Pixel 7 Pro // and Redmi Buds 3 Pro: From 6948e11fcae9d373fafb9d3be6180cc1eb3ef49e Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 3 Nov 2023 21:43:23 +0100 Subject: [PATCH 06/20] fix: Properly take persistable uri for open document tree --- app/build.gradle | 2 ++ .../SettingsScreen/atoms/SaveFolderTile.kt | 23 +++++++++++++------ .../java/app/myzel394/alibi/ui/utils/file.kt | 23 +++++++++++++++---- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1d605c8..0a9687f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -129,4 +129,6 @@ dependencies { implementation 'com.maxkeppeler.sheets-compose-dialogs:duration:1.2.0' implementation 'com.maxkeppeler.sheets-compose-dialogs:list:1.2.0' implementation 'com.maxkeppeler.sheets-compose-dialogs:input:1.2.0' + + implementation 'androidx.activity:activity-ktx:1.8.0' } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt index ce158d6..fff9aab 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt @@ -1,12 +1,11 @@ package app.myzel394.alibi.ui.components.SettingsScreen.atoms -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.background +import android.content.Intent +import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons @@ -29,19 +28,15 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.core.net.toFile 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.ui.components.atoms.SettingsTile import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog -import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import kotlinx.coroutines.launch @Composable @@ -53,6 +48,15 @@ fun SaveFolderTile( val dataStore = context.dataStore fun updateValue(path: String?) { + if (settings.audioRecorderSettings.saveFolder != null) { + runCatching { + context.contentResolver.releasePersistableUriPermission( + Uri.parse(settings.audioRecorderSettings.saveFolder), + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } + } + scope.launch { dataStore.updateData { it.setAudioRecorderSettings( @@ -67,6 +71,11 @@ fun SaveFolderTile( return@rememberFolderSelectorDialog } + context.contentResolver.takePersistableUriPermission( + folder, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + updateValue(folder.toString()) } diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt index 1dc5580..c6c2701 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt @@ -1,9 +1,12 @@ package app.myzel394.alibi.ui.utils +import android.app.Activity +import android.content.Intent import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -63,11 +66,23 @@ fun rememberFolderSelectorDialog( ): (() -> Unit) { val launcher = rememberLauncherForActivityResult( - ActivityResultContracts.OpenDocumentTree(), - callback, - ) + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + val uri = it.data?.data + + callback(uri) + } + } return { - launcher.launch(null) + launcher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { + addFlags( + Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + ) + }) } } From d69bc7f4b175c2f47c49c729ee1b9e23f98c874b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 5 Nov 2023 23:04:27 +0100 Subject: [PATCH 07/20] current stand: Added symlinks --- .../alibi/helpers/AudioRecorderExporter.kt | 60 +++++++++++++------ .../alibi/services/AudioRecorderService.kt | 15 ++--- .../alibi/ui/screens/AudioRecorderScreen.kt | 12 +++- 3 files changed, 57 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt index 5c1d54c..8ef3d4e 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -1,7 +1,11 @@ package app.myzel394.alibi.helpers import android.content.Context +import android.net.Uri +import android.system.Os import android.util.Log +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.ui.RECORDER_SUBFOLDER_NAME import com.arthenica.ffmpegkit.FFmpegKit @@ -12,16 +16,12 @@ import java.time.format.DateTimeFormatter data class AudioRecorderExporter( val recording: RecordingInformation, ) { - val filePaths: List - get() = - File(recording.folderPath).listFiles()?.filter { - val name = it.nameWithoutExtension + private fun getFilePaths(context: Context): List = + getFolder(context).listFiles()?.filter { + val name = it.nameWithoutExtension - name.toIntOrNull() != null - }?.toList() ?: emptyList() - - val hasRecordingAvailable: Boolean - get() = filePaths.isNotEmpty() + name.toIntOrNull() != null + }?.toList() ?: emptyList() private fun stripConcatenatedFileToExactDuration( outputFile: File @@ -50,8 +50,14 @@ data class AudioRecorderExporter( } } - suspend fun concatenateFiles(forceConcatenation: Boolean = false): File { - val paths = filePaths.joinToString("|") + suspend fun concatenateFiles( + context: Context, + forceConcatenation: Boolean = false, + ): File { + val filePaths = getFilePaths(context) + val paths = filePaths.joinToString("|") { + it.path + } val fileName = recording.recordingStart .format(DateTimeFormatter.ISO_DATE_TIME) .toString() @@ -97,14 +103,6 @@ data class AudioRecorderExporter( return outputFile } - suspend fun cleanupFiles() { - filePaths.forEach { - runCatching { - it.delete() - } - } - } - companion object { fun getFolder(context: Context) = File(context.filesDir, RECORDER_SUBFOLDER_NAME) @@ -114,5 +112,29 @@ data class AudioRecorderExporter( 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 + } + + println( + "symlinking ${folder.uri}/${it.name} to ${destinationFolder.absolutePath}/${it.name}" + ) + + Os.symlink( + "${folder.uri}/${it.name}", + "${destinationFolder.absolutePath}/${it.name}", + ) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt index 79450af..d13e4b9 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -58,8 +58,10 @@ class AudioRecorderService : IntervalRecorderService() { MediaRecorder() }.apply { // Setting file path - /* if (customOutputFolder == null) { + val newFilePath = "${defaultOutputFolder}/$counter.${settings!!.fileExtension}" + + setOutputFile(newFilePath) } else { customOutputFolder!!.createFile( "audio/${settings!!.fileExtension}", @@ -70,15 +72,6 @@ class AudioRecorderService : IntervalRecorderService() { setOutputFile(fileDescriptor) } } - */ - - val newFilePath = "${defaultOutputFolder}/$counter.${settings!!.fileExtension}" - - println("newfile path: ${newFilePath}") - - setOutputFile(newFilePath) - println("outputformat eta: ${settings!!.outputFormat}") - setOutputFormat(settings!!.outputFormat) // Audio Source is kinda strange, here are my experimental findings using a Pixel 7 Pro // and Redmi Buds 3 Pro: @@ -87,6 +80,8 @@ class AudioRecorderService : IntervalRecorderService() { // - VOICE_COMMUNICATION: Uses the bottom microphone of the phone (17) // - DEFAULT: Uses the bottom microphone of the phone (17) setAudioSource(MediaRecorder.AudioSource.MIC) + println("outputformat eta: ${settings!!.outputFormat}") + setOutputFormat(settings!!.outputFormat) setAudioEncoder(settings!!.encoder) setAudioEncodingBitRate(settings!!.bitRate) diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt index ce3cff6..54a370c 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt @@ -28,6 +28,8 @@ 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.navigation.NavController import app.myzel394.alibi.ui.components.AudioRecorder.organisms.RecordingStatus import app.myzel394.alibi.ui.components.AudioRecorder.molecules.StartRecording @@ -94,11 +96,19 @@ fun AudioRecorderScreen( delay(100) try { + if (settings.audioRecorderSettings.saveFolder != null) { + AudioRecorderExporter.linkBatches( + context, + settings.audioRecorderSettings.saveFolder.toUri(), + AudioRecorderExporter.getFolder(context), + ) + } + val file = AudioRecorderExporter( audioRecorder.recorderService?.getRecordingInformation() ?: settings.lastRecording ?: throw Exception("No recording information available"), - ).concatenateFiles() + ).concatenateFiles(context) saveFile(file, file.name) } catch (error: Exception) { From e237a5c99ef017c6754551ed36539c3af1dcbc55 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:48:45 +0100 Subject: [PATCH 08/20] current stand: Debugging commands --- .../alibi/helpers/AudioRecorderExporter.kt | 14 +++++++++++++- .../alibi/ui/screens/AudioRecorderScreen.kt | 8 +++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt index 8ef3d4e..9662199 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -4,11 +4,13 @@ import android.content.Context import android.net.Uri import android.system.Os import android.util.Log +import androidx.core.content.ContentProviderCompat.requireContext import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.ui.RECORDER_SUBFOLDER_NAME import com.arthenica.ffmpegkit.FFmpegKit +import com.arthenica.ffmpegkit.FFmpegKitConfig import com.arthenica.ffmpegkit.ReturnCode import java.io.File import java.time.format.DateTimeFormatter @@ -52,12 +54,17 @@ data class AudioRecorderExporter( suspend fun concatenateFiles( context: Context, + uri: Uri, forceConcatenation: Boolean = false, ): File { val filePaths = getFilePaths(context) val paths = filePaths.joinToString("|") { it.path } + val filePath = FFmpegKitConfig.getSafParameter(context, uri, "rw") + println("!!!!!!!!!!!!!!!!!!1") + println(getFolder(context).listFiles()?.map { it.name }) + println(filePath) val fileName = recording.recordingStart .format(DateTimeFormatter.ISO_DATE_TIME) .toString() @@ -69,7 +76,8 @@ data class AudioRecorderExporter( return outputFile } - val command = "-i 'concat:$paths' -y" + + val command = "-protocol_whitelist saf,concat,content,file,subfile " + + "-i 'concat:${filePath}' -y" + " -acodec copy" + " -metadata title='$fileName' " + " -metadata date='${recording.recordingStart.format(DateTimeFormatter.ISO_DATE_TIME)}'" + @@ -78,6 +86,10 @@ data class AudioRecorderExporter( " -metadata max_duration='${recording.maxDuration}'" + " $outputFile" + println("--------------------") + println(command) + println(outputFile) + val session = FFmpegKit.execute(command) if (!ReturnCode.isSuccess(session.returnCode)) { diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt index 54a370c..a8b90c1 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt @@ -108,7 +108,13 @@ fun AudioRecorderScreen( audioRecorder.recorderService?.getRecordingInformation() ?: settings.lastRecording ?: throw Exception("No recording information available"), - ).concatenateFiles(context) + ).concatenateFiles( + context, + DocumentFile.fromTreeUri( + context, + settings.audioRecorderSettings.saveFolder!!.toUri(), + )!!.findFile("1.aac")!!.uri, + ) saveFile(file, file.name) } catch (error: Exception) { From e94bfded6c448815420aaf335676feca98e010be Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 17 Nov 2023 17:40:33 +0100 Subject: [PATCH 09/20] current stand --- .../alibi/helpers/AudioRecorderExporter.kt | 16 ++++++---------- .../alibi/services/AudioRecorderService.kt | 15 ++++++++------- .../alibi/ui/screens/AudioRecorderScreen.kt | 14 +++++--------- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt index 9662199..760c415 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -55,8 +55,9 @@ data class AudioRecorderExporter( suspend fun concatenateFiles( context: Context, uri: Uri, + folder: DocumentFile, forceConcatenation: Boolean = false, - ): File { + ) { val filePaths = getFilePaths(context) val paths = filePaths.joinToString("|") { it.path @@ -70,11 +71,10 @@ data class AudioRecorderExporter( .toString() .replace(":", "-") .replace(".", "_") - val outputFile = File("${recording.folderPath}/$fileName.${recording.fileExtension}") - - if (outputFile.exists() && !forceConcatenation) { - return outputFile - } + val outputFile = FFmpegKitConfig.getSafParameterForWrite( + context, + (folder.uri.path + "/$fileName.aac").toUri() + ) val command = "-protocol_whitelist saf,concat,content,file,subfile " + "-i 'concat:${filePath}' -y" + @@ -108,11 +108,7 @@ data class AudioRecorderExporter( val minRequiredForPossibleInExactMaxDuration = recording.maxDuration / recording.intervalDuration - if (recording.forceExactMaxDuration && filePaths.size > minRequiredForPossibleInExactMaxDuration) { - stripConcatenatedFileToExactDuration(outputFile) - } - return outputFile } companion object { diff --git a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt index d13e4b9..ec415e9 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -57,6 +57,14 @@ class AudioRecorderService : IntervalRecorderService() { } else { MediaRecorder() }.apply { + // Audio Source is kinda strange, here are my experimental findings using a Pixel 7 Pro + // and Redmi Buds 3 Pro: + // - MIC: Uses the bottom microphone of the phone (17) + // - CAMCORDER: Uses the top microphone of the phone (2) + // - VOICE_COMMUNICATION: Uses the bottom microphone of the phone (17) + // - DEFAULT: Uses the bottom microphone of the phone (17) + setAudioSource(MediaRecorder.AudioSource.MIC) + // Setting file path if (customOutputFolder == null) { val newFilePath = "${defaultOutputFolder}/$counter.${settings!!.fileExtension}" @@ -73,13 +81,6 @@ class AudioRecorderService : IntervalRecorderService() { } } - // Audio Source is kinda strange, here are my experimental findings using a Pixel 7 Pro - // and Redmi Buds 3 Pro: - // - MIC: Uses the bottom microphone of the phone (17) - // - CAMCORDER: Uses the top microphone of the phone (2) - // - VOICE_COMMUNICATION: Uses the bottom microphone of the phone (17) - // - DEFAULT: Uses the bottom microphone of the phone (17) - setAudioSource(MediaRecorder.AudioSource.MIC) println("outputformat eta: ${settings!!.outputFormat}") setOutputFormat(settings!!.outputFormat) diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt index a8b90c1..8658f5d 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt @@ -96,14 +96,6 @@ fun AudioRecorderScreen( delay(100) try { - if (settings.audioRecorderSettings.saveFolder != null) { - AudioRecorderExporter.linkBatches( - context, - settings.audioRecorderSettings.saveFolder.toUri(), - AudioRecorderExporter.getFolder(context), - ) - } - val file = AudioRecorderExporter( audioRecorder.recorderService?.getRecordingInformation() ?: settings.lastRecording @@ -114,9 +106,13 @@ fun AudioRecorderScreen( context, settings.audioRecorderSettings.saveFolder!!.toUri(), )!!.findFile("1.aac")!!.uri, + DocumentFile.fromTreeUri( + context, + settings.audioRecorderSettings.saveFolder!!.toUri(), + )!! ) - saveFile(file, file.name) + //saveFile(file, file.name) } catch (error: Exception) { Log.getStackTraceString(error) } finally { From 7722127796d2204d6117c799a7f69c9ca47ce16a Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 17 Nov 2023 21:48:19 +0100 Subject: [PATCH 10/20] chore: Cleanup --- .../alibi/helpers/AudioRecorderExporter.kt | 18 ++++-------------- .../alibi/services/AudioRecorderService.kt | 1 - 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt index 760c415..56495c7 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -63,9 +63,6 @@ data class AudioRecorderExporter( it.path } val filePath = FFmpegKitConfig.getSafParameter(context, uri, "rw") - println("!!!!!!!!!!!!!!!!!!1") - println(getFolder(context).listFiles()?.map { it.name }) - println(filePath) val fileName = recording.recordingStart .format(DateTimeFormatter.ISO_DATE_TIME) .toString() @@ -73,23 +70,19 @@ data class AudioRecorderExporter( .replace(".", "_") val outputFile = FFmpegKitConfig.getSafParameterForWrite( context, - (folder.uri.path + "/$fileName.aac").toUri() + folder.createFile("audio/aac", "${fileName}.aac")!!.uri, ) - val command = "-protocol_whitelist saf,concat,content,file,subfile " + - "-i 'concat:${filePath}' -y" + + val command = "-protocol_whitelist saf,concat,content,file,subfile" + + " -i 'concat:${filePath}' -y" + " -acodec copy" + - " -metadata title='$fileName' " + + " -metadata title='$fileName'" + " -metadata date='${recording.recordingStart.format(DateTimeFormatter.ISO_DATE_TIME)}'" + " -metadata batch_count='${filePaths.size}'" + " -metadata batch_duration='${recording.intervalDuration}'" + " -metadata max_duration='${recording.maxDuration}'" + " $outputFile" - println("--------------------") - println(command) - println(outputFile) - val session = FFmpegKit.execute(command) if (!ReturnCode.isSuccess(session.returnCode)) { @@ -134,9 +127,6 @@ data class AudioRecorderExporter( return@forEach } - println( - "symlinking ${folder.uri}/${it.name} to ${destinationFolder.absolutePath}/${it.name}" - ) Os.symlink( "${folder.uri}/${it.name}", diff --git a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt index ec415e9..54bb572 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -81,7 +81,6 @@ class AudioRecorderService : IntervalRecorderService() { } } - println("outputformat eta: ${settings!!.outputFormat}") setOutputFormat(settings!!.outputFormat) setAudioEncoder(settings!!.encoder) From 6adff096d2d9d8616a6041f009d379616006d570 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 18 Nov 2023 18:15:10 +0100 Subject: [PATCH 11/20] feat: Adding BatchesFolder --- .../alibi/helpers/AudioRecorderExporter.kt | 95 ++++------ .../myzel394/alibi/helpers/BatchesFolder.kt | 164 ++++++++++++++++++ .../alibi/services/AudioRecorderService.kt | 24 +-- .../alibi/services/IntervalRecorderService.kt | 66 ++----- .../AudioRecorder/molecules/StartRecording.kt | 17 +- .../alibi/ui/models/AudioRecorderModel.kt | 7 +- .../alibi/ui/screens/AudioRecorderScreen.kt | 16 +- 7 files changed, 237 insertions(+), 152 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt index 56495c7..42c5af0 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -4,8 +4,6 @@ import android.content.Context import android.net.Uri import android.system.Os import android.util.Log -import androidx.core.content.ContentProviderCompat.requireContext -import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.ui.RECORDER_SUBFOLDER_NAME @@ -18,70 +16,35 @@ import java.time.format.DateTimeFormatter data class AudioRecorderExporter( val recording: RecordingInformation, ) { - private fun getFilePaths(context: Context): List = - getFolder(context).listFiles()?.filter { - val name = it.nameWithoutExtension + private fun getInternalFilePaths(context: Context): List = + getFolder(context) + .listFiles() + ?.filter { + val name = it.nameWithoutExtension - name.toIntOrNull() != null - }?.toList() ?: emptyList() - - private fun stripConcatenatedFileToExactDuration( - outputFile: File - ) { - // Move the concatenated file to a temporary file - val rawFile = - File("${recording.folderPath}/${outputFile.nameWithoutExtension}-raw.${recording.fileExtension}") - outputFile.renameTo(rawFile) - - val command = "-sseof ${recording.maxDuration / -1000} -i $rawFile -y $outputFile" - - val session = FFmpegKit.execute(command) - - if (!ReturnCode.isSuccess(session.returnCode)) { - Log.d( - "Audio Concatenation", - String.format( - "Command failed with state %s and rc %s.%s", - session.state, - session.returnCode, - session.failStackTrace, - ) - ) - - throw Exception("Failed to strip concatenated audio") - } - } + name.toIntOrNull() != null + } + ?.toList() + ?: emptyList() suspend fun concatenateFiles( context: Context, - uri: Uri, - folder: DocumentFile, + batchesFolder: BatchesFolder, forceConcatenation: Boolean = false, ) { - val filePaths = getFilePaths(context) - val paths = filePaths.joinToString("|") { - it.path - } - val filePath = FFmpegKitConfig.getSafParameter(context, uri, "rw") - val fileName = recording.recordingStart - .format(DateTimeFormatter.ISO_DATE_TIME) - .toString() - .replace(":", "-") - .replace(".", "_") - val outputFile = FFmpegKitConfig.getSafParameterForWrite( - context, - folder.createFile("audio/aac", "${fileName}.aac")!!.uri, - ) + val filePaths = batchesFolder.getBatchesForFFmpeg().joinToString("|") + val outputFile = + batchesFolder.getOutputFileForFFmpeg(recording.recordingStart, recording.fileExtension) - val command = "-protocol_whitelist saf,concat,content,file,subfile" + - " -i 'concat:${filePath}' -y" + - " -acodec copy" + - " -metadata title='$fileName'" + - " -metadata date='${recording.recordingStart.format(DateTimeFormatter.ISO_DATE_TIME)}'" + - " -metadata batch_count='${filePaths.size}'" + - " -metadata batch_duration='${recording.intervalDuration}'" + - " -metadata max_duration='${recording.maxDuration}'" + - " $outputFile" + val command = + "-protocol_whitelist saf,concat,content,file,subfile" + + " -i 'concat:${filePaths}' -y" + + " -acodec copy" + + " -metadata date='${recording.recordingStart.format(DateTimeFormatter.ISO_DATE_TIME)}'" + + " -metadata batch_count='${filePaths.length}'" + + " -metadata batch_duration='${recording.intervalDuration}'" + + " -metadata max_duration='${recording.maxDuration}'" + + " $outputFile" val session = FFmpegKit.execute(command) @@ -101,7 +64,6 @@ data class AudioRecorderExporter( val minRequiredForPossibleInExactMaxDuration = recording.maxDuration / recording.intervalDuration - } companion object { @@ -115,10 +77,11 @@ data class AudioRecorderExporter( getFolder(context).listFiles()?.isNotEmpty() ?: false fun linkBatches(context: Context, batchesFolder: Uri, destinationFolder: File) { - val folder = DocumentFile.fromTreeUri( - context, - batchesFolder, - )!! + val folder = + DocumentFile.fromTreeUri( + context, + batchesFolder, + )!! destinationFolder.mkdirs() @@ -127,7 +90,6 @@ data class AudioRecorderExporter( return@forEach } - Os.symlink( "${folder.uri}/${it.name}", "${destinationFolder.absolutePath}/${it.name}", @@ -135,4 +97,5 @@ data class AudioRecorderExporter( } } } -} \ No newline at end of file +} + diff --git a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt new file mode 100644 index 0000000..f76d5d5 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -0,0 +1,164 @@ +package app.myzel394.alibi.helpers + +import android.content.Context +import androidx.documentfile.provider.DocumentFile +import app.myzel394.alibi.ui.RECORDER_SUBFOLDER_NAME +import java.io.File +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import com.arthenica.ffmpegkit.FFmpegKitConfig +import android.net.Uri +import android.os.ParcelFileDescriptor +import java.io.FileDescriptor + +data class BatchesFolder( + val context: Context, + val type: BatchType, + val customFolder: DocumentFile? = null, + val subfolderName: String = ".recordings", +) { + private var customFileFileDescriptor: ParcelFileDescriptor? = null + + fun initFolders() { + when (type) { + BatchType.INTERNAL -> getFolder(context).mkdirs() + BatchType.CUSTOM -> customFolder?.createDirectory(subfolderName) + } + } + + fun cleanup() { + customFileFileDescriptor?.close() + } + + private fun getInternalFolder(): File { + return getFolder(context) + } + + private fun getCustomDefinedFolder(): DocumentFile { + return customFolder!!.findFile(subfolderName)!! + } + + fun getBatchesForFFmpeg(): List { + return when (type) { + BatchType.INTERNAL -> + (getInternalFolder() + .listFiles() + ?.filter { + it.nameWithoutExtension.toIntOrNull() != null + } + ?.toList() + ?: emptyList()) + .map { it.absolutePath } + + BatchType.CUSTOM -> getCustomDefinedFolder() + .listFiles() + .filter { + it.name?.substringBeforeLast(".")?.toIntOrNull() != null + } + .map { + FFmpegKitConfig.getSafParameterForRead( + context, + customFolder!!.findFile(it.name!!)!!.uri + )!! + } + } + } + + fun getOutputFileForFFmpeg( + date: LocalDateTime, + extension: String, + ): String { + val name = date + .format(DateTimeFormatter.ISO_DATE_TIME) + .toString() + .replace(":", "-") + .replace(".", "_") + + return when (type) { + BatchType.INTERNAL -> File(getInternalFolder(), "$name.$extension").absolutePath + BatchType.CUSTOM -> FFmpegKitConfig.getSafParameterForWrite( + context, + customFolder!!.createFile("audio/${extension}", "${name}.${extension}")!!.uri + )!! + } + } + + fun exportFolderForSettings(): String { + return when (type) { + BatchType.INTERNAL -> "_'internal" + BatchType.CUSTOM -> customFolder!!.uri.toString() + } + } + + fun deleteRecordings() { + when (type) { + BatchType.INTERNAL -> getInternalFolder().deleteRecursively() + BatchType.CUSTOM -> getCustomDefinedFolder().delete() + } + } + + fun deleteOldRecordings(earliestCounter: Long) { + when (type) { + BatchType.INTERNAL -> getInternalFolder().listFiles()?.forEach { + val fileCounter = it.nameWithoutExtension.toIntOrNull() ?: return@forEach + + if (fileCounter < earliestCounter) { + it.delete() + } + } + + BatchType.CUSTOM -> getCustomDefinedFolder().listFiles().forEach { + val fileCounter = it.name?.substringBeforeLast(".")?.toIntOrNull() ?: return@forEach + + if (fileCounter < earliestCounter) { + it.delete() + } + } + } + } + + fun checkIfFolderIsAccessible(): Boolean { + return when (type) { + BatchType.INTERNAL -> true + BatchType.CUSTOM -> customFolder!!.canWrite() && customFolder.canRead() + } + } + + fun asInternalGetOutputPath(counter: Long, fileExtension: String): String { + return getInternalFolder().absolutePath + "/$counter.$fileExtension" + } + + fun asCustomGetFileDescriptor( + counter: Long, + fileExtension: String, + ): FileDescriptor { + val file = customFolder!!.createFile("audio/$fileExtension", "$counter.$fileExtension")!! + + customFileFileDescriptor = context.contentResolver.openFileDescriptor(file.uri, "w")!! + + return customFileFileDescriptor!!.fileDescriptor + } + + enum class BatchType { + INTERNAL, + CUSTOM, + } + + companion object { + fun viaInternalFolder(context: Context): BatchesFolder { + return BatchesFolder(context, BatchType.INTERNAL) + } + + fun viaCustomFolder(context: Context, folder: DocumentFile): BatchesFolder { + return BatchesFolder(context, BatchType.CUSTOM, folder) + } + + fun getFolder(context: Context) = File(context.filesDir, RECORDER_SUBFOLDER_NAME) + + fun importFromFolder(folder: String, context: Context): BatchesFolder = when (folder) { + "_'internal" -> viaInternalFolder(context) + else -> viaCustomFolder(context, DocumentFile.fromTreeUri(context, Uri.parse(folder))!!) + } + } +} + diff --git a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt index 54bb572..4c1cf1d 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -12,6 +12,7 @@ import android.os.Handler import android.os.Looper import androidx.documentfile.provider.DocumentFile import app.myzel394.alibi.enums.RecorderState +import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.ui.utils.MicrophoneInfo import java.lang.IllegalStateException @@ -65,19 +66,17 @@ class AudioRecorderService : IntervalRecorderService() { // - DEFAULT: Uses the bottom microphone of the phone (17) setAudioSource(MediaRecorder.AudioSource.MIC) - // Setting file path - if (customOutputFolder == null) { - val newFilePath = "${defaultOutputFolder}/$counter.${settings!!.fileExtension}" + when (batchesFolder.type) { + BatchesFolder.BatchType.INTERNAL -> { + setOutputFile( + batchesFolder.asInternalGetOutputPath(counter, settings!!.fileExtension) + ) + } - setOutputFile(newFilePath) - } else { - customOutputFolder!!.createFile( - "audio/${settings!!.fileExtension}", - "${counter}.${settings!!.fileExtension}" - )!!.let { - val fileDescriptor = - contentResolver.openFileDescriptor(it.uri, "w")!!.fileDescriptor - setOutputFile(fileDescriptor) + BatchesFolder.BatchType.CUSTOM -> { + setOutputFile( + batchesFolder.asCustomGetFileDescriptor(counter, settings!!.fileExtension) + ) } } @@ -99,6 +98,7 @@ class AudioRecorderService : IntervalRecorderService() { it.release() } clearAudioDevice() + batchesFolder.cleanup() } } diff --git a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt index e7ac4d0..32bbac0 100644 --- a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt @@ -7,6 +7,7 @@ import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AudioRecorderSettings import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.helpers.AudioRecorderExporter +import app.myzel394.alibi.helpers.BatchesFolder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -22,7 +23,7 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { private var job = SupervisorJob() private var scope = CoroutineScope(Dispatchers.IO + job) - protected var counter = 0 + protected var counter = 0L private set var settings: Settings? = null @@ -33,12 +34,12 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { protected val defaultOutputFolder: File get() = AudioRecorderExporter.getFolder(this) - var customOutputFolder: DocumentFile? = null + var batchesFolder: BatchesFolder = BatchesFolder.viaInternalFolder(this) var onCustomOutputFolderNotAccessible: () -> Unit = {} fun getRecordingInformation(): RecordingInformation = RecordingInformation( - folderPath = customOutputFolder?.uri?.toString() ?: defaultOutputFolder.absolutePath, + folderPath = batchesFolder.exportFolderForSettings(), recordingStart = recordingStart, maxDuration = settings!!.maxDuration, fileExtension = settings!!.fileExtension, @@ -68,31 +69,14 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { override fun start() { super.start() - scope.launch { - dataStore.data.collectLatest { preferenceSettings -> - if (settings == null) { - settings = Settings.from(preferenceSettings.audioRecorderSettings) - - if (settings!!.folder != null) { - customOutputFolder = DocumentFile.fromTreeUri( - this@IntervalRecorderService, - Uri.parse(settings!!.folder) - ) - - if (!customOutputFolder!!.canRead() || !customOutputFolder!!.canWrite()) { - customOutputFolder = null - onCustomOutputFolderNotAccessible() - } - } - - createTimer() - } - - if (customOutputFolder == null) { - defaultOutputFolder.mkdirs() - } - } + if (!batchesFolder.checkIfFolderIsAccessible()) { + batchesFolder = + BatchesFolder.viaInternalFolder(this@IntervalRecorderService) + onCustomOutputFolderNotAccessible() } + batchesFolder.initFolders() + + createTimer() } override fun pause() { @@ -112,38 +96,14 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { } fun clearAllRecordings() { - if (customOutputFolder != null) { - customOutputFolder!!.listFiles().forEach { - it.delete() - } - } else { - defaultOutputFolder.listFiles()?.forEach { - it.delete() - } - } + batchesFolder.deleteRecordings() } private fun deleteOldRecordings() { val timeMultiplier = settings!!.maxDuration / settings!!.intervalDuration val earliestCounter = counter - timeMultiplier - if (customOutputFolder != null) { - customOutputFolder!!.listFiles().forEach { - val fileCounter = it.name?.substringBeforeLast(".")?.toIntOrNull() ?: return@forEach - - if (fileCounter < earliestCounter) { - it.delete() - } - } - } else { - defaultOutputFolder.listFiles()?.forEach { - val fileCounter = it.nameWithoutExtension.toIntOrNull() ?: return - - if (fileCounter < earliestCounter) { - it.delete() - } - } - } + batchesFolder.deleteOldRecordings(earliestCounter) } data class Settings( 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 21d9b86..5672c91 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 @@ -46,6 +46,7 @@ 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 @@ -86,12 +87,16 @@ fun StartRecording( it ) } - recorder.customOutputFolder = appSettings.audioRecorderSettings.saveFolder.let { - if (it == null) - null - else - DocumentFile.fromTreeUri(context, Uri.parse(it)) - } + recorder.batchesFolder = if (appSettings.audioRecorderSettings.saveFolder == null) + BatchesFolder.viaInternalFolder(context) + else + BatchesFolder.viaCustomFolder( + context, + DocumentFile.fromTreeUri( + context, + Uri.parse(appSettings.audioRecorderSettings.saveFolder) + )!! + ) recorder.startRecording(context) } 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 671ca02..a1f1648 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 @@ -14,6 +14,7 @@ import androidx.lifecycle.ViewModel import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.helpers.AudioRecorderExporter +import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.services.AudioRecorderService import app.myzel394.alibi.services.RecorderNotificationHelper import app.myzel394.alibi.services.RecorderService @@ -47,7 +48,7 @@ class AudioRecorderModel : ViewModel() { var onRecordingSave: () -> Unit = {} var onError: () -> Unit = {} var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null - var customOutputFolder: DocumentFile? = null + var batchesFolder: BatchesFolder? = null var microphoneStatus: MicrophoneConnectivityStatus = MicrophoneConnectivityStatus.CONNECTED private set @@ -61,8 +62,6 @@ class AudioRecorderModel : ViewModel() { override fun onServiceConnected(className: ComponentName, service: IBinder) { recorderService = ((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also { recorder -> - recorder.clearAllRecordings() - // Update UI when the service changes recorder.onStateChange = { state -> recorderState = state @@ -86,7 +85,7 @@ class AudioRecorderModel : ViewModel() { recorder.onMicrophoneReconnected = { microphoneStatus = MicrophoneConnectivityStatus.CONNECTED } - recorder.customOutputFolder = customOutputFolder + recorder.batchesFolder = batchesFolder ?: recorder.batchesFolder }.also { // Init UI from the service it.startRecording() diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt index 8658f5d..ca39684 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt @@ -40,6 +40,7 @@ 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 import app.myzel394.alibi.ui.models.AudioRecorderModel import kotlinx.coroutines.delay @@ -96,23 +97,16 @@ fun AudioRecorderScreen( delay(100) try { - val file = AudioRecorderExporter( + AudioRecorderExporter( audioRecorder.recorderService?.getRecordingInformation() ?: settings.lastRecording ?: throw Exception("No recording information available"), ).concatenateFiles( context, - DocumentFile.fromTreeUri( - context, - settings.audioRecorderSettings.saveFolder!!.toUri(), - )!!.findFile("1.aac")!!.uri, - DocumentFile.fromTreeUri( - context, - settings.audioRecorderSettings.saveFolder!!.toUri(), - )!! + audioRecorder.recorderService!!.batchesFolder ) - //saveFile(file, file.name) + // saveFile(file, file.name) } catch (error: Exception) { Log.getStackTraceString(error) } finally { @@ -241,4 +235,4 @@ fun AudioRecorderScreen( ) } } -} \ No newline at end of file +} From 79ba18630c05655854c3b5e5c0ba7ba7d0dce844 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 18 Nov 2023 19:27:54 +0100 Subject: [PATCH 12/20] fix: Fixing audio recorder --- .../myzel394/alibi/helpers/BatchesFolder.kt | 15 +++++--- .../alibi/services/AudioRecorderService.kt | 12 +++---- .../alibi/services/IntervalRecorderService.kt | 19 +++++----- .../AudioRecorder/molecules/StartRecording.kt | 24 +------------ .../alibi/ui/models/AudioRecorderModel.kt | 35 +++++++++++++++++-- 5 files changed, 59 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt index f76d5d5..f461a65 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -22,7 +22,11 @@ data class BatchesFolder( fun initFolders() { when (type) { BatchType.INTERNAL -> getFolder(context).mkdirs() - BatchType.CUSTOM -> customFolder?.createDirectory(subfolderName) + BatchType.CUSTOM -> { + if (customFolder!!.findFile(subfolderName) == null) { + customFolder.createDirectory(subfolderName) + } + } } } @@ -93,7 +97,9 @@ data class BatchesFolder( fun deleteRecordings() { when (type) { BatchType.INTERNAL -> getInternalFolder().deleteRecursively() - BatchType.CUSTOM -> getCustomDefinedFolder().delete() + BatchType.CUSTOM -> getCustomDefinedFolder().listFiles().forEach { + it.delete() + } } } @@ -120,7 +126,7 @@ data class BatchesFolder( fun checkIfFolderIsAccessible(): Boolean { return when (type) { BatchType.INTERNAL -> true - BatchType.CUSTOM -> customFolder!!.canWrite() && customFolder.canRead() + BatchType.CUSTOM -> getCustomDefinedFolder().canWrite() && getCustomDefinedFolder().canRead() } } @@ -132,7 +138,8 @@ data class BatchesFolder( counter: Long, fileExtension: String, ): FileDescriptor { - val file = customFolder!!.createFile("audio/$fileExtension", "$counter.$fileExtension")!! + val file = + getCustomDefinedFolder().createFile("audio/$fileExtension", "$counter.$fileExtension")!! customFileFileDescriptor = context.contentResolver.openFileDescriptor(file.uri, "w")!! diff --git a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt index 4c1cf1d..79d04fa 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -69,22 +69,22 @@ class AudioRecorderService : IntervalRecorderService() { when (batchesFolder.type) { BatchesFolder.BatchType.INTERNAL -> { setOutputFile( - batchesFolder.asInternalGetOutputPath(counter, settings!!.fileExtension) + batchesFolder.asInternalGetOutputPath(counter, settings.fileExtension) ) } BatchesFolder.BatchType.CUSTOM -> { setOutputFile( - batchesFolder.asCustomGetFileDescriptor(counter, settings!!.fileExtension) + batchesFolder.asCustomGetFileDescriptor(counter, settings.fileExtension) ) } } - setOutputFormat(settings!!.outputFormat) + setOutputFormat(settings.outputFormat) - setAudioEncoder(settings!!.encoder) - setAudioEncodingBitRate(settings!!.bitRate) - setAudioSamplingRate(settings!!.samplingRate) + setAudioEncoder(settings.encoder) + setAudioEncodingBitRate(settings.bitRate) + setAudioSamplingRate(settings.samplingRate) setOnErrorListener(OnErrorListener { _, _, _ -> onError() }) diff --git a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt index 32bbac0..c947ede 100644 --- a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt @@ -26,14 +26,10 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { protected var counter = 0L private set - var settings: Settings? = null - protected set + lateinit var settings: Settings private lateinit var cycleTimer: ScheduledExecutorService - protected val defaultOutputFolder: File - get() = AudioRecorderExporter.getFolder(this) - var batchesFolder: BatchesFolder = BatchesFolder.viaInternalFolder(this) var onCustomOutputFolderNotAccessible: () -> Unit = {} @@ -41,10 +37,10 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { fun getRecordingInformation(): RecordingInformation = RecordingInformation( folderPath = batchesFolder.exportFolderForSettings(), recordingStart = recordingStart, - maxDuration = settings!!.maxDuration, - fileExtension = settings!!.fileExtension, - intervalDuration = settings!!.intervalDuration, - forceExactMaxDuration = settings!!.forceExactMaxDuration, + maxDuration = settings.maxDuration, + fileExtension = settings.fileExtension, + intervalDuration = settings.intervalDuration, + forceExactMaxDuration = settings.forceExactMaxDuration, ) // Make overrideable @@ -60,7 +56,7 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { startNewCycle() }, 0, - settings!!.intervalDuration, + settings.intervalDuration, TimeUnit.MILLISECONDS ) } @@ -69,12 +65,13 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { override fun start() { super.start() + batchesFolder.initFolders() if (!batchesFolder.checkIfFolderIsAccessible()) { batchesFolder = BatchesFolder.viaInternalFolder(this@IntervalRecorderService) + batchesFolder.initFolders() onCustomOutputFolderNotAccessible() } - batchesFolder.initFolders() createTimer() } 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 5672c91..b583665 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 @@ -77,29 +77,7 @@ fun StartRecording( if (startRecording) { startRecording = false - audioRecorder.let { recorder -> - recorder.notificationDetails = appSettings.notificationSettings.let { - if (it == null) - null - else - RecorderNotificationHelper.NotificationDetails.fromNotificationSettings( - context, - it - ) - } - recorder.batchesFolder = if (appSettings.audioRecorderSettings.saveFolder == null) - BatchesFolder.viaInternalFolder(context) - else - BatchesFolder.viaCustomFolder( - context, - DocumentFile.fromTreeUri( - context, - Uri.parse(appSettings.audioRecorderSettings.saveFolder) - )!! - ) - - recorder.startRecording(context) - } + audioRecorder.startRecording(context, appSettings) } } 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 a1f1648..09e7adc 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 @@ -4,6 +4,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.net.Uri import android.os.IBinder import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -11,11 +12,13 @@ import androidx.compose.runtime.setValue import androidx.core.content.ContextCompat import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModel +import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.helpers.AudioRecorderExporter import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.services.AudioRecorderService +import app.myzel394.alibi.services.IntervalRecorderService import app.myzel394.alibi.services.RecorderNotificationHelper import app.myzel394.alibi.services.RecorderService import kotlinx.serialization.json.Json @@ -50,6 +53,8 @@ class AudioRecorderModel : ViewModel() { var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null var batchesFolder: BatchesFolder? = null + private lateinit var settings: AppSettings + var microphoneStatus: MicrophoneConnectivityStatus = MicrophoneConnectivityStatus.CONNECTED private set @@ -62,7 +67,9 @@ class AudioRecorderModel : ViewModel() { override fun onServiceConnected(className: ComponentName, service: IBinder) { recorderService = ((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also { recorder -> - // Update UI when the service changes + recorder.clearAllRecordings() + + // Init variables from us to the service recorder.onStateChange = { state -> recorderState = state } @@ -86,6 +93,8 @@ class AudioRecorderModel : ViewModel() { microphoneStatus = MicrophoneConnectivityStatus.CONNECTED } recorder.batchesFolder = batchesFolder ?: recorder.batchesFolder + recorder.settings = + IntervalRecorderService.Settings.from(settings.audioRecorderSettings) }.also { // Init UI from the service it.startRecording() @@ -111,11 +120,33 @@ class AudioRecorderModel : ViewModel() { microphoneStatus = MicrophoneConnectivityStatus.CONNECTED } - fun startRecording(context: Context) { + fun startRecording(context: Context, settings: AppSettings) { runCatching { context.unbindService(connection) + recorderService?.clearAllRecordings() } + notificationDetails = settings.notificationSettings.let { + if (it == null) + null + else + RecorderNotificationHelper.NotificationDetails.fromNotificationSettings( + context, + it + ) + } + batchesFolder = if (settings.audioRecorderSettings.saveFolder == null) + BatchesFolder.viaInternalFolder(context) + else + BatchesFolder.viaCustomFolder( + context, + DocumentFile.fromTreeUri( + context, + Uri.parse(settings.audioRecorderSettings.saveFolder) + )!! + ) + this.settings = settings + val intent = Intent(context, AudioRecorderService::class.java).apply { action = "init" From 18195c98937931dcd725f8f8356b427ac69cf3d8 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 18 Nov 2023 19:40:02 +0100 Subject: [PATCH 13/20] fix: Fix custom BatchesFolder --- .../java/app/myzel394/alibi/helpers/BatchesFolder.kt | 11 ++++++----- .../myzel394/alibi/ui/models/AudioRecorderModel.kt | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt index f461a65..9b95451 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -62,7 +62,7 @@ data class BatchesFolder( .map { FFmpegKitConfig.getSafParameterForRead( context, - customFolder!!.findFile(it.name!!)!!.uri + it.uri, )!! } } @@ -82,7 +82,10 @@ data class BatchesFolder( BatchType.INTERNAL -> File(getInternalFolder(), "$name.$extension").absolutePath BatchType.CUSTOM -> FFmpegKitConfig.getSafParameterForWrite( context, - customFolder!!.createFile("audio/${extension}", "${name}.${extension}")!!.uri + getCustomDefinedFolder().createFile( + "audio/${extension}", + "${name}.${extension}" + )!!.uri )!! } } @@ -97,9 +100,7 @@ data class BatchesFolder( fun deleteRecordings() { when (type) { BatchType.INTERNAL -> getInternalFolder().deleteRecursively() - BatchType.CUSTOM -> getCustomDefinedFolder().listFiles().forEach { - it.delete() - } + BatchType.CUSTOM -> customFolder?.findFile(subfolderName)?.delete() } } 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 09e7adc..14540c1 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 @@ -67,8 +67,6 @@ class AudioRecorderModel : ViewModel() { override fun onServiceConnected(className: ComponentName, service: IBinder) { recorderService = ((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also { recorder -> - recorder.clearAllRecordings() - // Init variables from us to the service recorder.onStateChange = { state -> recorderState = state @@ -95,6 +93,8 @@ class AudioRecorderModel : ViewModel() { recorder.batchesFolder = batchesFolder ?: recorder.batchesFolder recorder.settings = IntervalRecorderService.Settings.from(settings.audioRecorderSettings) + + recorder.clearAllRecordings() }.also { // Init UI from the service it.startRecording() @@ -122,8 +122,8 @@ class AudioRecorderModel : ViewModel() { fun startRecording(context: Context, settings: AppSettings) { runCatching { - context.unbindService(connection) recorderService?.clearAllRecordings() + context.unbindService(connection) } notificationDetails = settings.notificationSettings.let { From 0364a79dcb0c94d371d0dd14a716b2998686086e Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 19 Nov 2023 17:41:37 +0100 Subject: [PATCH 14/20] fix: Fix BatchesFolder export --- .../alibi/helpers/AudioRecorderExporter.kt | 52 ++-------- .../myzel394/alibi/helpers/BatchesFolder.kt | 55 +++++++++-- .../AudioRecorder/molecules/StartRecording.kt | 13 --- .../organisms/RecordingStatus.kt | 3 +- .../alibi/ui/screens/AudioRecorderScreen.kt | 94 ++++++++++++++++--- app/src/main/res/values/strings.xml | 2 + 6 files changed, 141 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt index 42c5af0..4eefbda 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -16,25 +16,21 @@ import java.time.format.DateTimeFormatter data class AudioRecorderExporter( val recording: RecordingInformation, ) { - private fun getInternalFilePaths(context: Context): List = - 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}", - ) - } - } } } diff --git a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt index 9b95451..8bc6f4d 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -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 { 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 b583665..3342774 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,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 diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt index 7aabe6e..ac2b163 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt @@ -107,8 +107,7 @@ fun RecordingStatus( DeleteButton( onDelete = { audioRecorder.stopRecording(context) - - AudioRecorderExporter.clearAllRecordings(context) + audioRecorder.batchesFolder!!.deleteRecordings(); } ) } diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt index ca39684..fd7ef17 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt @@ -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 = { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 52b5e83..4062115 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -118,4 +118,6 @@ 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! Yes, change folder Use private, encrypted storage + Recording has been saved successfully! + Open \ No newline at end of file From 703e783193b7df0d1a5d4684ced945ad343d2653 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 19 Nov 2023 17:51:04 +0100 Subject: [PATCH 15/20] fix: Remove ForceExactMaxDuration --- .../java/app/myzel394/alibi/db/AppSettings.kt | 6 --- .../alibi/helpers/AudioRecorderExporter.kt | 11 ++-- .../alibi/services/IntervalRecorderService.kt | 3 -- .../atoms/ForceExactMaxDurationTile.kt | 54 ------------------- .../alibi/ui/screens/AudioRecorderScreen.kt | 1 - .../alibi/ui/screens/SettingsScreen.kt | 3 -- app/src/main/res/values-es/strings.xml | 2 - 7 files changed, 4 insertions(+), 76 deletions(-) delete mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/ForceExactMaxDurationTile.kt 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 a86ea23..30191f6 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -76,7 +76,6 @@ data class RecordingInformation( val maxDuration: Long, val intervalDuration: Long, val fileExtension: String, - val forceExactMaxDuration: Boolean, ) { val hasRecordingsAvailable get() = File(folderPath).listFiles()?.isNotEmpty() ?: false @@ -88,7 +87,6 @@ data class AudioRecorderSettings( val maxDuration: Long = 30 * 60 * 1000L, // 60 seconds val intervalDuration: Long = 60 * 1000L, - val forceExactMaxDuration: Boolean = true, // 320 Kbps val bitRate: Int = 320000, val samplingRate: Int? = null, @@ -238,10 +236,6 @@ data class AudioRecorderSettings( return copy(maxDuration = duration) } - fun setForceExactMaxDuration(forceExactMaxDuration: Boolean): AudioRecorderSettings { - return copy(forceExactMaxDuration = forceExactMaxDuration) - } - fun setShowAllMicrophones(showAllMicrophones: Boolean): AudioRecorderSettings { return copy(showAllMicrophones = showAllMicrophones) } diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt index 4eefbda..7a30d5c 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -17,12 +17,11 @@ data class AudioRecorderExporter( val recording: RecordingInformation, ) { suspend fun concatenateFiles( - context: Context, batchesFolder: BatchesFolder, outputFilePath: String, forceConcatenation: Boolean = false, ) { - val filePaths = batchesFolder.getBatchesForFFmpeg().joinToString("|") + val filePaths = batchesFolder.getBatchesForFFmpeg() if (batchesFolder.checkIfOutputAlreadyExists( recording.recordingStart, @@ -32,12 +31,13 @@ data class AudioRecorderExporter( return } + val filePathsConcatenated = filePaths.joinToString("|") val command = "-protocol_whitelist saf,concat,content,file,subfile" + - " -i 'concat:${filePaths}' -y" + + " -i 'concat:$filePathsConcatenated' -y" + " -acodec copy" + " -metadata date='${recording.recordingStart.format(DateTimeFormatter.ISO_DATE_TIME)}'" + - " -metadata batch_count='${filePaths.length}'" + + " -metadata batch_count='${filePaths.size}'" + " -metadata batch_duration='${recording.intervalDuration}'" + " -metadata max_duration='${recording.maxDuration}'" + " $outputFilePath" @@ -57,9 +57,6 @@ data class AudioRecorderExporter( throw Exception("Failed to concatenate audios") } - - val minRequiredForPossibleInExactMaxDuration = - recording.maxDuration / recording.intervalDuration } companion object { diff --git a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt index c947ede..cc6ac55 100644 --- a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt @@ -40,7 +40,6 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { maxDuration = settings.maxDuration, fileExtension = settings.fileExtension, intervalDuration = settings.intervalDuration, - forceExactMaxDuration = settings.forceExactMaxDuration, ) // Make overrideable @@ -106,7 +105,6 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { data class Settings( val maxDuration: Long, val intervalDuration: Long, - val forceExactMaxDuration: Boolean, val bitRate: Int, val samplingRate: Int, val outputFormat: Int, @@ -135,7 +133,6 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() { outputFormat = audioRecorderSettings.getOutputFormat(), encoder = audioRecorderSettings.getEncoder(), maxDuration = audioRecorderSettings.maxDuration, - forceExactMaxDuration = audioRecorderSettings.forceExactMaxDuration, ) } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/ForceExactMaxDurationTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/ForceExactMaxDurationTile.kt deleted file mode 100644 index 5baea8c..0000000 --- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/ForceExactMaxDurationTile.kt +++ /dev/null @@ -1,54 +0,0 @@ -package app.myzel394.alibi.ui.components.SettingsScreen.atoms - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.GraphicEq -import androidx.compose.material3.Icon -import androidx.compose.material3.Switch -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.rememberCoroutineScope -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.ui.components.atoms.SettingsTile -import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState -import kotlinx.coroutines.launch - - -@Composable -fun ForceExactMaxDurationTile( - settings: AppSettings, -) { - val scope = rememberCoroutineScope() - val dataStore = LocalContext.current.dataStore - - fun updateValue(forceExactMaxDuration: Boolean) { - scope.launch { - dataStore.updateData { - it.setAudioRecorderSettings( - it.audioRecorderSettings.setForceExactMaxDuration(forceExactMaxDuration) - ) - } - } - } - - - SettingsTile( - title = stringResource(R.string.ui_settings_option_forceExactDuration_title), - description = stringResource(R.string.ui_settings_option_forceExactDuration_description), - leading = { - Icon( - Icons.Default.GraphicEq, - contentDescription = null, - ) - }, - trailing = { - Switch( - checked = settings.audioRecorderSettings.forceExactMaxDuration, - onCheckedChange = ::updateValue, - ) - }, - ) -} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt index fd7ef17..db6a049 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt @@ -136,7 +136,6 @@ fun AudioRecorderScreen( ) AudioRecorderExporter(recording).concatenateFiles( - context, audioRecorder.recorderService!!.batchesFolder, outputFile, ) diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt index b4f5247..cdad664 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt @@ -37,14 +37,12 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import app.myzel394.alibi.R import app.myzel394.alibi.dataStore -import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY import app.myzel394.alibi.ui.components.SettingsScreen.atoms.AboutTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.BitrateTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.CustomNotificationTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.DeleteRecordingsImmediatelyTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.EncoderTile -import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ForceExactMaxDurationTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ImportExport import app.myzel394.alibi.ui.components.SettingsScreen.atoms.InAppLanguagePicker import app.myzel394.alibi.ui.components.SettingsScreen.atoms.IntervalDurationTile @@ -146,7 +144,6 @@ fun SettingsScreen( ) MaxDurationTile(settings = settings) IntervalDurationTile(settings = settings) - ForceExactMaxDurationTile(settings = settings) InAppLanguagePicker() DeleteRecordingsImmediatelyTile(settings = settings) CustomNotificationTile(navController = navController, settings = settings) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 73e0687..fabc9a6 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -39,8 +39,6 @@ Set the maximum duration of the recording Batch duration Record a single batch for this duration. Alibi records multiple batches and deletes the oldest one. When exporting the audio, all batches will be merged together - Force exact duration - Force to strip the output file to be the exactly specified duration. If this is disabled, the output file may be a bit longer due to batches of audio samples being encoded together. Bitrate A higher bitrate means better quality but also larger file size Set the bitrate for the audio recording From d0701868cf972e4bc54cb366f2e5aa5f0714638d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:29:05 +0100 Subject: [PATCH 16/20] feat: Show better splitted path --- .../SettingsScreen/atoms/SaveFolderTile.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt index fff9aab..8f04cf7 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt @@ -2,6 +2,7 @@ package app.myzel394.alibi.ui.components.SettingsScreen.atoms import android.content.Intent import android.net.Uri +import android.text.TextUtils.split import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -38,6 +39,7 @@ import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.components.atoms.SettingsTile import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog import kotlinx.coroutines.launch +import java.net.URLDecoder @Composable fun SaveFolderTile( @@ -165,6 +167,7 @@ fun SaveFolderTile( } }, extra = { + println(settings.audioRecorderSettings.saveFolder) Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, @@ -174,11 +177,7 @@ fun SaveFolderTile( Text( text = stringResource( R.string.form_value_selected, - settings - .audioRecorderSettings - .saveFolder - .split(":")[1] - .replace("/", " > ") + splitPath(settings.audioRecorderSettings.saveFolder).joinToString(" > ") ), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -220,3 +219,14 @@ fun SaveFolderTile( } ) } + +fun splitPath(path: String): List { + return try { + URLDecoder + .decode(path, "UTF-8") + .split(":", limit = 3)[2] + .split("/") + } catch (e: Exception) { + listOf(path) + } +} From 89ac35e92eabb915132baa19cb23b4472702b642 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:36:02 +0100 Subject: [PATCH 17/20] fix: Delete custom saveFolder files after recording if enabled --- app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt | 3 +++ .../java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt index 8bc6f4d..86fb023 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -133,6 +133,9 @@ data class BatchesFolder( when (type) { BatchType.INTERNAL -> getInternalFolder().deleteRecursively() BatchType.CUSTOM -> customFolder?.findFile(subfolderName)?.delete() + ?: customFolder?.findFile(subfolderName)?.listFiles()?.forEach { + it.delete() + } } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt index db6a049..463badf 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt @@ -157,6 +157,10 @@ fun AudioRecorderScreen( BatchesFolder.BatchType.CUSTOM -> { showSnackbar(audioRecorder.batchesFolder!!.customFolder!!.uri) + + if (settings.audioRecorderSettings.deleteRecordingsImmediately) { + audioRecorder.batchesFolder!!.deleteRecordings() + } } } } catch (error: Exception) { From da7251fc80a2c1ed362a27468dce2d5ed32f05b6 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:36:08 +0100 Subject: [PATCH 18/20] fix: remove println --- .../alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt index 8f04cf7..4f2fd7f 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt @@ -167,7 +167,6 @@ fun SaveFolderTile( } }, extra = { - println(settings.audioRecorderSettings.saveFolder) Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, From b6bfac4eeee49bf500ec3de754bc40e26818bc41 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:53:32 +0100 Subject: [PATCH 19/20] fix: Properly check if recordings are available --- .../java/app/myzel394/alibi/db/AppSettings.kt | 5 +++-- .../app/myzel394/alibi/helpers/BatchesFolder.kt | 3 ++- .../AudioRecorder/molecules/StartRecording.kt | 2 +- .../alibi/ui/screens/AudioRecorderScreen.kt | 16 ++++++++-------- 4 files changed, 14 insertions(+), 12 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 30191f6..49b5481 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.media.MediaRecorder import android.os.Build import app.myzel394.alibi.R import app.myzel394.alibi.helpers.AudioRecorderExporter +import app.myzel394.alibi.helpers.BatchesFolder import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.ReturnCode import kotlinx.serialization.Serializable @@ -77,8 +78,8 @@ data class RecordingInformation( val intervalDuration: Long, val fileExtension: String, ) { - val hasRecordingsAvailable - get() = File(folderPath).listFiles()?.isNotEmpty() ?: false + fun hasRecordingsAvailable(context: Context): Boolean = + BatchesFolder.importFromFolder(folderPath, context).hasRecordingsAvailable() } @Serializable diff --git a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt index 86fb023..025a48a 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -142,7 +142,8 @@ data class BatchesFolder( fun hasRecordingsAvailable(): Boolean { return when (type) { BatchType.INTERNAL -> getInternalFolder().listFiles()?.isNotEmpty() ?: false - BatchType.CUSTOM -> getCustomDefinedFolder().listFiles().isNotEmpty() + BatchType.CUSTOM -> customFolder?.findFile(subfolderName)?.listFiles()?.isNotEmpty() + ?: false } } 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 3342774..6ea90d2 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 @@ -129,7 +129,7 @@ fun StartRecording( .fillMaxWidth(), textAlign = TextAlign.Center, ) - if (appSettings.lastRecording?.hasRecordingsAvailable == true) { + if (appSettings.lastRecording?.hasRecordingsAvailable(context) == true) { Column( modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt index 463badf..597a77a 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt @@ -36,7 +36,6 @@ 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.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 @@ -130,25 +129,26 @@ fun AudioRecorderScreen( val recording = audioRecorder.recorderService?.getRecordingInformation() ?: settings.lastRecording ?: throw Exception("No recording information available") - val outputFile = audioRecorder.batchesFolder!!.getOutputFileForFFmpeg( + val batchesFolder = BatchesFolder.importFromFolder(recording.folderPath, context) + val outputFile = batchesFolder.getOutputFileForFFmpeg( recording.recordingStart, recording.fileExtension ) AudioRecorderExporter(recording).concatenateFiles( - audioRecorder.recorderService!!.batchesFolder, + batchesFolder, outputFile, ) - val name = audioRecorder.batchesFolder!!.getName( + val name = batchesFolder.getName( recording.recordingStart, recording.fileExtension, ) - when (audioRecorder.batchesFolder!!.type) { + when (batchesFolder.type) { BatchesFolder.BatchType.INTERNAL -> { saveFile( - audioRecorder.batchesFolder!!.asInternalGetOutputFile( + batchesFolder.asInternalGetOutputFile( recording.recordingStart, recording.fileExtension, ), name @@ -156,10 +156,10 @@ fun AudioRecorderScreen( } BatchesFolder.BatchType.CUSTOM -> { - showSnackbar(audioRecorder.batchesFolder!!.customFolder!!.uri) + showSnackbar(batchesFolder.customFolder!!.uri) if (settings.audioRecorderSettings.deleteRecordingsImmediately) { - audioRecorder.batchesFolder!!.deleteRecordings() + batchesFolder.deleteRecordings() } } } From bc2e88ef0436a7ab568838d4b54624a671799dbf Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 19 Nov 2023 19:40:26 +0100 Subject: [PATCH 20/20] feat: Check if recording is available on app resume --- .../AudioRecorder/molecules/StartRecording.kt | 7 ++- .../myzel394/alibi/ui/effects/force-update.kt | 44 +++++++++++++++++++ .../alibi/ui/screens/AudioRecorderScreen.kt | 1 + 3 files changed, 51 insertions(+), 1 deletion(-) 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 6ea90d2..f1bb889 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 @@ -41,6 +41,7 @@ import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE import app.myzel394.alibi.ui.components.atoms.PermissionRequester +import app.myzel394.alibi.ui.effects.rememberForceUpdateOnLifeCycleChange import app.myzel394.alibi.ui.models.AudioRecorderModel import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -129,9 +130,13 @@ fun StartRecording( .fillMaxWidth(), textAlign = TextAlign.Center, ) + + val forceUpdate = rememberForceUpdateOnLifeCycleChange() if (appSettings.lastRecording?.hasRecordingsAvailable(context) == true) { Column( - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .then(forceUpdate), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Bottom, ) { diff --git a/app/src/main/java/app/myzel394/alibi/ui/effects/force-update.kt b/app/src/main/java/app/myzel394/alibi/ui/effects/force-update.kt index be81a62..a46a01a 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/effects/force-update.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/effects/force-update.kt @@ -1,11 +1,20 @@ package app.myzel394.alibi.ui.effects import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner import kotlinx.coroutines.delay @Composable @@ -20,4 +29,39 @@ fun rememberForceUpdate( } return tickTack +} + +@Composable +fun OnLifecycleEvent(onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit) { + val eventHandler = rememberUpdatedState(onEvent) + val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current) + + DisposableEffect(lifecycleOwner.value) { + val lifecycle = lifecycleOwner.value.lifecycle + val observer = LifecycleEventObserver { owner, event -> + eventHandler.value(owner, event) + } + + lifecycle.addObserver(observer) + onDispose { + lifecycle.removeObserver(observer) + } + } +} + +@Composable +fun rememberForceUpdateOnLifeCycleChange( + events: Array = arrayOf( + Lifecycle.Event.ON_RESUME + ), +): Modifier { + var tickTack by rememberSaveable { mutableStateOf(1f) } + + OnLifecycleEvent { owner, event -> + if (events.contains(event)) { + tickTack = if (tickTack == 1f) 0.99f else 1f + } + } + + return Modifier.alpha(tickTack) } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt index 597a77a..abc7d67 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp