mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-03-18 20:21:28 +00:00
android: bring back some accessiblity settings and add listeners for all config
This commit is contained in:
@@ -1,218 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
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 AccessibilitySettings() {
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
|
||||||
val service = ServiceManager.getService()!!
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.accessibility).uppercase(),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
color = textColor.copy(alpha = 0.6f)
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
|
||||||
.padding(top = 2.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.tone_volume),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 8.dp, bottom = 2.dp, start = 2.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
ToneVolumeSlider()
|
|
||||||
}
|
|
||||||
|
|
||||||
val pressSpeedOptions = mapOf(
|
|
||||||
0.toByte() to "Default",
|
|
||||||
1.toByte() to "Slower",
|
|
||||||
2.toByte() to "Slowest"
|
|
||||||
)
|
|
||||||
val selectedPressSpeedValue = service.aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL }?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
var selectedPressSpeed by remember { mutableStateOf(pressSpeedOptions[selectedPressSpeedValue] ?: pressSpeedOptions[0]) }
|
|
||||||
DropdownMenuComponent(
|
|
||||||
label = "Press Speed",
|
|
||||||
options = pressSpeedOptions.values.toList(),
|
|
||||||
selectedOption = selectedPressSpeed.toString(),
|
|
||||||
onOptionSelected = { newValue ->
|
|
||||||
selectedPressSpeed = newValue
|
|
||||||
service.aacpManager.sendControlCommand(
|
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL.value,
|
|
||||||
value = pressSpeedOptions.filterValues { it == newValue }.keys.firstOrNull() ?: 0.toByte()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
textColor = textColor
|
|
||||||
)
|
|
||||||
|
|
||||||
val pressAndHoldDurationOptions = mapOf(
|
|
||||||
0.toByte() to "Default",
|
|
||||||
1.toByte() to "Slower",
|
|
||||||
2.toByte() to "Slowest"
|
|
||||||
)
|
|
||||||
|
|
||||||
val selectedPressAndHoldDurationValue = service.aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL }?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
var selectedPressAndHoldDuration by remember { mutableStateOf(pressAndHoldDurationOptions[selectedPressAndHoldDurationValue] ?: pressAndHoldDurationOptions[0]) }
|
|
||||||
DropdownMenuComponent(
|
|
||||||
label = "Press and Hold Duration",
|
|
||||||
options = pressAndHoldDurationOptions.values.toList(),
|
|
||||||
selectedOption = selectedPressAndHoldDuration.toString(),
|
|
||||||
onOptionSelected = { newValue ->
|
|
||||||
selectedPressAndHoldDuration = newValue
|
|
||||||
service.aacpManager.sendControlCommand(
|
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL.value,
|
|
||||||
value = pressAndHoldDurationOptions.filterValues { it == newValue }.keys.firstOrNull() ?: 0.toByte()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
textColor = textColor
|
|
||||||
)
|
|
||||||
|
|
||||||
val volumeSwipeSpeedOptions = mapOf(
|
|
||||||
1.toByte() to "Default",
|
|
||||||
2.toByte() to "Longer",
|
|
||||||
3.toByte() to "Longest"
|
|
||||||
)
|
|
||||||
val selectedVolumeSwipeSpeedValue = service.aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL }?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
var selectedVolumeSwipeSpeed by remember { mutableStateOf(volumeSwipeSpeedOptions[selectedVolumeSwipeSpeedValue] ?: volumeSwipeSpeedOptions[1]) }
|
|
||||||
DropdownMenuComponent(
|
|
||||||
label = "Volume Swipe Speed",
|
|
||||||
options = volumeSwipeSpeedOptions.values.toList(),
|
|
||||||
selectedOption = selectedVolumeSwipeSpeed.toString(),
|
|
||||||
onOptionSelected = { newValue ->
|
|
||||||
selectedVolumeSwipeSpeed = newValue
|
|
||||||
service.aacpManager.sendControlCommand(
|
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL.value,
|
|
||||||
value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull() ?: 1.toByte()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
textColor = textColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DropdownMenuComponent(
|
|
||||||
label: String,
|
|
||||||
options: List<String>,
|
|
||||||
selectedOption: String,
|
|
||||||
onOptionSelected: (String) -> Unit,
|
|
||||||
textColor: Color
|
|
||||||
) {
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Column (
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { expanded = true }
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = selectedOption,
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = { expanded = false }
|
|
||||||
) {
|
|
||||||
options.forEach { option ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
onClick = {
|
|
||||||
onOptionSelected(option)
|
|
||||||
expanded = false
|
|
||||||
},
|
|
||||||
text = { Text(text = option) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun AccessibilitySettingsPreview() {
|
|
||||||
AccessibilitySettings()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import androidx.compose.material3.SliderDefaults
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -66,6 +67,31 @@ fun AdaptiveStrengthSlider() {
|
|||||||
sliderValueFromAACP?.toFloat()?.let { sliderValue.floatValue = (100 - it) }
|
sliderValueFromAACP?.toFloat()?.let { sliderValue.floatValue = (100 - it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val listener = remember {
|
||||||
|
object : AACPManager.ControlCommandListener {
|
||||||
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
|
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH.value) {
|
||||||
|
controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)?.toFloat()?.let {
|
||||||
|
sliderValue.floatValue = (100 - it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
service.aacpManager.registerControlCommandListener(
|
||||||
|
AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH,
|
||||||
|
listener
|
||||||
|
)
|
||||||
|
onDispose {
|
||||||
|
service.aacpManager.unregisterControlCommandListener(
|
||||||
|
AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH,
|
||||||
|
listener
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9)
|
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9)
|
||||||
@@ -81,11 +107,11 @@ fun AdaptiveStrengthSlider() {
|
|||||||
Slider(
|
Slider(
|
||||||
value = sliderValue.floatValue,
|
value = sliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
sliderValue.floatValue = it
|
sliderValue.floatValue = snapIfClose(it, listOf(0f, 50f, 100f))
|
||||||
},
|
},
|
||||||
valueRange = 0f..100f,
|
valueRange = 0f..100f,
|
||||||
onValueChangeFinished = {
|
onValueChangeFinished = {
|
||||||
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
sliderValue.floatValue = snapIfClose(sliderValue.floatValue.roundToInt().toFloat(), listOf(0f, 50f, 100f))
|
||||||
service.aacpManager.sendControlCommand(
|
service.aacpManager.sendControlCommand(
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH.value,
|
identifier = AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH.value,
|
||||||
value = (100 - sliderValue.floatValue).toInt()
|
value = (100 - sliderValue.floatValue).toInt()
|
||||||
@@ -156,3 +182,8 @@ fun AdaptiveStrengthSlider() {
|
|||||||
fun AdaptiveStrengthSliderPreview() {
|
fun AdaptiveStrengthSliderPreview() {
|
||||||
AdaptiveStrengthSlider()
|
AdaptiveStrengthSlider()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.05f): Float {
|
||||||
|
val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: value
|
||||||
|
return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value
|
||||||
|
}
|
||||||
@@ -34,7 +34,9 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -71,6 +73,30 @@ fun ConversationalAwarenessSwitch() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val conversationalAwarenessListener = object: AACPManager.ControlCommandListener {
|
||||||
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
|
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG.value) {
|
||||||
|
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
|
conversationalAwarenessEnabled = newValue == 1.toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
service.aacpManager.registerControlCommandListener(
|
||||||
|
AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
|
||||||
|
conversationalAwarenessListener
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
service.aacpManager.unregisterControlCommandListener(
|
||||||
|
AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
|
||||||
|
conversationalAwarenessListener
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -51,6 +52,7 @@ import me.kavishdevar.librepods.services.AirPodsService
|
|||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IndependentToggle(name: String, service: AirPodsService? = null, functionName: String? = null, sharedPreferences: SharedPreferences, default: Boolean = false, controlCommandIdentifier: AACPManager.Companion.ControlCommandIdentifiers? = null) {
|
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
|
|||||||
LaunchedEffect(sharedPreferences) {
|
LaunchedEffect(sharedPreferences) {
|
||||||
checked = sharedPreferences.getBoolean(snakeCasedName, true)
|
checked = sharedPreferences.getBoolean(snakeCasedName, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (controlCommandIdentifier != null) {
|
||||||
|
val listener = remember {
|
||||||
|
object : AACPManager.ControlCommandListener {
|
||||||
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
|
if (controlCommand.identifier == controlCommandIdentifier.value) {
|
||||||
|
Log.d("IndependentToggle", "Received control command for $name: ${controlCommand.value}")
|
||||||
|
checked = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0) == 1.toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
service?.aacpManager?.registerControlCommandListener(controlCommandIdentifier, listener)
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
service?.aacpManager?.unregisterControlCommandListener(controlCommandIdentifier, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Box (
|
Box (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -63,6 +64,7 @@ fun LoudSoundReductionSwitch(attManager: ATTManager) {
|
|||||||
while (attManager.socket?.isConnected != true) {
|
while (attManager.socket?.isConnected != true) {
|
||||||
delay(100)
|
delay(100)
|
||||||
}
|
}
|
||||||
|
attManager.enableNotifications(0x1b)
|
||||||
|
|
||||||
var parsed = false
|
var parsed = false
|
||||||
for (attempt in 1..3) {
|
for (attempt in 1..3) {
|
||||||
@@ -91,6 +93,29 @@ fun LoudSoundReductionSwitch(attManager: ATTManager) {
|
|||||||
attManager.write(0x1b, if (loudSoundReductionEnabled) byteArrayOf(1) else byteArrayOf(0))
|
attManager.write(0x1b, if (loudSoundReductionEnabled) byteArrayOf(1) else byteArrayOf(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val loudSoundListener = remember {
|
||||||
|
object : (ByteArray) -> Unit {
|
||||||
|
override fun invoke(value: ByteArray) {
|
||||||
|
if (value.isNotEmpty()) {
|
||||||
|
loudSoundReductionEnabled = value[0].toInt() != 0
|
||||||
|
Log.d("LoudSoundReduction", "Updated from notification: enabled=$loudSoundReductionEnabled")
|
||||||
|
} else {
|
||||||
|
Log.w("LoudSoundReduction", "Empty value in notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
attManager.registerListener(0x1b, loudSoundListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
attManager.unregisterListener(0x1b, loudSoundListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -60,6 +62,22 @@ fun SinglePodANCSwitch() {
|
|||||||
singleANCEnabledValue == 1.toByte()
|
singleANCEnabledValue == 1.toByte()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val listener = object : AACPManager.ControlCommandListener {
|
||||||
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
|
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE.value) {
|
||||||
|
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
|
singleANCEnabled = newValue == 1.toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
service.aacpManager.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE, listener)
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
service.aacpManager.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateSingleEnabled(enabled: Boolean) {
|
fun updateSingleEnabled(enabled: Boolean) {
|
||||||
singleANCEnabled = enabled
|
singleANCEnabled = enabled
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import androidx.compose.material3.Slider
|
|||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -66,6 +68,24 @@ fun ToneVolumeSlider() {
|
|||||||
val sliderValue = remember { mutableFloatStateOf(
|
val sliderValue = remember { mutableFloatStateOf(
|
||||||
sliderValueFromAACP?.toFloat() ?: -1f
|
sliderValueFromAACP?.toFloat() ?: -1f
|
||||||
) }
|
) }
|
||||||
|
val listener = object : AACPManager.ControlCommandListener {
|
||||||
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
|
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME.value) {
|
||||||
|
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)?.toFloat()
|
||||||
|
if (newValue != null) {
|
||||||
|
sliderValue.floatValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
service.aacpManager.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME, listener)
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
service.aacpManager.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
Log.d("ToneVolumeSlider", "Slider value: ${sliderValue.floatValue}")
|
Log.d("ToneVolumeSlider", "Slider value: ${sliderValue.floatValue}")
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
@@ -94,11 +114,11 @@ fun ToneVolumeSlider() {
|
|||||||
Slider(
|
Slider(
|
||||||
value = sliderValue.floatValue,
|
value = sliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
sliderValue.floatValue = it
|
sliderValue.floatValue = snapIfClose(it, listOf(100f))
|
||||||
},
|
},
|
||||||
valueRange = 0f..100f,
|
valueRange = 0f..125f,
|
||||||
onValueChangeFinished = {
|
onValueChangeFinished = {
|
||||||
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
sliderValue.floatValue = snapIfClose(sliderValue.floatValue.roundToInt().toFloat(), listOf(100f))
|
||||||
service.aacpManager.sendControlCommand(
|
service.aacpManager.sendControlCommand(
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME.value,
|
identifier = AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME.value,
|
||||||
value = byteArrayOf(sliderValue.floatValue.toInt().toByte(),
|
value = byteArrayOf(sliderValue.floatValue.toInt().toByte(),
|
||||||
@@ -163,3 +183,8 @@ fun ToneVolumeSlider() {
|
|||||||
fun ToneVolumeSliderPreview() {
|
fun ToneVolumeSliderPreview() {
|
||||||
ToneVolumeSlider()
|
ToneVolumeSlider()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.05f): Float {
|
||||||
|
val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: value
|
||||||
|
return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value
|
||||||
|
}
|
||||||
@@ -34,6 +34,8 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -60,6 +62,22 @@ fun VolumeControlSwitch() {
|
|||||||
volumeControlEnabledValue == 1.toByte()
|
volumeControlEnabledValue == 1.toByte()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val listener = object : AACPManager.ControlCommandListener {
|
||||||
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
|
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE.value) {
|
||||||
|
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
|
volumeControlEnabled = newValue == 1.toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
service.aacpManager.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE, listener)
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
service.aacpManager.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
fun updateVolumeControlEnabled(enabled: Boolean) {
|
fun updateVolumeControlEnabled(enabled: Boolean) {
|
||||||
volumeControlEnabled = enabled
|
volumeControlEnabled = enabled
|
||||||
service.aacpManager.sendControlCommand(
|
service.aacpManager.sendControlCommand(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.util.Log
|
|||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -42,6 +43,8 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CheckboxDefaults
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -67,6 +70,7 @@ import androidx.compose.ui.draw.scale
|
|||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
@@ -93,6 +97,7 @@ import me.kavishdevar.librepods.composables.ToneVolumeSlider
|
|||||||
import me.kavishdevar.librepods.composables.VolumeControlSwitch
|
import me.kavishdevar.librepods.composables.VolumeControlSwitch
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.ATTManager
|
import me.kavishdevar.librepods.utils.ATTManager
|
||||||
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
@@ -224,6 +229,98 @@ fun AccessibilitySettingsScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val transparencyListener = remember {
|
||||||
|
object : (ByteArray) -> Unit {
|
||||||
|
override fun invoke(value: ByteArray) {
|
||||||
|
val parsed = parseTransparencySettingsResponse(value)
|
||||||
|
if (parsed != null) {
|
||||||
|
enabled.value = parsed.enabled
|
||||||
|
amplificationSliderValue.floatValue = parsed.netAmplification
|
||||||
|
balanceSliderValue.floatValue = parsed.balance
|
||||||
|
toneSliderValue.floatValue = parsed.leftTone
|
||||||
|
ambientNoiseReductionSliderValue.floatValue = parsed.leftAmbientNoiseReduction
|
||||||
|
conversationBoostEnabled.value = parsed.leftConversationBoost
|
||||||
|
eq.value = parsed.leftEQ.copyOf()
|
||||||
|
Log.d(TAG, "Updated transparency settings from notification")
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Failed to parse transparency settings from notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pressSpeedOptions = mapOf(
|
||||||
|
0.toByte() to "Default",
|
||||||
|
1.toByte() to "Slower",
|
||||||
|
2.toByte() to "Slowest"
|
||||||
|
)
|
||||||
|
val selectedPressSpeedValue = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL }?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
|
var selectedPressSpeed by remember { mutableStateOf(pressSpeedOptions[selectedPressSpeedValue] ?: pressSpeedOptions[0]) }
|
||||||
|
val selectedPressSpeedListener = object : AACPManager.ControlCommandListener {
|
||||||
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
|
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL.value) {
|
||||||
|
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
|
selectedPressSpeed = pressSpeedOptions[newValue] ?: pressSpeedOptions[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL, selectedPressSpeedListener)
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL, selectedPressSpeedListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pressAndHoldDurationOptions = mapOf(
|
||||||
|
0.toByte() to "Default",
|
||||||
|
1.toByte() to "Slower",
|
||||||
|
2.toByte() to "Slowest"
|
||||||
|
)
|
||||||
|
val selectedPressAndHoldDurationValue = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL }?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
|
var selectedPressAndHoldDuration by remember { mutableStateOf(pressAndHoldDurationOptions[selectedPressAndHoldDurationValue] ?: pressAndHoldDurationOptions[0]) }
|
||||||
|
val selectedPressAndHoldDurationListener = object : AACPManager.ControlCommandListener {
|
||||||
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
|
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL.value) {
|
||||||
|
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
|
selectedPressAndHoldDuration = pressAndHoldDurationOptions[newValue] ?: pressAndHoldDurationOptions[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL, selectedPressAndHoldDurationListener)
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL, selectedPressAndHoldDurationListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val volumeSwipeSpeedOptions = mapOf(
|
||||||
|
1.toByte() to "Default",
|
||||||
|
2.toByte() to "Longer",
|
||||||
|
3.toByte() to "Longest"
|
||||||
|
)
|
||||||
|
val selectedVolumeSwipeSpeedValue = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL }?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
|
var selectedVolumeSwipeSpeed by remember { mutableStateOf(volumeSwipeSpeedOptions[selectedVolumeSwipeSpeedValue] ?: volumeSwipeSpeedOptions[1]) }
|
||||||
|
val selectedVolumeSwipeSpeedListener = object : AACPManager.ControlCommandListener {
|
||||||
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
|
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL.value) {
|
||||||
|
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
|
selectedVolumeSwipeSpeed = volumeSwipeSpeedOptions[newValue] ?: volumeSwipeSpeedOptions[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL, selectedVolumeSwipeSpeedListener)
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL, selectedVolumeSwipeSpeedListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(enabled.value, amplificationSliderValue.floatValue, balanceSliderValue.floatValue, toneSliderValue.floatValue, conversationBoostEnabled.value, ambientNoiseReductionSliderValue.floatValue, eq.value, initialLoadComplete.value, initialReadSucceeded.value) {
|
LaunchedEffect(enabled.value, amplificationSliderValue.floatValue, balanceSliderValue.floatValue, toneSliderValue.floatValue, conversationBoostEnabled.value, ambientNoiseReductionSliderValue.floatValue, eq.value, initialLoadComplete.value, initialReadSucceeded.value) {
|
||||||
if (!initialLoadComplete.value) {
|
if (!initialLoadComplete.value) {
|
||||||
Log.d(TAG, "Initial device load not complete - skipping send")
|
Log.d(TAG, "Initial device load not complete - skipping send")
|
||||||
@@ -239,8 +336,8 @@ fun AccessibilitySettingsScreen() {
|
|||||||
enabled = enabled.value,
|
enabled = enabled.value,
|
||||||
leftEQ = eq.value,
|
leftEQ = eq.value,
|
||||||
rightEQ = eq.value,
|
rightEQ = eq.value,
|
||||||
leftAmplification = amplificationSliderValue.floatValue + (0.5f - balanceSliderValue.floatValue) * amplificationSliderValue.floatValue * 2,
|
leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f,
|
||||||
rightAmplification = amplificationSliderValue.floatValue + (balanceSliderValue.floatValue - 0.5f) * amplificationSliderValue.floatValue * 2,
|
rightAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue > 0) balanceSliderValue.floatValue else 0f,
|
||||||
leftTone = toneSliderValue.floatValue,
|
leftTone = toneSliderValue.floatValue,
|
||||||
rightTone = toneSliderValue.floatValue,
|
rightTone = toneSliderValue.floatValue,
|
||||||
leftConversationBoost = conversationBoostEnabled.value,
|
leftConversationBoost = conversationBoostEnabled.value,
|
||||||
@@ -254,6 +351,12 @@ fun AccessibilitySettingsScreen() {
|
|||||||
sendTransparencySettings(attManager, transparencySettings.value)
|
sendTransparencySettings(attManager, transparencySettings.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
attManager.unregisterListener(0x18, transparencyListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
Log.d(TAG, "Connecting to ATT...")
|
Log.d(TAG, "Connecting to ATT...")
|
||||||
try {
|
try {
|
||||||
@@ -261,6 +364,10 @@ fun AccessibilitySettingsScreen() {
|
|||||||
while (attManager.socket?.isConnected != true) {
|
while (attManager.socket?.isConnected != true) {
|
||||||
delay(100)
|
delay(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attManager.enableNotifications(0x18)
|
||||||
|
attManager.registerListener(0x18, transparencyListener)
|
||||||
|
|
||||||
// If we have an AACP manager, prefer its EQ data to populate EQ controls first
|
// If we have an AACP manager, prefer its EQ data to populate EQ controls first
|
||||||
try {
|
try {
|
||||||
if (aacpManager != null) {
|
if (aacpManager != null) {
|
||||||
@@ -375,26 +482,26 @@ fun AccessibilitySettingsScreen() {
|
|||||||
) {
|
) {
|
||||||
AccessibilitySlider(
|
AccessibilitySlider(
|
||||||
label = "Amplification",
|
label = "Amplification",
|
||||||
valueRange = 0f..1f,
|
valueRange = -1f..1f,
|
||||||
value = amplificationSliderValue.floatValue,
|
value = amplificationSliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
amplificationSliderValue.floatValue = it
|
amplificationSliderValue.floatValue = snapIfClose(it, listOf(-0.5f, -0.25f, 0f, 0.25f, 0.5f))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
AccessibilitySlider(
|
AccessibilitySlider(
|
||||||
label = "Balance",
|
label = "Balance",
|
||||||
valueRange = 0f..1f,
|
valueRange = -1f..1f,
|
||||||
value = balanceSliderValue.floatValue,
|
value = balanceSliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
balanceSliderValue.floatValue = it
|
balanceSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
AccessibilitySlider(
|
AccessibilitySlider(
|
||||||
label = "Tone",
|
label = "Tone",
|
||||||
valueRange = 0f..1f,
|
valueRange = -1f..1f,
|
||||||
value = toneSliderValue.floatValue,
|
value = toneSliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
toneSliderValue.floatValue = it
|
toneSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
AccessibilitySlider(
|
AccessibilitySlider(
|
||||||
@@ -402,7 +509,7 @@ fun AccessibilitySettingsScreen() {
|
|||||||
valueRange = 0f..1f,
|
valueRange = 0f..1f,
|
||||||
value = ambientNoiseReductionSliderValue.floatValue,
|
value = ambientNoiseReductionSliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
ambientNoiseReductionSliderValue.floatValue = it
|
ambientNoiseReductionSliderValue.floatValue = snapIfClose(it, listOf(0.1f, 0.3f, 0.5f, 0.7f, 0.9f))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
AccessibilityToggle(
|
AccessibilityToggle(
|
||||||
@@ -445,6 +552,46 @@ fun AccessibilitySettingsScreen() {
|
|||||||
SinglePodANCSwitch()
|
SinglePodANCSwitch()
|
||||||
VolumeControlSwitch()
|
VolumeControlSwitch()
|
||||||
LoudSoundReductionSwitch(attManager)
|
LoudSoundReductionSwitch(attManager)
|
||||||
|
|
||||||
|
DropdownMenuComponent(
|
||||||
|
label = "Press Speed",
|
||||||
|
options = pressSpeedOptions.values.toList(),
|
||||||
|
selectedOption = selectedPressSpeed.toString(),
|
||||||
|
onOptionSelected = { newValue ->
|
||||||
|
selectedPressSpeed = newValue
|
||||||
|
aacpManager?.sendControlCommand(
|
||||||
|
identifier = AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL.value,
|
||||||
|
value = pressSpeedOptions.filterValues { it == newValue }.keys.firstOrNull() ?: 0.toByte()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
textColor = textColor
|
||||||
|
)
|
||||||
|
DropdownMenuComponent(
|
||||||
|
label = "Press and Hold Duration",
|
||||||
|
options = pressAndHoldDurationOptions.values.toList(),
|
||||||
|
selectedOption = selectedPressAndHoldDuration.toString(),
|
||||||
|
onOptionSelected = { newValue ->
|
||||||
|
selectedPressAndHoldDuration = newValue
|
||||||
|
aacpManager?.sendControlCommand(
|
||||||
|
identifier = AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL.value,
|
||||||
|
value = pressAndHoldDurationOptions.filterValues { it == newValue }.keys.firstOrNull() ?: 0.toByte()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
textColor = textColor
|
||||||
|
)
|
||||||
|
DropdownMenuComponent(
|
||||||
|
label = "Volume Swipe Speed",
|
||||||
|
options = volumeSwipeSpeedOptions.values.toList(),
|
||||||
|
selectedOption = selectedVolumeSwipeSpeed.toString(),
|
||||||
|
onOptionSelected = { newValue ->
|
||||||
|
selectedVolumeSwipeSpeed = newValue
|
||||||
|
aacpManager?.sendControlCommand(
|
||||||
|
identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL.value,
|
||||||
|
value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull() ?: 1.toByte()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
textColor = textColor
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
|
||||||
@@ -515,13 +662,13 @@ fun AccessibilitySettingsScreen() {
|
|||||||
color = textColor.copy(alpha = 0.6f),
|
color = textColor.copy(alpha = 0.6f),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
),
|
),
|
||||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
.padding(top = 0.dp, bottom = 12.dp)
|
.padding(vertical = 0.dp)
|
||||||
) {
|
) {
|
||||||
val darkModeLocal = isSystemInDarkTheme()
|
val darkModeLocal = isSystemInDarkTheme()
|
||||||
|
|
||||||
@@ -666,7 +813,6 @@ fun AccessibilitySettingsScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -816,13 +962,9 @@ private fun parseTransparencySettingsResponse(data: ByteArray): TransparencySett
|
|||||||
Log.d(TAG, "Settings parsed successfully")
|
Log.d(TAG, "Settings parsed successfully")
|
||||||
|
|
||||||
val avg = (leftAmplification + rightAmplification) / 2
|
val avg = (leftAmplification + rightAmplification) / 2
|
||||||
val amplification = avg.coerceIn(0f, 1f)
|
val amplification = avg.coerceIn(-1f, 1f)
|
||||||
val diff = rightAmplification - leftAmplification
|
val diff = rightAmplification - leftAmplification
|
||||||
val balance = if (avg == 0f) {
|
val balance = diff.coerceIn(-1f, 1f)
|
||||||
0.5f
|
|
||||||
} else {
|
|
||||||
(0.5f + diff / (2 * avg)).coerceIn(0f, 1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
return TransparencySettings(
|
return TransparencySettings(
|
||||||
enabled = enabled > 0.5f,
|
enabled = enabled > 0.5f,
|
||||||
@@ -902,3 +1044,61 @@ private fun sendPhoneMediaEQ(aacpManager: me.kavishdevar.librepods.utils.AACPMan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.05f): Float {
|
||||||
|
val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: value
|
||||||
|
return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DropdownMenuComponent(
|
||||||
|
label: String,
|
||||||
|
options: List<String>,
|
||||||
|
selectedOption: String,
|
||||||
|
onOptionSelected: (String) -> Unit,
|
||||||
|
textColor: Color
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { expanded = true }
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = selectedOption,
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
options.forEach { option ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
onOptionSelected(option)
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
text = { Text(text = option) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.CustomDevice
|
import me.kavishdevar.librepods.CustomDevice
|
||||||
import me.kavishdevar.librepods.composables.AccessibilitySettings
|
|
||||||
import me.kavishdevar.librepods.composables.AudioSettings
|
import me.kavishdevar.librepods.composables.AudioSettings
|
||||||
import me.kavishdevar.librepods.composables.BatteryView
|
import me.kavishdevar.librepods.composables.BatteryView
|
||||||
import me.kavishdevar.librepods.composables.IndependentToggle
|
import me.kavishdevar.librepods.composables.IndependentToggle
|
||||||
|
|||||||
@@ -272,6 +272,13 @@ class AACPManager {
|
|||||||
controlCommandListeners.getOrPut(identifier) { mutableListOf() }.add(callback)
|
controlCommandListeners.getOrPut(identifier) { mutableListOf() }.add(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun unregisterControlCommandListener(
|
||||||
|
identifier: ControlCommandIdentifiers,
|
||||||
|
callback: ControlCommandListener
|
||||||
|
) {
|
||||||
|
controlCommandListeners[identifier]?.remove(callback)
|
||||||
|
}
|
||||||
|
|
||||||
private var callback: PacketCallback? = null
|
private var callback: PacketCallback? = null
|
||||||
|
|
||||||
fun setPacketCallback(callback: PacketCallback) {
|
fun setPacketCallback(callback: PacketCallback) {
|
||||||
@@ -558,13 +565,6 @@ class AACPManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendEqualizerData(eqData: FloatArray, eqOnPhone: Boolean, eqOnMedia: Boolean): Boolean {
|
|
||||||
if (eqData.size != 8) {
|
|
||||||
throw IllegalArgumentException("EQ data must be 8 floats")
|
|
||||||
}
|
|
||||||
return sendDataPacket(createEqualizerDataPacket(eqData, eqOnPhone, eqOnMedia))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createEqualizerDataPacket(eqData: FloatArray, eqOnPhone: Boolean, eqOnMedia: Boolean): ByteArray {
|
fun createEqualizerDataPacket(eqData: FloatArray, eqOnPhone: Boolean, eqOnMedia: Boolean): ByteArray {
|
||||||
val opcode = byteArrayOf(Opcodes.EQ_DATA, 0x00)
|
val opcode = byteArrayOf(Opcodes.EQ_DATA, 0x00)
|
||||||
val identifier = byteArrayOf(0x84.toByte(), 0x00)
|
val identifier = byteArrayOf(0x84.toByte(), 0x00)
|
||||||
@@ -1120,6 +1120,9 @@ class AACPManager {
|
|||||||
val payload = buffer.array()
|
val payload = buffer.array()
|
||||||
val packet = header + payload
|
val packet = header + payload
|
||||||
sendPacket(packet)
|
sendPacket(packet)
|
||||||
|
this.eqData = eq.copyOf()
|
||||||
|
this.eqOnPhone = phone == 0x01.toByte()
|
||||||
|
this.eqOnMedia = media == 0x01.toByte()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseAudioSourceResponse(data: ByteArray): Pair<String, AudioSourceType> {
|
fun parseAudioSourceResponse(data: ByteArray): Pair<String, AudioSourceType> {
|
||||||
|
|||||||
@@ -5,9 +5,14 @@ import android.bluetooth.BluetoothDevice
|
|||||||
import android.bluetooth.BluetoothSocket
|
import android.bluetooth.BluetoothSocket
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class ATTManager(private val device: BluetoothDevice) {
|
class ATTManager(private val device: BluetoothDevice) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -15,11 +20,17 @@ class ATTManager(private val device: BluetoothDevice) {
|
|||||||
|
|
||||||
private const val OPCODE_READ_REQUEST: Byte = 0x0A
|
private const val OPCODE_READ_REQUEST: Byte = 0x0A
|
||||||
private const val OPCODE_WRITE_REQUEST: Byte = 0x12
|
private const val OPCODE_WRITE_REQUEST: Byte = 0x12
|
||||||
|
private const val OPCODE_HANDLE_VALUE_NTF: Byte = 0x1B
|
||||||
}
|
}
|
||||||
|
|
||||||
var socket: BluetoothSocket? = null
|
var socket: BluetoothSocket? = null
|
||||||
private var input: InputStream? = null
|
private var input: InputStream? = null
|
||||||
private var output: OutputStream? = null
|
private var output: OutputStream? = null
|
||||||
|
private val listeners = mutableMapOf<Int, MutableList<(ByteArray) -> Unit>>()
|
||||||
|
private var notificationJob: kotlinx.coroutines.Job? = null
|
||||||
|
|
||||||
|
// queue for non-notification PDUs (responses to requests)
|
||||||
|
private val responses = LinkedBlockingQueue<ByteArray>()
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun connect() {
|
fun connect() {
|
||||||
@@ -31,22 +42,63 @@ class ATTManager(private val device: BluetoothDevice) {
|
|||||||
input = socket!!.inputStream
|
input = socket!!.inputStream
|
||||||
output = socket!!.outputStream
|
output = socket!!.outputStream
|
||||||
Log.d(TAG, "Connected to ATT")
|
Log.d(TAG, "Connected to ATT")
|
||||||
|
|
||||||
|
notificationJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
while (socket?.isConnected == true) {
|
||||||
|
try {
|
||||||
|
val pdu = readPDU()
|
||||||
|
if (pdu.isNotEmpty() && pdu[0] == OPCODE_HANDLE_VALUE_NTF) {
|
||||||
|
// notification -> dispatch to listeners
|
||||||
|
val handle = (pdu[1].toInt() and 0xFF) or ((pdu[2].toInt() and 0xFF) shl 8)
|
||||||
|
val value = pdu.copyOfRange(2, pdu.size)
|
||||||
|
listeners[handle]?.forEach { listener ->
|
||||||
|
try {
|
||||||
|
listener(value)
|
||||||
|
Log.d(TAG, "Dispatched notification for handle $handle to listener, with value ${value.joinToString(" ") { String.format("%02X", it) }}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error in listener for handle $handle: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// not a notification -> treat as a response for pending request(s)
|
||||||
|
responses.put(pdu)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error reading notification/response: ${e.message}")
|
||||||
|
if (socket?.isConnected != true) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
try {
|
try {
|
||||||
|
notificationJob?.cancel()
|
||||||
socket?.close()
|
socket?.close()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Error closing socket: ${e.message}")
|
Log.w(TAG, "Error closing socket: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun registerListener(handle: Int, listener: (ByteArray) -> Unit) {
|
||||||
|
listeners.getOrPut(handle) { mutableListOf() }.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregisterListener(handle: Int, listener: (ByteArray) -> Unit) {
|
||||||
|
listeners[handle]?.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableNotifications(handle: Int) {
|
||||||
|
write(handle + 1, byteArrayOf(0x01, 0x00))
|
||||||
|
}
|
||||||
|
|
||||||
fun read(handle: Int): ByteArray {
|
fun read(handle: Int): ByteArray {
|
||||||
val lsb = (handle and 0xFF).toByte()
|
val lsb = (handle and 0xFF).toByte()
|
||||||
val msb = ((handle shr 8) and 0xFF).toByte()
|
val msb = ((handle shr 8) and 0xFF).toByte()
|
||||||
val pdu = byteArrayOf(OPCODE_READ_REQUEST, lsb, msb)
|
val pdu = byteArrayOf(OPCODE_READ_REQUEST, lsb, msb)
|
||||||
writeRaw(pdu)
|
writeRaw(pdu)
|
||||||
return readRaw()
|
// wait for response placed into responses queue by the reader coroutine
|
||||||
|
return readResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun write(handle: Int, value: ByteArray) {
|
fun write(handle: Int, value: ByteArray) {
|
||||||
@@ -54,7 +106,12 @@ class ATTManager(private val device: BluetoothDevice) {
|
|||||||
val msb = ((handle shr 8) and 0xFF).toByte()
|
val msb = ((handle shr 8) and 0xFF).toByte()
|
||||||
val pdu = byteArrayOf(OPCODE_WRITE_REQUEST, lsb, msb) + value
|
val pdu = byteArrayOf(OPCODE_WRITE_REQUEST, lsb, msb) + value
|
||||||
writeRaw(pdu)
|
writeRaw(pdu)
|
||||||
readRaw() // usually a Write Response (0x13)
|
// usually a Write Response (0x13) will arrive; wait for it (but discard return)
|
||||||
|
try {
|
||||||
|
readResponse()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "No write response received: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun writeRaw(pdu: ByteArray) {
|
private fun writeRaw(pdu: ByteArray) {
|
||||||
@@ -63,17 +120,33 @@ class ATTManager(private val device: BluetoothDevice) {
|
|||||||
Log.d(TAG, "writeRaw: ${pdu.joinToString(" ") { String.format("%02X", it) }}")
|
Log.d(TAG, "writeRaw: ${pdu.joinToString(" ") { String.format("%02X", it) }}")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readRaw(): ByteArray {
|
// rename / specialize: read raw PDU directly from input stream (blocking)
|
||||||
|
private fun readPDU(): ByteArray {
|
||||||
val inp = input ?: throw IllegalStateException("Not connected")
|
val inp = input ?: throw IllegalStateException("Not connected")
|
||||||
val buffer = ByteArray(512)
|
val buffer = ByteArray(512)
|
||||||
val len = inp.read(buffer)
|
val len = inp.read(buffer)
|
||||||
if (len <= 0) throw IllegalStateException("No data read from ATT socket")
|
if (len <= 0) throw IllegalStateException("No data read from ATT socket")
|
||||||
val data = buffer.copyOfRange(0, len)
|
val data = buffer.copyOfRange(0, len)
|
||||||
Log.wtf(TAG, "Read ${data.size} bytes from ATT")
|
Log.wtf(TAG, "Read ${data.size} bytes from ATT")
|
||||||
Log.d(TAG, "readRaw: ${data.joinToString(" ") { String.format("%02X", it) }}")
|
Log.d(TAG, "readPDU: ${data.joinToString(" ") { String.format("%02X", it) }}")
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wait for a response PDU produced by the background reader
|
||||||
|
private fun readResponse(timeoutMs: Long = 2000): ByteArray {
|
||||||
|
try {
|
||||||
|
val resp = responses.poll(timeoutMs, TimeUnit.MILLISECONDS)
|
||||||
|
if (resp == null) {
|
||||||
|
throw IllegalStateException("No response read from ATT socket within $timeoutMs ms")
|
||||||
|
}
|
||||||
|
Log.d(TAG, "readResponse: ${resp.joinToString(" ") { String.format("%02X", it) }}")
|
||||||
|
return resp
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
Thread.currentThread().interrupt()
|
||||||
|
throw IllegalStateException("Interrupted while waiting for ATT response", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createBluetoothSocket(device: BluetoothDevice, uuid: ParcelUuid): BluetoothSocket {
|
private fun createBluetoothSocket(device: BluetoothDevice, uuid: ParcelUuid): BluetoothSocket {
|
||||||
val type = 3 // L2CAP
|
val type = 3 // L2CAP
|
||||||
val constructorSpecs = listOf(
|
val constructorSpecs = listOf(
|
||||||
|
|||||||
Reference in New Issue
Block a user