android: add microphone setting

also, un-hardcoded strings, and updated text sizes
This commit is contained in:
Kavish Devar
2025-09-20 22:55:35 +05:30
parent 3cca786cf9
commit 5aeb47b835
5 changed files with 254 additions and 38 deletions

View File

@@ -91,8 +91,8 @@ fun CallControlSettings() {
}?.value ?: byteArrayOf(0x00, 0x03)
var flipped by remember { mutableStateOf(callControlEnabledValue.contentEquals(byteArrayOf(0x00, 0x02))) }
var singlePressAction by remember { mutableStateOf(if (flipped) "Double Press" else "Single Press") }
var doublePressAction by remember { mutableStateOf(if (flipped) "Single Press" else "Double Press") }
var singlePressAction by remember { mutableStateOf(if (flipped) "Press Twice" else "Press Once") }
var doublePressAction by remember { mutableStateOf(if (flipped) "Press Once" else "Press Twice") }
var showSinglePressDropdown by remember { mutableStateOf(false) }
var showDoublePressDropdown by remember { mutableStateOf(false) }
@@ -103,8 +103,8 @@ fun CallControlSettings() {
AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG) {
val newFlipped = controlCommand.value.contentEquals(byteArrayOf(0x00, 0x02))
flipped = newFlipped
singlePressAction = if (newFlipped) "Double Press" else "Single Press"
doublePressAction = if (newFlipped) "Single Press" else "Double Press"
singlePressAction = if (newFlipped) "Press Twice" else "Press Once"
doublePressAction = if (newFlipped) "Press Once" else "Press Twice"
Log.d("CallControlSettings", "Control command received, flipped: $newFlipped")
}
}
@@ -134,19 +134,19 @@ fun CallControlSettings() {
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp)
.height(55.dp),
.height(50.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Answer call",
fontSize = 18.sp,
text = stringResource(R.string.answer_call),
fontSize = 16.sp,
color = textColor,
modifier = Modifier.padding(bottom = 4.dp)
)
Text(
text = "Single Press",
fontSize = 18.sp,
text = stringResource(R.string.press_once),
fontSize = 16.sp,
color = textColor.copy(alpha = 0.6f)
)
}
@@ -161,13 +161,13 @@ fun CallControlSettings() {
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp)
.height(55.dp),
.height(50.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Mute/Unmute",
fontSize = 18.sp,
text = stringResource(R.string.mute_unmute),
fontSize = 16.sp,
color = textColor,
modifier = Modifier.padding(bottom = 4.dp)
)
@@ -177,8 +177,8 @@ fun CallControlSettings() {
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = singlePressAction,
fontSize = 18.sp,
text = if (singlePressAction == "Press Once") stringResource(R.string.press_once) else stringResource(R.string.press_twice),
fontSize = 16.sp,
color = textColor.copy(alpha = 0.8f)
)
Icon(
@@ -193,19 +193,19 @@ fun CallControlSettings() {
onDismissRequest = { showSinglePressDropdown = false }
) {
DropdownMenuItem(
text = { Text("Single Press") },
text = { Text(stringResource(R.string.press_once)) },
onClick = {
singlePressAction = "Single Press"
doublePressAction = "Double Press"
singlePressAction = "Press Once"
doublePressAction = "Press Twice"
showSinglePressDropdown = false
service.aacpManager.sendControlCommand(0x24, byteArrayOf(0x00, 0x03))
}
)
DropdownMenuItem(
text = { Text("Double Press") },
text = { Text(stringResource(R.string.press_twice)) },
onClick = {
singlePressAction = "Double Press"
doublePressAction = "Single Press"
singlePressAction = "Press Twice"
doublePressAction = "Press Once"
showSinglePressDropdown = false
service.aacpManager.sendControlCommand(0x24, byteArrayOf(0x00, 0x02))
}
@@ -224,13 +224,13 @@ fun CallControlSettings() {
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp)
.height(55.dp),
.height(50.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Hang Up",
fontSize = 18.sp,
text = stringResource(R.string.hang_up),
fontSize = 16.sp,
color = textColor,
modifier = Modifier.padding(bottom = 4.dp)
)
@@ -240,8 +240,8 @@ fun CallControlSettings() {
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = doublePressAction,
fontSize = 18.sp,
text = if (doublePressAction == "Press Once") stringResource(R.string.press_once) else stringResource(R.string.press_twice),
fontSize = 16.sp,
color = textColor.copy(alpha = 0.8f)
)
Icon(
@@ -256,19 +256,19 @@ fun CallControlSettings() {
onDismissRequest = { showDoublePressDropdown = false }
) {
DropdownMenuItem(
text = { Text("Single Press") },
text = { Text(stringResource(R.string.press_once)) },
onClick = {
doublePressAction = "Single Press"
singlePressAction = "Double Press"
doublePressAction = "Press Once"
singlePressAction = "Press Twice"
showDoublePressDropdown = false
service.aacpManager.sendControlCommand(0x24, byteArrayOf(0x00, 0x02))
}
)
DropdownMenuItem(
text = { Text("Double Press") },
text = { Text(stringResource(R.string.press_twice)) },
onClick = {
doublePressAction = "Double Press"
singlePressAction = "Single Press"
doublePressAction = "Press Twice"
singlePressAction = "Press Once"
showDoublePressDropdown = false
service.aacpManager.sendControlCommand(0x24, byteArrayOf(0x00, 0x03))
}

View File

@@ -0,0 +1,204 @@
/*
* LibrePods - AirPods liberated from Apples ecosystem
*
* Copyright (C) 2025 LibrePods contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.composables
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
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.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.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.HorizontalDivider
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
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.AACPManager
import kotlin.io.encoding.ExperimentalEncodingApi
@Composable
fun MicrophoneSettings() {
val isDarkTheme = isSystemInDarkTheme()
val textColor = if (isDarkTheme) Color.White else Color.Black
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
Column(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor, RoundedCornerShape(14.dp))
.padding(top = 2.dp)
) {
val service = ServiceManager.getService()!!
val micModeValue = service.aacpManager.controlCommandStatusList.find {
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE
}?.value?.get(0) ?: 0x00.toByte()
var selectedMode by remember {
mutableStateOf(
when (micModeValue) {
0x00.toByte() -> "Automatic"
0x01.toByte() -> "Always Right"
0x02.toByte() -> "Always Left"
else -> "Automatic"
}
)
}
var showDropdown by remember { mutableStateOf(false) }
val listener = object : AACPManager.ControlCommandListener {
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
if (AACPManager.Companion.ControlCommandIdentifiers.fromByte(controlCommand.identifier) ==
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE) {
selectedMode = when (controlCommand.value.get(0)) {
0x00.toByte() -> "Automatic"
0x01.toByte() -> "Always Right"
0x02.toByte() -> "Always Left"
else -> "Automatic"
}
Log.d("MicrophoneSettings", "Microphone mode received: $selectedMode")
}
}
}
LaunchedEffect(Unit) {
service.aacpManager.registerControlCommandListener(
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE,
listener
)
}
DisposableEffect(Unit) {
onDispose {
service.aacpManager.unregisterControlCommandListener(
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE,
listener
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp)
.height(55.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.microphone_mode),
fontSize = 16.sp,
color = textColor,
modifier = Modifier.padding(bottom = 4.dp)
)
Box {
Row(
modifier = Modifier.clickable { showDropdown = true },
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = selectedMode,
fontSize = 16.sp,
color = textColor.copy(alpha = 0.8f)
)
Icon(
Icons.Default.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = textColor.copy(alpha = 0.6f)
)
}
DropdownMenu(
expanded = showDropdown,
onDismissRequest = { showDropdown = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.microphone_automatic)) },
onClick = {
selectedMode = "Automatic"
showDropdown = false
service.aacpManager.sendControlCommand(
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE.value,
byteArrayOf(0x00)
)
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.microphone_always_right)) },
onClick = {
selectedMode = "Always Right"
showDropdown = false
service.aacpManager.sendControlCommand(
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE.value,
byteArrayOf(0x01)
)
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.microphone_always_left)) },
onClick = {
selectedMode = "Always Left"
showDropdown = false
service.aacpManager.sendControlCommand(
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE.value,
byteArrayOf(0x02)
)
}
)
}
}
}
}
}
@Preview
@Composable
fun MicrophoneSettingsPreview() {
MicrophoneSettings()
}

View File

@@ -111,7 +111,7 @@ fun PressAndHoldSettings(navController: NavController) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(55.dp)
.height(50.dp)
.background(animatedLeftBackgroundColor, RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp))
.pointerInput(Unit) {
detectTapGestures(
@@ -135,7 +135,7 @@ fun PressAndHoldSettings(navController: NavController) {
Text(
text = stringResource(R.string.left),
style = TextStyle(
fontSize = 18.sp,
fontSize = 16.sp,
color = textColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
@@ -144,7 +144,7 @@ fun PressAndHoldSettings(navController: NavController) {
Text(
text = leftActionText,
style = TextStyle(
fontSize = 18.sp,
fontSize = 16.sp,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
@@ -171,7 +171,7 @@ fun PressAndHoldSettings(navController: NavController) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(55.dp)
.height(50.dp)
.background(animatedRightBackgroundColor, RoundedCornerShape(bottomEnd = 14.dp, bottomStart = 14.dp))
.pointerInput(Unit) {
detectTapGestures(
@@ -195,7 +195,7 @@ fun PressAndHoldSettings(navController: NavController) {
Text(
text = stringResource(R.string.right),
style = TextStyle(
fontSize = 18.sp,
fontSize = 16.sp,
color = textColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
@@ -204,7 +204,7 @@ fun PressAndHoldSettings(navController: NavController) {
Text(
text = rightActionText,
style = TextStyle(
fontSize = 18.sp,
fontSize = 16.sp,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),

View File

@@ -97,6 +97,7 @@ import me.kavishdevar.librepods.composables.BatteryView
import me.kavishdevar.librepods.composables.CallControlSettings
import me.kavishdevar.librepods.composables.ConnectionSettings
import me.kavishdevar.librepods.composables.IndependentToggle
import me.kavishdevar.librepods.composables.MicrophoneSettings
import me.kavishdevar.librepods.composables.NameField
import me.kavishdevar.librepods.composables.NavigationButton
import me.kavishdevar.librepods.composables.NoiseControlSettings
@@ -373,7 +374,8 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
Spacer(modifier = Modifier.height(16.dp))
ConnectionSettings()
// microphone settings
Spacer(modifier = Modifier.height(16.dp))
MicrophoneSettings()
Spacer(modifier = Modifier.height(16.dp))
IndependentToggle(

View File

@@ -90,4 +90,14 @@
<string name="sleep_detection">Pause media when falling asleep</string>
<string name="off_listening_mode">Off Listening Mode</string>
<string name="off_listening_mode_description">When this is on, AirPods listening modes will include an Off option. Loud sound levels are not reduced when the listening mode is set to Off.</string>
<string name="microphone">Microphone</string>
<string name="microphone_mode">Microphone Mode</string>
<string name="microphone_automatic">Automatic</string>
<string name="microphone_always_right">Always Right</string>
<string name="microphone_always_left">Always Left</string>
<string name="answer_call">Answer call</string>
<string name="mute_unmute">Mute/Unmute</string>
<string name="hang_up">Hang Up</string>
<string name="press_once">Press Once</string>
<string name="press_twice">Press Twice</string>
</resources>