From 862de214365a0ba8ad37fd23e026ebb1de43cb4e Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 17:18:57 +0200 Subject: [PATCH 01/29] feat: Add PoC for sources for AudioRecorderService --- .../alibi/services/AudioRecorderService.kt | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) 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 0d8617a..413fa91 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -1,12 +1,19 @@ package app.myzel394.alibi.services +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.media.AudioDeviceInfo +import android.media.AudioManager import android.media.MediaRecorder import android.media.MediaRecorder.OnErrorListener +import android.media.MediaRecorder.getAudioSourceMax import android.os.Build import java.lang.IllegalStateException class AudioRecorderService: IntervalRecorderService() { var amplitudesAmount = 1000 + var selectedDevice: AudioDeviceInfo? = null var recorder: MediaRecorder? = null private set @@ -15,12 +22,39 @@ class AudioRecorderService: IntervalRecorderService() { val filePath: String get() = "$folder/$counter.${settings!!.fileExtension}" + private fun _setAudioDevice() { + val audioManger = getSystemService(AUDIO_SERVICE)!! as AudioManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (selectedDevice == null) { + audioManger.clearCommunicationDevice() + } else { + audioManger.setCommunicationDevice(selectedDevice!!) + } + } else { + if (selectedDevice == null) { + audioManger.stopBluetoothSco() + } else { + audioManger.startBluetoothSco() + } + } + + } + private fun createRecorder(): MediaRecorder { + _setAudioDevice() + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { MediaRecorder(this) } 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) setOutputFile(filePath) setOutputFormat(settings!!.outputFormat) @@ -82,4 +116,30 @@ class AudioRecorderService: IntervalRecorderService() { 0 } } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent?.action == "changeAudioDevice") { + selectedDevice = intent.getStringExtra("deviceID")!!.let { + if (it == "null") { + null + } else { + val audioManager = getSystemService(AUDIO_SERVICE)!! as AudioManager + audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).find { device -> + device.id == it.toInt() + } + } + } + } + + return super.onStartCommand(intent, flags, startId) + } + + companion object { + fun changeAudioDevice(deviceID: String?, context: Context) { + val intent = Intent("changeAudioDevice").apply { + putExtra("deviceID", deviceID ?: "null") + } + context.startService(intent) + } + } } \ No newline at end of file From df1d7ce8ffb6e9ac95cdac6cc344feb460838a83 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 18:16:08 +0200 Subject: [PATCH 02/29] feat: Add microphone selection --- .../alibi/services/AudioRecorderService.kt | 31 +---- .../atoms/MicrophoneSelectionButton.kt | 48 ++++++++ .../AudioRecorder/atoms/MicrophoneTypeIcon.kt | 32 +++++ .../molecules/MicrophoneSelection.kt | 115 ++++++++++++++++++ .../RecordingStatus.kt | 22 ++-- .../alibi/ui/models/AudioRecorderModel.kt | 8 +- .../alibi/ui/screens/AudioRecorder.kt | 6 +- .../alibi/ui/utils/available-microphones.kt | 43 +++++++ app/src/main/res/values/strings.xml | 5 +- 9 files changed, 262 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneTypeIcon.kt create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt rename app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/{molecules => organisms}/RecordingStatus.kt (94%) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt 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 413fa91..1cc73fd 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -9,11 +9,12 @@ import android.media.MediaRecorder import android.media.MediaRecorder.OnErrorListener import android.media.MediaRecorder.getAudioSourceMax import android.os.Build +import app.myzel394.alibi.ui.utils.MicrophoneInfo import java.lang.IllegalStateException class AudioRecorderService: IntervalRecorderService() { var amplitudesAmount = 1000 - var selectedDevice: AudioDeviceInfo? = null + var selectedDevice: MicrophoneInfo? = null var recorder: MediaRecorder? = null private set @@ -29,7 +30,7 @@ class AudioRecorderService: IntervalRecorderService() { if (selectedDevice == null) { audioManger.clearCommunicationDevice() } else { - audioManger.setCommunicationDevice(selectedDevice!!) + audioManger.setCommunicationDevice(selectedDevice!!.deviceInfo) } } else { if (selectedDevice == null) { @@ -116,30 +117,4 @@ class AudioRecorderService: IntervalRecorderService() { 0 } } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (intent?.action == "changeAudioDevice") { - selectedDevice = intent.getStringExtra("deviceID")!!.let { - if (it == "null") { - null - } else { - val audioManager = getSystemService(AUDIO_SERVICE)!! as AudioManager - audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).find { device -> - device.id == it.toInt() - } - } - } - } - - return super.onStartCommand(intent, flags, startId) - } - - companion object { - fun changeAudioDevice(deviceID: String?, context: Context) { - val intent = Intent("changeAudioDevice").apply { - putExtra("deviceID", deviceID ?: "null") - } - context.startService(intent) - } - } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt new file mode 100644 index 0000000..26cbd31 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt @@ -0,0 +1,48 @@ +package app.myzel394.alibi.ui.components.AudioRecorder.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.ui.utils.MicrophoneInfo +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.stringResource +import app.myzel394.alibi.R + +@Composable +fun MicrophoneSelectionButton( + microphone: MicrophoneInfo? = null, + selected: Boolean = false, + onSelect: () -> Unit, +) { + Button( + onClick = onSelect, + modifier = Modifier + .fillMaxWidth() + .height(64.dp), + colors = if (selected) ButtonDefaults.buttonColors( + ) else ButtonDefaults.textButtonColors(), + ) { + MicrophoneTypeInfo( + type = microphone?.type ?: MicrophoneInfo.MicrophoneType.PHONE, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text( + text = microphone?.name + ?: stringResource(R.string.ui_audioRecorder_info_microphone_deviceMicrophone), + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + ) + } +} diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneTypeIcon.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneTypeIcon.kt new file mode 100644 index 0000000..e54a5a1 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneTypeIcon.kt @@ -0,0 +1,32 @@ +package app.myzel394.alibi.ui.components.AudioRecorder.atoms + +import android.R +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BluetoothAudio +import androidx.compose.material.icons.filled.Memory +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.MicExternalOn +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Smartphone +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import app.myzel394.alibi.ui.utils.MicrophoneInfo + +@Composable +fun MicrophoneTypeInfo( + modifier: Modifier = Modifier, + type: MicrophoneInfo.MicrophoneType, +) { + Icon( + imageVector = when (type) { + MicrophoneInfo.MicrophoneType.BLUETOOTH -> Icons.Filled.BluetoothAudio + MicrophoneInfo.MicrophoneType.WIRED -> Icons.Filled.MicExternalOn + MicrophoneInfo.MicrophoneType.PHONE -> Icons.Filled.Smartphone + MicrophoneInfo.MicrophoneType.OTHER -> Icons.Filled.Mic + }, + modifier = modifier, + contentDescription = null, + ) +} diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt new file mode 100644 index 0000000..8c6963a --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt @@ -0,0 +1,115 @@ +package app.myzel394.alibi.ui.components.AudioRecorder.molecules + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneSelectionButton +import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneTypeInfo +import app.myzel394.alibi.ui.models.AudioRecorderModel +import app.myzel394.alibi.ui.utils.MicrophoneInfo + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MicrophoneSelection( + audioRecorder: AudioRecorderModel, + microphones: List, +) { + var showSelection by rememberSaveable { + mutableStateOf(false) + } + + if (showSelection) { + ModalBottomSheet( + onDismissRequest = { + showSelection = false + } + ) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(48.dp), + ) { + Text( + stringResource(R.string.ui_audioRecorder_info_microphone_changeExplanation), + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + ) + + LazyColumn( + modifier = Modifier + .padding(horizontal = 32.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + item { + MicrophoneSelectionButton( + selected = audioRecorder.recorderService!!.selectedDevice == null, + onSelect = { + audioRecorder.changeMicrophone(null) + showSelection = false + } + ) + } + + items(microphones.size) { + val microphone = microphones[it] + + MicrophoneSelectionButton( + microphone = microphone, + selected = audioRecorder.recorderService!!.selectedDevice == microphone, + onSelect = { + audioRecorder.changeMicrophone(microphone) + showSelection = false + }, + ) + } + } + } + } + } + + Button( + onClick = { + showSelection = true + }, + colors = ButtonDefaults.textButtonColors(), + ) { + MicrophoneTypeInfo( + type = audioRecorder.recorderService!!.selectedDevice?.type + ?: MicrophoneInfo.MicrophoneType.PHONE, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text( + text = audioRecorder.recorderService!!.selectedDevice.let { + it?.name + ?: stringResource(R.string.ui_audioRecorder_info_microphone_deviceMicrophone) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/RecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt similarity index 94% rename from app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/RecordingStatus.kt rename to app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt index f1d832b..7130fa7 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/RecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/organisms/RecordingStatus.kt @@ -1,11 +1,9 @@ -package app.myzel394.alibi.ui.components.AudioRecorder.molecules +package app.myzel394.alibi.ui.components.AudioRecorder.organisms import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.expandHorizontally -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -26,14 +24,12 @@ import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Save import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.LargeFloatingActionButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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 @@ -50,20 +46,17 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import app.myzel394.alibi.R -import app.myzel394.alibi.services.RecorderService import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE import app.myzel394.alibi.ui.components.AudioRecorder.atoms.ConfirmDeletionDialog import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RealtimeAudioVisualizer -import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveRecordingButton +import app.myzel394.alibi.ui.components.AudioRecorder.molecules.MicrophoneSelection import app.myzel394.alibi.ui.components.atoms.Pulsating import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.utils.KeepScreenOn +import app.myzel394.alibi.ui.utils.MicrophoneInfo import app.myzel394.alibi.ui.utils.formatDuration import kotlinx.coroutines.delay -import java.io.File -import java.time.Duration import java.time.LocalDateTime -import java.time.ZoneId @Composable fun RecordingStatus( @@ -215,5 +208,14 @@ fun RecordingStatus( Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) Text(stringResource(R.string.ui_audioRecorder_action_save_label)) } + + val microphones = MicrophoneInfo.fetchDeviceMicrophones(context) + + if (microphones.isNotEmpty()) { + MicrophoneSelection( + audioRecorder = audioRecorder, + microphones = microphones + ) + } } } \ No newline at end of file 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 e9638cf..d13bd84 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,10 +4,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection -import android.media.MediaRecorder import android.os.IBinder -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -17,6 +14,7 @@ import app.myzel394.alibi.db.LastRecording import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.services.AudioRecorderService import app.myzel394.alibi.services.RecorderService +import app.myzel394.alibi.ui.utils.MicrophoneInfo class AudioRecorderModel: ViewModel() { var recorderState by mutableStateOf(RecorderState.IDLE) @@ -121,6 +119,10 @@ class AudioRecorderModel: ViewModel() { recorderService?.amplitudesAmount = amount } + fun changeMicrophone(microphone: MicrophoneInfo?) { + recorderService!!.selectedDevice = microphone + } + fun bindToService(context: Context) { Intent(context, AudioRecorderService::class.java).also { intent -> context.bindService(intent, connection, 0) diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt index 0db7196..40fee55 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt @@ -28,16 +28,12 @@ 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.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController -import app.myzel394.alibi.ui.components.AudioRecorder.molecules.RecordingStatus +import app.myzel394.alibi.ui.components.AudioRecorder.organisms.RecordingStatus import app.myzel394.alibi.ui.components.AudioRecorder.molecules.StartRecording import app.myzel394.alibi.ui.enums.Screen 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.LastRecording import app.myzel394.alibi.ui.effects.rememberSettings import app.myzel394.alibi.ui.models.AudioRecorderModel import kotlinx.coroutines.launch diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt new file mode 100644 index 0000000..cb9ebe1 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt @@ -0,0 +1,43 @@ +package app.myzel394.alibi.ui.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.media.AudioDeviceInfo +import android.media.AudioManager + +data class MicrophoneInfo( + val deviceInfo: AudioDeviceInfo, +) { + val name: String + get() = deviceInfo.productName.toString() + + val type: MicrophoneType + get() = when (deviceInfo.type) { + AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> MicrophoneType.BLUETOOTH + AudioDeviceInfo.TYPE_WIRED_HEADSET -> MicrophoneType.WIRED + AudioDeviceInfo.TYPE_BUILTIN_MIC -> MicrophoneType.PHONE + else -> MicrophoneType.OTHER + } + + companion object { + fun fromDeviceInfo(deviceInfo: AudioDeviceInfo): MicrophoneInfo { + return MicrophoneInfo(deviceInfo) + } + + @SuppressLint("NewApi") + fun fetchDeviceMicrophones(context: Context): List { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE)!! as AudioManager + return audioManager.availableCommunicationDevices.let { + it.subList(2, it.size) + }.map(::fromDeviceInfo) + } + } + + + enum class MicrophoneType { + BLUETOOTH, + WIRED, + PHONE, + OTHER, + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 15ab739..fc63b3a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,4 @@ - + Alibi Cancel @@ -64,4 +63,6 @@ Alibi encountered an error during recording. Would you like to try saving the recording? Language Change + Device Microphone + The selected microphone will be immediately activated \ No newline at end of file From 6e26681acf4a9399daefab9f436c7c30716ed896 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 19:05:58 +0200 Subject: [PATCH 03/29] feat: Improve RecordingStatus layout --- .../AudioRecorder/atoms/DeleteButton.kt | 59 +++++++ .../AudioRecorder/atoms/PauseResumeButton.kt | 39 +++++ .../AudioRecorder/atoms/RecordingTime.kt | 45 ++++++ .../AudioRecorder/atoms/SaveButton.kt | 53 +++++++ .../molecules/MicrophoneSelection.kt | 23 +-- .../organisms/RecordingStatus.kt | 150 +++++++----------- 6 files changed, 264 insertions(+), 105 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/DeleteButton.kt create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/PauseResumeButton.kt create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/RecordingTime.kt create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/SaveButton.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/DeleteButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/DeleteButton.kt new file mode 100644 index 0000000..9c4b4df --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/DeleteButton.kt @@ -0,0 +1,59 @@ +package app.myzel394.alibi.ui.components.AudioRecorder.atoms + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +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.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import app.myzel394.alibi.R + +@Composable +fun DeleteButton( + modifier: Modifier = Modifier, + onDelete: () -> Unit, +) { + var showDeleteDialog by remember { mutableStateOf(false) } + + if (showDeleteDialog) { + ConfirmDeletionDialog( + onDismiss = { + showDeleteDialog = false + }, + onConfirm = { + showDeleteDialog = false + onDelete() + }, + ) + } + val label = stringResource(R.string.ui_audioRecorder_action_delete_label) + Button( + modifier = Modifier + .semantics { + contentDescription = label + } + .then(modifier), + onClick = { + showDeleteDialog = true + }, + colors = ButtonDefaults.textButtonColors(), + ) { + Text( + label, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/PauseResumeButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/PauseResumeButton.kt new file mode 100644 index 0000000..653645c --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/PauseResumeButton.kt @@ -0,0 +1,39 @@ +package app.myzel394.alibi.ui.components.AudioRecorder.atoms + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.LargeFloatingActionButton +import androidx.compose.material3.SmallFloatingActionButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import app.myzel394.alibi.R + +@Composable +fun PauseResumeButton( + modifier: Modifier = Modifier, + isPaused: Boolean, + onChange: () -> Unit, +) { + val pauseLabel = stringResource(R.string.ui_audioRecorder_action_pause_label) + val resumeLabel = stringResource(R.string.ui_audioRecorder_action_resume_label) + + FloatingActionButton( + modifier = Modifier + .semantics { + contentDescription = if (isPaused) resumeLabel else pauseLabel + } + .then(modifier), + onClick = onChange, + ) { + Icon( + if (isPaused) Icons.Default.PlayArrow else Icons.Default.Pause, + contentDescription = null, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/RecordingTime.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/RecordingTime.kt new file mode 100644 index 0000000..3e3a21f --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/RecordingTime.kt @@ -0,0 +1,45 @@ +package app.myzel394.alibi.ui.components.AudioRecorder.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.ui.components.atoms.Pulsating +import app.myzel394.alibi.ui.models.AudioRecorderModel +import app.myzel394.alibi.ui.utils.formatDuration + +@Composable +fun RecordingTime( + time: Long, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Pulsating { + Box( + modifier = Modifier + .size(16.dp) + .clip(CircleShape) + .background(Color.Red) + ) + } + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = formatDuration(time), + style = MaterialTheme.typography.headlineLarge, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/SaveButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/SaveButton.kt new file mode 100644 index 0000000..0a65bcc --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/SaveButton.kt @@ -0,0 +1,53 @@ +package app.myzel394.alibi.ui.components.AudioRecorder.atoms + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +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.Save +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.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE + +@Composable +fun SaveButton( + modifier: Modifier = Modifier, + onSave: () -> Unit, +) { + val label = stringResource(R.string.ui_audioRecorder_action_save_label) + + Button( + modifier = Modifier + .semantics { + contentDescription = label + } + .then(modifier), + onClick = onSave, + colors = ButtonDefaults.textButtonColors(), + ) { + Icon( + Icons.Default.Save, + contentDescription = null, + modifier = Modifier + .size(ButtonDefaults.IconSize) + ) + Spacer(Modifier.width(ButtonDefaults.IconSpacing)) + Text( + label, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt index 8c6963a..4a11024 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -35,18 +36,22 @@ import app.myzel394.alibi.ui.utils.MicrophoneInfo @OptIn(ExperimentalMaterial3Api::class) @Composable fun MicrophoneSelection( - audioRecorder: AudioRecorderModel, microphones: List, + selectedMicrophone: MicrophoneInfo?, + onSelect: (MicrophoneInfo?) -> Unit, ) { var showSelection by rememberSaveable { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState() if (showSelection) { ModalBottomSheet( + modifier = Modifier.fillMaxSize(), onDismissRequest = { showSelection = false - } + }, + sheetState = sheetState, ) { Column( modifier = Modifier @@ -68,9 +73,9 @@ fun MicrophoneSelection( ) { item { MicrophoneSelectionButton( - selected = audioRecorder.recorderService!!.selectedDevice == null, + selected = selectedMicrophone == null, onSelect = { - audioRecorder.changeMicrophone(null) + onSelect(null) showSelection = false } ) @@ -81,11 +86,11 @@ fun MicrophoneSelection( MicrophoneSelectionButton( microphone = microphone, - selected = audioRecorder.recorderService!!.selectedDevice == microphone, + selected = selectedMicrophone == microphone, onSelect = { - audioRecorder.changeMicrophone(microphone) + onSelect(microphone) showSelection = false - }, + } ) } } @@ -100,13 +105,13 @@ fun MicrophoneSelection( colors = ButtonDefaults.textButtonColors(), ) { MicrophoneTypeInfo( - type = audioRecorder.recorderService!!.selectedDevice?.type + type = selectedMicrophone?.type ?: MicrophoneInfo.MicrophoneType.PHONE, modifier = Modifier.size(ButtonDefaults.IconSize), ) Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) Text( - text = audioRecorder.recorderService!!.selectedDevice.let { + text = selectedMicrophone.let { it?.name ?: stringResource(R.string.ui_audioRecorder_info_microphone_deviceMicrophone) } 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 7130fa7..9cc6a4c 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 @@ -48,7 +48,11 @@ import androidx.compose.ui.unit.dp import app.myzel394.alibi.R import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE import app.myzel394.alibi.ui.components.AudioRecorder.atoms.ConfirmDeletionDialog +import app.myzel394.alibi.ui.components.AudioRecorder.atoms.DeleteButton +import app.myzel394.alibi.ui.components.AudioRecorder.atoms.PauseResumeButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RealtimeAudioVisualizer +import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RecordingTime +import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveButton import app.myzel394.alibi.ui.components.AudioRecorder.molecules.MicrophoneSelection import app.myzel394.alibi.ui.components.atoms.Pulsating import app.myzel394.alibi.ui.models.AudioRecorderModel @@ -93,24 +97,7 @@ fun RecordingStatus( Column( horizontalAlignment = Alignment.CenterHorizontally, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - ) { - Pulsating { - Box( - modifier = Modifier - .size(16.dp) - .clip(CircleShape) - .background(Color.Red) - ) - } - Spacer(modifier = Modifier.width(16.dp)) - Text( - text = formatDuration(audioRecorder.recordingTime!!), - style = MaterialTheme.typography.headlineLarge, - ) - } + RecordingTime(audioRecorder.recordingTime!!) Spacer(modifier = Modifier.height(16.dp)) AnimatedVisibility( visible = progressVisible, @@ -125,96 +112,67 @@ fun RecordingStatus( ) } Spacer(modifier = Modifier.height(32.dp)) + } - var showDeleteDialog by remember { mutableStateOf(false) } - - if (showDeleteDialog) { - ConfirmDeletionDialog( - onDismiss = { - showDeleteDialog = false - }, - onConfirm = { - showDeleteDialog = false - audioRecorder.stopRecording(context, saveAsLastRecording = false) - }, - ) - } - val label = stringResource(R.string.ui_audioRecorder_action_delete_label) - Button( + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Box( modifier = Modifier - .semantics { - contentDescription = label - }, - onClick = { - showDeleteDialog = true - }, - colors = ButtonDefaults.textButtonColors(), + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center, ) { - Icon( - Icons.Default.Delete, - contentDescription = null, - modifier = Modifier.size(ButtonDefaults.IconSize), + DeleteButton( + onDelete = { + audioRecorder.stopRecording(context, saveAsLastRecording = false) + } + ) + } + + Box( + contentAlignment = Alignment.Center, + ) { + PauseResumeButton( + isPaused = audioRecorder.isPaused, + onChange = { + if (audioRecorder.isPaused) { + audioRecorder.resumeRecording() + } else { + audioRecorder.pauseRecording() + } + }, + ) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center, + ) { + SaveButton( + onSave = { + runCatching { + audioRecorder.stopRecording(context) + } + audioRecorder.onRecordingSave() + } ) - Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) - Text(label) } } - val pauseLabel = stringResource(R.string.ui_audioRecorder_action_pause_label) - val resumeLabel = stringResource(R.string.ui_audioRecorder_action_resume_label) - LargeFloatingActionButton( - modifier = Modifier - .semantics { - contentDescription = if (audioRecorder.isPaused) resumeLabel else pauseLabel - }, - onClick = { - if (audioRecorder.isPaused) { - audioRecorder.resumeRecording() - } else { - audioRecorder.pauseRecording() - } - }, - ) { - Icon( - if (audioRecorder.isPaused) Icons.Default.PlayArrow else Icons.Default.Pause, - contentDescription = null, - ) - } - - val alpha by animateFloatAsState(if (progressVisible) 1f else 0f, tween(1000)) - val label = stringResource(R.string.ui_audioRecorder_action_save_label) - - Button( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth() - .height(BIG_PRIMARY_BUTTON_SIZE) - .alpha(alpha) - .semantics { - contentDescription = label - }, - onClick = { - runCatching { - audioRecorder.stopRecording(context) - } - audioRecorder.onRecordingSave() - }, - ) { - Icon( - Icons.Default.Save, - contentDescription = null, - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) - Text(stringResource(R.string.ui_audioRecorder_action_save_label)) - } val microphones = MicrophoneInfo.fetchDeviceMicrophones(context) if (microphones.isNotEmpty()) { MicrophoneSelection( - audioRecorder = audioRecorder, - microphones = microphones + microphones = microphones, + selectedMicrophone = audioRecorder.recorderService!!.selectedDevice, + onSelect = { + audioRecorder.changeMicrophone(it) + audioRecorder.recorderService!!.startNewCycle() + } ) } } From 57424cc1d3f1b34d5059a39443c85a8c41539c0a Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 19:09:04 +0200 Subject: [PATCH 04/29] fix: Fix button jumping --- .../AudioRecorder/molecules/MicrophoneSelection.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt index 4a11024..4530d82 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt @@ -1,6 +1,7 @@ package app.myzel394.alibi.ui.components.AudioRecorder.molecules import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -47,7 +48,6 @@ fun MicrophoneSelection( if (showSelection) { ModalBottomSheet( - modifier = Modifier.fillMaxSize(), onDismissRequest = { showSelection = false }, @@ -96,6 +96,9 @@ fun MicrophoneSelection( } } } + } else { + // We need to show a placeholder box to keep the the rest aligned correctly + Box {} } Button( From a515d2b36c5a1cb8ae444cbad94b3af54f9cd88a Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 19:15:15 +0200 Subject: [PATCH 05/29] refactor: Improve selectedDevice; Use own rememberState; Reset selectedDevice on stop --- .../myzel394/alibi/services/AudioRecorderService.kt | 11 ++++++----- .../AudioRecorder/organisms/RecordingStatus.kt | 7 +++++-- 2 files changed, 11 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 1cc73fd..07d9fc2 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -12,7 +12,7 @@ import android.os.Build import app.myzel394.alibi.ui.utils.MicrophoneInfo import java.lang.IllegalStateException -class AudioRecorderService: IntervalRecorderService() { +class AudioRecorderService : IntervalRecorderService() { var amplitudesAmount = 1000 var selectedDevice: MicrophoneInfo? = null @@ -27,11 +27,11 @@ class AudioRecorderService: IntervalRecorderService() { val audioManger = getSystemService(AUDIO_SERVICE)!! as AudioManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (selectedDevice == null) { + if (selectedDevice == null) { audioManger.clearCommunicationDevice() - } else { - audioManger.setCommunicationDevice(selectedDevice!!.deviceInfo) - } + } else { + audioManger.setCommunicationDevice(selectedDevice!!.deviceInfo) + } } else { if (selectedDevice == null) { audioManger.stopBluetoothSco() @@ -104,6 +104,7 @@ class AudioRecorderService: IntervalRecorderService() { super.stop() resetRecorder() + selectedDevice = null } override fun getAmplitudeAmount(): Int = amplitudesAmount 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 9cc6a4c..e8a550c 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 @@ -168,10 +168,13 @@ fun RecordingStatus( if (microphones.isNotEmpty()) { MicrophoneSelection( microphones = microphones, - selectedMicrophone = audioRecorder.recorderService!!.selectedDevice, + selectedMicrophone = audioRecorder.selectedDevice, onSelect = { audioRecorder.changeMicrophone(it) - audioRecorder.recorderService!!.startNewCycle() + + if (!audioRecorder.isPaused) { + audioRecorder.recorderService!!.startNewCycle() + } } ) } From 69b4207124423865160fbdf82e7379c65de85faa Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 19:17:51 +0200 Subject: [PATCH 06/29] fix(ci-cd): Upload all APKs --- .github/workflows/build-testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-testing.yaml b/.github/workflows/build-testing.yaml index c2793e9..680ea90 100644 --- a/.github/workflows/build-testing.yaml +++ b/.github/workflows/build-testing.yaml @@ -25,4 +25,4 @@ jobs: - name: Upload APK uses: actions/upload-artifact@v3 with: - path: app/build/outputs/apk/debug/app-debug.apk + path: app/build/outputs/apk/debug/app-*-debug.apk From 027e41d6b63a74fb0dc35734b223daea73b5fa83 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 19:23:04 +0200 Subject: [PATCH 07/29] fix: Improve microphone selection --- .../alibi/services/AudioRecorderService.kt | 21 +++++++------- .../organisms/RecordingStatus.kt | 29 +------------------ 2 files changed, 12 insertions(+), 38 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 07d9fc2..ade478f 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -1,24 +1,20 @@ package app.myzel394.alibi.services -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.media.AudioDeviceInfo import android.media.AudioManager import android.media.MediaRecorder import android.media.MediaRecorder.OnErrorListener -import android.media.MediaRecorder.getAudioSourceMax import android.os.Build import app.myzel394.alibi.ui.utils.MicrophoneInfo import java.lang.IllegalStateException class AudioRecorderService : IntervalRecorderService() { var amplitudesAmount = 1000 - var selectedDevice: MicrophoneInfo? = null + var selectedMicrophone: MicrophoneInfo? = null var recorder: MediaRecorder? = null private set var onError: () -> Unit = {} + var onSelectedMicrophoneChange: (MicrophoneInfo?) -> Unit = {} val filePath: String get() = "$folder/$counter.${settings!!.fileExtension}" @@ -27,13 +23,13 @@ class AudioRecorderService : IntervalRecorderService() { val audioManger = getSystemService(AUDIO_SERVICE)!! as AudioManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (selectedDevice == null) { + if (selectedMicrophone == null) { audioManger.clearCommunicationDevice() } else { - audioManger.setCommunicationDevice(selectedDevice!!.deviceInfo) + audioManger.setCommunicationDevice(selectedMicrophone!!.deviceInfo) } } else { - if (selectedDevice == null) { + if (selectedMicrophone == null) { audioManger.stopBluetoothSco() } else { audioManger.startBluetoothSco() @@ -104,7 +100,7 @@ class AudioRecorderService : IntervalRecorderService() { super.stop() resetRecorder() - selectedDevice = null + selectedMicrophone = null } override fun getAmplitudeAmount(): Int = amplitudesAmount @@ -118,4 +114,9 @@ class AudioRecorderService : IntervalRecorderService() { 0 } } + + fun changeMicrophone(microphone: MicrophoneInfo?) { + selectedMicrophone = microphone + onSelectedMicrophoneChange(microphone) + } } \ No newline at end of file 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 e8a550c..d928f09 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 @@ -1,10 +1,8 @@ package app.myzel394.alibi.ui.components.AudioRecorder.organisms import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.expandHorizontally -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,22 +11,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Pause -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.LargeFloatingActionButton import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -37,28 +21,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import app.myzel394.alibi.R -import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE -import app.myzel394.alibi.ui.components.AudioRecorder.atoms.ConfirmDeletionDialog import app.myzel394.alibi.ui.components.AudioRecorder.atoms.DeleteButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.PauseResumeButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RealtimeAudioVisualizer import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RecordingTime import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveButton import app.myzel394.alibi.ui.components.AudioRecorder.molecules.MicrophoneSelection -import app.myzel394.alibi.ui.components.atoms.Pulsating import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.utils.KeepScreenOn import app.myzel394.alibi.ui.utils.MicrophoneInfo -import app.myzel394.alibi.ui.utils.formatDuration import kotlinx.coroutines.delay import java.time.LocalDateTime @@ -168,7 +141,7 @@ fun RecordingStatus( if (microphones.isNotEmpty()) { MicrophoneSelection( microphones = microphones, - selectedMicrophone = audioRecorder.selectedDevice, + selectedMicrophone = audioRecorder.selectedMicrophone, onSelect = { audioRecorder.changeMicrophone(it) From 5b7ce77ad3e918e3341cc3170ce7afb85b253a81 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 19:23:53 +0200 Subject: [PATCH 08/29] fix: Improve microphone selection --- .../alibi/ui/models/AudioRecorderModel.kt | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) 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 d13bd84..2c1c718 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 @@ -16,13 +16,15 @@ import app.myzel394.alibi.services.AudioRecorderService import app.myzel394.alibi.services.RecorderService import app.myzel394.alibi.ui.utils.MicrophoneInfo -class AudioRecorderModel: ViewModel() { +class AudioRecorderModel : ViewModel() { var recorderState by mutableStateOf(RecorderState.IDLE) private set var recordingTime by mutableStateOf(null) private set var amplitudes by mutableStateOf>(emptyList()) private set + var selectedMicrophone by mutableStateOf(null) + private set var onAmplitudeChange: () -> Unit = {} @@ -46,28 +48,35 @@ class AudioRecorderModel: ViewModel() { private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { - recorderService = ((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also {recorder -> - recorder.onStateChange = { state -> - recorderState = state - } - recorder.onRecordingTimeChange = { time -> - recordingTime = time - } - recorder.onAmplitudeChange = { amps -> - amplitudes = amps - onAmplitudeChange() - } - recorder.onError = { - recorderService!!.createLastRecording() - onError() - } - }.also { - it.startRecording() + recorderService = + ((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also { recorder -> + // Update UI when the service changes + recorder.onStateChange = { state -> + recorderState = state + } + recorder.onRecordingTimeChange = { time -> + recordingTime = time + } + recorder.onAmplitudeChange = { amps -> + amplitudes = amps + onAmplitudeChange() + } + recorder.onError = { + recorderService!!.createLastRecording() + onError() + } + recorder.onSelectedMicrophoneChange = { microphone -> + selectedMicrophone = microphone + } + }.also { + // Init UI from the service + it.startRecording() - recorderState = it.state - recordingTime = it.recordingTime - amplitudes = it.amplitudes - } + recorderState = it.state + recordingTime = it.recordingTime + amplitudes = it.amplitudes + selectedMicrophone = it.selectedMicrophone + } } override fun onServiceDisconnected(arg0: ComponentName) { @@ -80,6 +89,7 @@ class AudioRecorderModel: ViewModel() { recorderState = RecorderState.IDLE recordingTime = null amplitudes = emptyList() + selectedMicrophone = null } fun startRecording(context: Context) { @@ -120,7 +130,7 @@ class AudioRecorderModel: ViewModel() { } fun changeMicrophone(microphone: MicrophoneInfo?) { - recorderService!!.selectedDevice = microphone + recorderService!!.changeMicrophone(microphone) } fun bindToService(context: Context) { From 825f0eb33f02c904e68a9745c5a491bbcee11b68 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 19:37:17 +0200 Subject: [PATCH 09/29] fix: Properly stop RecorderService onDestroy --- .../alibi/services/AudioRecorderService.kt | 31 +++++++----- .../alibi/services/RecorderService.kt | 47 +++++++++++++------ 2 files changed, 50 insertions(+), 28 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 ade478f..0a39a03 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -19,28 +19,31 @@ class AudioRecorderService : IntervalRecorderService() { val filePath: String get() = "$folder/$counter.${settings!!.fileExtension}" - private fun _setAudioDevice() { + private fun clearAudioDevice() { val audioManger = getSystemService(AUDIO_SERVICE)!! as AudioManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (selectedMicrophone == null) { - audioManger.clearCommunicationDevice() - } else { - audioManger.setCommunicationDevice(selectedMicrophone!!.deviceInfo) - } + audioManger.clearCommunicationDevice() } else { - if (selectedMicrophone == null) { - audioManger.stopBluetoothSco() - } else { - audioManger.startBluetoothSco() - } + audioManger.stopBluetoothSco() + } + } + + private fun startAudioDevice() { + if (selectedMicrophone == null) { + return } + val audioManger = getSystemService(AUDIO_SERVICE)!! as AudioManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + audioManger.setCommunicationDevice(selectedMicrophone!!.deviceInfo) + } else { + audioManger.startBluetoothSco() + } } private fun createRecorder(): MediaRecorder { - _setAudioDevice() - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { MediaRecorder(this) } else { @@ -70,6 +73,7 @@ class AudioRecorderService : IntervalRecorderService() { it.stop() it.release() } + clearAudioDevice() } } @@ -81,6 +85,7 @@ class AudioRecorderService : IntervalRecorderService() { } resetRecorder() + startAudioDevice() try { recorder = newRecorder diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt index 939f933..2f27783 100644 --- a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt @@ -23,7 +23,7 @@ import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit -abstract class RecorderService: Service() { +abstract class RecorderService : Service() { private val binder = RecorderBinder() private var isPaused: Boolean = false @@ -61,7 +61,7 @@ abstract class RecorderService: Service() { return super.onStartCommand(intent, flags, startId) } - inner class RecorderBinder: Binder() { + inner class RecorderBinder : Binder() { fun getService(): RecorderService = this@RecorderService } @@ -95,10 +95,12 @@ abstract class RecorderService: Service() { start() } } + RecorderState.PAUSED -> { pause() isPaused = true } + RecorderState.IDLE -> { stop() onDestroy() @@ -109,6 +111,7 @@ abstract class RecorderService: Service() { RecorderState.RECORDING -> { createRecordingTimeTimer() } + RecorderState.PAUSED, RecorderState.IDLE -> { recordingTimeTimer.shutdown() } @@ -121,7 +124,7 @@ abstract class RecorderService: Service() { RecorderState.PAUSED ).contains(newState) && PermissionHelper.hasGranted(this, android.Manifest.permission.POST_NOTIFICATIONS) - ){ + ) { val notification = buildNotification() NotificationManagerCompat.from(this).notify( NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID, @@ -145,22 +148,28 @@ abstract class RecorderService: Service() { override fun onDestroy() { super.onDestroy() + stop() changeState(RecorderState.IDLE) stopForeground(STOP_FOREGROUND_REMOVE) - NotificationManagerCompat.from(this).cancel(NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID) + NotificationManagerCompat.from(this) + .cancel(NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID) stopSelf() } - private fun buildStartNotification(): Notification = NotificationCompat.Builder(this, NotificationHelper.RECORDER_CHANNEL_ID) - .setContentTitle(getString(R.string.ui_audioRecorder_state_recording_title)) - .setContentText(getString(R.string.ui_audioRecorder_state_recording_description)) - .setSmallIcon(R.drawable.launcher_foreground) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .build() + private fun buildStartNotification(): Notification = + NotificationCompat.Builder(this, NotificationHelper.RECORDER_CHANNEL_ID) + .setContentTitle(getString(R.string.ui_audioRecorder_state_recording_title)) + .setContentText(getString(R.string.ui_audioRecorder_state_recording_description)) + .setSmallIcon(R.drawable.launcher_foreground) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .build() - private fun getNotificationChangeStateIntent(newState: RecorderState, requestCode: Int): PendingIntent { + private fun getNotificationChangeStateIntent( + newState: RecorderState, + requestCode: Int + ): PendingIntent { return PendingIntent.getService( this, requestCode, @@ -172,8 +181,11 @@ abstract class RecorderService: Service() { ) } - private fun buildNotification(): Notification = when(state) { - RecorderState.RECORDING -> NotificationCompat.Builder(this, NotificationHelper.RECORDER_CHANNEL_ID) + private fun buildNotification(): Notification = when (state) { + RecorderState.RECORDING -> NotificationCompat.Builder( + this, + NotificationHelper.RECORDER_CHANNEL_ID + ) .setContentTitle(getString(R.string.ui_audioRecorder_state_recording_title)) .setContentText(getString(R.string.ui_audioRecorder_state_recording_description)) .setSmallIcon(R.drawable.launcher_foreground) @@ -211,7 +223,11 @@ abstract class RecorderService: Service() { getNotificationChangeStateIntent(RecorderState.PAUSED, 2), ) .build() - RecorderState.PAUSED -> NotificationCompat.Builder(this, NotificationHelper.RECORDER_CHANNEL_ID) + + RecorderState.PAUSED -> NotificationCompat.Builder( + this, + NotificationHelper.RECORDER_CHANNEL_ID + ) .setContentTitle(getString(R.string.ui_audioRecorder_state_paused_title)) .setContentText(getString(R.string.ui_audioRecorder_state_paused_description)) .setSmallIcon(R.drawable.launcher_foreground) @@ -236,6 +252,7 @@ abstract class RecorderService: Service() { getNotificationChangeStateIntent(RecorderState.RECORDING, 3), ) .build() + else -> throw IllegalStateException("Invalid state passed to `buildNotification()`") } } \ No newline at end of file From 78453f1c4d6570b2845bc2cc7667299da1ce1c15 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 20:37:27 +0200 Subject: [PATCH 10/29] feat: Add microphone connectivity status --- .../alibi/services/AudioRecorderService.kt | 64 +++++++++++++++++++ .../alibi/ui/models/AudioRecorderModel.kt | 19 ++++++ app/src/main/res/values/strings.xml | 2 + 3 files changed, 85 insertions(+) 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 0a39a03..f95b913 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -1,11 +1,20 @@ 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.os.Build +import android.os.Handler +import android.os.Looper +import androidx.core.content.ContextCompat.getSystemService +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 @@ -15,6 +24,8 @@ class AudioRecorderService : IntervalRecorderService() { private set var onError: () -> Unit = {} var onSelectedMicrophoneChange: (MicrophoneInfo?) -> Unit = {} + var onMicrophoneDisconnected: () -> Unit = {} + var onMicrophoneReconnected: () -> Unit = {} val filePath: String get() = "$folder/$counter.${settings!!.fileExtension}" @@ -95,6 +106,12 @@ class AudioRecorderService : IntervalRecorderService() { } } + override fun start() { + super.start() + + registerMicrophoneListener() + } + override fun pause() { super.pause() @@ -106,6 +123,7 @@ class AudioRecorderService : IntervalRecorderService() { resetRecorder() selectedMicrophone = null + unregisterMicrophoneListener() } override fun getAmplitudeAmount(): Int = amplitudesAmount @@ -123,5 +141,51 @@ class AudioRecorderService : IntervalRecorderService() { fun changeMicrophone(microphone: MicrophoneInfo?) { selectedMicrophone = microphone onSelectedMicrophoneChange(microphone) + + if (state == RecorderState.RECORDING) { + startNewCycle() + } + } + + private val audioDeviceCallback = object : AudioDeviceCallback() { + override fun onAudioDevicesAdded(addedDevices: Array?) { + super.onAudioDevicesAdded(addedDevices) + + if (selectedMicrophone == null) { + return; + } + + if (addedDevices?.find { it.id == selectedMicrophone!!.deviceInfo.id } != null) { + onMicrophoneReconnected() + } + } + + override fun onAudioDevicesRemoved(removedDevices: Array?) { + super.onAudioDevicesRemoved(removedDevices) + + if (selectedMicrophone == null) { + return; + } + + if (removedDevices?.find { it.id == selectedMicrophone!!.deviceInfo.id } != null) { + onMicrophoneDisconnected() + } + } + } + + @SuppressLint("NewApi") + private fun registerMicrophoneListener() { + val audioManager = getSystemService(Context.AUDIO_SERVICE)!! as AudioManager + + audioManager.registerAudioDeviceCallback( + audioDeviceCallback, + Handler(Looper.getMainLooper()) + ) + } + + private fun unregisterMicrophoneListener() { + val audioManager = getSystemService(Context.AUDIO_SERVICE)!! as AudioManager + + audioManager.unregisterAudioDeviceCallback(audioDeviceCallback) } } \ No newline at end of file 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 2c1c718..6fcb718 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 @@ -46,6 +46,14 @@ class AudioRecorderModel : ViewModel() { var onRecordingSave: () -> Unit = {} var onError: () -> Unit = {} + var microphoneStatus: MicrophoneConnectivityStatus = MicrophoneConnectivityStatus.CONNECTED + private set + + enum class MicrophoneConnectivityStatus { + CONNECTED, + DISCONNECTED + } + private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { recorderService = @@ -68,6 +76,12 @@ class AudioRecorderModel : ViewModel() { recorder.onSelectedMicrophoneChange = { microphone -> selectedMicrophone = microphone } + recorder.onMicrophoneDisconnected = { + microphoneStatus = MicrophoneConnectivityStatus.DISCONNECTED + } + recorder.onMicrophoneReconnected = { + microphoneStatus = MicrophoneConnectivityStatus.CONNECTED + } }.also { // Init UI from the service it.startRecording() @@ -90,6 +104,7 @@ class AudioRecorderModel : ViewModel() { recordingTime = null amplitudes = emptyList() selectedMicrophone = null + microphoneStatus = MicrophoneConnectivityStatus.CONNECTED } fun startRecording(context: Context) { @@ -131,6 +146,10 @@ class AudioRecorderModel : ViewModel() { fun changeMicrophone(microphone: MicrophoneInfo?) { recorderService!!.changeMicrophone(microphone) + + if (microphone == null) { + microphoneStatus = MicrophoneConnectivityStatus.CONNECTED + } } fun bindToService(context: Context) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fc63b3a..653871b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -65,4 +65,6 @@ Change Device Microphone The selected microphone will be immediately activated + Microphone disconnected + %s disconnected. Alibi will use the default microphone instead. We will automatically switch back to %s once it reconnects. \ No newline at end of file From 07757f34bb90b861eaab5aa580e2e5787ce667b8 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 20:37:35 +0200 Subject: [PATCH 11/29] feat: Add MicrophoneDisconnectedDialog --- .../atoms/MicrophoneDisconnectedDialog.kt | 62 +++++++++++++++++++ .../organisms/RecordingStatus.kt | 38 +++++++++--- .../alibi/ui/effects/remember-previous.kt | 41 ++++++++++++ 3 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneDisconnectedDialog.kt create mode 100644 app/src/main/java/app/myzel394/alibi/ui/effects/remember-previous.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneDisconnectedDialog.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneDisconnectedDialog.kt new file mode 100644 index 0000000..338293b --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneDisconnectedDialog.kt @@ -0,0 +1,62 @@ +package app.myzel394.alibi.ui.components.AudioRecorder.atoms + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MicOff +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import app.myzel394.alibi.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MicrophoneDisconnectedDialog( + microphoneName: String, + onClose: () -> Unit, +) { + AlertDialog( + onDismissRequest = onClose, + title = { + Text( + stringResource( + R.string.ui_audioRecorder_error_microphoneDisconnected_title, + ), + textAlign = TextAlign.Center, + ) + }, + text = { + Text( + stringResource( + R.string.ui_audioRecorder_error_microphoneDisconnected_message, + microphoneName, + ) + ) + }, + icon = { + Icon( + Icons.Default.MicOff, + contentDescription = null, + ) + }, + confirmButton = { + val label = stringResource(R.string.dialog_close_neutral_label) + + Button( + modifier = Modifier + .semantics { + contentDescription = label + }, + onClick = onClose, + ) { + Text(label) + } + } + ) +} \ No newline at end of file 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 d928f09..c2c1cb2 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 @@ -24,11 +24,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import app.myzel394.alibi.ui.components.AudioRecorder.atoms.DeleteButton +import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneDisconnectedDialog import app.myzel394.alibi.ui.components.AudioRecorder.atoms.PauseResumeButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RealtimeAudioVisualizer import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RecordingTime import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveButton import app.myzel394.alibi.ui.components.AudioRecorder.molecules.MicrophoneSelection +import app.myzel394.alibi.ui.effects.rememberPrevious import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.utils.KeepScreenOn import app.myzel394.alibi.ui.utils.MicrophoneInfo @@ -137,19 +139,41 @@ fun RecordingStatus( val microphones = MicrophoneInfo.fetchDeviceMicrophones(context) + val microphoneStatus = audioRecorder.microphoneStatus + val previousStatus = rememberPrevious(microphoneStatus) + + var showMicrophoneStatusDialog by remember { + // null = no dialog + // `MicrophoneConnectivityStatus.CONNECTED` = Reconnected dialog + // `MicrophoneConnectivityStatus.DISCONNECTED` = Disconnected dialog + mutableStateOf(null) + } + + LaunchedEffect(microphoneStatus) { + println(microphoneStatus) + println(previousStatus) + if (microphoneStatus != previousStatus && showMicrophoneStatusDialog == null && previousStatus != null) { + showMicrophoneStatusDialog = microphoneStatus + } + } + + if (showMicrophoneStatusDialog == AudioRecorderModel.MicrophoneConnectivityStatus.DISCONNECTED) { + MicrophoneDisconnectedDialog( + onClose = { + showMicrophoneStatusDialog = null + }, + microphoneName = audioRecorder.selectedMicrophone?.name ?: "", + ) + } if (microphones.isNotEmpty()) { MicrophoneSelection( microphones = microphones, selectedMicrophone = audioRecorder.selectedMicrophone, - onSelect = { - audioRecorder.changeMicrophone(it) - - if (!audioRecorder.isPaused) { - audioRecorder.recorderService!!.startNewCycle() - } - } + onSelect = audioRecorder::changeMicrophone, ) + } else { + Box {} } } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/effects/remember-previous.kt b/app/src/main/java/app/myzel394/alibi/ui/effects/remember-previous.kt new file mode 100644 index 0000000..7dc2adc --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/effects/remember-previous.kt @@ -0,0 +1,41 @@ +package app.myzel394.alibi.ui.effects + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember + +/** + * Returns a dummy MutableState that does not cause render when setting it + */ +@Composable +private fun rememberRef(): MutableState { + // for some reason it always recreated the value with vararg keys, + // leaving out the keys as a parameter for remember for now + return remember() { + object : MutableState { + override var value: T? = null + + override fun component1(): T? = value + + override fun component2(): (T?) -> Unit = { value = it } + } + } +} + +@Composable +fun rememberPrevious( + current: T, + shouldUpdate: (prev: T?, curr: T) -> Boolean = { a: T?, b: T -> a != b }, +): T? { + val ref = rememberRef() + + // launched after render, so the current render will have the old value anyway + SideEffect { + if (shouldUpdate(ref.value, current)) { + ref.value = current + } + } + + return ref.value +} \ No newline at end of file From 14abd1aee05b08d304ab2061d4bb5541f43f8a65 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 21:37:03 +0200 Subject: [PATCH 12/29] feat: Add MicrophoneReconnectedDialog functionality --- .../alibi/services/AudioRecorderService.kt | 14 ++++- .../atoms/MicrophoneDisconnectedDialog.kt | 1 + .../atoms/MicrophoneReconnectedDialog.kt | 63 +++++++++++++++++++ .../organisms/RecordingStatus.kt | 10 +++ app/src/main/res/values/strings.xml | 4 +- 5 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneReconnectedDialog.kt 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 f95b913..0ba3e88 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -155,7 +155,19 @@ class AudioRecorderService : IntervalRecorderService() { return; } - if (addedDevices?.find { it.id == selectedMicrophone!!.deviceInfo.id } != null) { + // We can't compare the ID, as it seems to be changing on each reconnect + val newDevice = addedDevices?.find { + it.productName == selectedMicrophone!!.deviceInfo.productName && + it.isSink == selectedMicrophone!!.deviceInfo.isSink && + it.type == selectedMicrophone!!.deviceInfo.type && ( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + it.address == selectedMicrophone!!.deviceInfo.address + } else true + ) + } + if (newDevice != null) { + changeMicrophone(MicrophoneInfo.fromDeviceInfo(newDevice)) + onMicrophoneReconnected() } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneDisconnectedDialog.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneDisconnectedDialog.kt index 338293b..e15cba5 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneDisconnectedDialog.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneDisconnectedDialog.kt @@ -36,6 +36,7 @@ fun MicrophoneDisconnectedDialog( stringResource( R.string.ui_audioRecorder_error_microphoneDisconnected_message, microphoneName, + microphoneName, ) ) }, diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneReconnectedDialog.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneReconnectedDialog.kt new file mode 100644 index 0000000..b8825ac --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneReconnectedDialog.kt @@ -0,0 +1,63 @@ +package app.myzel394.alibi.ui.components.AudioRecorder.atoms + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MicOff +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import app.myzel394.alibi.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MicrophoneReconnectedDialog( + microphoneName: String, + onClose: () -> Unit, +) { + AlertDialog( + onDismissRequest = onClose, + title = { + Text( + stringResource( + R.string.ui_audioRecorder_error_microphoneReconnected_title, + ), + textAlign = TextAlign.Center, + ) + }, + text = { + Text( + stringResource( + R.string.ui_audioRecorder_error_microphoneReconnected_message, + microphoneName, + ) + ) + }, + icon = { + Icon( + Icons.Default.Star, + contentDescription = null, + ) + }, + confirmButton = { + val label = stringResource(R.string.dialog_close_neutral_label) + + Button( + modifier = Modifier + .semantics { + contentDescription = label + }, + onClick = onClose, + ) { + Text(label) + } + } + ) +} \ No newline at end of file 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 c2c1cb2..930afc6 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 @@ -25,6 +25,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import app.myzel394.alibi.ui.components.AudioRecorder.atoms.DeleteButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneDisconnectedDialog +import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneReconnectedDialog import app.myzel394.alibi.ui.components.AudioRecorder.atoms.PauseResumeButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RealtimeAudioVisualizer import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RecordingTime @@ -166,6 +167,15 @@ fun RecordingStatus( ) } + if (showMicrophoneStatusDialog == AudioRecorderModel.MicrophoneConnectivityStatus.CONNECTED) { + MicrophoneReconnectedDialog( + onClose = { + showMicrophoneStatusDialog = null + }, + microphoneName = audioRecorder.selectedMicrophone?.name ?: "", + ) + } + if (microphones.isNotEmpty()) { MicrophoneSelection( microphones = microphones, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 653871b..607a281 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -66,5 +66,7 @@ Device Microphone The selected microphone will be immediately activated Microphone disconnected - %s disconnected. Alibi will use the default microphone instead. We will automatically switch back to %s once it reconnects. + %s disconnected. Alibi will use the default microphone instead. We will automatically switch back to %s once it reconnects. + Microphone reconnected + %s reconnected! Alibi automatically changed the microphone input to it. \ No newline at end of file From e4e8ae0158a666d568e116d7fda19baef3408eeb Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 22:14:53 +0200 Subject: [PATCH 13/29] fix: Bugfixes for recording behavior --- .../java/app/myzel394/alibi/db/AppSettings.kt | 18 +++++++-------- .../alibi/services/AudioRecorderService.kt | 22 +++++++++---------- .../alibi/services/RecorderService.kt | 2 -- .../organisms/RecordingStatus.kt | 2 -- .../alibi/ui/utils/available-microphones.kt | 17 +++++++++----- 5 files changed, 32 insertions(+), 29 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 89df173..f7fdfbd 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -154,7 +154,7 @@ data class AudioRecorderSettings( else MediaRecorder.OutputFormat.THREE_GPP } - return when(encoder) { + return when (encoder) { MediaRecorder.AudioEncoder.AAC -> MediaRecorder.OutputFormat.AAC_ADTS MediaRecorder.AudioEncoder.AAC_ELD -> MediaRecorder.OutputFormat.AAC_ADTS MediaRecorder.AudioEncoder.AMR_NB -> MediaRecorder.OutputFormat.AMR_NB @@ -167,6 +167,7 @@ data class AudioRecorderSettings( MediaRecorder.OutputFormat.AAC_ADTS } } + MediaRecorder.AudioEncoder.OPUS -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { MediaRecorder.OutputFormat.OGG @@ -174,11 +175,12 @@ data class AudioRecorderSettings( MediaRecorder.OutputFormat.AAC_ADTS } } + else -> MediaRecorder.OutputFormat.DEFAULT } } - fun getMimeType(): String = when(getOutputFormat()) { + fun getMimeType(): String = when (getOutputFormat()) { MediaRecorder.OutputFormat.AAC_ADTS -> "audio/aac" MediaRecorder.OutputFormat.THREE_GPP -> "audio/3gpp" MediaRecorder.OutputFormat.MPEG_4 -> "audio/mp4" @@ -190,7 +192,7 @@ data class AudioRecorderSettings( else -> "audio/3gpp" } - fun getSamplingRate(): Int = samplingRate ?: when(getOutputFormat()) { + fun getSamplingRate(): Int = samplingRate ?: when (getOutputFormat()) { MediaRecorder.OutputFormat.AAC_ADTS -> 96000 MediaRecorder.OutputFormat.THREE_GPP -> 44100 MediaRecorder.OutputFormat.MPEG_4 -> 44100 @@ -202,11 +204,10 @@ data class AudioRecorderSettings( else -> 48000 } - fun getEncoder(): Int = encoder ?: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - MediaRecorder.AudioEncoder.AAC - else - MediaRecorder.AudioEncoder.AMR_NB + fun getEncoder(): Int = encoder ?: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + MediaRecorder.AudioEncoder.AAC + else + MediaRecorder.AudioEncoder.AMR_NB fun setIntervalDuration(duration: Long): AudioRecorderSettings { if (duration < 10 * 1000L || duration > 60 * 60 * 1000L) { @@ -221,7 +222,6 @@ data class AudioRecorderSettings( } fun setBitRate(bitRate: Int): AudioRecorderSettings { - println("bitRate: $bitRate") if (bitRate !in 1000..320000) { throw Exception("Bit rate must be between 1000 and 320000") } 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 0ba3e88..2c5fcf3 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -30,16 +30,7 @@ class AudioRecorderService : IntervalRecorderService() { val filePath: String get() = "$folder/$counter.${settings!!.fileExtension}" - private fun clearAudioDevice() { - val audioManger = getSystemService(AUDIO_SERVICE)!! as AudioManager - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - audioManger.clearCommunicationDevice() - } else { - audioManger.stopBluetoothSco() - } - } - + /// Tell Android to use the correct bluetooth microphone, if any selected private fun startAudioDevice() { if (selectedMicrophone == null) { return @@ -54,6 +45,16 @@ class AudioRecorderService : IntervalRecorderService() { } } + private fun clearAudioDevice() { + val audioManger = getSystemService(AUDIO_SERVICE)!! as AudioManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + audioManger.clearCommunicationDevice() + } else { + audioManger.stopBluetoothSco() + } + } + private fun createRecorder(): MediaRecorder { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { MediaRecorder(this) @@ -185,7 +186,6 @@ class AudioRecorderService : IntervalRecorderService() { } } - @SuppressLint("NewApi") private fun registerMicrophoneListener() { val audioManager = getSystemService(Context.AUDIO_SERVICE)!! as AudioManager diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt index 2f27783..daadb7a 100644 --- a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt @@ -102,7 +102,6 @@ abstract class RecorderService : Service() { } RecorderState.IDLE -> { - stop() onDestroy() } } @@ -149,7 +148,6 @@ abstract class RecorderService : Service() { super.onDestroy() stop() - changeState(RecorderState.IDLE) stopForeground(STOP_FOREGROUND_REMOVE) NotificationManagerCompat.from(this) 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 930afc6..36d1ebf 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 @@ -151,8 +151,6 @@ fun RecordingStatus( } LaunchedEffect(microphoneStatus) { - println(microphoneStatus) - println(previousStatus) if (microphoneStatus != previousStatus && showMicrophoneStatusDialog == null && previousStatus != null) { showMicrophoneStatusDialog = microphoneStatus } diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt index cb9ebe1..b804f24 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt @@ -1,9 +1,9 @@ package app.myzel394.alibi.ui.utils -import android.annotation.SuppressLint import android.content.Context import android.media.AudioDeviceInfo import android.media.AudioManager +import android.os.Build data class MicrophoneInfo( val deviceInfo: AudioDeviceInfo, @@ -24,12 +24,19 @@ data class MicrophoneInfo( return MicrophoneInfo(deviceInfo) } - @SuppressLint("NewApi") fun fetchDeviceMicrophones(context: Context): List { val audioManager = context.getSystemService(Context.AUDIO_SERVICE)!! as AudioManager - return audioManager.availableCommunicationDevices.let { - it.subList(2, it.size) - }.map(::fromDeviceInfo) + val mics = + audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).map(::fromDeviceInfo) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + audioManager.availableCommunicationDevices.let { + it.subList(2, it.size) + }.map(::fromDeviceInfo) + } else { + audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).let { + it.slice(1 until it.size) + }.map(::fromDeviceInfo) + } } } From a4edfa539fb2d312f967ecca04a15753a338df20 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 22:50:15 +0200 Subject: [PATCH 14/29] fix: Filter out microphones better --- .../alibi/ui/utils/available-microphones.kt | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt index b804f24..1c7fe84 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt @@ -5,6 +5,33 @@ import android.media.AudioDeviceInfo import android.media.AudioManager import android.os.Build +val ALLOWED_MICROPHONE_TYPES = + setOf( + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + AudioDeviceInfo.TYPE_USB_DEVICE, + AudioDeviceInfo.TYPE_USB_ACCESSORY, + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, + AudioDeviceInfo.TYPE_IP, + AudioDeviceInfo.TYPE_DOCK, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + AudioDeviceInfo.TYPE_DOCK_ANALOG + } else { + }, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + AudioDeviceInfo.TYPE_BLE_HEADSET + } else { + }, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + AudioDeviceInfo.TYPE_REMOTE_SUBMIX + } else { + }, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + AudioDeviceInfo.TYPE_USB_HEADSET + } else { + }, + ) + data class MicrophoneInfo( val deviceInfo: AudioDeviceInfo, ) { @@ -26,16 +53,16 @@ data class MicrophoneInfo( fun fetchDeviceMicrophones(context: Context): List { val audioManager = context.getSystemService(Context.AUDIO_SERVICE)!! as AudioManager - val mics = - audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).map(::fromDeviceInfo) - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - audioManager.availableCommunicationDevices.let { - it.subList(2, it.size) - }.map(::fromDeviceInfo) + return (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + audioManager.availableCommunicationDevices.map(::fromDeviceInfo) } else { - audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).let { - it.slice(1 until it.size) - }.map(::fromDeviceInfo) + audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).map(::fromDeviceInfo) + }).filter { + ALLOWED_MICROPHONE_TYPES.contains(it.deviceInfo.type) && it.deviceInfo.isSink + }.also { + it.forEach { + println("Microphone: ${it.name} - ${it.deviceInfo.type}") + } } } } From c73f2c31897d5bd7a4b95dac1dca4381669d11ee Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 22:56:52 +0200 Subject: [PATCH 15/29] fix: change RecorderService state onDestroy --- .../main/java/app/myzel394/alibi/services/RecorderService.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt index daadb7a..36746cc 100644 --- a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt @@ -101,9 +101,7 @@ abstract class RecorderService : Service() { isPaused = true } - RecorderState.IDLE -> { - onDestroy() - } + else -> {} } when (newState) { @@ -148,6 +146,7 @@ abstract class RecorderService : Service() { super.onDestroy() stop() + changeState(RecorderState.IDLE) stopForeground(STOP_FOREGROUND_REMOVE) NotificationManagerCompat.from(this) From e9e83e00d142fb22e6b91d0be1bce56d5388bd1c Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:01:44 +0200 Subject: [PATCH 16/29] fix(i18n): Improve emphasis --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 607a281..f93dd1b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,7 +64,7 @@ Language Change Device Microphone - The selected microphone will be immediately activated + The selected microphone will be activated immediately Microphone disconnected %s disconnected. Alibi will use the default microphone instead. We will automatically switch back to %s once it reconnects. Microphone reconnected From dc7a5648a551d09531923e80ac76320de3723286 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:14:36 +0200 Subject: [PATCH 17/29] chore: Remove debugging --- .../java/app/myzel394/alibi/ui/utils/available-microphones.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt index 1c7fe84..23991b5 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt @@ -59,10 +59,6 @@ data class MicrophoneInfo( audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).map(::fromDeviceInfo) }).filter { ALLOWED_MICROPHONE_TYPES.contains(it.deviceInfo.type) && it.deviceInfo.isSink - }.also { - it.forEach { - println("Microphone: ${it.name} - ${it.deviceInfo.type}") - } } } } From 07f3c49a8835a1ef19017083dba37ba016776432 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:30:39 +0200 Subject: [PATCH 18/29] feat: Return empty list on error for fetchDeviceMicrophones --- .../alibi/ui/utils/available-microphones.kt | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt index 23991b5..8020ecb 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt @@ -4,6 +4,7 @@ import android.content.Context import android.media.AudioDeviceInfo import android.media.AudioManager import android.os.Build +import android.util.Log val ALLOWED_MICROPHONE_TYPES = setOf( @@ -52,13 +53,19 @@ data class MicrophoneInfo( } fun fetchDeviceMicrophones(context: Context): List { - val audioManager = context.getSystemService(Context.AUDIO_SERVICE)!! as AudioManager - return (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - audioManager.availableCommunicationDevices.map(::fromDeviceInfo) - } else { - audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).map(::fromDeviceInfo) - }).filter { - ALLOWED_MICROPHONE_TYPES.contains(it.deviceInfo.type) && it.deviceInfo.isSink + return try { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE)!! as AudioManager + (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + audioManager.availableCommunicationDevices.map(::fromDeviceInfo) + } else { + audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).map(::fromDeviceInfo) + }).filter { + ALLOWED_MICROPHONE_TYPES.contains(it.deviceInfo.type) && it.deviceInfo.isSink + } + } catch (error: Exception) { + Log.getStackTraceString(error) + + emptyList() } } } From 3f72efc8e6cf0a33df7e92577d67922582a598c0 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:37:41 +0200 Subject: [PATCH 19/29] refactor: Small code improvement --- .../app/myzel394/alibi/ui/utils/available-microphones.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt index 8020ecb..3601a5c 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt @@ -55,11 +55,13 @@ data class MicrophoneInfo( fun fetchDeviceMicrophones(context: Context): List { return try { val audioManager = context.getSystemService(Context.AUDIO_SERVICE)!! as AudioManager - (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val availableDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { audioManager.availableCommunicationDevices.map(::fromDeviceInfo) } else { audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).map(::fromDeviceInfo) - }).filter { + } + + availableDevices.filter { ALLOWED_MICROPHONE_TYPES.contains(it.deviceInfo.type) && it.deviceInfo.isSink } } catch (error: Exception) { From c15c4b59faf2288a4770bb2ed1a421ec9b759d9d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:47:04 +0200 Subject: [PATCH 20/29] feat: Allow user to show all hidden microphones --- .../java/app/myzel394/alibi/db/AppSettings.kt | 5 ++ .../organisms/RecordingStatus.kt | 17 +++++- .../atoms/ShowAllMicrophonesTile.kt | 57 +++++++++++++++++++ .../alibi/ui/screens/SettingsScreen.kt | 4 +- .../alibi/ui/utils/available-microphones.kt | 13 +++-- app/src/main/res/values/strings.xml | 2 + 6 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/ShowAllMicrophonesTile.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 f7fdfbd..0dc32f9 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -142,6 +142,7 @@ data class AudioRecorderSettings( val samplingRate: Int? = null, val outputFormat: Int? = null, val encoder: Int? = null, + val showAllMicrophones: Boolean = false, ) { fun getOutputFormat(): Int { if (outputFormat != null) { @@ -269,6 +270,10 @@ data class AudioRecorderSettings( return copy(forceExactMaxDuration = forceExactMaxDuration) } + fun setShowAllMicrophones(showAllMicrophones: Boolean): AudioRecorderSettings { + return copy(showAllMicrophones = showAllMicrophones) + } + fun isEncoderCompatible(encoder: Int): Boolean { if (outputFormat == null || outputFormat == MediaRecorder.OutputFormat.DEFAULT) { return true 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 36d1ebf..c395f39 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 @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.LinearProgressIndicator 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.remember @@ -23,6 +24,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import app.myzel394.alibi.dataStore +import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.components.AudioRecorder.atoms.DeleteButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneDisconnectedDialog import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneReconnectedDialog @@ -138,8 +141,18 @@ fun RecordingStatus( } } - - val microphones = MicrophoneInfo.fetchDeviceMicrophones(context) + val dataStore = LocalContext.current.dataStore + val settings = dataStore + .data + .collectAsState(initial = AppSettings.getDefaultInstance()) + .value + val microphones = MicrophoneInfo.fetchDeviceMicrophones(context).let { + if (settings.audioRecorderSettings.showAllMicrophones) { + it + } else { + MicrophoneInfo.filterMicrophones(it) + } + } val microphoneStatus = audioRecorder.microphoneStatus val previousStatus = rememberPrevious(microphoneStatus) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/ShowAllMicrophonesTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/ShowAllMicrophonesTile.kt new file mode 100644 index 0000000..f2a1d8e --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/ShowAllMicrophonesTile.kt @@ -0,0 +1,57 @@ +package app.myzel394.alibi.ui.components.SettingsScreen.atoms + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.GraphicEq +import androidx.compose.material.icons.filled.MicExternalOn +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 ShowAllMicrophonesTile() { + val scope = rememberCoroutineScope() + val dataStore = LocalContext.current.dataStore + val settings = dataStore + .data + .collectAsState(initial = AppSettings.getDefaultInstance()) + .value + + fun updateValue(showAllMicrophones: Boolean) { + scope.launch { + dataStore.updateData { + it.setAudioRecorderSettings( + it.audioRecorderSettings.setShowAllMicrophones(showAllMicrophones) + ) + } + } + } + + + SettingsTile( + title = stringResource(R.string.ui_settings_option_showAllMicrophones_title), + description = stringResource(R.string.ui_settings_option_showAllMicrophones_description), + leading = { + Icon( + Icons.Default.MicExternalOn, + contentDescription = null, + ) + }, + trailing = { + Switch( + checked = settings.audioRecorderSettings.showAllMicrophones, + onCheckedChange = ::updateValue, + ) + }, + ) +} \ No newline at end of file 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 a1db378..c12ad60 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 @@ -43,6 +43,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.ShowAllMicrophonesTile import app.myzel394.alibi.ui.components.atoms.GlobalSwitch import app.myzel394.alibi.ui.components.atoms.MessageBox import app.myzel394.alibi.ui.components.atoms.MessageType @@ -80,7 +81,7 @@ fun SettingsScreen( }, modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection) - ) {padding -> + ) { padding -> Column( modifier = Modifier .fillMaxSize() @@ -129,6 +130,7 @@ fun SettingsScreen( .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 32.dp) ) + ShowAllMicrophonesTile() BitrateTile() SamplingRateTile() EncoderTile(snackbarHostState = snackbarHostState) diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt index 3601a5c..ec6aa4a 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt @@ -55,21 +55,24 @@ data class MicrophoneInfo( fun fetchDeviceMicrophones(context: Context): List { return try { val audioManager = context.getSystemService(Context.AUDIO_SERVICE)!! as AudioManager - val availableDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { audioManager.availableCommunicationDevices.map(::fromDeviceInfo) } else { audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).map(::fromDeviceInfo) } - - availableDevices.filter { - ALLOWED_MICROPHONE_TYPES.contains(it.deviceInfo.type) && it.deviceInfo.isSink - } } catch (error: Exception) { Log.getStackTraceString(error) emptyList() } } + + /// Filter microphones to only show normal ones + fun filterMicrophones(microphones: List): List { + return microphones.filter { + ALLOWED_MICROPHONE_TYPES.contains(it.deviceInfo.type) && it.deviceInfo.isSink + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f93dd1b..c56fa4f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,4 +69,6 @@ %s disconnected. Alibi will use the default microphone instead. We will automatically switch back to %s once it reconnects. Microphone reconnected %s reconnected! Alibi automatically changed the microphone input to it. + Show hidden microphones + Show all microphones, including internal ones \ No newline at end of file From 38df00898dd03fafbb21187220a8b978b4cee32f Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 22 Oct 2023 00:04:37 +0200 Subject: [PATCH 21/29] feat: Show address for hidden microphones --- .../atoms/MicrophoneSelectionButton.kt | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt index 26cbd31..c38033a 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt @@ -1,7 +1,11 @@ package app.myzel394.alibi.ui.components.AudioRecorder.atoms +import android.os.Build import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -13,12 +17,16 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.myzel394.alibi.ui.utils.MicrophoneInfo import androidx.compose.ui.Alignment +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 @Composable fun MicrophoneSelectionButton( @@ -26,6 +34,12 @@ fun MicrophoneSelectionButton( selected: Boolean = false, onSelect: () -> Unit, ) { + val dataStore = LocalContext.current.dataStore + val settings = dataStore + .data + .collectAsState(initial = AppSettings.getDefaultInstance()) + .value + Button( onClick = onSelect, modifier = Modifier @@ -34,15 +48,27 @@ fun MicrophoneSelectionButton( colors = if (selected) ButtonDefaults.buttonColors( ) else ButtonDefaults.textButtonColors(), ) { - MicrophoneTypeInfo( - type = microphone?.type ?: MicrophoneInfo.MicrophoneType.PHONE, - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) - Text( - text = microphone?.name - ?: stringResource(R.string.ui_audioRecorder_info_microphone_deviceMicrophone), - fontSize = MaterialTheme.typography.bodyLarge.fontSize, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(ButtonDefaults.IconSpacing), + ) { + MicrophoneTypeInfo( + type = microphone?.type ?: MicrophoneInfo.MicrophoneType.PHONE, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Column { + Text( + text = microphone?.name + ?: stringResource(R.string.ui_audioRecorder_info_microphone_deviceMicrophone), + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && settings.audioRecorderSettings.showAllMicrophones && microphone?.deviceInfo?.address?.isNotBlank() == true) + Text( + microphone.deviceInfo.address.toString(), + fontSize = MaterialTheme.typography.bodySmall.toSpanStyle().fontSize, + color = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.secondary, + ) + } + } } } From 76e10a15122fd68826cdc33d18bd1a1f7e708b11 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 22 Oct 2023 00:23:02 +0200 Subject: [PATCH 22/29] refactor: Move microphone related stuff to MicrophoneStatus --- .../molecules/MicrophoneSelection.kt | 111 +++++++++++++----- .../molecules/MicrophoneStatus.kt | 61 ++++++++++ .../organisms/RecordingStatus.kt | 57 +-------- app/src/main/res/values/strings.xml | 1 + 4 files changed, 148 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneStatus.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt index 4530d82..e401c34 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt @@ -3,6 +3,7 @@ package app.myzel394.alibi.ui.components.AudioRecorder.molecules import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -13,22 +14,27 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable 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 app.myzel394.alibi.R +import app.myzel394.alibi.dataStore +import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneSelectionButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneTypeInfo import app.myzel394.alibi.ui.models.AudioRecorderModel @@ -37,15 +43,26 @@ import app.myzel394.alibi.ui.utils.MicrophoneInfo @OptIn(ExperimentalMaterial3Api::class) @Composable fun MicrophoneSelection( - microphones: List, - selectedMicrophone: MicrophoneInfo?, - onSelect: (MicrophoneInfo?) -> Unit, + audioRecorder: AudioRecorderModel ) { + val context = LocalContext.current + var showSelection by rememberSaveable { mutableStateOf(false) } val sheetState = rememberModalBottomSheetState() + val allMicrophones = MicrophoneInfo.fetchDeviceMicrophones(context) + val visibleMicrophones = MicrophoneInfo.filterMicrophones(allMicrophones) + val hiddenMicrophones = allMicrophones - visibleMicrophones.toSet() + + val dataStore = LocalContext.current.dataStore + val settings = dataStore + .data + .collectAsState(initial = AppSettings.getDefaultInstance()) + .value + + if (showSelection) { ModalBottomSheet( onDismissRequest = { @@ -73,26 +90,64 @@ fun MicrophoneSelection( ) { item { MicrophoneSelectionButton( - selected = selectedMicrophone == null, + selected = audioRecorder.selectedMicrophone == null, onSelect = { - onSelect(null) + audioRecorder.changeMicrophone(null) showSelection = false } ) } - items(microphones.size) { - val microphone = microphones[it] + items(visibleMicrophones.size) { + val microphone = visibleMicrophones[it] MicrophoneSelectionButton( microphone = microphone, - selected = selectedMicrophone == microphone, + selected = audioRecorder.selectedMicrophone == microphone, onSelect = { - onSelect(microphone) + audioRecorder.changeMicrophone(microphone) showSelection = false } ) } + + if (settings.audioRecorderSettings.showAllMicrophones) { + item { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(vertical = 32.dp), + ) { + Divider( + modifier = Modifier + .weight(1f) + ) + Text( + stringResource(R.string.ui_audioRecorder_info_microphone_hiddenMicrophones), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.tertiary, + textAlign = TextAlign.Center, + ) + Divider( + modifier = Modifier + .weight(1f), + ) + } + } + + items(hiddenMicrophones.size) { + val microphone = hiddenMicrophones[it] + + MicrophoneSelectionButton( + microphone = microphone, + selected = audioRecorder.selectedMicrophone == microphone, + onSelect = { + audioRecorder.changeMicrophone(microphone) + showSelection = false + } + ) + } + } } } } @@ -101,23 +156,25 @@ fun MicrophoneSelection( Box {} } - Button( - onClick = { - showSelection = true - }, - colors = ButtonDefaults.textButtonColors(), - ) { - MicrophoneTypeInfo( - type = selectedMicrophone?.type - ?: MicrophoneInfo.MicrophoneType.PHONE, - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) - Text( - text = selectedMicrophone.let { - it?.name - ?: stringResource(R.string.ui_audioRecorder_info_microphone_deviceMicrophone) - } - ) + if (visibleMicrophones.isNotEmpty() || (settings.audioRecorderSettings.showAllMicrophones && hiddenMicrophones.isNotEmpty())) { + Button( + onClick = { + showSelection = true + }, + colors = ButtonDefaults.textButtonColors(), + ) { + MicrophoneTypeInfo( + type = audioRecorder.selectedMicrophone?.type + ?: MicrophoneInfo.MicrophoneType.PHONE, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text( + text = audioRecorder.selectedMicrophone.let { + it?.name + ?: stringResource(R.string.ui_audioRecorder_info_microphone_deviceMicrophone) + } + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneStatus.kt new file mode 100644 index 0000000..b18c339 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneStatus.kt @@ -0,0 +1,61 @@ +package app.myzel394.alibi.ui.components.AudioRecorder.molecules + +import androidx.compose.foundation.layout.Box +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.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import app.myzel394.alibi.dataStore +import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneDisconnectedDialog +import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneReconnectedDialog +import app.myzel394.alibi.ui.effects.rememberPrevious +import app.myzel394.alibi.ui.models.AudioRecorderModel +import app.myzel394.alibi.ui.utils.MicrophoneInfo + +@Composable +fun MicrophoneStatus( + audioRecorder: AudioRecorderModel, +) { + val microphoneStatus = audioRecorder.microphoneStatus + val previousStatus = rememberPrevious(microphoneStatus) + + var showMicrophoneStatusDialog by remember { + // null = no dialog + // `MicrophoneConnectivityStatus.CONNECTED` = Reconnected dialog + // `MicrophoneConnectivityStatus.DISCONNECTED` = Disconnected dialog + mutableStateOf(null) + } + + LaunchedEffect(microphoneStatus) { + if (microphoneStatus != previousStatus && showMicrophoneStatusDialog == null && previousStatus != null) { + showMicrophoneStatusDialog = microphoneStatus + } + } + + if (showMicrophoneStatusDialog == AudioRecorderModel.MicrophoneConnectivityStatus.DISCONNECTED) { + MicrophoneDisconnectedDialog( + onClose = { + showMicrophoneStatusDialog = null + }, + microphoneName = audioRecorder.selectedMicrophone?.name ?: "", + ) + } + + if (showMicrophoneStatusDialog == AudioRecorderModel.MicrophoneConnectivityStatus.CONNECTED) { + MicrophoneReconnectedDialog( + onClose = { + showMicrophoneStatusDialog = null + }, + microphoneName = audioRecorder.selectedMicrophone?.name ?: "", + ) + } + + MicrophoneSelection( + audioRecorder = audioRecorder, + ) +} \ No newline at end of file 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 c395f39..ba4d330 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 @@ -34,6 +34,7 @@ import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RealtimeAudioVisuali import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RecordingTime import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveButton import app.myzel394.alibi.ui.components.AudioRecorder.molecules.MicrophoneSelection +import app.myzel394.alibi.ui.components.AudioRecorder.molecules.MicrophoneStatus import app.myzel394.alibi.ui.effects.rememberPrevious import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.utils.KeepScreenOn @@ -141,60 +142,6 @@ fun RecordingStatus( } } - val dataStore = LocalContext.current.dataStore - val settings = dataStore - .data - .collectAsState(initial = AppSettings.getDefaultInstance()) - .value - val microphones = MicrophoneInfo.fetchDeviceMicrophones(context).let { - if (settings.audioRecorderSettings.showAllMicrophones) { - it - } else { - MicrophoneInfo.filterMicrophones(it) - } - } - val microphoneStatus = audioRecorder.microphoneStatus - val previousStatus = rememberPrevious(microphoneStatus) - - var showMicrophoneStatusDialog by remember { - // null = no dialog - // `MicrophoneConnectivityStatus.CONNECTED` = Reconnected dialog - // `MicrophoneConnectivityStatus.DISCONNECTED` = Disconnected dialog - mutableStateOf(null) - } - - LaunchedEffect(microphoneStatus) { - if (microphoneStatus != previousStatus && showMicrophoneStatusDialog == null && previousStatus != null) { - showMicrophoneStatusDialog = microphoneStatus - } - } - - if (showMicrophoneStatusDialog == AudioRecorderModel.MicrophoneConnectivityStatus.DISCONNECTED) { - MicrophoneDisconnectedDialog( - onClose = { - showMicrophoneStatusDialog = null - }, - microphoneName = audioRecorder.selectedMicrophone?.name ?: "", - ) - } - - if (showMicrophoneStatusDialog == AudioRecorderModel.MicrophoneConnectivityStatus.CONNECTED) { - MicrophoneReconnectedDialog( - onClose = { - showMicrophoneStatusDialog = null - }, - microphoneName = audioRecorder.selectedMicrophone?.name ?: "", - ) - } - - if (microphones.isNotEmpty()) { - MicrophoneSelection( - microphones = microphones, - selectedMicrophone = audioRecorder.selectedMicrophone, - onSelect = audioRecorder::changeMicrophone, - ) - } else { - Box {} - } + MicrophoneStatus(audioRecorder) } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c56fa4f..8a45393 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -71,4 +71,5 @@ %s reconnected! Alibi automatically changed the microphone input to it. Show hidden microphones Show all microphones, including internal ones + Hidden Microphones \ No newline at end of file From d559fb45a58170032b4e804e775919b1f693e032 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 22 Oct 2023 00:33:03 +0200 Subject: [PATCH 23/29] feat: Add message when microphone is disconnected --- .../atoms/MicrophoneSelectionButton.kt | 13 +++++++++ .../molecules/MicrophoneSelection.kt | 27 +++++++++++++++++++ .../alibi/ui/components/atoms/MessageBox.kt | 8 +++--- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt index c38033a..3ca6486 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt @@ -12,8 +12,12 @@ import androidx.compose.foundation.layout.height 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.MicExternalOn +import androidx.compose.material.icons.filled.Warning 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 @@ -32,6 +36,7 @@ import app.myzel394.alibi.db.AppSettings fun MicrophoneSelectionButton( microphone: MicrophoneInfo? = null, selected: Boolean = false, + selectedAsFallback: Boolean = false, onSelect: () -> Unit, ) { val dataStore = LocalContext.current.dataStore @@ -69,6 +74,14 @@ fun MicrophoneSelectionButton( color = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.secondary, ) } + if (selectedAsFallback) + Icon( + Icons.Default.MicExternalOn, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier + .size(ButtonDefaults.IconSize), + ) } } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt index e401c34..d14887e 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt @@ -12,10 +12,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text @@ -37,6 +40,8 @@ import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneSelectionButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneTypeInfo +import app.myzel394.alibi.ui.components.atoms.MessageBox +import app.myzel394.alibi.ui.components.atoms.MessageType import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.utils.MicrophoneInfo @@ -62,6 +67,8 @@ fun MicrophoneSelection( .collectAsState(initial = AppSettings.getDefaultInstance()) .value + val isTryingToReconnect = + audioRecorder.selectedMicrophone != null && audioRecorder.microphoneStatus == AudioRecorderModel.MicrophoneConnectivityStatus.DISCONNECTED if (showSelection) { ModalBottomSheet( @@ -83,6 +90,16 @@ fun MicrophoneSelection( textAlign = TextAlign.Center, ) + if (isTryingToReconnect) + MessageBox( + type = MessageType.INFO, + message = stringResource( + R.string.ui_audioRecorder_error_microphoneDisconnected_message, + audioRecorder.recorderService!!.selectedMicrophone?.name ?: "", + audioRecorder.recorderService!!.selectedMicrophone?.name ?: "", + ) + ) + LazyColumn( modifier = Modifier .padding(horizontal = 32.dp), @@ -91,6 +108,7 @@ fun MicrophoneSelection( item { MicrophoneSelectionButton( selected = audioRecorder.selectedMicrophone == null, + selectedAsFallback = isTryingToReconnect, onSelect = { audioRecorder.changeMicrophone(null) showSelection = false @@ -175,6 +193,15 @@ fun MicrophoneSelection( ?: stringResource(R.string.ui_audioRecorder_info_microphone_deviceMicrophone) } ) + if (isTryingToReconnect) { + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Icon( + Icons.Default.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt b/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt index cf0f0c9..01156cc 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt @@ -29,14 +29,14 @@ fun MessageBox( title: String? = null, ) { val isDark = rememberIsInDarkMode() - val containerColor = when(type) { + val containerColor = when (type) { MessageType.ERROR -> MaterialTheme.colorScheme.errorContainer MessageType.INFO -> MaterialTheme.colorScheme.tertiaryContainer MessageType.SUCCESS -> Color.Green.copy(alpha = 0.3f) MessageType.WARNING -> Color.Yellow.copy(alpha = 0.3f) } - val onContainerColor = when(type) { - MessageType.ERROR -> MaterialTheme.colorScheme.onError + val onContainerColor = when (type) { + MessageType.ERROR -> MaterialTheme.colorScheme.onErrorContainer MessageType.INFO -> MaterialTheme.colorScheme.onTertiaryContainer MessageType.SUCCESS -> Color.Green MessageType.WARNING -> Color.Yellow @@ -53,7 +53,7 @@ fun MessageBox( .then(modifier) ) { Icon( - imageVector = when(type) { + imageVector = when (type) { MessageType.ERROR -> Icons.Default.Error MessageType.INFO -> Icons.Default.Info MessageType.SUCCESS -> Icons.Default.Check From 8fd57aace331e47b72308ae31949a742c878f336 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 22 Oct 2023 00:42:01 +0200 Subject: [PATCH 24/29] feat: Allow user to select default microphone if selected on disconnected --- .../atoms/MicrophoneSelectionButton.kt | 5 ++-- .../molecules/MicrophoneSelection.kt | 25 ++++++++++++------- .../molecules/MicrophoneStatus.kt | 2 +- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt index 3ca6486..d72dacd 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt @@ -37,6 +37,7 @@ fun MicrophoneSelectionButton( microphone: MicrophoneInfo? = null, selected: Boolean = false, selectedAsFallback: Boolean = false, + disabled: Boolean = false, onSelect: () -> Unit, ) { val dataStore = LocalContext.current.dataStore @@ -47,11 +48,11 @@ fun MicrophoneSelectionButton( Button( onClick = onSelect, + enabled = !disabled, modifier = Modifier .fillMaxWidth() .height(64.dp), - colors = if (selected) ButtonDefaults.buttonColors( - ) else ButtonDefaults.textButtonColors(), + colors = if (selected) ButtonDefaults.buttonColors() else ButtonDefaults.textButtonColors(), ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt index d14887e..df055ce 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneSelection.kt @@ -57,19 +57,25 @@ fun MicrophoneSelection( } val sheetState = rememberModalBottomSheetState() - val allMicrophones = MicrophoneInfo.fetchDeviceMicrophones(context) - val visibleMicrophones = MicrophoneInfo.filterMicrophones(allMicrophones) - val hiddenMicrophones = allMicrophones - visibleMicrophones.toSet() - val dataStore = LocalContext.current.dataStore val settings = dataStore .data .collectAsState(initial = AppSettings.getDefaultInstance()) .value + val allMicrophones = MicrophoneInfo.fetchDeviceMicrophones(context) + val visibleMicrophones = MicrophoneInfo.filterMicrophones(allMicrophones) + val hiddenMicrophones = allMicrophones - visibleMicrophones.toSet() + val isTryingToReconnect = audioRecorder.selectedMicrophone != null && audioRecorder.microphoneStatus == AudioRecorderModel.MicrophoneConnectivityStatus.DISCONNECTED + val shownMicrophones = if (isTryingToReconnect && visibleMicrophones.isEmpty()) { + listOf(audioRecorder.selectedMicrophone!!) + } else { + visibleMicrophones + } + if (showSelection) { ModalBottomSheet( onDismissRequest = { @@ -95,8 +101,8 @@ fun MicrophoneSelection( type = MessageType.INFO, message = stringResource( R.string.ui_audioRecorder_error_microphoneDisconnected_message, - audioRecorder.recorderService!!.selectedMicrophone?.name ?: "", - audioRecorder.recorderService!!.selectedMicrophone?.name ?: "", + audioRecorder.selectedMicrophone?.name ?: "", + audioRecorder.selectedMicrophone?.name ?: "", ) ) @@ -116,12 +122,13 @@ fun MicrophoneSelection( ) } - items(visibleMicrophones.size) { - val microphone = visibleMicrophones[it] + items(shownMicrophones.size) { + val microphone = shownMicrophones[it] MicrophoneSelectionButton( microphone = microphone, selected = audioRecorder.selectedMicrophone == microphone, + disabled = isTryingToReconnect && microphone == audioRecorder.selectedMicrophone, onSelect = { audioRecorder.changeMicrophone(microphone) showSelection = false @@ -174,7 +181,7 @@ fun MicrophoneSelection( Box {} } - if (visibleMicrophones.isNotEmpty() || (settings.audioRecorderSettings.showAllMicrophones && hiddenMicrophones.isNotEmpty())) { + if (shownMicrophones.isNotEmpty() || (settings.audioRecorderSettings.showAllMicrophones && hiddenMicrophones.isNotEmpty())) { Button( onClick = { showSelection = true diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneStatus.kt index b18c339..8966224 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/MicrophoneStatus.kt @@ -32,7 +32,7 @@ fun MicrophoneStatus( } LaunchedEffect(microphoneStatus) { - if (microphoneStatus != previousStatus && showMicrophoneStatusDialog == null && previousStatus != null) { + if (microphoneStatus != previousStatus && showMicrophoneStatusDialog == null && previousStatus != null && audioRecorder.selectedMicrophone != null) { showMicrophoneStatusDialog = microphoneStatus } } From 689d830c77ff020ca9f7c5ddd8fc4d86a546330d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 22 Oct 2023 13:18:04 +0200 Subject: [PATCH 25/29] fix: Filter microphones that are sources (not sinks) --- .../myzel394/alibi/ui/utils/available-microphones.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt index ec6aa4a..c36c91b 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/available-microphones.kt @@ -1,5 +1,6 @@ package app.myzel394.alibi.ui.utils +import android.bluetooth.BluetoothAdapter import android.content.Context import android.media.AudioDeviceInfo import android.media.AudioManager @@ -70,7 +71,16 @@ data class MicrophoneInfo( /// Filter microphones to only show normal ones fun filterMicrophones(microphones: List): List { return microphones.filter { - ALLOWED_MICROPHONE_TYPES.contains(it.deviceInfo.type) && it.deviceInfo.isSink + it.deviceInfo.isSource && ( + ALLOWED_MICROPHONE_TYPES.contains(it.deviceInfo.type) || + // `type` doesn't seem to be reliably as its sometimes -2147483644 even + // for valid microphones + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && + it.deviceInfo.type == -2147483644 && + BluetoothAdapter.checkBluetoothAddress(it.deviceInfo.address) && + it.deviceInfo.productName.isNotBlank() + ) + ) } } } From 7b2df0ae0d75e7c70e18d301dd7fc4abde33ae43 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 22 Oct 2023 13:28:18 +0200 Subject: [PATCH 26/29] fix: Add MODIFY_AUDIO_SETTINGS to allow bluetooth sco --- app/src/main/AndroidManifest.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6d91da0..8f67b7c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,10 @@ + + @@ -39,7 +43,9 @@ - + Date: Sun, 22 Oct 2023 13:44:54 +0200 Subject: [PATCH 27/29] fix(ui): Improve colors --- .../components/AudioRecorder/atoms/MicrophoneSelectionButton.kt | 2 +- .../java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt index d72dacd..0f5d835 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt @@ -72,7 +72,7 @@ fun MicrophoneSelectionButton( Text( microphone.deviceInfo.address.toString(), fontSize = MaterialTheme.typography.bodySmall.toSpanStyle().fontSize, - color = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.secondary, + color = if (selected || disabled) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.secondary, ) } if (selectedAsFallback) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt b/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt index 01156cc..4f43df2 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt @@ -42,7 +42,7 @@ fun MessageBox( MessageType.WARNING -> Color.Yellow } val textColor = if (isDark) onContainerColor else MaterialTheme.colorScheme.onSurface - val backgroundColor = if (isDark) containerColor else onContainerColor + val backgroundColor = if (isDark) containerColor else containerColor Row( verticalAlignment = Alignment.CenterVertically, From 35cff4b6eb55899c05ba425e247a11628416d14d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 22 Oct 2023 13:50:26 +0200 Subject: [PATCH 28/29] fix(ui): Improve colors --- .../AudioRecorder/atoms/MicrophoneSelectionButton.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt index 0f5d835..47b0ca2 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/atoms/MicrophoneSelectionButton.kt @@ -46,6 +46,9 @@ fun MicrophoneSelectionButton( .collectAsState(initial = AppSettings.getDefaultInstance()) .value + // Copied from Android's [FilledButtonTokens] + val disabledTextColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + Button( onClick = onSelect, enabled = !disabled, @@ -72,7 +75,7 @@ fun MicrophoneSelectionButton( Text( microphone.deviceInfo.address.toString(), fontSize = MaterialTheme.typography.bodySmall.toSpanStyle().fontSize, - color = if (selected || disabled) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.secondary, + color = if (disabled) disabledTextColor else if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.secondary, ) } if (selectedAsFallback) From 013096235161c9b7172ecc55d393d98450da8339 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:56:17 +0200 Subject: [PATCH 29/29] fix: Move ShowAllMicrophonesTile down --- .../main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c46c0f1..c69a8b8 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 @@ -153,13 +153,13 @@ fun SettingsScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(32.dp), ) { - ShowAllMicrophonesTile() Column { Divider( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 32.dp) ) + ShowAllMicrophonesTile() BitrateTile() SamplingRateTile() EncoderTile(snackbarHostState = snackbarHostState)