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.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'

View File

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

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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) }

View File

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