From 01432ce9c70656ddd44adb9bb099a5099a5d9ac9 Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Sun, 11 May 2025 19:40:57 +0530 Subject: [PATCH] andoid: add option to not disconnect airpods when none are worn --- .../librepods/screens/AppSettingsScreen.kt | 261 ++++++++++++++---- .../librepods/services/AirPodsService.kt | 8 +- android/app/src/main/res/values/strings.xml | 3 +- 3 files changed, 213 insertions(+), 59 deletions(-) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt index 67bf3cd..6dbf8cc 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt @@ -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 }, diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt index f89f650..7f8e171 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt @@ -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( diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index cc67374..06bcc06 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -34,8 +34,7 @@ AirPods not connected 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!) Back - App Settings - Conversational Awareness + Customizations Relative volume Reduces to a percentage of the current volume instead of the maximum volume. Pause Music