Skip to content

Commit 65d074e

Browse files
committed
android: bring back some accessiblity settings and add listeners for all config
1 parent 93328d2 commit 65d074e

File tree

12 files changed

+476
-253
lines changed

12 files changed

+476
-253
lines changed
Lines changed: 0 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -1,218 +0,0 @@
1-
/*
2-
* LibrePods - AirPods liberated from Apple’s ecosystem
3-
*
4-
* Copyright (C) 2025 LibrePods contributors
5-
*
6-
* This program is free software: you can redistribute it and/or modify
7-
* it under the terms of the GNU Affero General Public License as published
8-
* by the Free Software Foundation, either version 3 of the License.
9-
*
10-
* This program is distributed in the hope that it will be useful,
11-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13-
* GNU Affero General Public License for more details.
14-
*
15-
* You should have received a copy of the GNU Affero General Public License
16-
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17-
*/
18-
19-
@file:OptIn(ExperimentalEncodingApi::class)
20-
21-
package me.kavishdevar.librepods.composables
22-
23-
import androidx.compose.foundation.background
24-
import androidx.compose.foundation.clickable
25-
import androidx.compose.foundation.isSystemInDarkTheme
26-
import androidx.compose.foundation.layout.Box
27-
import androidx.compose.foundation.layout.Column
28-
import androidx.compose.foundation.layout.fillMaxWidth
29-
import androidx.compose.foundation.layout.padding
30-
import androidx.compose.foundation.shape.RoundedCornerShape
31-
import androidx.compose.material3.DropdownMenu
32-
import androidx.compose.material3.DropdownMenuItem
33-
import androidx.compose.material3.Text
34-
import androidx.compose.runtime.Composable
35-
import androidx.compose.runtime.getValue
36-
import androidx.compose.runtime.mutableStateOf
37-
import androidx.compose.runtime.remember
38-
import androidx.compose.runtime.setValue
39-
import androidx.compose.ui.Modifier
40-
import androidx.compose.ui.graphics.Color
41-
import androidx.compose.ui.res.stringResource
42-
import androidx.compose.ui.text.TextStyle
43-
import androidx.compose.ui.text.font.FontWeight
44-
import androidx.compose.ui.tooling.preview.Preview
45-
import androidx.compose.ui.unit.dp
46-
import androidx.compose.ui.unit.sp
47-
import me.kavishdevar.librepods.R
48-
import me.kavishdevar.librepods.services.ServiceManager
49-
import me.kavishdevar.librepods.utils.AACPManager
50-
import kotlin.io.encoding.ExperimentalEncodingApi
51-
52-
@Composable
53-
fun AccessibilitySettings() {
54-
val isDarkTheme = isSystemInDarkTheme()
55-
val textColor = if (isDarkTheme) Color.White else Color.Black
56-
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
57-
val service = ServiceManager.getService()!!
58-
Text(
59-
text = stringResource(R.string.accessibility).uppercase(),
60-
style = TextStyle(
61-
fontSize = 14.sp,
62-
fontWeight = FontWeight.Light,
63-
color = textColor.copy(alpha = 0.6f)
64-
),
65-
modifier = Modifier.padding(8.dp, bottom = 2.dp)
66-
)
67-
68-
Column(
69-
modifier = Modifier
70-
.fillMaxWidth()
71-
.background(backgroundColor, RoundedCornerShape(14.dp))
72-
.padding(top = 2.dp)
73-
) {
74-
Column(
75-
modifier = Modifier
76-
.fillMaxWidth()
77-
.padding(12.dp)
78-
) {
79-
Text(
80-
text = stringResource(R.string.tone_volume),
81-
modifier = Modifier
82-
.padding(end = 8.dp, bottom = 2.dp, start = 2.dp)
83-
.fillMaxWidth(),
84-
style = TextStyle(
85-
fontSize = 16.sp,
86-
fontWeight = FontWeight.Medium,
87-
color = textColor
88-
)
89-
)
90-
91-
ToneVolumeSlider()
92-
}
93-
94-
val pressSpeedOptions = mapOf(
95-
0.toByte() to "Default",
96-
1.toByte() to "Slower",
97-
2.toByte() to "Slowest"
98-
)
99-
val selectedPressSpeedValue = service.aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL }?.value?.takeIf { it.isNotEmpty() }?.get(0)
100-
var selectedPressSpeed by remember { mutableStateOf(pressSpeedOptions[selectedPressSpeedValue] ?: pressSpeedOptions[0]) }
101-
DropdownMenuComponent(
102-
label = "Press Speed",
103-
options = pressSpeedOptions.values.toList(),
104-
selectedOption = selectedPressSpeed.toString(),
105-
onOptionSelected = { newValue ->
106-
selectedPressSpeed = newValue
107-
service.aacpManager.sendControlCommand(
108-
identifier = AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL.value,
109-
value = pressSpeedOptions.filterValues { it == newValue }.keys.firstOrNull() ?: 0.toByte()
110-
)
111-
},
112-
textColor = textColor
113-
)
114-
115-
val pressAndHoldDurationOptions = mapOf(
116-
0.toByte() to "Default",
117-
1.toByte() to "Slower",
118-
2.toByte() to "Slowest"
119-
)
120-
121-
val selectedPressAndHoldDurationValue = service.aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL }?.value?.takeIf { it.isNotEmpty() }?.get(0)
122-
var selectedPressAndHoldDuration by remember { mutableStateOf(pressAndHoldDurationOptions[selectedPressAndHoldDurationValue] ?: pressAndHoldDurationOptions[0]) }
123-
DropdownMenuComponent(
124-
label = "Press and Hold Duration",
125-
options = pressAndHoldDurationOptions.values.toList(),
126-
selectedOption = selectedPressAndHoldDuration.toString(),
127-
onOptionSelected = { newValue ->
128-
selectedPressAndHoldDuration = newValue
129-
service.aacpManager.sendControlCommand(
130-
identifier = AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL.value,
131-
value = pressAndHoldDurationOptions.filterValues { it == newValue }.keys.firstOrNull() ?: 0.toByte()
132-
)
133-
},
134-
textColor = textColor
135-
)
136-
137-
val volumeSwipeSpeedOptions = mapOf(
138-
1.toByte() to "Default",
139-
2.toByte() to "Longer",
140-
3.toByte() to "Longest"
141-
)
142-
val selectedVolumeSwipeSpeedValue = service.aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL }?.value?.takeIf { it.isNotEmpty() }?.get(0)
143-
var selectedVolumeSwipeSpeed by remember { mutableStateOf(volumeSwipeSpeedOptions[selectedVolumeSwipeSpeedValue] ?: volumeSwipeSpeedOptions[1]) }
144-
DropdownMenuComponent(
145-
label = "Volume Swipe Speed",
146-
options = volumeSwipeSpeedOptions.values.toList(),
147-
selectedOption = selectedVolumeSwipeSpeed.toString(),
148-
onOptionSelected = { newValue ->
149-
selectedVolumeSwipeSpeed = newValue
150-
service.aacpManager.sendControlCommand(
151-
identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL.value,
152-
value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull() ?: 1.toByte()
153-
)
154-
},
155-
textColor = textColor
156-
)
157-
}
158-
}
159-
160-
@Composable
161-
fun DropdownMenuComponent(
162-
label: String,
163-
options: List<String>,
164-
selectedOption: String,
165-
onOptionSelected: (String) -> Unit,
166-
textColor: Color
167-
) {
168-
var expanded by remember { mutableStateOf(false) }
169-
170-
Column (
171-
modifier = Modifier
172-
.fillMaxWidth()
173-
.padding(horizontal = 12.dp)
174-
) {
175-
Text(
176-
text = label,
177-
style = TextStyle(
178-
fontSize = 16.sp,
179-
fontWeight = FontWeight.Medium,
180-
color = textColor
181-
)
182-
)
183-
184-
Box(
185-
modifier = Modifier
186-
.fillMaxWidth()
187-
.clickable { expanded = true }
188-
.padding(8.dp)
189-
) {
190-
Text(
191-
text = selectedOption,
192-
modifier = Modifier.padding(16.dp),
193-
color = textColor
194-
)
195-
}
196-
197-
DropdownMenu(
198-
expanded = expanded,
199-
onDismissRequest = { expanded = false }
200-
) {
201-
options.forEach { option ->
202-
DropdownMenuItem(
203-
onClick = {
204-
onOptionSelected(option)
205-
expanded = false
206-
},
207-
text = { Text(text = option) }
208-
)
209-
}
210-
}
211-
}
212-
}
213-
214-
@Preview
215-
@Composable
216-
fun AccessibilitySettingsPreview() {
217-
AccessibilitySettings()
218-
}

