Compare commits

...

5 Commits

Author SHA1 Message Date
Kavish Devar
216c97f9ca android: add CP1A.260505.005 to comptible build ids on Pixel 2026-05-06 17:29:23 +05:30
Kavish Devar
fd3774b513 android: bump version 2026-05-05 13:18:08 +05:30
Kavish Devar
b7336940e6 android: add convo detect broadcast 2026-05-05 13:17:31 +05:30
Kavish Devar
b2ba830a80 android: hide reconnect when app hasn't connected once 2026-05-05 13:11:50 +05:30
Kavish Devar
f08769e62f android: add optmized charge limit config 2026-05-05 13:05:54 +05:30
7 changed files with 79 additions and 27 deletions

View File

@@ -1,6 +1,6 @@
import java.util.Properties
val appVersionName = "0.2.8"
val appVersionName = "0.2.9"
plugins {
alias(libs.plugins.android.application)
@@ -30,7 +30,7 @@ android {
applicationId = "me.kavishdevar.librepods"
minSdk = 33
targetSdk = 37
versionCode = 49
versionCode = 50
versionName = appVersionName
}
buildTypes {

View File

@@ -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? =

View File

@@ -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(
@@ -542,19 +552,22 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
}
Spacer(Modifier.height(16.dp))
}
StyledButton(
onClick = {
viewModel.reconnectFromSavedMac()
}, backdrop = backdrop, modifier = Modifier.fillMaxWidth(0.9f)
) {
Text(
text = stringResource(R.string.reconnect_to_last_device), style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = if (isSystemInDarkTheme()) Color.White else Color.Black
if (state.connectionSuccessful) {
StyledButton(
onClick = {
viewModel.reconnectFromSavedMac()
}, backdrop = backdrop, modifier = Modifier.fillMaxWidth(0.9f)
) {
Text(
text = stringResource(R.string.reconnect_to_last_device),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = if (isSystemInDarkTheme()) Color.White else Color.Black
)
)
)
}
}
}
}

View File

@@ -89,7 +89,11 @@ data class AirPodsUiState(
val hearingAidData: ByteArray = byteArrayOf(),
val isPremium: Boolean = false,
val vendorIdHook: Boolean = false
val vendorIdHook: Boolean = false,
val dynamicEndOfCharge: Boolean = false,
val connectionSuccessful: Boolean = false
)
class AirPodsViewModel(
@@ -268,9 +272,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 +316,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 +354,9 @@ class AirPodsViewModel(
) ?: "CYCLE_NOISE_CONTROL_MODES"
)
val vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false)
val dynamicEndOfCharge = sharedPreferences.getBoolean("dynamic_end_of_charge", false)
val connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false)
_uiState.update {
it.copy(
@@ -351,7 +366,9 @@ class AirPodsViewModel(
headGesturesEnabled = headGesturesEnabled,
leftAction = leftAction,
rightAction = rightAction,
vendorIdHook = vendorIdHook
vendorIdHook = vendorIdHook,
dynamicEndOfCharge = dynamicEndOfCharge,
connectionSuccessful = connectionSuccessful
)
}
}
@@ -371,6 +388,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(

View File

@@ -526,7 +526,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
initializeConfig()
ancModeReceiver = object : BroadcastReceiver() {
externalBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "me.kavishdevar.librepods.SET_ANC_MODE") {
if (intent.hasExtra("mode")) {
@@ -555,15 +555,23 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
"Cycling ANC mode from $currentMode to $nextMode"
)
}
} else if (intent?.action == "me.kavishdevar.librepods.CONVO_DETECT") {
if (intent.hasExtra("enabled")) {
val enabled = intent.getBooleanExtra("enabled", false)
aacpManager.sendControlCommand(
AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG.value,
enabled
)
}
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(ancModeReceiver, ancModeFilter, RECEIVER_EXPORTED)
registerReceiver(externalBroadcastReceiver, externalBroadcastFilter, RECEIVER_EXPORTED)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag") registerReceiver(
ancModeReceiver, ancModeFilter
externalBroadcastReceiver, externalBroadcastFilter
)
}
val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
@@ -2397,8 +2405,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
}
val ancModeFilter = IntentFilter("me.kavishdevar.librepods.SET_ANC_MODE")
var ancModeReceiver: BroadcastReceiver? = null
val externalBroadcastFilter = IntentFilter().apply {
addAction("me.kavishdevar.librepods.SET_ANC_MODE")
addAction("me.kavishdevar.librepods.CONVO_DETECT")
}
var externalBroadcastReceiver: BroadcastReceiver? = null
@SuppressLint("InlinedApi", "MissingPermission", "UnspecifiedRegisterReceiverFlag")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -3104,7 +3115,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
e.printStackTrace()
}
try {
unregisterReceiver(ancModeReceiver)
unregisterReceiver(externalBroadcastReceiver)
} catch (e: Exception) {
e.printStackTrace()
}

View File

@@ -31,7 +31,7 @@ fun isSupported(sharedPreferences: SharedPreferences): Boolean {
if (isPixel) {
when (Build.VERSION.SDK_INT) {
36 -> {
return Build.ID == "CP1A.260305.018" || Build.ID == "CP1A.260405.005"
return Build.ID == "CP1A.260305.018" || Build.ID == "CP1A.260405.005" || Build.ID == "CP1A.260505.005"
}
37 -> {

View File

@@ -272,4 +272,6 @@
<string name="app_enabled_in_xposed">App enabled in Xposed</string>
<string name="subject">Subject</string>
<string name="describe_your_issue">Describe your issue</string>
<string name="optimized_charging">Optimized Charge Limit</string>
<string name="optimized_charging_description">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.</string>
</resources>