feat: Add microphone selection

This commit is contained in:
Myzel394 2023-10-21 18:16:08 +02:00
parent 862de21436
commit df1d7ce8ff
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
9 changed files with 262 additions and 48 deletions

View File

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

View File

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

View File

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

View File

@ -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<MicrophoneInfo>,
) {
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)
}
)
}
}

View File

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

View File

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

View File

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

View File

@ -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<MicrophoneInfo> {
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,
}
}

View File

@ -1,5 +1,4 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">Alibi</string>
<string name="dialog_close_cancel_label">Cancel</string>
@ -64,4 +63,6 @@
<string name="ui_audioRecorder_error_recording_description">Alibi encountered an error during recording. Would you like to try saving the recording?</string>
<string name="ui_settings_language_title">Language</string>
<string name="ui_settings_language_update_label">Change</string>
<string name="ui_audioRecorder_info_microphone_deviceMicrophone">Device Microphone</string>
<string name="ui_audioRecorder_info_microphone_changeExplanation">The selected microphone will be immediately activated</string>
</resources>