android/app/src/main/java/me/kavishdevar/librepods/composables/AdaptiveStrengthSlider.kt

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import androidx.compose.material3.SliderDefaults
3838
import androidx.compose.material3.Text
3939
import androidx.compose.runtime.Composable
4040
import androidx.compose.runtime.LaunchedEffect
41+
import androidx.compose.runtime.DisposableEffect
4142
import androidx.compose.runtime.mutableFloatStateOf
4243
import androidx.compose.runtime.remember
4344
import androidx.compose.ui.Alignment
@@ -66,6 +67,31 @@ fun AdaptiveStrengthSlider() {
6667
sliderValueFromAACP?.toFloat()?.let { sliderValue.floatValue = (100 - it) }
6768
}
6869

70+
val listener = remember {
71+
object : AACPManager.ControlCommandListener {
72+
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
73+
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH.value) {
74+
controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)?.toFloat()?.let {
75+
sliderValue.floatValue = (100 - it)
76+
}
77+
}
78+
}
79+
}
80+
}
81+
82+
DisposableEffect(Unit) {
83+
service.aacpManager.registerControlCommandListener(
84+
AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH,
85+
listener
86+
)
87+
onDispose {
88+
service.aacpManager.unregisterControlCommandListener(
89+
AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH,
90+
listener
91+
)
92+
}
93+
}
94+
6995
val isDarkTheme = isSystemInDarkTheme()
7096

