mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-19 13:49:53 +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
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Column
|
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.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.material3.HorizontalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
|
import me.kavishdevar.librepods.utils.ATTManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioSettings() {
|
fun AudioSettings() {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
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(
|
||||||
text = stringResource(R.string.audio).uppercase(),
|
text = stringResource(R.string.audio).uppercase(),
|
||||||
@@ -63,7 +87,29 @@ fun AudioSettings() {
|
|||||||
.padding(top = 2.dp)
|
.padding(top = 2.dp)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
PersonalizedVolumeSwitch()
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.5.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 12.dp, end = 0.dp)
|
||||||
|
)
|
||||||
|
|
||||||
ConversationalAwarenessSwitch()
|
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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -91,7 +137,6 @@ fun AudioSettings() {
|
|||||||
color = textColor.copy(alpha = 0.6f)
|
color = textColor.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
AdaptiveStrengthSlider()
|
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)
|
.padding(end = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Conversational Awareness",
|
text = stringResource(R.string.conversational_awareness),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
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.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.input.pointer.PointerEventPass
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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
|
||||||
@@ -102,6 +103,7 @@ 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 me.kavishdevar.librepods.utils.AACPManager
|
||||||
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
@@ -124,6 +126,9 @@ fun AccessibilitySettingsScreen() {
|
|||||||
val attManager = ATTManager(ServiceManager.getService()?.device?: throw IllegalStateException("No device connected"))
|
val attManager = ATTManager(ServiceManager.getService()?.device?: throw IllegalStateException("No device connected"))
|
||||||
// get the AACP manager if available (used for EQ read/write)
|
// get the AACP manager if available (used for EQ read/write)
|
||||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
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 trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
||||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||||
@@ -457,76 +462,80 @@ fun AccessibilitySettingsScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AccessibilityToggle(
|
// Only show transparency mode section if SDP offset is available
|
||||||
text = "Transparency Mode",
|
if (isSdpOffsetAvailable.value) {
|
||||||
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(
|
AccessibilityToggle(
|
||||||
text = "Conversation Boost",
|
text = "Transparency Mode",
|
||||||
mutableState = conversationBoostEnabled
|
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(
|
||||||
text = "AUDIO",
|
text = "AUDIO",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
@@ -604,101 +613,105 @@ fun AccessibilitySettingsScreen() {
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
|
||||||
Text(
|
// Only show transparency mode EQ section if SDP offset is available
|
||||||
text = "Equalizer".uppercase(),
|
if (isSdpOffsetAvailable.value) {
|
||||||
style = TextStyle(
|
Text(
|
||||||
fontSize = 14.sp,
|
text = "Equalizer".uppercase(),
|
||||||
fontWeight = FontWeight.Light,
|
style = TextStyle(
|
||||||
color = textColor.copy(alpha = 0.6f),
|
fontSize = 14.sp,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
fontWeight = FontWeight.Light,
|
||||||
),
|
color = textColor.copy(alpha = 0.6f),
|
||||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
)
|
),
|
||||||
|
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.SpaceBetween
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
for (i in 0 until 8) {
|
for (i in 0 until 8) {
|
||||||
val eqValue = remember(eq.value[i]) { mutableFloatStateOf(eq.value[i]) }
|
val eqValue = remember(eq.value[i]) { mutableFloatStateOf(eq.value[i]) }
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
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,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(0.9f)
|
.fillMaxWidth()
|
||||||
.height(36.dp),
|
.height(38.dp)
|
||||||
colors = SliderDefaults.colors(
|
) {
|
||||||
thumbColor = thumbColor,
|
Text(
|
||||||
activeTrackColor = activeTrackColor,
|
text = String.format("%.2f", eqValue.floatValue),
|
||||||
inactiveTrackColor = trackColor
|
fontSize = 12.sp,
|
||||||
),
|
color = textColor,
|
||||||
thumb = {
|
modifier = Modifier.padding(bottom = 4.dp)
|
||||||
Box(
|
)
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
Slider(
|
||||||
.shadow(4.dp, CircleShape)
|
value = eqValue.floatValue,
|
||||||
.background(thumbColor, CircleShape)
|
onValueChange = { newVal ->
|
||||||
)
|
eqValue.floatValue = newVal
|
||||||
},
|
val newEQ = eq.value.copyOf()
|
||||||
track = {
|
newEQ[i] = eqValue.floatValue
|
||||||
Box (
|
eq.value = newEQ
|
||||||
modifier = Modifier
|
},
|
||||||
.fillMaxWidth()
|
valueRange = 0f..100f,
|
||||||
.height(12.dp),
|
modifier = Modifier
|
||||||
contentAlignment = Alignment.CenterStart
|
.fillMaxWidth(0.9f)
|
||||||
)
|
.height(36.dp),
|
||||||
{
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = thumbColor,
|
||||||
|
activeTrackColor = activeTrackColor,
|
||||||
|
inactiveTrackColor = trackColor
|
||||||
|
),
|
||||||
|
thumb = {
|
||||||
Box(
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.shadow(4.dp, CircleShape)
|
||||||
|
.background(thumbColor, CircleShape)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
track = {
|
||||||
|
Box (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(4.dp)
|
.height(12.dp),
|
||||||
.background(trackColor, RoundedCornerShape(4.dp))
|
contentAlignment = Alignment.CenterStart
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(eqValue.floatValue / 100f)
|
|
||||||
.height(4.dp)
|
|
||||||
.background(activeTrackColor, RoundedCornerShape(4.dp))
|
|
||||||
)
|
)
|
||||||
|
{
|
||||||
|
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(
|
||||||
text = "Band ${i + 1}",
|
text = "Band ${i + 1}",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
modifier = Modifier.padding(top = 4.dp)
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = "Apply EQ to".uppercase(),
|
text = "Apply EQ to".uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ import me.kavishdevar.librepods.R
|
|||||||
import me.kavishdevar.librepods.CustomDevice
|
import me.kavishdevar.librepods.CustomDevice
|
||||||
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.CallControlSettings
|
||||||
|
import me.kavishdevar.librepods.composables.ConnectionSettings
|
||||||
import me.kavishdevar.librepods.composables.IndependentToggle
|
import me.kavishdevar.librepods.composables.IndependentToggle
|
||||||
import me.kavishdevar.librepods.composables.NameField
|
import me.kavishdevar.librepods.composables.NameField
|
||||||
import me.kavishdevar.librepods.composables.NavigationButton
|
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) {
|
if (!bleOnlyMode) {
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
NoiseControlSettings(service = service)
|
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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.head_gestures).uppercase(),
|
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)
|
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
NavigationButton(to = "head_tracking", "Head Tracking", navController)
|
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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
NavigationButton(to = "", "Accessibility", navController = navController, onClick = {
|
NavigationButton(to = "", "Accessibility", navController = navController, onClick = {
|
||||||
val intent = Intent(context, CustomDevice::class.java)
|
val intent = Intent(context, CustomDevice::class.java)
|
||||||
context.startActivity(intent)
|
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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
NavigationButton("debug", "Debug", navController)
|
NavigationButton("debug", "Debug", navController)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,10 @@ class AACPManager {
|
|||||||
HEARING_ASSIST_CONFIG(0x33),
|
HEARING_ASSIST_CONFIG(0x33),
|
||||||
ALLOW_OFF_OPTION(0x34),
|
ALLOW_OFF_OPTION(0x34),
|
||||||
STEM_CONFIG(0x39),
|
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);
|
OWNS_CONNECTION(0x06);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -594,6 +598,8 @@ class AACPManager {
|
|||||||
fun createRequestNotificationPacket(): ByteArray {
|
fun createRequestNotificationPacket(): ByteArray {
|
||||||
val opcode = byteArrayOf(Opcodes.REQUEST_NOTIFICATIONS, 0x00)
|
val opcode = byteArrayOf(Opcodes.REQUEST_NOTIFICATIONS, 0x00)
|
||||||
val data = byteArrayOf(0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte())
|
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
|
return opcode + data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
<string name="head_gestures">头部手势</string>
|
<string name="head_gestures">头部手势</string>
|
||||||
<string name="left">左耳</string>
|
<string name="left">左耳</string>
|
||||||
<string name="right">右耳</string>
|
<string name="right">右耳</string>
|
||||||
<string name="adjusts_volume">根据环境调整媒体音量</string>
|
|
||||||
<string name="conversational_awareness">对话感知</string>
|
<string name="conversational_awareness">对话感知</string>
|
||||||
<string name="conversational_awareness_description">当你开始与他人交谈时,会降低媒体音量并减少背景噪音。</string>
|
<string name="conversational_awareness_description">当你开始与他人交谈时,会降低媒体音量并减少背景噪音。</string>
|
||||||
<string name="personalized_volume">个性化音量</string>
|
<string name="personalized_volume">个性化音量</string>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
<string name="head_gestures">Head Gestures</string>
|
<string name="head_gestures">Head Gestures</string>
|
||||||
<string name="left">Left</string>
|
<string name="left">Left</string>
|
||||||
<string name="right">Right</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">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="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>
|
<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="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_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="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>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user