From 5aeb47b835dd7e3a0bfa4fa98bc2969812abbfa2 Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Sat, 20 Sep 2025 22:55:35 +0530 Subject: [PATCH] android: add microphone setting also, un-hardcoded strings, and updated text sizes --- .../composables/CallControlSettings.kt | 62 +++--- .../composables/MicrophoneSettings.kt | 204 ++++++++++++++++++ .../composables/PressAndHoldSettings.kt | 12 +- .../screens/AirPodsSettingsScreen.kt | 4 +- android/app/src/main/res/values/strings.xml | 10 + 5 files changed, 254 insertions(+), 38 deletions(-) create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/composables/MicrophoneSettings.kt diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/CallControlSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/CallControlSettings.kt index 56bd43f..9e96653 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/CallControlSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/CallControlSettings.kt @@ -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)) } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/MicrophoneSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/MicrophoneSettings.kt new file mode 100644 index 0000000..88996b1 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/MicrophoneSettings.kt @@ -0,0 +1,204 @@ +/* + * LibrePods - AirPods liberated from Apple’s 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 . + */ + +@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() +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/PressAndHoldSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/PressAndHoldSettings.kt index eb83542..53af719 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/PressAndHoldSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/PressAndHoldSettings.kt @@ -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)) ), diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt index 4c1042c..0f449a4 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt @@ -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( diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index fc1043d..e569933 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -90,4 +90,14 @@ Pause media when falling asleep Off Listening Mode 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. + Microphone + Microphone Mode + Automatic + Always Right + Always Left + Answer call + Mute/Unmute + Hang Up + Press Once + Press Twice