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