current stand

This commit is contained in:
Myzel394 2023-10-31 20:43:19 +01:00
parent 542170f189
commit 47b85e74d2
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
6 changed files with 106 additions and 30 deletions

View File

@ -102,6 +102,7 @@ dependencies {
implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.material3:material3'
implementation "androidx.compose.material:material-icons-extended:1.5.1" implementation "androidx.compose.material:material-icons-extended:1.5.1"
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.documentfile:documentfile:1.0.1'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

View File

@ -1,20 +1,19 @@
package app.myzel394.alibi.services package app.myzel394.alibi.services
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.media.AudioDeviceCallback import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo import android.media.AudioDeviceInfo
import android.media.AudioManager import android.media.AudioManager
import android.media.MediaRecorder import android.media.MediaRecorder
import android.media.MediaRecorder.OnErrorListener import android.media.MediaRecorder.OnErrorListener
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper 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.enums.RecorderState
import app.myzel394.alibi.ui.utils.MicrophoneInfo import app.myzel394.alibi.ui.utils.MicrophoneInfo
import java.lang.IllegalStateException import java.lang.IllegalStateException
import java.util.concurrent.Executor
class AudioRecorderService : IntervalRecorderService() { class AudioRecorderService : IntervalRecorderService() {
var amplitudesAmount = 1000 var amplitudesAmount = 1000
@ -27,9 +26,6 @@ class AudioRecorderService : IntervalRecorderService() {
var onMicrophoneDisconnected: () -> Unit = {} var onMicrophoneDisconnected: () -> Unit = {}
var onMicrophoneReconnected: () -> Unit = {} var onMicrophoneReconnected: () -> Unit = {}
val filePath: String
get() = "${outputFolder}/$counter.${settings!!.fileExtension}"
/// Tell Android to use the correct bluetooth microphone, if any selected /// Tell Android to use the correct bluetooth microphone, if any selected
private fun startAudioDevice() { private fun startAudioDevice() {
if (selectedMicrophone == null) { if (selectedMicrophone == null) {
@ -61,6 +57,26 @@ class AudioRecorderService : IntervalRecorderService() {
} else { } else {
MediaRecorder() MediaRecorder()
}.apply { }.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 // Audio Source is kinda strange, here are my experimental findings using a Pixel 7 Pro
// and Redmi Buds 3 Pro: // and Redmi Buds 3 Pro:
// - MIC: Uses the bottom microphone of the phone (17) // - 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) // - VOICE_COMMUNICATION: Uses the bottom microphone of the phone (17)
// - DEFAULT: Uses the bottom microphone of the phone (17) // - DEFAULT: Uses the bottom microphone of the phone (17)
setAudioSource(MediaRecorder.AudioSource.MIC) setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFile(filePath)
setOutputFormat(settings!!.outputFormat)
setAudioEncoder(settings!!.encoder) setAudioEncoder(settings!!.encoder)
setAudioEncodingBitRate(settings!!.bitRate) setAudioEncodingBitRate(settings!!.bitRate)
setAudioSamplingRate(settings!!.samplingRate) setAudioSamplingRate(settings!!.samplingRate)

View File

@ -1,6 +1,8 @@
package app.myzel394.alibi.services package app.myzel394.alibi.services
import android.media.MediaRecorder import android.media.MediaRecorder
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import app.myzel394.alibi.dataStore import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AudioRecorderSettings import app.myzel394.alibi.db.AudioRecorderSettings
import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.db.RecordingInformation
@ -10,6 +12,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.w3c.dom.DocumentFragment
import java.io.File import java.io.File
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledExecutorService
@ -27,11 +30,15 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() {
private lateinit var cycleTimer: ScheduledExecutorService private lateinit var cycleTimer: ScheduledExecutorService
protected val outputFolder: File protected val defaultOutputFolder: File
get() = AudioRecorderExporter.getFolder(this) get() = AudioRecorderExporter.getFolder(this)
var customOutputFolder: DocumentFile? = null
var onCustomOutputFolderNotAccessible: () -> Unit = {}
fun getRecordingInformation(): RecordingInformation = RecordingInformation( fun getRecordingInformation(): RecordingInformation = RecordingInformation(
folderPath = outputFolder.absolutePath, folderPath = customOutputFolder?.uri?.toString() ?: defaultOutputFolder.absolutePath,
recordingStart = recordingStart, recordingStart = recordingStart,
maxDuration = settings!!.maxDuration, maxDuration = settings!!.maxDuration,
fileExtension = settings!!.fileExtension, fileExtension = settings!!.fileExtension,
@ -61,15 +68,29 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() {
override fun start() { override fun start() {
super.start() super.start()
outputFolder.mkdirs()
scope.launch { scope.launch {
dataStore.data.collectLatest { preferenceSettings -> dataStore.data.collectLatest { preferenceSettings ->
if (settings == null) { if (settings == null) {
settings = Settings.from(preferenceSettings.audioRecorderSettings) 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() createTimer()
} }
if (customOutputFolder == null) {
defaultOutputFolder.mkdirs()
}
} }
} }
} }
@ -90,15 +111,37 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() {
cycleTimer.shutdown() cycleTimer.shutdown()
} }
fun clearAllRecordings() {
if (customOutputFolder != null) {
customOutputFolder!!.listFiles().forEach {
it.delete()
}
} else {
defaultOutputFolder.listFiles()?.forEach {
it.delete()
}
}
}
private fun deleteOldRecordings() { private fun deleteOldRecordings() {
val timeMultiplier = settings!!.maxDuration / settings!!.intervalDuration val timeMultiplier = settings!!.maxDuration / settings!!.intervalDuration
val earliestCounter = counter - timeMultiplier val earliestCounter = counter - timeMultiplier
outputFolder.listFiles()?.forEach { file -> if (customOutputFolder != null) {
val fileCounter = file.nameWithoutExtension.toIntOrNull() ?: return customOutputFolder!!.listFiles().forEach {
val fileCounter = it.name?.substringBeforeLast(".")?.toIntOrNull() ?: return@forEach
if (fileCounter < earliestCounter) { if (fileCounter < earliestCounter) {
file.delete() 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 samplingRate: Int,
val outputFormat: Int, val outputFormat: Int,
val encoder: Int, val encoder: Int,
val folder: String? = null,
) { ) {
val fileExtension: String val fileExtension: String
get() = when (outputFormat) { get() = when (outputFormat) {

View File

@ -1,6 +1,7 @@
package app.myzel394.alibi.ui.components.AudioRecorder.molecules package app.myzel394.alibi.ui.components.AudioRecorder.molecules
import android.Manifest import android.Manifest
import android.net.Uri
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -38,11 +39,13 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.documentfile.provider.DocumentFile
import app.myzel394.alibi.NotificationHelper import app.myzel394.alibi.NotificationHelper
import app.myzel394.alibi.R import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.helpers.AudioRecorderExporter import app.myzel394.alibi.helpers.AudioRecorderExporter
import app.myzel394.alibi.helpers.AudioRecorderExporter.Companion.clearAllRecordings
import app.myzel394.alibi.services.RecorderNotificationHelper import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
import app.myzel394.alibi.ui.components.atoms.PermissionRequester import app.myzel394.alibi.ui.components.atoms.PermissionRequester
@ -72,19 +75,26 @@ fun StartRecording(
LaunchedEffect(startRecording) { LaunchedEffect(startRecording) {
if (startRecording) { if (startRecording) {
startRecording = false startRecording = false
audioRecorder.notificationDetails = appSettings.notificationSettings.let {
if (it == null) audioRecorder.let { recorder ->
null recorder.notificationDetails = appSettings.notificationSettings.let {
else if (it == null)
RecorderNotificationHelper.NotificationDetails.fromNotificationSettings( null
context, else
it 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)
} }
} }

View File

@ -1,5 +1,6 @@
package app.myzel394.alibi.ui.components.SettingsScreen.atoms package app.myzel394.alibi.ui.components.SettingsScreen.atoms
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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 app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
@Composable @Composable
fun SaveFolderTile( fun SaveFolderTile(
@ -67,7 +67,7 @@ fun SaveFolderTile(
return@rememberFolderSelectorDialog return@rememberFolderSelectorDialog
} }
updateValue(folder.path) updateValue(folder.toString())
} }
var showWarning by remember { mutableStateOf(false) } var showWarning by remember { mutableStateOf(false) }

View File

@ -9,9 +9,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.enums.RecorderState
import app.myzel394.alibi.helpers.AudioRecorderExporter
import app.myzel394.alibi.services.AudioRecorderService import app.myzel394.alibi.services.AudioRecorderService
import app.myzel394.alibi.services.RecorderNotificationHelper import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.services.RecorderService import app.myzel394.alibi.services.RecorderService
@ -45,6 +47,7 @@ class AudioRecorderModel : ViewModel() {
var onRecordingSave: () -> Unit = {} var onRecordingSave: () -> Unit = {}
var onError: () -> Unit = {} var onError: () -> Unit = {}
var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null
var customOutputFolder: DocumentFile? = null
var microphoneStatus: MicrophoneConnectivityStatus = MicrophoneConnectivityStatus.CONNECTED var microphoneStatus: MicrophoneConnectivityStatus = MicrophoneConnectivityStatus.CONNECTED
private set private set
@ -58,6 +61,8 @@ class AudioRecorderModel : ViewModel() {
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
recorderService = recorderService =
((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also { recorder -> ((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also { recorder ->
recorder.clearAllRecordings()
// Update UI when the service changes // Update UI when the service changes
recorder.onStateChange = { state -> recorder.onStateChange = { state ->
recorderState = state recorderState = state
@ -81,6 +86,7 @@ class AudioRecorderModel : ViewModel() {
recorder.onMicrophoneReconnected = { recorder.onMicrophoneReconnected = {
microphoneStatus = MicrophoneConnectivityStatus.CONNECTED microphoneStatus = MicrophoneConnectivityStatus.CONNECTED
} }
recorder.customOutputFolder = customOutputFolder
}.also { }.also {
// Init UI from the service // Init UI from the service
it.startRecording() it.startRecording()