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 413fa9149..1cc73fd21 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 000000000..26cbd314b --- /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 000000000..e54a5a11d --- /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 000000000..8c6963ab1 --- /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 f1d832b80..7130fa706 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 e9638cfb2..d13bd84eb 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 0db71966f..40fee5548 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 000000000..cb9ebe13e --- /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 15ab739c5..fc63b3a26 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