mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-01 07:39:11 +00:00
android: add a few options
ik not the right branch/pr but, eh, i am not merging this hook until i test further, and if i don't merge, conflicts, a lot of 'em
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
|
||||
package me.kavishdevar.librepods.composables
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -27,7 +28,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -37,12 +41,32 @@ 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.ATTManager
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@Composable
|
||||
fun AudioSettings() {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val attManager = ATTManager(ServiceManager.getService()?.device?: throw IllegalStateException("No device connected"))
|
||||
DisposableEffect(attManager) {
|
||||
onDispose {
|
||||
try {
|
||||
attManager.disconnect()
|
||||
} catch (e: Exception) {
|
||||
Log.w("AirPodsAudioSettings", "Error while disconnecting ATTManager: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
Log.d("AirPodsAudioSettings", "Connecting to ATT...")
|
||||
try {
|
||||
attManager.connect()
|
||||
} catch (e: Exception) {
|
||||
Log.w("AirPodsAudioSettings", "Error while connecting ATTManager: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.audio).uppercase(),
|
||||
@@ -63,7 +87,29 @@ fun AudioSettings() {
|
||||
.padding(top = 2.dp)
|
||||
) {
|
||||
|
||||
PersonalizedVolumeSwitch()
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
|
||||
ConversationalAwarenessSwitch()
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
|
||||
LoudSoundReductionSwitch(attManager)
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -91,7 +137,6 @@ fun AudioSettings() {
|
||||
color = textColor.copy(alpha = 0.6f)
|
||||
)
|
||||
)
|
||||
|
||||
AdaptiveStrengthSlider()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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 AutomaticConnectionSwitch() {
|
||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val service = ServiceManager.getService()!!
|
||||
|
||||
val shared_preference_key = "automatic_connection_ctrl_cmd"
|
||||
|
||||
val automaticConnectionEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG
|
||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||
|
||||
var automaticConnectionEnabled by remember {
|
||||
mutableStateOf(
|
||||
if (automaticConnectionEnabledValue != null) {
|
||||
automaticConnectionEnabledValue == 1.toByte()
|
||||
} else {
|
||||
sharedPreferences.getBoolean(shared_preference_key, false)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun updateAutomaticConnection(enabled: Boolean) {
|
||||
automaticConnectionEnabled = enabled
|
||||
service.aacpManager.sendControlCommand(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG.value,
|
||||
enabled
|
||||
)
|
||||
// todo: send other connected devices smartAudioRoutingDisabled or something, check packets again.
|
||||
|
||||
sharedPreferences.edit()
|
||||
.putBoolean(shared_preference_key, enabled)
|
||||
.apply()
|
||||
}
|
||||
|
||||
val automaticConnectionListener = object: AACPManager.ControlCommandListener {
|
||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG.value) {
|
||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
||||
val enabled = newValue == 1.toByte()
|
||||
automaticConnectionEnabled = enabled
|
||||
|
||||
sharedPreferences.edit()
|
||||
.putBoolean(shared_preference_key, enabled)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
service.aacpManager.registerControlCommandListener(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
|
||||
automaticConnectionListener
|
||||
)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
service.aacpManager.unregisterControlCommandListener(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
|
||||
automaticConnectionListener
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
|
||||
val isPressed = remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
isPressed.value = true
|
||||
tryAwaitRelease()
|
||||
isPressed.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
updateAutomaticConnection(!automaticConnectionEnabled)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.automatically_connect),
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.automatically_connect_description),
|
||||
fontSize = 12.sp,
|
||||
color = textColor.copy(0.6f),
|
||||
lineHeight = 14.sp,
|
||||
)
|
||||
}
|
||||
StyledSwitch(
|
||||
checked = automaticConnectionEnabled,
|
||||
onCheckedChange = {
|
||||
updateAutomaticConnection(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AutomaticConnectionSwitchPreview() {
|
||||
AutomaticConnectionSwitch()
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
* 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 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 CallControlSettings() {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.call_controls).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)
|
||||
) {
|
||||
val service = ServiceManager.getService()!!
|
||||
val callControlEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG
|
||||
}?.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 showSinglePressDropdown by remember { mutableStateOf(false) }
|
||||
var showDoublePressDropdown by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val listener = object : AACPManager.ControlCommandListener {
|
||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||
if (AACPManager.Companion.ControlCommandIdentifiers.fromByte(controlCommand.identifier) ==
|
||||
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"
|
||||
Log.d("CallControlSettings", "Control command received, flipped: $newFlipped")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service.aacpManager.registerControlCommandListener(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG,
|
||||
listener
|
||||
)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
service.aacpManager.controlCommandListeners[AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG]?.clear()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(flipped) {
|
||||
Log.d("CallControlSettings", "Call control flipped: $flipped")
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp, end = 12.dp)
|
||||
.height(55.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Answer call",
|
||||
fontSize = 18.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Single Press",
|
||||
fontSize = 18.sp,
|
||||
color = textColor.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp, end = 12.dp)
|
||||
.height(55.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Mute/Unmute",
|
||||
fontSize = 18.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
Box {
|
||||
Row(
|
||||
modifier = Modifier.clickable { showSinglePressDropdown = true },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = singlePressAction,
|
||||
fontSize = 18.sp,
|
||||
color = textColor.copy(alpha = 0.8f)
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = textColor.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showSinglePressDropdown,
|
||||
onDismissRequest = { showSinglePressDropdown = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Single Press") },
|
||||
onClick = {
|
||||
singlePressAction = "Single Press"
|
||||
doublePressAction = "Double Press"
|
||||
showSinglePressDropdown = false
|
||||
service.aacpManager.sendControlCommand(0x24, byteArrayOf(0x00, 0x03))
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Double Press") },
|
||||
onClick = {
|
||||
singlePressAction = "Double Press"
|
||||
doublePressAction = "Single Press"
|
||||
showSinglePressDropdown = false
|
||||
service.aacpManager.sendControlCommand(0x24, byteArrayOf(0x00, 0x02))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp, end = 12.dp)
|
||||
.height(55.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Hang Up",
|
||||
fontSize = 18.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
Box {
|
||||
Row(
|
||||
modifier = Modifier.clickable { showDoublePressDropdown = true },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = doublePressAction,
|
||||
fontSize = 18.sp,
|
||||
color = textColor.copy(alpha = 0.8f)
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = textColor.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showDoublePressDropdown,
|
||||
onDismissRequest = { showDoublePressDropdown = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Single Press") },
|
||||
onClick = {
|
||||
doublePressAction = "Single Press"
|
||||
singlePressAction = "Double Press"
|
||||
showDoublePressDropdown = false
|
||||
service.aacpManager.sendControlCommand(0x24, byteArrayOf(0x00, 0x02))
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Double Press") },
|
||||
onClick = {
|
||||
doublePressAction = "Double Press"
|
||||
singlePressAction = "Single Press"
|
||||
showDoublePressDropdown = false
|
||||
service.aacpManager.sendControlCommand(0x24, byteArrayOf(0x00, 0x03))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CallControlSettingsPreview() {
|
||||
CallControlSettings()
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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 android.util.Log
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
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.Text
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.ATTManager
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@Composable
|
||||
fun ConnectionSettings() {
|
||||
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)
|
||||
) {
|
||||
EarDetectionSwitch()
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
|
||||
AutomaticConnectionSwitch()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ConnectionSettingsPreview() {
|
||||
ConnectionSettings()
|
||||
}
|
||||
@@ -133,7 +133,7 @@ fun ConversationalAwarenessSwitch() {
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Conversational Awareness",
|
||||
text = stringResource(R.string.conversational_awareness),
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
|
||||
@@ -1,182 +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/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.composables
|
||||
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
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.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import me.kavishdevar.librepods.R
|
||||
|
||||
class DropdownItem(val name: String, val onSelect: () -> Unit) {
|
||||
fun select() {
|
||||
onSelect()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CustomDropdown(name: String, description: String = "", items: List<DropdownItem>) {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var offset by remember { mutableStateOf(IntOffset.Zero) }
|
||||
var popupHeight by remember { mutableStateOf(0.dp) }
|
||||
|
||||
val animatedHeight by animateDpAsState(
|
||||
targetValue = if (expanded) popupHeight else 0.dp,
|
||||
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow)
|
||||
)
|
||||
val animatedScale by animateFloatAsState(
|
||||
targetValue = if (expanded) 1f else 0f,
|
||||
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = Color.Transparent
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
expanded = true
|
||||
}
|
||||
.onGloballyPositioned { coordinates ->
|
||||
val windowPosition = coordinates.localToWindow(Offset.Zero)
|
||||
offset = IntOffset(windowPosition.x.toInt(), windowPosition.y.toInt() + coordinates.size.height)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
fontSize = 16.sp,
|
||||
color = textColor,
|
||||
maxLines = 1
|
||||
)
|
||||
if (description.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = description,
|
||||
fontSize = 12.sp,
|
||||
color = textColor.copy(0.6f),
|
||||
lineHeight = 14.sp,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = "\uDBC0\uDD8F",
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
Popup(
|
||||
alignment = Alignment.TopStart,
|
||||
offset = offset ,
|
||||
properties = PopupProperties(focusable = true),
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(backgroundColor, RoundedCornerShape(8.dp))
|
||||
.padding(8.dp)
|
||||
.widthIn(max = 50.dp)
|
||||
.height(animatedHeight)
|
||||
.scale(animatedScale)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
popupHeight = with(density) { coordinates.size.height.toDp() }
|
||||
}
|
||||
) {
|
||||
items.forEach { item ->
|
||||
Text(
|
||||
text = item.name,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
item.select()
|
||||
expanded = false
|
||||
}
|
||||
.padding(8.dp),
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CustomDropdownPreview() {
|
||||
CustomDropdown(
|
||||
name = "Volume Swipe Speed",
|
||||
items = listOf(
|
||||
DropdownItem("Always On") { },
|
||||
DropdownItem("Off") { },
|
||||
DropdownItem("Only when speaking") { }
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.SharedPreferences
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@Composable
|
||||
fun EarDetectionSwitch() {
|
||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val service = ServiceManager.getService()!!
|
||||
|
||||
val shared_preference_key = "automatic_ear_detection"
|
||||
|
||||
val earDetectionEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG
|
||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||
|
||||
var earDetectionEnabled by remember {
|
||||
mutableStateOf(
|
||||
if (earDetectionEnabledValue != null) {
|
||||
earDetectionEnabledValue == 1.toByte()
|
||||
} else {
|
||||
sharedPreferences.getBoolean(shared_preference_key, false)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun updateEarDetection(enabled: Boolean) {
|
||||
earDetectionEnabled = enabled
|
||||
service.aacpManager.sendControlCommand(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG.value,
|
||||
enabled
|
||||
)
|
||||
service.setEarDetection(enabled)
|
||||
|
||||
sharedPreferences.edit()
|
||||
.putBoolean(shared_preference_key, enabled)
|
||||
.apply()
|
||||
}
|
||||
|
||||
val earDetectionListener = object: AACPManager.ControlCommandListener {
|
||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG.value) {
|
||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
||||
val enabled = newValue == 1.toByte()
|
||||
earDetectionEnabled = enabled
|
||||
|
||||
sharedPreferences.edit()
|
||||
.putBoolean(shared_preference_key, enabled)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
service.aacpManager.registerControlCommandListener(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG,
|
||||
earDetectionListener
|
||||
)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
service.aacpManager.unregisterControlCommandListener(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG,
|
||||
earDetectionListener
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
|
||||
val isPressed = remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
isPressed.value = true
|
||||
tryAwaitRelease()
|
||||
isPressed.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
updateEarDetection(!earDetectionEnabled)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.ear_detection),
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
StyledSwitch(
|
||||
checked = earDetectionEnabled,
|
||||
onCheckedChange = {
|
||||
updateEarDetection(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun EarDetectionSwitchPreview() {
|
||||
EarDetectionSwitch()
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.input.pointer.pointerInput
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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 PersonalizedVolumeSwitch() {
|
||||
val service = ServiceManager.getService()!!
|
||||
|
||||
val adaptiveVolumeEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG
|
||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||
|
||||
var adaptiveVolumeEnabled by remember {
|
||||
mutableStateOf(
|
||||
adaptiveVolumeEnabledValue == 1.toByte()
|
||||
)
|
||||
}
|
||||
|
||||
fun updatePersonalizedVolume(enabled: Boolean) {
|
||||
adaptiveVolumeEnabled = enabled
|
||||
service.aacpManager.sendControlCommand(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG.value,
|
||||
enabled
|
||||
)
|
||||
}
|
||||
|
||||
val adaptiveVolumeListener = object: AACPManager.ControlCommandListener {
|
||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG.value) {
|
||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
||||
adaptiveVolumeEnabled = newValue == 1.toByte()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
service.aacpManager.registerControlCommandListener(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
|
||||
adaptiveVolumeListener
|
||||
)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
service.aacpManager.unregisterControlCommandListener(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
|
||||
adaptiveVolumeListener
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
|
||||
val isPressed = remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
isPressed.value = true
|
||||
tryAwaitRelease()
|
||||
isPressed.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
updatePersonalizedVolume(!adaptiveVolumeEnabled)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.personalized_volume),
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.personalized_volume_description),
|
||||
fontSize = 12.sp,
|
||||
color = textColor.copy(0.6f),
|
||||
lineHeight = 14.sp,
|
||||
)
|
||||
}
|
||||
StyledSwitch(
|
||||
checked = adaptiveVolumeEnabled,
|
||||
onCheckedChange = {
|
||||
updatePersonalizedVolume(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PersonalizedVolumeSwitchPreview() {
|
||||
PersonalizedVolumeSwitch()
|
||||
}
|
||||
@@ -75,6 +75,7 @@ import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
@@ -102,6 +103,7 @@ import me.kavishdevar.librepods.composables.VolumeControlSwitch
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.utils.ATTManager
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
@@ -124,6 +126,9 @@ fun AccessibilitySettingsScreen() {
|
||||
val attManager = ATTManager(ServiceManager.getService()?.device?: throw IllegalStateException("No device connected"))
|
||||
// get the AACP manager if available (used for EQ read/write)
|
||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
||||
val context = LocalContext.current
|
||||
val radareOffsetFinder = remember { RadareOffsetFinder(context) }
|
||||
val isSdpOffsetAvailable = remember { mutableStateOf(RadareOffsetFinder.isSdpOffsetAvailable()) }
|
||||
|
||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||
@@ -457,76 +462,80 @@ fun AccessibilitySettingsScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
AccessibilityToggle(
|
||||
text = "Transparency Mode",
|
||||
mutableState = enabled,
|
||||
independent = true
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.customize_transparency_mode_description),
|
||||
style = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
color = textColor.copy(0.6f),
|
||||
lineHeight = 14.sp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Customize Transparency Mode".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(8.dp)
|
||||
) {
|
||||
AccessibilitySlider(
|
||||
label = "Amplification",
|
||||
valueRange = -1f..1f,
|
||||
value = amplificationSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
amplificationSliderValue.floatValue = snapIfClose(it, listOf(-0.5f, -0.25f, 0f, 0.25f, 0.5f))
|
||||
},
|
||||
)
|
||||
AccessibilitySlider(
|
||||
label = "Balance",
|
||||
valueRange = -1f..1f,
|
||||
value = balanceSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
balanceSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
||||
},
|
||||
)
|
||||
AccessibilitySlider(
|
||||
label = "Tone",
|
||||
valueRange = -1f..1f,
|
||||
value = toneSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
toneSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
||||
},
|
||||
)
|
||||
AccessibilitySlider(
|
||||
label = "Ambient Noise Reduction",
|
||||
valueRange = 0f..1f,
|
||||
value = ambientNoiseReductionSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
ambientNoiseReductionSliderValue.floatValue = snapIfClose(it, listOf(0.1f, 0.3f, 0.5f, 0.7f, 0.9f))
|
||||
},
|
||||
)
|
||||
// Only show transparency mode section if SDP offset is available
|
||||
if (isSdpOffsetAvailable.value) {
|
||||
AccessibilityToggle(
|
||||
text = "Conversation Boost",
|
||||
mutableState = conversationBoostEnabled
|
||||
text = "Transparency Mode",
|
||||
mutableState = enabled,
|
||||
independent = true
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.customize_transparency_mode_description),
|
||||
style = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
color = textColor.copy(0.6f),
|
||||
lineHeight = 14.sp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Customize Transparency Mode".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(8.dp)
|
||||
) {
|
||||
AccessibilitySlider(
|
||||
label = "Amplification",
|
||||
valueRange = -1f..1f,
|
||||
value = amplificationSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
amplificationSliderValue.floatValue = snapIfClose(it, listOf(-0.5f, -0.25f, 0f, 0.25f, 0.5f))
|
||||
},
|
||||
)
|
||||
AccessibilitySlider(
|
||||
label = "Balance",
|
||||
valueRange = -1f..1f,
|
||||
value = balanceSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
balanceSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
||||
},
|
||||
)
|
||||
AccessibilitySlider(
|
||||
label = "Tone",
|
||||
valueRange = -1f..1f,
|
||||
value = toneSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
toneSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
||||
},
|
||||
)
|
||||
AccessibilitySlider(
|
||||
label = "Ambient Noise Reduction",
|
||||
valueRange = 0f..1f,
|
||||
value = ambientNoiseReductionSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
ambientNoiseReductionSliderValue.floatValue = snapIfClose(it, listOf(0.1f, 0.3f, 0.5f, 0.7f, 0.9f))
|
||||
},
|
||||
)
|
||||
AccessibilityToggle(
|
||||
text = "Conversation Boost",
|
||||
mutableState = conversationBoostEnabled
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Text(
|
||||
text = "AUDIO",
|
||||
style = TextStyle(
|
||||
@@ -604,101 +613,105 @@ fun AccessibilitySettingsScreen() {
|
||||
}
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Text(
|
||||
text = "Equalizer".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||
)
|
||||
// Only show transparency mode EQ section if SDP offset is available
|
||||
if (isSdpOffsetAvailable.value) {
|
||||
Text(
|
||||
text = "Equalizer".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
for (i in 0 until 8) {
|
||||
val eqValue = remember(eq.value[i]) { mutableFloatStateOf(eq.value[i]) }
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(38.dp)
|
||||
) {
|
||||
Text(
|
||||
text = String.format("%.2f", eqValue.floatValue),
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
|
||||
Slider(
|
||||
value = eqValue.floatValue,
|
||||
onValueChange = { newVal ->
|
||||
eqValue.floatValue = newVal
|
||||
val newEQ = eq.value.copyOf()
|
||||
newEQ[i] = eqValue.floatValue
|
||||
eq.value = newEQ
|
||||
},
|
||||
valueRange = 0f..100f,
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
for (i in 0 until 8) {
|
||||
val eqValue = remember(eq.value[i]) { mutableFloatStateOf(eq.value[i]) }
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.height(36.dp),
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = thumbColor,
|
||||
activeTrackColor = activeTrackColor,
|
||||
inactiveTrackColor = trackColor
|
||||
),
|
||||
thumb = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.shadow(4.dp, CircleShape)
|
||||
.background(thumbColor, CircleShape)
|
||||
)
|
||||
},
|
||||
track = {
|
||||
Box (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
)
|
||||
{
|
||||
.fillMaxWidth()
|
||||
.height(38.dp)
|
||||
) {
|
||||
Text(
|
||||
text = String.format("%.2f", eqValue.floatValue),
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
|
||||
Slider(
|
||||
value = eqValue.floatValue,
|
||||
onValueChange = { newVal ->
|
||||
eqValue.floatValue = newVal
|
||||
val newEQ = eq.value.copyOf()
|
||||
newEQ[i] = eqValue.floatValue
|
||||
eq.value = newEQ
|
||||
},
|
||||
valueRange = 0f..100f,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.height(36.dp),
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = thumbColor,
|
||||
activeTrackColor = activeTrackColor,
|
||||
inactiveTrackColor = trackColor
|
||||
),
|
||||
thumb = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.shadow(4.dp, CircleShape)
|
||||
.background(thumbColor, CircleShape)
|
||||
)
|
||||
},
|
||||
track = {
|
||||
Box (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.background(trackColor, RoundedCornerShape(4.dp))
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(eqValue.floatValue / 100f)
|
||||
.height(4.dp)
|
||||
.background(activeTrackColor, RoundedCornerShape(4.dp))
|
||||
.height(12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
)
|
||||
{
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.background(trackColor, RoundedCornerShape(4.dp))
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(eqValue.floatValue / 100f)
|
||||
.height(4.dp)
|
||||
.background(activeTrackColor, RoundedCornerShape(4.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Band ${i + 1}",
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Band ${i + 1}",
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Apply EQ to".uppercase(),
|
||||
style = TextStyle(
|
||||
|
||||
@@ -94,6 +94,8 @@ import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.CustomDevice
|
||||
import me.kavishdevar.librepods.composables.AudioSettings
|
||||
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.NameField
|
||||
import me.kavishdevar.librepods.composables.NavigationButton
|
||||
@@ -353,11 +355,35 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
)
|
||||
}
|
||||
|
||||
// Only show L2CAP-dependent features when not in BLE-only mode
|
||||
if (!bleOnlyMode) {
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
NoiseControlSettings(service = service)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
CallControlSettings()
|
||||
|
||||
// camera control goes here, airpods side is done, i just need to figure out how to listen to app open/close events
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
PressAndHoldSettings(navController = navController)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AudioSettings()
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ConnectionSettings()
|
||||
|
||||
// microphone settings
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
IndependentToggle(
|
||||
name = stringResource(R.string.sleep_detection),
|
||||
service = service,
|
||||
sharedPreferences = sharedPreferences,
|
||||
default = false,
|
||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.head_gestures).uppercase(),
|
||||
@@ -369,43 +395,36 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
NavigationButton(to = "head_tracking", "Head Tracking", navController)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
PressAndHoldSettings(navController = navController)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AudioSettings()
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
IndependentToggle(
|
||||
name = "Off Listening Mode",
|
||||
service = service,
|
||||
sharedPreferences = sharedPreferences,
|
||||
default = false,
|
||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
IndependentToggle(
|
||||
name = "Automatic Ear Detection",
|
||||
service = service,
|
||||
functionName = "setEarDetection",
|
||||
sharedPreferences = sharedPreferences,
|
||||
default = true,
|
||||
)
|
||||
|
||||
// Only show debug when not in BLE-only mode
|
||||
if (!bleOnlyMode) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
NavigationButton(to = "", "Accessibility", navController = navController, onClick = {
|
||||
val intent = Intent(context, CustomDevice::class.java)
|
||||
context.startActivity(intent)
|
||||
})
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
IndependentToggle(
|
||||
name = stringResource(R.string.off_listening_mode),
|
||||
service = service,
|
||||
sharedPreferences = sharedPreferences,
|
||||
default = false,
|
||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.off_listening_mode_description),
|
||||
style = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, top = 0.dp)
|
||||
)
|
||||
|
||||
// an about card- everything but the version number is unknown - will add later if i find out
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
NavigationButton("debug", "Debug", navController)
|
||||
}
|
||||
|
||||
@@ -114,6 +114,10 @@ class AACPManager {
|
||||
HEARING_ASSIST_CONFIG(0x33),
|
||||
ALLOW_OFF_OPTION(0x34),
|
||||
STEM_CONFIG(0x39),
|
||||
SLEEP_DETECTION_CONFIG(0x35),
|
||||
ALLOW_AUTO_CONNECT(0x36), // not sure what this does, AUTOMATIC_CONNECTION is the only one used, but this is newer... so ¯\_(ツ)_/¯
|
||||
EAR_DETECTION_CONFIG(0x0A),
|
||||
AUTOMATIC_CONNECTION_CONFIG(0x20),
|
||||
OWNS_CONNECTION(0x06);
|
||||
|
||||
companion object {
|
||||
@@ -594,6 +598,8 @@ class AACPManager {
|
||||
fun createRequestNotificationPacket(): ByteArray {
|
||||
val opcode = byteArrayOf(Opcodes.REQUEST_NOTIFICATIONS, 0x00)
|
||||
val data = byteArrayOf(0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte())
|
||||
// note to self #1: third byte is 0xfd when ear detection is disabled
|
||||
// note to self #2: this can be sent any time, not just at the start of the aacp connection
|
||||
return opcode + data
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
<string name="head_gestures">头部手势</string>
|
||||
<string name="left">左耳</string>
|
||||
<string name="right">右耳</string>
|
||||
<string name="adjusts_volume">根据环境调整媒体音量</string>
|
||||
<string name="conversational_awareness">对话感知</string>
|
||||
<string name="conversational_awareness_description">当你开始与他人交谈时,会降低媒体音量并减少背景噪音。</string>
|
||||
<string name="personalized_volume">个性化音量</string>
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
<string name="head_gestures">Head Gestures</string>
|
||||
<string name="left">Left</string>
|
||||
<string name="right">Right</string>
|
||||
<string name="adjusts_volume">Adjusts the volume of media in response to your environment</string>
|
||||
<string name="conversational_awareness">Conversational Awareness</string>
|
||||
<string name="conversational_awareness_description">Lowers media volume and reduces background noise when you start speaking to other people.</string>
|
||||
<string name="personalized_volume">Personalized Volume</string>
|
||||
@@ -85,4 +84,10 @@
|
||||
<string name="customize_transparency_mode_description">You can customize Transparency mode for your AirPods Pro to help you hear what\'s around you.</string>
|
||||
<string name="loud_sound_reduction_description">AirPods Pro automatically reduce your exposure to loud environmental noises when in Transparency and Adaptive mode.</string>
|
||||
<string name="loud_sound_reduction">Loud Sound Reduction</string>
|
||||
<string name="call_controls">Call Controls</string>
|
||||
<string name="automatically_connect">Connect to this device automatically</string>
|
||||
<string name="automatically_connect_description">When enabled, AirPods will try to connect to this device automatically. Else, they will try to autoconnect only when last connected.</string>
|
||||
<string name="sleep_detection">Pause media when falling asleep</string>
|
||||
<string name="off_listening_mode">Off Listening Mode</string>
|
||||
<string name="off_listening_mode_description">When this is on, AirPods listening modes will include an Off option. Loud sound levels are not reduced when the listening mode is set to Off.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user