mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-18 23:05:26 +02:00
feat: Add microphone selection
This commit is contained in:
parent
862de21436
commit
df1d7ce8ff
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user