mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-09-02 17:29:57 +02:00
Compare commits
5 Commits
v0.2.0
..
1804e80cba
| Author | SHA1 | Date | |
|---|---|---|---|
| 1804e80cba | |||
| 0b8bd5a5b8 | |||
| d1d48562d7 | |||
| c84e64e656 | |||
| 51739514fa |
@@ -76,10 +76,23 @@ https://github.com/user-attachments/assets/43911243-0576-4093-8c55-89c1db5ea533
|
||||
|
||||
### 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:
|
||||
**You must have a rooted device with Xposed to use LibrePods on Android.**
|
||||
[https://issuetracker.google.com/issues/371713238](https://issuetracker.google.com/issues/371713238)
|
||||
|
||||
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
|
||||
|
||||
@@ -129,7 +142,7 @@ A huge thank you to everyone supporting the project!
|
||||
- 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/))
|
||||
|
||||
# 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:
|
||||
|
||||
@@ -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
|
||||
|
||||
> [!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)
|
||||
1. Go to the [Actions tab](https://github.com/kavishdevar/librepods/actions/workflows/ci-linux-rust.yml)
|
||||
|
||||
Binary file not shown.
@@ -221,7 +221,7 @@ class MainActivity : ComponentActivity() {
|
||||
fun Main() {
|
||||
val context = LocalContext.current
|
||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (!isSupported(sharedPreferences)) {
|
||||
if (false) { // !isSupported(sharedPreferences) && BuildConfig.PLAY_BUILD == true) {
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val blockTouches = remember { mutableStateOf(false) }
|
||||
val tapCount = remember { mutableIntStateOf(0) }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user