Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 55 additions & 234 deletions README.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@
#-renamesourcefileattribute SourceFile

-keep class me.kavishdevar.librepods.utils.KotlinModule { *; }
-keep class me.kavishdevar.librepods.utils.NativeBridge { *; }
-keepclasseswithmembernames class * {
native <methods>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import me.kavishdevar.librepods.utils.XposedState
class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener, DefaultLifecycleObserver {

override fun onCreate() {
try {
System.loadLibrary("bluetooth_socket")
} catch (e: Throwable) {
android.util.Log.e("LibrePodsApp", "Failed to load bluetooth_socket: ${e.message}")
}
XposedServiceHelper.registerListener(this)
BillingManager.provider = BillingProviderFactory.create(this)
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
Expand All @@ -22,16 +27,28 @@ class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener

}

private fun isBluetoothScopeEnabled(): Boolean {
val scope = XposedServiceHolder.service?.scope ?: return false
return scope.any {
it in listOf(
"com.google.android.bluetooth",
"com.android.bluetooth",
"com.google.android.btservices",
"com.android.btservices"
)
}
}

override fun onResume(owner: LifecycleOwner) {
BillingManager.provider.queryPurchases()
XposedState.isAvailable = XposedServiceHolder.service != null
XposedState.bluetoothScopeEnabled = XposedServiceHolder.service?.scope?.contains("com.google.android.bluetooth") == true || XposedServiceHolder.service?.scope?.contains("com.android.bluetooth") == true
XposedState.bluetoothScopeEnabled = isBluetoothScopeEnabled()
}

override fun onServiceBind(service: XposedService) {
XposedServiceHolder.service = service
XposedState.isAvailable = true
XposedState.bluetoothScopeEnabled = XposedServiceHolder.service?.scope?.contains("com.google.android.bluetooth") == true || XposedServiceHolder.service?.scope?.contains("com.android.bluetooth") == true
XposedState.bluetoothScopeEnabled = isBluetoothScopeEnabled()
}

override fun onServiceDied(p0: XposedService) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ fun createBluetoothSocket(
val constructorSpecs = listOf(
arrayOf(adapter, device, type, true, true, psm, uuid), // A16QPR3
arrayOf(device, type, true, true, psm, uuid),
arrayOf(device, type, 1, true, true, psm, uuid),
arrayOf(type, 1, true, true, device, psm, uuid),
arrayOf(type, true, true, device, psm, uuid)
arrayOf(device, type, -1, true, true, psm, uuid),
arrayOf(type, -1, true, true, device, psm, uuid),
arrayOf(type, true, true, device, psm, uuid),
arrayOf(type, device, true, true, psm, uuid)
)

val constructors = BluetoothSocket::class.java.declaredConstructors
Expand Down Expand Up @@ -69,6 +70,60 @@ fun createBluetoothSocket(
}
}

// Dynamic constructor resolution fallback
for (constructor in constructors) {
try {
val paramTypes = constructor.parameterTypes
val args = arrayOfNulls<Any>(paramTypes.size)
var matched = true

for (i in paramTypes.indices) {
val pt = paramTypes[i]
when {
pt == BluetoothAdapter::class.java -> args[i] = adapter
pt == BluetoothDevice::class.java -> args[i] = device
pt == ParcelUuid::class.java -> args[i] = uuid
pt == java.lang.Boolean.TYPE || pt == java.lang.Boolean::class.java -> args[i] = true
pt == java.lang.Integer.TYPE || pt == java.lang.Integer::class.java -> {
// Will populate in second pass based on total integer count
}
else -> {
matched = false
break
}
}
}

if (matched) {
val totalInts = paramTypes.count { it == java.lang.Integer.TYPE || it == java.lang.Integer::class.java }
var currentIntIndex = 0
for (i in paramTypes.indices) {
val pt = paramTypes[i]
if (pt == java.lang.Integer.TYPE || pt == java.lang.Integer::class.java) {
currentIntIndex++
args[i] = when (totalInts) {
1 -> psm
2 -> if (currentIntIndex == 1) type else psm
3 -> when (currentIntIndex) {
1 -> type
2 -> -1
else -> psm
}
else -> if (currentIntIndex == totalInts) psm else type
}
}
}

constructor.isAccessible = true
Log.d("createSocket<psm>", "Successfully created socket using dynamic fallback constructor (${paramTypes.joinToString { it.simpleName }})")
return constructor.newInstance(*args) as BluetoothSocket
}
} catch (e: Exception) {
Log.w("createSocket<psm>", "Dynamic constructor invocation failed: ${e.message}")
lastException = e
}
}

val errorMessage =
"Failed to create BluetoothSocket after trying $attemptedConstructors constructor signatures"
Log.e("createSocket<psm>", errorMessage)
Expand Down
15 changes: 10 additions & 5 deletions android/app/src/main/java/me/kavishdevar/librepods/data/AirPods.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ enum class Capability {
ADAPTIVE_AUDIO,
ADAPTIVE_VOLUME,
SWIPE_FOR_VOLUME,
HRM
HRM,
CASE_SOUNDS
}

class AirPods: AirPodsBase(
Expand Down Expand Up @@ -137,7 +138,8 @@ class AirPods4ANC: AirPodsBase(
Capability.ADAPTIVE_AUDIO,
Capability.SLEEP_DETECTION,
Capability.ADAPTIVE_VOLUME,
Capability.STEM_CONFIG
Capability.STEM_CONFIG,
Capability.CASE_SOUNDS
)
)

Expand Down Expand Up @@ -184,7 +186,8 @@ class AirPodsPro2Lightning: AirPodsBase(
Capability.ADAPTIVE_AUDIO,
Capability.ADAPTIVE_VOLUME,
Capability.SWIPE_FOR_VOLUME,
Capability.HEAD_GESTURES
Capability.HEAD_GESTURES,
Capability.CASE_SOUNDS
)
)

Expand Down Expand Up @@ -212,7 +215,8 @@ class AirPodsPro2USBC: AirPodsBase(
Capability.ADAPTIVE_AUDIO,
Capability.ADAPTIVE_VOLUME,
Capability.SWIPE_FOR_VOLUME,
Capability.HEAD_GESTURES
Capability.HEAD_GESTURES,
Capability.CASE_SOUNDS
)
)

