Skip to content
Open
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
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 @@ -112,7 +112,11 @@ class AACPManager {
0x37
),
PPE_CAP_LEVEL_CONFIG(0x38),
DYNAMIC_END_OF_CHARGE(0x3B);
DYNAMIC_END_OF_CHARGE(0x3B),
UPLINK_EQ_BUD_CONFIG(0x3E),
UPLINK_EQ_SOURCE_CONFIG(0x3F),
IN_CASE_TONE_VOLUME(0x40),
DISABLE_BUTTON_INPUT(0x41);

companion object {
fun fromByte(byte: Byte): ControlCommandIdentifiers? =
Expand Down Expand Up @@ -214,10 +218,7 @@ class AACPManager {
private fun setControlCommandStatusValue(
identifier: ControlCommandIdentifiers, value: ByteArray
) {
val existingStatus = getControlCommandStatus(identifier)
if (existingStatus?.value.contentEquals(value)) {
controlCommandStatusList.remove(existingStatus)
}
controlCommandStatusList.removeAll { it.identifier == identifier }
controlCommandListeners[identifier]?.forEach { listener ->
listener.onControlCommandReceived(ControlCommand(identifier.value, value))
}
Expand Down Expand Up @@ -308,38 +309,36 @@ class AACPManager {

fun sendControlCommand(identifier: Byte, value: ByteArray): Boolean {
val controlPacket = createControlCommandPacket(identifier, value)
setControlCommandStatusValue(
ControlCommandIdentifiers.fromByte(identifier) ?: return false, value
)
ControlCommandIdentifiers.fromByte(identifier)?.let {
setControlCommandStatusValue(it, value)
}
return sendDataPacket(controlPacket)
}

@OptIn(ExperimentalStdlibApi::class)
fun sendControlCommand(identifier: Byte, value: Byte): Boolean {
val controlPacket = createControlCommandPacket(identifier, byteArrayOf(value))
setControlCommandStatusValue(
ControlCommandIdentifiers.fromByte(identifier) ?: return false, byteArrayOf(value)
)
ControlCommandIdentifiers.fromByte(identifier)?.let {
setControlCommandStatusValue(it, byteArrayOf(value))
}
return sendDataPacket(controlPacket)
}

fun sendControlCommand(identifier: Byte, value: Boolean): Boolean {
val controlPacket = createControlCommandPacket(
identifier, if (value) byteArrayOf(0x01) else byteArrayOf(0x02)
)
setControlCommandStatusValue(
ControlCommandIdentifiers.fromByte(identifier) ?: return false,
if (value) byteArrayOf(0x01) else byteArrayOf(0x02)
)
val bytes = if (value) byteArrayOf(0x01) else byteArrayOf(0x02)
val controlPacket = createControlCommandPacket(identifier, bytes)
ControlCommandIdentifiers.fromByte(identifier)?.let {
setControlCommandStatusValue(it, bytes)
}
return sendDataPacket(controlPacket)
}

fun sendControlCommand(identifier: Byte, value: Int): Boolean {
val controlPacket = createControlCommandPacket(identifier, byteArrayOf(value.toByte()))
setControlCommandStatusValue(
ControlCommandIdentifiers.fromByte(identifier) ?: return false,
byteArrayOf(value.toByte())
)
val bytes = byteArrayOf(value.toByte())
val controlPacket = createControlCommandPacket(identifier, bytes)
ControlCommandIdentifiers.fromByte(identifier)?.let {
setControlCommandStatusValue(it, bytes)
}
return sendDataPacket(controlPacket)
}

Expand Down Expand Up @@ -1153,10 +1152,9 @@ class AACPManager {
TAG, "Control command: ${controlCommand.identifier.toHexString()} - ${
controlCommand.value.joinToString(" ") { "%02X".format(it) }
}")
setControlCommandStatusValue(
ControlCommandIdentifiers.fromByte(controlCommand.identifier) ?: return false,
controlCommand.value
)
ControlCommandIdentifiers.fromByte(controlCommand.identifier)?.let {
setControlCommandStatusValue(it, controlCommand.value)
}
}

val socket = BluetoothConnectionManager.aacpSocket ?: return false
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
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ class AirPodsPro2USBC: AirPodsBase(
)

class AirPodsPro3: AirPodsBase(
modelNumber = listOf("A3063", "A3064", "A3065"),
modelNumber = listOf("A3063", "A3064", "A3065", "A3122"),
name = "AirPods Pro 3",
displayName = "AirPods Pro",
// budCaseRes = R.drawable.airpods_pro_3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ fun AirPodsSettingsScreen(
automaticEarDetectionEnabled = state.automaticEarDetectionEnabled,
onAutomaticEarDetectionChanged = onAutomaticEarDetectionChanged,
automaticConnectionEnabled = state.automaticConnectionEnabled,
onAutomaticConnectionChanged = onAutomaticConnectionChanged
onAutomaticConnectionChanged = onAutomaticConnectionChanged,
)
}

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 Expand Up @@ -493,7 +495,6 @@ class AirPodsViewModel(
)
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 {
Expand Down
Loading