From f08769e62fac3c013ab3b29d936afabfff07906c Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Tue, 5 May 2026 13:05:54 +0530 Subject: [PATCH] android: add optmized charge limit config --- .../librepods/bluetooth/AACPManager.kt | 3 +- .../screens/AirPodsSettingsScreen.kt | 10 +++++++ .../viewmodel/AirPodsViewModel.kt | 30 +++++++++++++++---- android/app/src/main/res/values/strings.xml | 2 ++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt index 726430e..aed865c 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt @@ -109,7 +109,8 @@ class AACPManager { EAR_DETECTION_CONFIG(0x0A), AUTOMATIC_CONNECTION_CONFIG(0x20), OWNS_CONNECTION(0x06), PPE_TOGGLE_CONFIG( 0x37 ), - PPE_CAP_LEVEL_CONFIG(0x38); + PPE_CAP_LEVEL_CONFIG(0x38), + DYNAMIC_END_OF_CHARGE(0x3B); companion object { fun fromByte(byte: Byte): ControlCommandIdentifiers? = diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt index 08b9b2c..a822e77 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt @@ -398,6 +398,16 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl } } + item(key = "spacer_dynamic_end_of_charge") { Spacer(modifier = Modifier.height(16.dp)) } + item(key = "dynamic_end_of_charge") { + StyledToggle( + label = stringResource(R.string.optimized_charging), + description = stringResource(R.string.optimized_charging_description), + checked = state.dynamicEndOfCharge, + onCheckedChange = viewModel::setDynamicEndOfCharge + ) + } + item(key = "spacer_accessibility") { Spacer(modifier = Modifier.height(16.dp)) } item(key = "accessibility") { NavigationButton( diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt index 7018ae9..c8b22c2 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt @@ -89,7 +89,9 @@ data class AirPodsUiState( val hearingAidData: ByteArray = byteArrayOf(), val isPremium: Boolean = false, - val vendorIdHook: Boolean = false + val vendorIdHook: Boolean = false, + + val dynamicEndOfCharge: Boolean = false ) class AirPodsViewModel( @@ -268,9 +270,16 @@ class AirPodsViewModel( val current = state.controlStates[identifier] if (current?.contentEquals(value) == true) return@update state - state.copy( - controlStates = state.controlStates + (identifier to value) - ) + if (identifier == ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE) { + state.copy( + dynamicEndOfCharge = value[0] == 0x01.toByte(), + controlStates = state.controlStates + (identifier to value) + ) + } else { + state.copy( + controlStates = state.controlStates + (identifier to value) + ) + } } } @@ -305,6 +314,7 @@ class AirPodsViewModel( ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG, ControlCommandIdentifiers.OWNS_CONNECTION, ControlCommandIdentifiers.PPE_TOGGLE_CONFIG, + ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE ) for (identifier in identifiersList) { observeControl(identifier) @@ -342,6 +352,7 @@ class AirPodsViewModel( ) ?: "CYCLE_NOISE_CONTROL_MODES" ) val vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false) + val dynamicEndOfCharge = sharedPreferences.getBoolean("dynamic_end_of_charge", false) _uiState.update { it.copy( @@ -351,7 +362,8 @@ class AirPodsViewModel( headGesturesEnabled = headGesturesEnabled, leftAction = leftAction, rightAction = rightAction, - vendorIdHook = vendorIdHook + vendorIdHook = vendorIdHook, + dynamicEndOfCharge = dynamicEndOfCharge ) } } @@ -371,6 +383,14 @@ class AirPodsViewModel( } } + fun setDynamicEndOfCharge(enabled: Boolean) { + service.aacpManager.sendControlCommand(ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE.value, enabled) + sharedPreferences.edit { putBoolean("dynamic_end_of_charge", enabled) } + _uiState.update { + it.copy(dynamicEndOfCharge = enabled) + } + } + private fun loadControlList() { _uiState.update { it.copy( diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index fbbea37..f769983 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -272,4 +272,6 @@ App enabled in Xposed Subject Describe your issue + Optimized Charge Limit + AirPods can learn from your daily usage and determine when to charge to an optmized limit and when to allow or full charge. This limit adapts to your daily usage and preserves your battery lifespan over time.\\nThis setting may not affect unsupported AirPods, or AirPods on an older firmware version.