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