Compare commits

...

15 Commits

Author SHA1 Message Date
Kavish Devar 6ac6700be6 android: format l2c_fcr_hook.cpp 2026-04-23 18:45:00 +05:30
Kavish Devar 113ee0a966 android: fallback to .dynsym when .gnu_debugdata fails 2026-04-23 18:45:00 +05:30
Kavish Devar d82e4e2427 android: bump version 2026-04-23 18:45:00 +05:30
Kavish Devar 481d5f13cf android: fix automatically pausing when media changes without vendorid hook enabled 2026-04-23 18:45:00 +05:30
Kavish Devar ef221af505 android: bump version 2026-04-23 18:45:00 +05:30
Kavish Devar c19190f031 android: fix convo detect not restoring volume when in Transparency mode 2026-04-23 18:45:00 +05:30
Kavish Devar d0b8574c68 android: hide disconnect when not wearing config on play builds 2026-04-23 18:45:00 +05:30
Kavish Devar 294d733e71 android: add 'required xposed' text to vendorid config toggle 2026-04-23 18:45:00 +05:30
Kavish Devar f6d7e97796 android: show price in buy button 2026-04-23 18:45:00 +05:30
Kavish Devar ae174bc9ea android: add confirmation step for unsupported devices 2026-04-23 18:45:00 +05:30
Kavish Devar 1804e80cba docs: fix typo 2026-04-23 05:43:43 +05:30
Kavish Devar 0b8bd5a5b8 docs: fix issuetracker link in README 2026-04-23 05:42:58 +05:30
Kavish Devar d1d48562d7 docs: clarify root requirements 2026-04-23 05:41:56 +05:30
Kavish Devar c84e64e656 android: remove unsupported device message 2026-04-23 01:27:55 +05:30
Kavish Devar 51739514fa android: fix normal builds 2026-04-23 01:22:49 +05:30
14 changed files with 221 additions and 333 deletions
+18 -5
View File
@@ -76,10 +76,23 @@ https://github.com/user-attachments/assets/43911243-0576-4093-8c55-89c1db5ea533
### Root Requirement ### Root Requirement
If you are using ColorOS/OxygenOS 16, Android 16 QPR3, Android 17 Beta 3 or higher, you don't need root except for customizing transparency mode, setting up hearing aid, and use Bluetooth Multipoint. Changing ANC, conversational awareness, ear detection, and other customizations will work without root. The app needs root because of a bug in the Android Bluetooth stack Fluoride/non-compliance of Apple with Bluetooth standards. You must have Xposed installed for the app to workaround this bug and connect to AirPods.
For everyone else: [https://issuetracker.google.com/issues/371713238](https://issuetracker.google.com/issues/371713238)
**You must have a rooted device with Xposed to use LibrePods on Android.**
Please do not comment in the thread. The issue has already been resolved and should be available in Android 17 for all devices.
However, if you are using ColorOS/OxygenOS 16, Android 16 QPR3 on Pixel (ensure you're on the latest Play system update), you don't need root for most features.
> [!IMPORTANT]
> This workaround with Xposed is not guaranteed to work on all devices.
Features requiring the VendorID hook will still require root. These features include customizing transparency mode, setting up hearing aid, and use Bluetooth Multipoint.
### Troubleshooting steps for common errors
- Ensure the correct scope is set in LSPosed/Vector.
- Ensure there is no root-hiding module preventing the hook from loading on the Bluetooth app.
- Restart your phone after confirming the scope.
### A few notes ### A few notes
@@ -129,7 +142,7 @@ A huge thank you to everyone supporting the project!
- MagicPods for Steam Deck ([website](https://magicpods.app/steamdeck/)) - MagicPods for Steam Deck ([website](https://magicpods.app/steamdeck/))
- MagicPods - if you're looking for "LibrePods for Windows" ([ms store](https://apps.microsoft.com/store/detail/9P6SKKFKSHKM) [installer](https://magicpods.app/installer/MagicPods.appinstaller) | [website](https://magicpods.app/)) - MagicPods - if you're looking for "LibrePods for Windows" ([ms store](https://apps.microsoft.com/store/detail/9P6SKKFKSHKM) [installer](https://magicpods.app/installer/MagicPods.appinstaller) | [website](https://magicpods.app/))
# Nightly / Development Builds # Nightly/Development Builds
Want to try the latest features before they're officially released? You can grab nightly builds from GitHub Actions: Want to try the latest features before they're officially released? You can grab nightly builds from GitHub Actions:
@@ -140,7 +153,7 @@ Want to try the latest features before they're officially released? You can grab
4. Extract the zip and install the `.apk` on your device 4. Extract the zip and install the `.apk` on your device
> [!NOTE] > [!NOTE]
> You need to be signed in to GitHub to download artifacts. Nightly builds are debug-signed and may not auto-update — you may need to uninstall the stable version first. > You need to be signed in to GitHub to download artifacts. Nightly builds are debug-signed and may not auto-update. You may need to uninstall the stable version first.
### Linux (Rust) ### Linux (Rust)
1. Go to the [Actions tab](https://github.com/kavishdevar/librepods/actions/workflows/ci-linux-rust.yml) 1. Go to the [Actions tab](https://github.com/kavishdevar/librepods/actions/workflows/ci-linux-rust.yml)
+2 -2
View File
@@ -28,8 +28,8 @@ android {
applicationId = "me.kavishdevar.librepods" applicationId = "me.kavishdevar.librepods"
minSdk = 33 minSdk = 33
targetSdk = 37 targetSdk = 37
versionCode = 28 versionCode = 33
versionName = "0.2.0" versionName = "0.2.2"
} }
buildTypes { buildTypes {
release { release {
Binary file not shown.
@@ -128,6 +128,7 @@ import me.kavishdevar.librepods.billing.BillingProviderFactory
import me.kavishdevar.librepods.data.AirPodsNotifications import me.kavishdevar.librepods.data.AirPodsNotifications
import me.kavishdevar.librepods.data.ControlCommandRepository import me.kavishdevar.librepods.data.ControlCommandRepository
import me.kavishdevar.librepods.presentation.components.ConfirmationDialog import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
import me.kavishdevar.librepods.presentation.components.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledIconButton import me.kavishdevar.librepods.presentation.components.StyledIconButton
import me.kavishdevar.librepods.presentation.screens.AccessibilitySettingsScreen import me.kavishdevar.librepods.presentation.screens.AccessibilitySettingsScreen
import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen
@@ -223,18 +224,9 @@ fun Main() {
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE) val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
if (!isSupported(sharedPreferences)) { if (!isSupported(sharedPreferences)) {
val showDialog = remember { mutableStateOf(false) } val showDialog = remember { mutableStateOf(false) }
val blockTouches = remember { mutableStateOf(false) }
val tapCount = remember { mutableIntStateOf(0) }
val lastTapTime = remember { mutableLongStateOf(0L) }
val hazeState = rememberHazeState() val hazeState = rememberHazeState()
LaunchedEffect(blockTouches) {
if (blockTouches.value) {
delay(500)
blockTouches.value = false
}
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -245,26 +237,12 @@ fun Main() {
Box ( Box (
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.then(
if (blockTouches.value)
{
Modifier.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Initial)
event.changes.forEach { it.consume() }
}
}
}
}
else Modifier
)
) )
Column ( Column (
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Text( Text(
text = "Not supported", text = stringResource(R.string.not_supported),
style = TextStyle( style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)), fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
@@ -275,25 +253,7 @@ fun Main() {
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Row ( Row (
modifier = Modifier.fillMaxWidth().pointerInput(Unit) { modifier = Modifier.fillMaxWidth(),
detectTapGestures(
onTap = {
val now = System.currentTimeMillis()
if (now - lastTapTime.longValue > 400) {
tapCount.intValue = 0
}
tapCount.intValue++
lastTapTime.longValue = now
if (tapCount.intValue >= 7) {
showDialog.value = true
blockTouches.value = true
}
}
)
},
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
Text( Text(
@@ -323,7 +283,7 @@ fun Main() {
) )
} }
Text( Text(
text = "Check the repository for more info.", text = stringResource(R.string.check_the_repository_for_more_info),
style = TextStyle( style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)), fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
@@ -333,19 +293,35 @@ fun Main() {
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
StyledButton(
onClick = { showDialog.value = true },
backdrop = rememberLayerBackdrop(),
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text(
text = stringResource(R.string.bypass_compatibility_check),
style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium,
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
fontSize = 16.sp
),
)
}
} }
} }
ConfirmationDialog( ConfirmationDialog(
showDialog = showDialog, showDialog = showDialog,
title = "Confirm device check bypass?", title = stringResource(R.string.bypass_compatibility_check),
message = "Are you sure your device is supported with LibrePods?", message = stringResource(R.string.bypass_compatiblity_check_confirmation),
confirmText = "Yes", confirmText = "Yes",
dismissText = "No", dismissText = "No",
onConfirm = { onConfirm = {
showDialog.value = false showDialog.value = false
sharedPreferences.edit { sharedPreferences.edit {
tapCount.intValue = 0
putBoolean("bypass_device_check", true) putBoolean("bypass_device_check", true)
val intent = Intent(context, MainActivity::class.java) val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
@@ -217,7 +217,7 @@ fun AppSettingsScreen(
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
if (BuildConfig.FLAVOR == "xposed") { if (!BuildConfig.PLAY_BUILD) {
StyledToggle( StyledToggle(
title = stringResource(R.string.ear_detection), title = stringResource(R.string.ear_detection),
label = stringResource(R.string.disconnect_when_not_wearing), label = stringResource(R.string.disconnect_when_not_wearing),
@@ -367,10 +367,8 @@ fun AppSettingsScreen(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
val restartBluetoothText = stringResource(R.string.found_offset_restart_bluetooth) val restartBluetoothText = stringResource(R.string.found_offset_restart_bluetooth)
StyledToggle( StyledToggle(
label = stringResource(R.string.act_as_an_apple_device), label = stringResource(R.string.act_as_an_apple_device) + " (${stringResource(R.string.requires_xposed)})",
description = stringResource(R.string.act_as_an_apple_device_description) + "\n" + stringResource( description = stringResource(R.string.act_as_an_apple_device_description),
R.string.requires_xposed
).replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() },
checked = state.vendorIdHook, checked = state.vendorIdHook,
onCheckedChange = { enabled -> onCheckedChange = { enabled ->
Toast.makeText(context, restartBluetoothText, Toast.LENGTH_SHORT).show() Toast.makeText(context, restartBluetoothText, Toast.LENGTH_SHORT).show()
@@ -459,7 +459,7 @@ fun PurchaseScreen(
tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900) tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
) { ) {
Text( Text(
stringResource(R.string.buy), stringResource(R.string.buy_price, state.price),
style = TextStyle( style = TextStyle(
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
@@ -69,7 +69,14 @@ import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat.START_STICKY
import androidx.core.app.ServiceCompat.startForeground
import androidx.core.content.ContextCompat.RECEIVER_EXPORTED
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.ContextCompat.registerReceiver
import androidx.core.content.ContextCompat.startActivity
import androidx.core.content.edit import androidx.core.content.edit
import com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule_PackageNameFactory.packageName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -128,6 +135,7 @@ import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.jvm.java
private const val TAG = "AirPodsService" private const val TAG = "AirPodsService"
@@ -913,7 +921,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (conversationAwarenessNotification.status == 1.toByte() || conversationAwarenessNotification.status == 2.toByte()) { if (conversationAwarenessNotification.status == 1.toByte() || conversationAwarenessNotification.status == 2.toByte()) {
MediaController.startSpeaking() MediaController.startSpeaking()
} else if (conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) { } else if (conversationAwarenessNotification.status == 6.toByte() ||conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) {
MediaController.stopSpeaking() MediaController.stopSpeaking()
} }
@@ -2434,16 +2442,13 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
) )
otherDeviceTookOver = false otherDeviceTookOver = false
} }
val ownsConnection = aacpManager.getControlCommandStatus(AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION)?.value?.get(0)?.toInt()
Log.d( Log.d(
TAG, "owns connection: ${ TAG, "owns connection: $ownsConnection"
aacpManager.getControlCommandStatus(AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION)?.value?.get(
0
)?.toInt()
}"
) )
if (!::socket.isInitialized) return if (!::socket.isInitialized) return
if (socket.isConnected) { if (socket.isConnected) {
if (BuildConfig.FLAVOR != "xposed") { if (!XposedRemotePrefProvider.create().getBoolean("vendor_id_hook", false) || ownsConnection == 0) {
Log.d(TAG, "not taking over, vendorid is probably not set to apple") Log.d(TAG, "not taking over, vendorid is probably not set to apple")
return return
} }
@@ -39,6 +39,5 @@ fun isSupported(sharedPreferences: SharedPreferences): Boolean {
} else if (isOppoOrOnePlus) { } else if (isOppoOrOnePlus) {
return true return true
} }
return if (BuildConfig.FLAVOR == "xposed") true return sharedPreferences.getBoolean("bypass_device_check", false)
else sharedPreferences.getBoolean("bypass_device_check", false)
} }
+5 -1
View File
@@ -211,7 +211,7 @@
<string name="listening_mode_adaptive_description">Dynamically adjust external noise</string> <string name="listening_mode_adaptive_description">Dynamically adjust external noise</string>
<string name="listening_mode_noise_cancellation_description">Blocks out external sounds</string> <string name="listening_mode_noise_cancellation_description">Blocks out external sounds</string>
<string name="unlock_advanced_features">Unlock advanced features</string> <string name="unlock_advanced_features">Unlock advanced features</string>
<string name="buy">Buy</string> <string name="buy_price">Buy %s</string>
<string name="restore_purchases">Restore purchases</string> <string name="restore_purchases">Restore purchases</string>
<string name="ear_detection_description">Automatically stop playing audio when you take them off, and resume playback when you put them back on.</string> <string name="ear_detection_description">Automatically stop playing audio when you take them off, and resume playback when you put them back on.</string>
<string name="battery">Battery</string> <string name="battery">Battery</string>
@@ -236,4 +236,8 @@
<string name="yes">Yes</string> <string name="yes">Yes</string>
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="requires_xposed">requires xposed</string> <string name="requires_xposed">requires xposed</string>
<string name="bypass_compatibility_check">Bypass compatibility check</string>
<string name="bypass_compatiblity_check_confirmation">Are you sure your device is supported natively/you have Xposed module enabled?</string>
<string name="not_supported">Not supported</string>
<string name="check_the_repository_for_more_info">Check the repository for more info.</string>
</resources> </resources>
@@ -0,0 +1,5 @@
package me.kavishdevar.librepods.utils
object NativeBridge {
fun setSdpHook(enabled: Boolean) { }
}
@@ -1,125 +0,0 @@
package me.kavishdevar.librepods.utils
import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.ImageView
import androidx.core.net.toUri
import io.github.libxposed.api.XposedModule
import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam
import io.github.libxposed.api.XposedModuleInterface.PackageLoadedParam
private const val TAG = "LibrePodsHook"
@SuppressLint("DiscouragedApi", "PrivateApi")
class KotlinModule: XposedModule() {
override fun onModuleLoaded(param: ModuleLoadedParam) {
log(Log.INFO, TAG, "module initialized at :: ${param.processName}")
log(Log.INFO, TAG, "framework: $frameworkName($frameworkVersionCode) API $apiVersion")
}
override fun onPackageLoaded(param: PackageLoadedParam) {
log(Log.INFO, TAG, "onPackageLoaded :: ${param.packageName}")
if (param.packageName == "com.google.android.bluetooth" || param.packageName == "com.android.bluetooth") {
log(Log.INFO, TAG, "Bluetooth app detected, hooking l2c_fcr_chk_chan_modes")
try {
if (param.isFirstPackage) {
log(Log.INFO, TAG, "Loading native library for Bluetooth hook")
NativeBridge.setSdpHook(getRemotePreferences("me.kavishdevar.librepods").getBoolean("vendor_id_hook", false))
System.loadLibrary("l2c_fcr_hook")
log(Log.INFO, TAG, "Native library loaded successfully")
}
} catch (e: Exception) {
log(Log.ERROR, TAG, "Failed to load native library: ${e.message}")
}
}
if (param.packageName == "com.google.android.settings") {
hookSettingsController(param, "com.google.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
}
if (param.packageName == "com.android.settings") {
hookSettingsController(param, "com.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
}
}
private fun hookSettingsController(param: PackageLoadedParam, className: String) {
log(Log.INFO, TAG, "Settings app detected, hooking Bluetooth icon handling")
try {
val headerControllerClass = Class.forName(className, false, param.defaultClassLoader)
val updateIconMethod = headerControllerClass.getDeclaredMethod(
"updateIcon",
ImageView::class.java,
String::class.java
)
hook(updateIconMethod).intercept { chain ->
try {
log(Log.INFO, TAG, "Bluetooth icon hook called with args: ${chain.args.joinToString(", ")}")
val imageView = chain.args[0] as? ImageView
val iconUri = chain.args[1] as? String
if (imageView == null || iconUri == null) {
return@intercept chain.proceed()
}
val uri = iconUri.toUri()
if (!uri.toString().startsWith("android.resource://me.kavishdevar.librepods")) {
return@intercept chain.proceed()
}
log(Log.INFO, TAG, "Handling AirPods icon URI: $uri")
Handler(Looper.getMainLooper()).post {
try {
val context = imageView.context
val packageName = uri.authority ?: return@post
val packageContext = context.createPackageContext(
packageName,
Context.CONTEXT_IGNORE_SECURITY
)
val resPath = uri.pathSegments
if (resPath.size >= 2 && resPath[0] == "drawable") {
val resourceName = resPath[1]
val resourceId = packageContext.resources.getIdentifier(
resourceName, "drawable", packageName
)
if (resourceId != 0) {
val drawable = packageContext.resources.getDrawable(
resourceId, packageContext.theme
)
imageView.setImageDrawable(drawable)
imageView.alpha = 1.0f
log(Log.INFO, TAG, "Successfully loaded icon from resource: $resourceName")
} else {
log(Log.ERROR, TAG, "Resource not found: $resourceName")
}
}
} catch (e: Exception) {
log(Log.ERROR, TAG, "Error loading resource from URI $uri: ${e.message}")
}
}
null
} catch (e: Exception) {
log(Log.ERROR, TAG, "Error in Bluetooth icon hook: ${e.message}")
chain.proceed()
}
}
log(Log.INFO, TAG, "Successfully hooked updateIcon method in Bluetooth settings")
} catch (e: Exception) {
log(Log.ERROR, TAG, "Failed to hook Bluetooth icon handler: ${e.message}")
}
}
}
object NativeBridge {
external fun setSdpHook(enabled: Boolean)
}
@@ -1,28 +0,0 @@
package me.kavishdevar.librepods.utils
import android.content.Context
import io.github.libxposed.service.XposedService
import io.github.libxposed.service.XposedServiceHelper
object XposedServiceHolder {
var service: XposedService? = null
}
object XposedInitializer: XposedServiceHelper.OnServiceListener {
private var initialized = false
fun ensureInit(context: Context) {
if (initialized) return
initialized = true
XposedServiceHelper.registerListener(this)
}
override fun onServiceBind(service: XposedService) {
XposedServiceHolder.service = service
}
override fun onServiceDied(service: XposedService) {
XposedServiceHolder.service = null
}
}
+140 -99
View File
@@ -30,7 +30,7 @@
#include "l2c_fcr_hook.h" #include "l2c_fcr_hook.h"
extern "C" { extern "C" {
#include "xz.h" #include "xz.h"
} }
#define LOG_TAG "LibrePodsHook" #define LOG_TAG "LibrePodsHook"
@@ -39,13 +39,13 @@ extern "C" {
static HookFunType hook_func = nullptr; static HookFunType hook_func = nullptr;
static uint8_t (*original_l2c_fcr_chk_chan_modes)(void*) = nullptr; static uint8_t (*original_l2c_fcr_chk_chan_modes)(void *) = nullptr;
static tBTA_STATUS (*original_BTA_DmSetLocalDiRecord)(
tSDP_DI_RECORD*, uint32_t*) = nullptr; static tBTA_STATUS (*original_BTA_DmSetLocalDiRecord)(tSDP_DI_RECORD *, uint32_t *) = nullptr;
static std::atomic<bool> enableSdpHook(false); static std::atomic<bool> enableSdpHook(false);
uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) { uint8_t fake_l2c_fcr_chk_chan_modes(void *p_ccb) {
LOGI("fake_l2c_fcr_chk_chan_modes called"); LOGI("fake_l2c_fcr_chk_chan_modes called");
uint8_t orig = 0; uint8_t orig = 0;
if (original_l2c_fcr_chk_chan_modes) if (original_l2c_fcr_chk_chan_modes)
@@ -55,13 +55,13 @@ uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) {
return 1; return 1;
} }
tBTA_STATUS fake_BTA_DmSetLocalDiRecord( tBTA_STATUS fake_BTA_DmSetLocalDiRecord(tSDP_DI_RECORD *p_device_info, uint32_t *p_handle) {
tSDP_DI_RECORD* p_device_info,
uint32_t* p_handle) {
LOGI("fake_BTA_DmSetLocalDiRecord called"); LOGI("fake_BTA_DmSetLocalDiRecord called");
if (original_BTA_DmSetLocalDiRecord && enableSdpHook.load(std::memory_order_relaxed)) original_BTA_DmSetLocalDiRecord(p_device_info, p_handle); if (original_BTA_DmSetLocalDiRecord &&
enableSdpHook.load(std::memory_order_relaxed))
original_BTA_DmSetLocalDiRecord(p_device_info, p_handle);
LOGI("fake_BTA_DmSetLocalDiRecord: modifying vendor to 0x004C, vendor_id_source to 0x0001"); LOGI("fake_BTA_DmSetLocalDiRecord: modifying vendor to 0x004C, vendor_id_source to 0x0001");
@@ -70,14 +70,15 @@ tBTA_STATUS fake_BTA_DmSetLocalDiRecord(
p_device_info->vendor_id_source = 0x0001; p_device_info->vendor_id_source = 0x0001;
} }
LOGI("fake_BTA_DmSetLocalDiRecord: returning status %d", original_BTA_DmSetLocalDiRecord ? original_BTA_DmSetLocalDiRecord(p_device_info, p_handle) : BTA_FAILURE); LOGI("fake_BTA_DmSetLocalDiRecord: returning status %d",
return original_BTA_DmSetLocalDiRecord ? original_BTA_DmSetLocalDiRecord(p_device_info, p_handle) : BTA_FAILURE; original_BTA_DmSetLocalDiRecord ? original_BTA_DmSetLocalDiRecord(p_device_info, p_handle)
: BTA_FAILURE);
return original_BTA_DmSetLocalDiRecord ? original_BTA_DmSetLocalDiRecord(p_device_info,
p_handle)
: BTA_FAILURE;
} }
static bool decompressXZ( static bool decompressXZ(const uint8_t *input, size_t input_size, std::vector<uint8_t> &output) {
const uint8_t* input,
size_t input_size,
std::vector<uint8_t>& output) {
LOGI("decompressXZ called with input_size: %zu", input_size); LOGI("decompressXZ called with input_size: %zu", input_size);
@@ -86,7 +87,7 @@ static bool decompressXZ(
xz_crc64_init(); xz_crc64_init();
#endif #endif
struct xz_dec* dec = xz_dec_init(XZ_DYNALLOC, 64U << 20); struct xz_dec *dec = xz_dec_init(XZ_DYNALLOC, 64U << 20);
if (!dec) { if (!dec) {
LOGE("decompressXZ: xz_dec_init failed"); LOGE("decompressXZ: xz_dec_init failed");
return false; return false;
@@ -106,7 +107,8 @@ static bool decompressXZ(
LOGI("decompressXZ: entering decompression loop"); LOGI("decompressXZ: entering decompression loop");
while (true) { while (true) {
LOGI("decompressXZ: xz_dec_run iteration, buf.in_pos: %zu, buf.out_pos: %zu", buf.in_pos, buf.out_pos); LOGI("decompressXZ: xz_dec_run iteration, buf.in_pos: %zu, buf.out_pos: %zu", buf.in_pos,
buf.out_pos);
enum xz_ret ret = xz_dec_run(dec, &buf); enum xz_ret ret = xz_dec_run(dec, &buf);
LOGI("decompressXZ: xz_dec_run returned %d", ret); LOGI("decompressXZ: xz_dec_run returned %d", ret);
@@ -135,10 +137,10 @@ static bool decompressXZ(
return true; return true;
} }
static bool getLibraryPath(const char* name, std::string& out) { static bool getLibraryPath(const char *name, std::string &out) {
LOGI("getLibraryPath called with name: %s", name); LOGI("getLibraryPath called with name: %s", name);
FILE* fp = fopen("/proc/self/maps", "r"); FILE *fp = fopen("/proc/self/maps", "r");
if (!fp) { if (!fp) {
LOGE("getLibraryPath: fopen failed"); LOGE("getLibraryPath: fopen failed");
return false; return false;
@@ -150,7 +152,7 @@ static bool getLibraryPath(const char* name, std::string& out) {
while (fgets(line, sizeof(line), fp)) { while (fgets(line, sizeof(line), fp)) {
if (strstr(line, name)) { if (strstr(line, name)) {
LOGI("getLibraryPath: found line containing %s", name); LOGI("getLibraryPath: found line containing %s", name);
char* path = strchr(line, '/'); char *path = strchr(line, '/');
if (path) { if (path) {
out = path; out = path;
out.erase(out.find('\n')); out.erase(out.find('\n'));
@@ -166,10 +168,10 @@ static bool getLibraryPath(const char* name, std::string& out) {
return false; return false;
} }
static uintptr_t getModuleBase(const char* name) { static uintptr_t getModuleBase(const char *name) {
LOGI("getModuleBase called with name: %s", name); LOGI("getModuleBase called with name: %s", name);
FILE* fp = fopen("/proc/self/maps", "r"); FILE *fp = fopen("/proc/self/maps", "r");
if (!fp) { if (!fp) {
LOGE("getModuleBase: fopen failed"); LOGE("getModuleBase: fopen failed");
return 0; return 0;
@@ -192,26 +194,78 @@ static uintptr_t getModuleBase(const char* name) {
return base; return base;
} }
static uint64_t findSymbolOffset( static uint64_t
const std::vector<uint8_t>& elf, findSymbolOffsetDynsym(const std::vector<uint8_t> &elf, const char *symbol_substring) {
const char* symbol_substring) {
LOGI("findSymbolOffsetDynsym called with %s", symbol_substring);
auto *eh = reinterpret_cast<const Elf64_Ehdr *>(elf.data());
auto *shdr = reinterpret_cast<const Elf64_Shdr *>(
elf.data() + eh->e_shoff);
const char *shstr = reinterpret_cast<const char *>(
elf.data() + shdr[eh->e_shstrndx].sh_offset);
const Elf64_Shdr *dynsym = nullptr;
const Elf64_Shdr *dynstr = nullptr;
for (int i = 0; i < eh->e_shnum; ++i) {
const char *secname = shstr + shdr[i].sh_name;
if (!strcmp(secname, ".dynsym"))
dynsym = &shdr[i];
if (!strcmp(secname, ".dynstr"))
dynstr = &shdr[i];
}
if (!dynsym || !dynstr) {
LOGE("findSymbolOffsetDynsym: dynsym or dynstr not found");
return 0;
}
auto *symbols = reinterpret_cast<const Elf64_Sym *>(
elf.data() + dynsym->sh_offset);
const char *strings = reinterpret_cast<const char *>(
elf.data() + dynstr->sh_offset);
size_t count = dynsym->sh_size / sizeof(Elf64_Sym);
LOGI("findSymbolOffsetDynsym: scanning %zu symbols", count);
for (size_t i = 0; i < count; ++i) {
const char *name = strings + symbols[i].st_name;
if (strstr(name, symbol_substring) && ELF64_ST_TYPE(symbols[i].st_info) == STT_FUNC) {
LOGI("findSymbolOffsetDynsym: matched %s @ 0x%lx", name,
(unsigned long) symbols[i].st_value);
return symbols[i].st_value;
}
}
LOGI("findSymbolOffsetDynsym: no match for %s", symbol_substring);
return 0;
}
static uint64_t findSymbolOffset(const std::vector<uint8_t> &elf, const char *symbol_substring) {
LOGI("findSymbolOffset called with symbol_substring: %s", symbol_substring); LOGI("findSymbolOffset called with symbol_substring: %s", symbol_substring);
auto* eh = reinterpret_cast<const Elf64_Ehdr*>(elf.data()); auto *eh = reinterpret_cast<const Elf64_Ehdr *>(elf.data());
auto* shdr = reinterpret_cast<const Elf64_Shdr*>( auto *shdr = reinterpret_cast<const Elf64_Shdr *>(
elf.data() + eh->e_shoff); elf.data() + eh->e_shoff);
const char* shstr = const char *shstr = reinterpret_cast<const char *>(
reinterpret_cast<const char*>(
elf.data() + shdr[eh->e_shstrndx].sh_offset); elf.data() + shdr[eh->e_shstrndx].sh_offset);
const Elf64_Shdr* symtab = nullptr; const Elf64_Shdr *symtab = nullptr;
const Elf64_Shdr* strtab = nullptr; const Elf64_Shdr *strtab = nullptr;
LOGI("findSymbolOffset: parsing ELF sections"); LOGI("findSymbolOffset: parsing ELF sections");
for (int i = 0; i < eh->e_shnum; ++i) { for (int i = 0; i < eh->e_shnum; ++i) {
const char* secname = shstr + shdr[i].sh_name; const char *secname = shstr + shdr[i].sh_name;
if (!strcmp(secname, ".symtab")) if (!strcmp(secname, ".symtab"))
symtab = &shdr[i]; symtab = &shdr[i];
if (!strcmp(secname, ".strtab")) if (!strcmp(secname, ".strtab"))
@@ -224,23 +278,22 @@ static uint64_t findSymbolOffset(
} }
LOGI("findSymbolOffset: found symtab and strtab"); LOGI("findSymbolOffset: found symtab and strtab");
auto* symbols = reinterpret_cast<const Elf64_Sym*>( auto *symbols = reinterpret_cast<const Elf64_Sym *>(
elf.data() + symtab->sh_offset); elf.data() + symtab->sh_offset);
const char* strings = const char *strings = reinterpret_cast<const char *>(
reinterpret_cast<const char*>(
elf.data() + strtab->sh_offset); elf.data() + strtab->sh_offset);
size_t count = symtab->sh_size / sizeof(Elf64_Sym); size_t count = symtab->sh_size / sizeof(Elf64_Sym);
LOGI("findSymbolOffset: scanning %zu symbols", count); LOGI("findSymbolOffset: scanning %zu symbols", count);
for (size_t i = 0; i < count; ++i) { for (size_t i = 0; i < count; ++i) {
const char* name = strings + symbols[i].st_name; const char *name = strings + symbols[i].st_name;
if (strstr(name, symbol_substring) && if (strstr(name, symbol_substring) && ELF64_ST_TYPE(symbols[i].st_info) == STT_FUNC) {
ELF64_ST_TYPE(symbols[i].st_info) == STT_FUNC) {
LOGI("findSymbolOffset: matched symbol %s at 0x%lx", name, (unsigned long)symbols[i].st_value); LOGI("findSymbolOffset: matched symbol %s at 0x%lx", name,
(unsigned long) symbols[i].st_value);
return symbols[i].st_value; return symbols[i].st_value;
} }
@@ -250,7 +303,7 @@ static uint64_t findSymbolOffset(
return 0; return 0;
} }
static bool hookLibrary(const char* libname) { static bool hookLibrary(const char *libname) {
LOGI("hookLibrary called with libname: %s", libname); LOGI("hookLibrary called with libname: %s", libname);
if (!hook_func) { if (!hook_func) {
@@ -277,89 +330,78 @@ static bool hookLibrary(const char* libname) {
close(fd); close(fd);
return false; return false;
} }
LOGI("hookLibrary: opened file, size: %lld", (long long)st.st_size); LOGI("hookLibrary: opened file, size: %lld", (long long) st.st_size);
std::vector<uint8_t> file(st.st_size); std::vector<uint8_t> file(st.st_size);
read(fd, file.data(), st.st_size); read(fd, file.data(), st.st_size);
close(fd); close(fd);
auto* eh = reinterpret_cast<Elf64_Ehdr*>(file.data()); auto *eh = reinterpret_cast<Elf64_Ehdr *>(file.data());
auto* shdr = reinterpret_cast<Elf64_Shdr*>( auto *shdr = reinterpret_cast<Elf64_Shdr *>(
file.data() + eh->e_shoff); file.data() + eh->e_shoff);
const char* shstr = const char *shstr = reinterpret_cast<const char *>(
reinterpret_cast<const char*>(
file.data() + shdr[eh->e_shstrndx].sh_offset); file.data() + shdr[eh->e_shstrndx].sh_offset);
LOGI("hookLibrary: parsing ELF header and sections"); uint64_t chk_offset = 0;
for (int i = 0; i < eh->e_shnum; ++i) { uint64_t sdp_offset = 0;
for (int i = 0; i < eh->e_shnum; ++i) {
if (!strcmp(shstr + shdr[i].sh_name, ".gnu_debugdata")) { if (!strcmp(shstr + shdr[i].sh_name, ".gnu_debugdata")) {
LOGI("hookLibrary: found .gnu_debugdata section"); LOGI("hookLibrary: found .gnu_debugdata section");
std::vector<uint8_t> compressed( std::vector<uint8_t> compressed(file.begin() + shdr[i].sh_offset,
file.begin() + shdr[i].sh_offset,
file.begin() + shdr[i].sh_offset + shdr[i].sh_size); file.begin() + shdr[i].sh_offset + shdr[i].sh_size);
std::vector<uint8_t> decompressed; std::vector<uint8_t> decompressed;
if (!decompressXZ( if (decompressXZ(compressed.data(), compressed.size(), decompressed)) {
compressed.data(),
compressed.size(), chk_offset = findSymbolOffset(decompressed, "l2c_fcr_chk_chan_modes");
decompressed)) {
LOGE("hookLibrary: decompressXZ failed"); sdp_offset = findSymbolOffset(decompressed, "BTA_DmSetLocalDiRecord");
return false; } else {
LOGE("debugdata decompress failed");
}
break;
}
}
if (!chk_offset) {
LOGI("fallback dynsym chk");
chk_offset = findSymbolOffsetDynsym(file, "l2c_fcr_chk_chan_modes");
}
if (!sdp_offset) {
LOGI("fallback dynsym sdp");
sdp_offset = findSymbolOffsetDynsym(file, "BTA_DmSetLocalDiRecord");
} }
LOGI("hookLibrary: decompressed debug data, size: %zu", decompressed.size());
uintptr_t base = getModuleBase(libname); uintptr_t base = getModuleBase(libname);
if (!base) { if (!base) {
LOGE("hookLibrary: getModuleBase failed"); LOGE("hookLibrary: getModuleBase failed");
return false; return false;
} }
LOGI("hookLibrary: module base: 0x%lx", base);
uint64_t chk_offset =
findSymbolOffset(decompressed,
"l2c_fcr_chk_chan_modes");
uint64_t sdp_offset =
findSymbolOffset(decompressed,
"BTA_DmSetLocalDiRecord");
LOGI("hookLibrary: chk_offset: 0x%lx, sdp_offset: 0x%lx", chk_offset, sdp_offset);
if (chk_offset) { if (chk_offset) {
void* target = void *target = reinterpret_cast<void *>(base + chk_offset);
reinterpret_cast<void*>(base + chk_offset); hook_func(target, (void *) fake_l2c_fcr_chk_chan_modes,
(void **) &original_l2c_fcr_chk_chan_modes);
hook_func(target, LOGI("hooked chk");
(void*)fake_l2c_fcr_chk_chan_modes,
(void**)&original_l2c_fcr_chk_chan_modes);
LOGI("hookLibrary: hooked l2c_fcr_chk_chan_modes");
} }
if (sdp_offset) { if (sdp_offset) {
void* target = void *target = reinterpret_cast<void *>(base + sdp_offset);
reinterpret_cast<void*>(base + sdp_offset); hook_func(target, (void *) fake_BTA_DmSetLocalDiRecord,
(void **) &original_BTA_DmSetLocalDiRecord);
hook_func(target, LOGI("hooked sdp");
(void*)fake_BTA_DmSetLocalDiRecord,
(void**)&original_BTA_DmSetLocalDiRecord);
LOGI("hookLibrary: hooked BTA_DmSetLocalDiRecord");
} }
return true; return chk_offset || sdp_offset;
}
}
LOGI("hookLibrary: failed for %s", libname);
return false;
} }
static void on_library_loaded(const char* name, void*) { static void on_library_loaded(const char *name, void *) {
LOGI("on_library_loaded called with name: %s", name); LOGI("on_library_loaded called with name: %s", name);
if (strstr(name, "libbluetooth_jni.so")) { if (strstr(name, "libbluetooth_jni.so")) {
@@ -373,20 +415,19 @@ static void on_library_loaded(const char* name, void*) {
} }
} }
extern "C" extern "C" [[gnu::visibility("default")]]
[[gnu::visibility("default")]]
[[gnu::used]] [[gnu::used]]
NativeOnModuleLoaded native_init(const NativeAPIEntries* entries) { NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) {
LOGI("native_init called with entries: %p", entries); LOGI("native_init called with entries: %p", entries);
hook_func = (HookFunType)entries->hook_func; hook_func = (HookFunType) entries->hook_func;
LOGI("LibrePodsNativeHook initialized, sdp hook enabled: %d", enableSdpHook.load(std::memory_order_relaxed)); LOGI("LibrePodsNativeHook initialized, sdp hook enabled: %d",
enableSdpHook.load(std::memory_order_relaxed));
return on_library_loaded; return on_library_loaded;
} }
extern "C" extern "C" JNIEXPORT void JNICALL
JNIEXPORT void JNICALL Java_me_kavishdevar_librepods_utils_NativeBridge_setSdpHook(JNIEnv *, jobject thiz,
Java_me_kavishdevar_librepods_utils_NativeBridge_setSdpHook( jboolean enable) {
JNIEnv*, jobject thiz, jboolean enable) {
LOGI("setSdpHook called with enable: %d", enable); LOGI("setSdpHook called with enable: %d", enable);
enableSdpHook.store(enable, std::memory_order_relaxed); enableSdpHook.store(enable, std::memory_order_relaxed);