7197
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9)
@@ -81,11 +107,11 @@ fun AdaptiveStrengthSlider() {
81107
Slider(
82108
value = sliderValue.floatValue,
83109
onValueChange = {
84-
sliderValue.floatValue = it
110+
sliderValue.floatValue = snapIfClose(it, listOf(0f, 50f, 100f))
85111
},
86112
valueRange = 0f..100f,
87113
onValueChangeFinished = {
88-
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
114+
sliderValue.floatValue = snapIfClose(sliderValue.floatValue.roundToInt().toFloat(), listOf(0f, 50f, 100f))
89115
service.aacpManager.sendControlCommand(
90116
identifier = AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH.value,
91117
value = (100 - sliderValue.floatValue).toInt()
@@ -156,3 +182,8 @@ fun AdaptiveStrengthSlider() {
156182
fun AdaptiveStrengthSliderPreview() {
157183
AdaptiveStrengthSlider()
158184
}
185+
186+
private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.05f): Float {
187+
val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: value
188+
return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value
189+
}

android/app/src/main/java/me/kavishdevar/librepods/composables/ConversationalAwarenessSwitch.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ import androidx.compose.foundation.layout.padding
3434
import androidx.compose.foundation.shape.RoundedCornerShape
3535
import androidx.compose.material3.Text
3636
import androidx.compose.runtime.Composable
37+
import androidx.compose.runtime.DisposableEffect
3738
import androidx.compose.runtime.getValue
39+
import androidx.compose.runtime.LaunchedEffect
3840
import androidx.compose.runtime.mutableStateOf
3941
import androidx.compose.runtime.remember
4042
import androidx.compose.runtime.setValue
@@ -71,6 +73,30 @@ fun ConversationalAwarenessSwitch() {
7173
)
7274
}
7375

76+
val conversationalAwarenessListener = object: AACPManager.ControlCommandListener {
77+
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
78+
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG.value) {
79+
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
80+
conversationalAwarenessEnabled = newValue == 1.toByte()
81+
}
82+
}
83+
}
84+
85+
LaunchedEffect(Unit) {
86+
service.aacpManager.registerControlCommandListener(
87+
AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
88+
conversationalAwarenessListener
89+
)
90+
}
91+
DisposableEffect(Unit) {
92+
onDispose {
93+
service.aacpManager.unregisterControlCommandListener(
94+
AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
95+
conversationalAwarenessListener
96+
)
97+
}
98+
}
99+
74100
val isDarkTheme = isSystemInDarkTheme()
75101
val textColor = if (isDarkTheme) Color.White else Color.Black
76102

android/app/src/main/java/me/kavishdevar/librepods/composables/IndependentToggle.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.padding
3434
import androidx.compose.foundation.shape.RoundedCornerShape
3535
import androidx.compose.material3.Text
3636
import androidx.compose.runtime.Composable
37+
import androidx.compose.runtime.DisposableEffect
3738
import androidx.compose.runtime.LaunchedEffect
3839
import androidx.compose.runtime.getValue
3940
import androidx.compose.runtime.mutableStateOf
@@ -51,6 +52,7 @@ import me.kavishdevar.librepods.services.AirPodsService
5152
import me.kavishdevar.librepods.utils.AACPManager
5253
import kotlin.io.encoding.ExperimentalEncodingApi
5354
import androidx.core.content.edit
55+
import android.util.Log
5456

5557
@Composable
5658
fun IndependentToggle(name: String, service: AirPodsService? = null, functionName: String? = null, sharedPreferences: SharedPreferences, default: Boolean = false, controlCommandIdentifier: AACPManager.Companion.ControlCommandIdentifiers? = null) {
@@ -86,6 +88,27 @@ fun IndependentToggle(name: String, service: AirPodsService? = null, functionNam
8688
LaunchedEffect(sharedPreferences) {
8789
checked = sharedPreferences.getBoolean(snakeCasedName, true)
8890
}
91+
92+
if (controlCommandIdentifier != null) {
93+
val listener = remember {
94+
object : AACPManager.ControlCommandListener {
95+
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
96+
if (controlCommand.identifier == controlCommandIdentifier.value) {
97+
Log.d("IndependentToggle", "Received control command for $name: ${controlCommand.value}")
98+
checked = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0) == 1.toByte()
99+
}
100+
}
101+
}
102+
}
103+
LaunchedEffect(Unit) {
104+
service?.aacpManager?.registerControlCommandListener(controlCommandIdentifier, listener)
105+
}
106+
DisposableEffect(Unit) {
107+
onDispose {
108+
service?.aacpManager?.unregisterControlCommandListener(controlCommandIdentifier, listener)
109+
}
110+
}
111+
}
89112
Box (
90113
modifier = Modifier
91114
.padding(vertical = 8.dp)

0 commit comments

Comments
 (0)