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()