andoid: add option to not disconnect airpods when none are worn

This commit is contained in:
Kavish Devar
2025-05-11 19:40:57 +05:30
parent 9baa3c9b60
commit 01432ce9c7
3 changed files with 213 additions and 59 deletions

View File

@@ -35,8 +35,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
@@ -76,9 +78,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.composables.IndependentToggle
import me.kavishdevar.librepods.composables.StyledSwitch import me.kavishdevar.librepods.composables.StyledSwitch
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.RadareOffsetFinder import me.kavishdevar.librepods.utils.RadareOffsetFinder
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -89,9 +89,26 @@ fun AppSettingsScreen(navController: NavController) {
val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") } val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") }
val isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
val context = LocalContext.current val context = LocalContext.current
val scrollState = rememberScrollState()
var showResetDialog by remember { mutableStateOf(false) } var showResetDialog by remember { mutableStateOf(false) }
var showPhoneBatteryInWidget by remember {
mutableStateOf(sharedPreferences.getBoolean("show_phone_battery_in_widget", true))
}
var conversationalAwarenessPauseMusicEnabled by remember {
mutableStateOf(sharedPreferences.getBoolean("conversational_awareness_pause_music", false))
}
var relativeConversationalAwarenessVolumeEnabled by remember {
mutableStateOf(sharedPreferences.getBoolean("relative_conversational_awareness_volume", true))
}
var openDialogForControlling by remember {
mutableStateOf(sharedPreferences.getString("qs_click_behavior", "dialog") == "dialog")
}
var disconnectWhenNotWearing by remember {
mutableStateOf(sharedPreferences.getBoolean("disconnect_when_not_wearing", false))
}
Scaffold( Scaffold(
topBar = { topBar = {
CenterAlignedTopAppBar( CenterAlignedTopAppBar(
@@ -141,24 +158,24 @@ fun AppSettingsScreen(navController: NavController) {
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.padding(horizontal = 12.dp) .padding(horizontal = 16.dp)
.verticalScroll(scrollState)
) { ) {
val isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black val textColor = if (isDarkTheme) Color.White else Color.Black
IndependentToggle("Show phone battery in widget", ServiceManager.getService()!!, "setPhoneBatteryInWidget", sharedPreferences) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = stringResource(R.string.conversational_awareness_customization).uppercase(), text = "Widget".uppercase(),
style = TextStyle( style = TextStyle(
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Light, fontWeight = FontWeight.Light,
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f), color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro)) fontFamily = FontFamily(Font(R.font.sf_pro))
), ),
modifier = Modifier.padding(8.dp, bottom = 2.dp) modifier = Modifier.padding(8.dp, bottom = 2.dp, top = 8.dp)
) )
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
@@ -166,44 +183,88 @@ fun AppSettingsScreen(navController: NavController) {
Column ( Column (
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(275.sp.value.dp)
.background( .background(
backgroundColor, backgroundColor,
RoundedCornerShape(14.dp) RoundedCornerShape(14.dp)
) )
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
showPhoneBatteryInWidget = !showPhoneBatteryInWidget
sharedPreferences.edit().putBoolean("show_phone_battery_in_widget", showPhoneBatteryInWidget).apply()
},
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 8.dp)
.padding(end = 4.dp)
) {
Text(
text = "Show phone battery in widget",
fontSize = 16.sp,
color = textColor
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Display your phone's battery level in the widget alongside AirPods battery",
fontSize = 14.sp,
color = textColor.copy(0.6f),
lineHeight = 16.sp,
)
}
StyledSwitch(
checked = showPhoneBatteryInWidget,
onCheckedChange = {
showPhoneBatteryInWidget = it
sharedPreferences.edit().putBoolean("show_phone_battery_in_widget", it).apply()
}
)
}
}
Text(
text = "Conversational Awareness".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, top = 24.dp)
)
Spacer(modifier = Modifier.height(2.dp))
Column (
modifier = Modifier
.fillMaxWidth()
.background(
backgroundColor,
RoundedCornerShape(14.dp)
)
.padding(horizontal = 16.dp, vertical = 4.dp)
) { ) {
val sliderValue = remember { mutableFloatStateOf(0f) } val sliderValue = remember { mutableFloatStateOf(0f) }
LaunchedEffect(sliderValue) { LaunchedEffect(sliderValue) {
if (sharedPreferences.contains("conversational_awareness_volume")) { if (sharedPreferences.contains("conversational_awareness_volume")) {
sliderValue.floatValue = sharedPreferences.getInt("conversational_awareness_volume", 0).toFloat() sliderValue.floatValue = sharedPreferences.getInt("conversational_awareness_volume", 43).toFloat()
} }
} }
LaunchedEffect(sliderValue.floatValue) {
sharedPreferences.edit().putInt("conversational_awareness_volume", sliderValue.floatValue.toInt()).apply()
}
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9)
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
var conversationalAwarenessPauseMusicEnabled by remember {
mutableStateOf(
sharedPreferences.getBoolean("conversational_awareness_pause_music", true)
)
}
fun updateConversationalAwarenessPauseMusic(enabled: Boolean) { fun updateConversationalAwarenessPauseMusic(enabled: Boolean) {
conversationalAwarenessPauseMusicEnabled = enabled conversationalAwarenessPauseMusicEnabled = enabled
sharedPreferences.edit().putBoolean("conversational_awareness_pause_music", enabled).apply() sharedPreferences.edit().putBoolean("conversational_awareness_pause_music", enabled).apply()
} }
var relativeConversationalAwarenessVolumeEnabled by remember {
mutableStateOf(
sharedPreferences.getBoolean("relative_conversational_awareness_volume", true)
)
}
fun updateRelativeConversationalAwarenessVolume(enabled: Boolean) { fun updateRelativeConversationalAwarenessVolume(enabled: Boolean) {
relativeConversationalAwarenessVolumeEnabled = enabled relativeConversationalAwarenessVolumeEnabled = enabled
sharedPreferences.edit().putBoolean("relative_conversational_awareness_volume", enabled).apply() sharedPreferences.edit().putBoolean("relative_conversational_awareness_volume", enabled).apply()
@@ -212,11 +273,6 @@ fun AppSettingsScreen(navController: NavController) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(85.sp.value.dp)
.background(
shape = RoundedCornerShape(14.dp),
color = Color.Transparent
)
.clickable( .clickable(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
@@ -228,6 +284,7 @@ fun AppSettingsScreen(navController: NavController) {
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(vertical = 8.dp)
.padding(end = 4.dp) .padding(end = 4.dp)
) { ) {
Text( Text(
@@ -255,11 +312,6 @@ fun AppSettingsScreen(navController: NavController) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(85.sp.value.dp)
.background(
shape = RoundedCornerShape(14.dp),
color = Color.Transparent
)
.clickable( .clickable(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
@@ -271,6 +323,7 @@ fun AppSettingsScreen(navController: NavController) {
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(vertical = 8.dp)
.padding(end = 4.dp) .padding(end = 4.dp)
) { ) {
Text( Text(
@@ -295,20 +348,31 @@ fun AppSettingsScreen(navController: NavController) {
) )
} }
Text(
text = "Conversational Awareness Volume",
fontSize = 16.sp,
color = textColor,
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
)
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9)
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5) val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
Slider( Slider(
value = sliderValue.floatValue, value = sliderValue.floatValue,
onValueChange = { onValueChange = {
sliderValue.floatValue = it sliderValue.floatValue = it
sharedPreferences.edit().putInt("conversational_awareness_volume", it.toInt()).apply()
}, },
valueRange = 10f..85f, valueRange = 10f..85f,
onValueChangeFinished = { onValueChangeFinished = {
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat() sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
}, },
modifier = Modifier modifier = Modifier
.weight(1f) .fillMaxWidth()
.height(36.dp), .height(36.dp)
.padding(vertical = 4.dp),
colors = SliderDefaults.colors( colors = SliderDefaults.colors(
thumbColor = thumbColor, thumbColor = thumbColor,
activeTrackColor = activeTrackColor, activeTrackColor = activeTrackColor,
@@ -347,7 +411,9 @@ fun AppSettingsScreen(navController: NavController) {
) )
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( Text(
@@ -355,7 +421,7 @@ fun AppSettingsScreen(navController: NavController) {
style = TextStyle( style = TextStyle(
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Light, fontWeight = FontWeight.Light,
color = labelTextColor color = textColor.copy(alpha = 0.7f)
), ),
modifier = Modifier.padding(start = 4.dp) modifier = Modifier.padding(start = 4.dp)
) )
@@ -364,14 +430,25 @@ fun AppSettingsScreen(navController: NavController) {
style = TextStyle( style = TextStyle(
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Light, fontWeight = FontWeight.Light,
color = labelTextColor color = textColor.copy(alpha = 0.7f)
), ),
modifier = Modifier.padding(end = 4.dp) modifier = Modifier.padding(end = 4.dp)
) )
} }
} }
Spacer(modifier = Modifier.height(24.dp)) Text(
text = "Quick Settings Tile".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, top = 24.dp)
)
Spacer(modifier = Modifier.height(2.dp))
Column( Column(
modifier = Modifier modifier = Modifier
@@ -380,14 +457,8 @@ fun AppSettingsScreen(navController: NavController) {
backgroundColor, backgroundColor,
RoundedCornerShape(14.dp) RoundedCornerShape(14.dp)
) )
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 4.dp)
) { ) {
var openDialogForControlling by remember {
mutableStateOf(
sharedPreferences.getString("qs_click_behavior", "dialog") == "dialog"
)
}
fun updateQsClickBehavior(enabled: Boolean) { fun updateQsClickBehavior(enabled: Boolean) {
openDialogForControlling = enabled openDialogForControlling = enabled
sharedPreferences.edit().putString("qs_click_behavior", if (enabled) "dialog" else "cycle").apply() sharedPreferences.edit().putString("qs_click_behavior", if (enabled) "dialog" else "cycle").apply()
@@ -407,7 +478,7 @@ fun AppSettingsScreen(navController: NavController) {
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(vertical = 16.dp) .padding(vertical = 8.dp)
.padding(end = 4.dp) .padding(end = 4.dp)
) { ) {
Text( Text(
@@ -435,7 +506,83 @@ fun AppSettingsScreen(navController: NavController) {
} }
} }
Spacer(modifier = Modifier.height(24.dp)) Text(
text = "Ear Detection".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, top = 24.dp)
)
Spacer(modifier = Modifier.height(2.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.background(
backgroundColor,
RoundedCornerShape(14.dp)
)
.padding(horizontal = 16.dp, vertical = 4.dp)
) {
fun updateDisconnectWhenNotWearing(enabled: Boolean) {
disconnectWhenNotWearing = enabled
sharedPreferences.edit().putBoolean("disconnect_when_not_wearing", enabled).apply()
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
updateDisconnectWhenNotWearing(!disconnectWhenNotWearing)
},
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 8.dp)
.padding(end = 4.dp)
) {
Text(
text = "Disconnect AirPods when not wearing",
fontSize = 16.sp,
color = textColor
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "You will still be able to control them with the app - this just disconnects the audio.",
fontSize = 14.sp,
color = textColor.copy(0.6f),
lineHeight = 16.sp,
)
}
StyledSwitch(
checked = disconnectWhenNotWearing,
onCheckedChange = {
updateDisconnectWhenNotWearing(it)
}
)
}
}
Text(
text = "Advanced Options".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, top = 24.dp)
)
Button( Button(
onClick = { showResetDialog = true }, onClick = { showResetDialog = true },
@@ -470,6 +617,8 @@ fun AppSettingsScreen(navController: NavController) {
} }
} }
Spacer(modifier = Modifier.height(32.dp))
if (showResetDialog) { if (showResetDialog) {
AlertDialog( AlertDialog(
onDismissRequest = { showResetDialog = false }, onDismissRequest = { showResetDialog = false },

View File

@@ -166,6 +166,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
var longPressOff: Boolean = false, var longPressOff: Boolean = false,
var volumeControl: Boolean = true, var volumeControl: Boolean = true,
var headGestures: Boolean = true, var headGestures: Boolean = true,
var disconnectWhenNotWearing: Boolean = false,
var adaptiveStrength: Int = 51, var adaptiveStrength: Int = 51,
var toneVolume: Int = 75, var toneVolume: Int = 75,
var conversationalAwarenessVolume: Int = 43, var conversationalAwarenessVolume: Int = 43,
@@ -221,6 +222,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
longPressOff = sharedPreferences.getBoolean("long_press_off", false), longPressOff = sharedPreferences.getBoolean("long_press_off", false),
volumeControl = sharedPreferences.getBoolean("volume_control", true), volumeControl = sharedPreferences.getBoolean("volume_control", true),
headGestures = sharedPreferences.getBoolean("head_gestures", true), headGestures = sharedPreferences.getBoolean("head_gestures", true),
disconnectWhenNotWearing = sharedPreferences.getBoolean("disconnect_when_not_wearing", false),
adaptiveStrength = sharedPreferences.getInt("adaptive_strength", 51), adaptiveStrength = sharedPreferences.getInt("adaptive_strength", 51),
toneVolume = sharedPreferences.getInt("tone_volume", 75), toneVolume = sharedPreferences.getInt("tone_volume", 75),
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43), conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43),
@@ -256,6 +258,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
"long_press_off" -> config.longPressOff = preferences.getBoolean(key, false) "long_press_off" -> config.longPressOff = preferences.getBoolean(key, false)
"volume_control" -> config.volumeControl = preferences.getBoolean(key, true) "volume_control" -> config.volumeControl = preferences.getBoolean(key, true)
"head_gestures" -> config.headGestures = preferences.getBoolean(key, true) "head_gestures" -> config.headGestures = preferences.getBoolean(key, true)
"disconnect_when_not_wearing" -> config.disconnectWhenNotWearing = preferences.getBoolean(key, false)
"adaptive_strength" -> config.adaptiveStrength = preferences.getInt(key, 51) "adaptive_strength" -> config.adaptiveStrength = preferences.getInt(key, 51)
"tone_volume" -> config.toneVolume = preferences.getInt(key, 75) "tone_volume" -> config.toneVolume = preferences.getInt(key, 75)
"conversational_awareness_volume" -> config.conversationalAwarenessVolume = preferences.getInt(key, 43) "conversational_awareness_volume" -> config.conversationalAwarenessVolume = preferences.getInt(key, 43)
@@ -1026,6 +1029,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (!contains("long_press_off")) editor.putBoolean("long_press_off", false) if (!contains("long_press_off")) editor.putBoolean("long_press_off", false)
if (!contains("volume_control")) editor.putBoolean("volume_control", true) if (!contains("volume_control")) editor.putBoolean("volume_control", true)
if (!contains("head_gestures")) editor.putBoolean("head_gestures", true) if (!contains("head_gestures")) editor.putBoolean("head_gestures", true)
if (!contains("disconnect_when_not_wearing")) editor.putBoolean("disconnect_when_not_wearing", false)
if (!contains("adaptive_strength")) editor.putInt("adaptive_strength", 51) if (!contains("adaptive_strength")) editor.putInt("adaptive_strength", 51)
if (!contains("tone_volume")) editor.putInt("tone_volume", 75) if (!contains("tone_volume")) editor.putInt("tone_volume", 75)
@@ -1498,8 +1502,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} }
} else if (newInEarData == listOf(false, false)) { } else if (newInEarData == listOf(false, false)) {
MediaController.sendPause(force = true) MediaController.sendPause(force = true)
if (config.disconnectWhenNotWearing) {
disconnectAudio(this@AirPodsService, device) disconnectAudio(this@AirPodsService, device)
} }
}
if (inEarData.contains(false) && newInEarData == listOf( if (inEarData.contains(false) && newInEarData == listOf(
true, true,

View File

@@ -34,8 +34,7 @@
<string name="airpods_not_connected">AirPods not connected</string> <string name="airpods_not_connected">AirPods not connected</string>
<string name="airpods_not_connected_description">Please connect your AirPods to access settings. If you\'re stuck here, then try reopening the app again after closing it from the recents.\n(DO NOT KILL THE APP!)</string> <string name="airpods_not_connected_description">Please connect your AirPods to access settings. If you\'re stuck here, then try reopening the app again after closing it from the recents.\n(DO NOT KILL THE APP!)</string>
<string name="back">Back</string> <string name="back">Back</string>
<string name="app_settings">App Settings</string> <string name="app_settings">Customizations</string>
<string name="conversational_awareness_customization">Conversational Awareness</string>
<string name="relative_conversational_awareness_volume">Relative volume</string> <string name="relative_conversational_awareness_volume">Relative volume</string>
<string name="relative_conversational_awareness_volume_description">Reduces to a percentage of the current volume instead of the maximum volume.</string> <string name="relative_conversational_awareness_volume_description">Reduces to a percentage of the current volume instead of the maximum volume.</string>
<string name="conversational_awareness_pause_music">Pause Music</string> <string name="conversational_awareness_pause_music">Pause Music</string>