Expand Down Expand Up @@ -242,7 +246,8 @@ class AirPodsPro3: AirPodsBase(
Capability.ADAPTIVE_AUDIO,
Capability.ADAPTIVE_VOLUME,
Capability.SWIPE_FOR_VOLUME,
Capability.HRM
Capability.HRM,
Capability.CASE_SOUNDS
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ fun ConnectionSettings(
onAutomaticEarDetectionChanged: (Boolean) -> Unit,
automaticConnectionEnabled: Boolean,
onAutomaticConnectionChanged: (Boolean) -> Unit,
caseSoundsCapability: Boolean = false,
caseSoundsEnabled: Boolean = false,
onCaseSoundsChanged: ((Boolean) -> Unit)? = null,
) {
StyledList {
StyledToggle(
Expand All @@ -45,5 +48,14 @@ fun ConnectionSettings(
checked = automaticConnectionEnabled,
onCheckedChange = onAutomaticConnectionChanged
)

if (caseSoundsCapability && onCaseSoundsChanged != null) {
StyledToggle(
label = stringResource(R.string.case_sounds),
description = stringResource(R.string.case_sounds_description),
checked = caseSoundsEnabled,
onCheckedChange = onCaseSoundsChanged
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -491,11 +491,23 @@ fun AirPodsSettingsScreen(

item(key = "spacer_connection") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "connection") {
val model = state.instance?.model ?: AirPodsPro3()
val caseSoundsCapability = model.capabilities.contains(Capability.CASE_SOUNDS)
val caseSoundsChecked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.IN_CASE_TONE_CONFIG]?.getOrNull(0) != 0x02.toByte()

ConnectionSettings(
automaticEarDetectionEnabled = state.automaticEarDetectionEnabled,
onAutomaticEarDetectionChanged = onAutomaticEarDetectionChanged,
automaticConnectionEnabled = state.automaticConnectionEnabled,
onAutomaticConnectionChanged = onAutomaticConnectionChanged
onAutomaticConnectionChanged = onAutomaticConnectionChanged,
caseSoundsCapability = caseSoundsCapability,
caseSoundsEnabled = caseSoundsChecked,
onCaseSoundsChanged = { checked ->
setControlCommandBoolean(
AACPManager.Companion.ControlCommandIdentifiers.IN_CASE_TONE_CONFIG,
checked
)
}
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ val demoState = AirPodsUiState(
ControlCommandIdentifiers.PPE_TOGGLE_CONFIG to byteArrayOf(0x01),
ControlCommandIdentifiers.PPE_CAP_LEVEL_CONFIG to byteArrayOf(0x52),
ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE to byteArrayOf(0x01),
ControlCommandIdentifiers.IN_CASE_TONE_CONFIG to byteArrayOf(0x01),
ControlCommandIdentifiers.LISTENING_MODE to byteArrayOf(0x04)
)
)
Expand Down Expand Up @@ -448,7 +449,8 @@ class AirPodsViewModel(
ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
ControlCommandIdentifiers.OWNS_CONNECTION,
ControlCommandIdentifiers.PPE_TOGGLE_CONFIG,
ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE
ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE,
ControlCommandIdentifiers.IN_CASE_TONE_CONFIG
)
for (identifier in identifiersList) {
observeControl(identifier)
Expand Down
Loading