mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-09-03 17:59:34 +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
|
### 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)
|
||||||
|
|||||||
Binary file not shown.
@@ -221,7 +221,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
fun Main() {
|
fun Main() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
if (!isSupported(sharedPreferences)) {
|
if (false) { // !isSupported(sharedPreferences) && BuildConfig.PLAY_BUILD == true) {
|
||||||
val showDialog = remember { mutableStateOf(false) }
|
val showDialog = remember { mutableStateOf(false) }
|
||||||
val blockTouches = remember { mutableStateOf(false) }
|
val blockTouches = remember { mutableStateOf(false) }
|
||||||
val tapCount = remember { mutableIntStateOf(0) }
|
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