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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
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.navigation.NavController
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.composables.IndependentToggle
import me.kavishdevar.librepods.composables.StyledSwitch
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.RadareOffsetFinder
import kotlin.math.roundToInt
@@ -89,9 +89,26 @@ fun AppSettingsScreen(navController: NavController) {
val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") }
val isDarkTheme = isSystemInDarkTheme()
val context = LocalContext.current
val scrollState = rememberScrollState()
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(
topBar = {
CenterAlignedTopAppBar(
@@ -141,24 +158,88 @@ fun AppSettingsScreen(navController: NavController) {
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 12.dp)
.padding(horizontal = 16.dp)
.verticalScroll(scrollState)
) {
val isDarkTheme = isSystemInDarkTheme()
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
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 = stringResource(R.string.conversational_awareness_customization).uppercase(),
text = "Widget".uppercase(),
style = TextStyle(
fontSize = 14.sp,
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))
),
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))
Column (
modifier = Modifier
.fillMaxWidth()
.background(
backgroundColor,
RoundedCornerShape(14.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))
@@ -166,44 +247,24 @@ fun AppSettingsScreen(navController: NavController) {
Column (
modifier = Modifier
.fillMaxWidth()
.height(275.sp.value.dp)
.background(
backgroundColor,
RoundedCornerShape(14.dp)
)
.padding(horizontal = 16.dp, vertical = 8.dp)
.padding(horizontal = 16.dp, vertical = 4.dp)
) {
val sliderValue = remember { mutableFloatStateOf(0f) }
LaunchedEffect(sliderValue) {
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) {
conversationalAwarenessPauseMusicEnabled = enabled
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) {
relativeConversationalAwarenessVolumeEnabled = enabled
sharedPreferences.edit().putBoolean("relative_conversational_awareness_volume", enabled).apply()
@@ -212,11 +273,6 @@ fun AppSettingsScreen(navController: NavController) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(85.sp.value.dp)
.background(
shape = RoundedCornerShape(14.dp),
color = Color.Transparent
)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
@@ -228,6 +284,7 @@ fun AppSettingsScreen(navController: NavController) {
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 8.dp)
.padding(end = 4.dp)
) {
Text(
@@ -255,11 +312,6 @@ fun AppSettingsScreen(navController: NavController) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(85.sp.value.dp)
.background(
shape = RoundedCornerShape(14.dp),
color = Color.Transparent
)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
@@ -271,6 +323,7 @@ fun AppSettingsScreen(navController: NavController) {
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 8.dp)
.padding(end = 4.dp)
) {
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 thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
Slider(
value = sliderValue.floatValue,
onValueChange = {
sliderValue.floatValue = it
sharedPreferences.edit().putInt("conversational_awareness_volume", it.toInt()).apply()
},
valueRange = 10f..85f,
onValueChangeFinished = {
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
},
modifier = Modifier
.weight(1f)
.height(36.dp),
.fillMaxWidth()
.height(36.dp)
.padding(vertical = 4.dp),
colors = SliderDefaults.colors(
thumbColor = thumbColor,
activeTrackColor = activeTrackColor,
@@ -347,7 +411,9 @@ fun AppSettingsScreen(navController: NavController) {
)
Row(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
@@ -355,7 +421,7 @@ fun AppSettingsScreen(navController: NavController) {
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = labelTextColor
color = textColor.copy(alpha = 0.7f)
),
modifier = Modifier.padding(start = 4.dp)
)
@@ -364,15 +430,26 @@ fun AppSettingsScreen(navController: NavController) {
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = labelTextColor
color = textColor.copy(alpha = 0.7f)
),
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(
modifier = Modifier
.fillMaxWidth()
@@ -380,14 +457,8 @@ fun AppSettingsScreen(navController: NavController) {
backgroundColor,
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) {
openDialogForControlling = enabled
sharedPreferences.edit().putString("qs_click_behavior", if (enabled) "dialog" else "cycle").apply()
@@ -407,7 +478,7 @@ fun AppSettingsScreen(navController: NavController) {
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 16.dp)
.padding(vertical = 8.dp)
.padding(end = 4.dp)
) {
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(
onClick = { showResetDialog = true },
@@ -470,6 +617,8 @@ fun AppSettingsScreen(navController: NavController) {
}
}
Spacer(modifier = Modifier.height(32.dp))
if (showResetDialog) {
AlertDialog(
onDismissRequest = { showResetDialog = false },

View File

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

View File

@@ -34,8 +34,7 @@
<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="back">Back</string>
<string name="app_settings">App Settings</string>
<string name="conversational_awareness_customization">Conversational Awareness</string>
<string name="app_settings">Customizations</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="conversational_awareness_pause_music">Pause Music</string>