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