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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.worktrees/
root-module/radare2-5.9.9-android-aarch64.tar.gz
wak.toml
log.txt
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,27 @@ If you are using ColorOS/OxygenOS 16, you don't need root except for customizing

Until then, you must xposed. I used to provide a non-xposed method too, where the module used overlayfs to replace the bluetooth library with a locally patched one, but that was broken due to how various devices handled overlayfs and a patched library. With xposed, you can also enable the DID hook enabling a few extra features.

#### Setup for OxygenOS/ColorOS 16 (Non-rooted)

For multi-device audio switching to work properly on non-rooted OxygenOS 16, you need to inject your phone's Bluetooth MAC address into the app's settings. This is a one-time setup.

> [!IMPORTANT]
> The `run-as` command only works with **debug builds** (e.g., the nightly APK from CI). If you installed a release build, reinstall with the debug APK first.

1. **Get your phone's Bluetooth MAC address:**
- Go to Settings → About → Device Details → Bluetooth Address

2. **Inject the MAC address via adb:**
```bash
adb shell "run-as me.kavishdevar.librepods sed -i 's|<string name=\"self_mac_address\"></string>|<string name=\"self_mac_address\">XX:XX:XX:XX:XX:XX</string>|' shared_prefs/settings.xml"
```
Replace `XX:XX:XX:XX:XX:XX` with your actual Bluetooth MAC address (e.g., `AC:C0:48:67:E6:EA`)

3. **Restart the app** for the changes to take effect

> [!NOTE]
> This is needed because non-rooted apps on SDK 36+ cannot access the system's `bluetooth_address` setting. Without this, audio source switching between devices won't work correctly, and the app will lose ANC/transparency control when you switch to another device.

## Changing VendorID in the DID profile to that of Apple

Turns out, if you change the VendorID in DID Profile to that of Apple, you get access to several special features!
Expand Down
47 changes: 32 additions & 15 deletions android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,10 @@ class MainActivity : ComponentActivity() {

override fun onDestroy() {
try {
unbindService(serviceConnection)
Log.d("MainActivity", "Unbound service")
} catch (e: Exception) {
Log.e("MainActivity", "Error while unbinding service: $e")
}
try {
unregisterReceiver(connectionStatusReceiver)
Log.d("MainActivity", "Unregistered receiver")
if (::connectionStatusReceiver.isInitialized) {
unregisterReceiver(connectionStatusReceiver)
Log.d("MainActivity", "Unregistered receiver")
}
} catch (e: Exception) {
Log.e("MainActivity", "Error while unregistering receiver: $e")
}
Expand All @@ -182,14 +178,18 @@ class MainActivity : ComponentActivity() {

override fun onStop() {
try {
unbindService(serviceConnection)
Log.d("MainActivity", "Unbound service")
if (::serviceConnection.isInitialized) {
unbindService(serviceConnection)
Log.d("MainActivity", "Unbound service")
}
} catch (e: Exception) {
Log.e("MainActivity", "Error while unbinding service: $e")
}
try {
unregisterReceiver(connectionStatusReceiver)
Log.d("MainActivity", "Unregistered receiver")
if (::connectionStatusReceiver.isInitialized) {
unregisterReceiver(connectionStatusReceiver)
Log.d("MainActivity", "Unregistered receiver")
}
} catch (e: Exception) {
Log.e("MainActivity", "Error while unregistering receiver: $e")
}
Expand Down Expand Up @@ -303,7 +303,11 @@ fun Main() {
canDrawOverlays = Settings.canDrawOverlays(context)
}

if (permissionState.allPermissionsGranted && (canDrawOverlays || overlaySkipped.value)) {
val bluetoothPermissionsGranted = permissionState.permissions.filter {
it.permission.contains("BLUETOOTH") || it.permission.contains("LOCATION")
}.all { it.status.isGranted }

if (bluetoothPermissionsGranted && (canDrawOverlays || overlaySkipped.value)) {
val context = LocalContext.current

val navController = rememberNavController()
Expand Down Expand Up @@ -356,6 +360,15 @@ fun Main() {
isConnected = isConnected.value,
isRemotelyConnected = isRemotelyConnected.value
)
} else {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
androidx.compose.material3.CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary
)
}
}
}
composable("debug") {
Expand Down Expand Up @@ -457,7 +470,9 @@ fun Main() {
}
}

context.bindService(Intent(context, AirPodsService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
val serviceIntent = Intent(context, AirPodsService::class.java)
context.startForegroundService(serviceIntent)
context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE)

if (airPodsService.value?.isConnectedLocally == true) {
isConnected.value = true
Expand Down Expand Up @@ -486,7 +501,9 @@ fun PermissionsScreen(

val scrollState = rememberScrollState()

val basicPermissionsGranted = permissionState.permissions.all { it.status.isGranted }
val basicPermissionsGranted = permissionState.permissions.filter {
it.permission.contains("BLUETOOTH") || it.permission.contains("LOCATION")
}.all { it.status.isGranted }

val infiniteTransition = rememberInfiniteTransition(label = "pulse")
val pulseScale by infiniteTransition.animateFloat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -170,14 +171,21 @@ fun NoiseControlSettings(
}
}

val noiseControlIntentFilter = IntentFilter().apply {
addAction(AirPodsNotifications.ANC_DATA)
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter, Context.RECEIVER_EXPORTED)
} else {
context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter)
DisposableEffect(Unit) {
val noiseControlIntentFilter = IntentFilter().apply {
addAction(AirPodsNotifications.ANC_DATA)
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter, Context.RECEIVER_EXPORTED)
} else {
context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter)
}
onDispose {
try {
context.unregisterReceiver(noiseControlReceiver)
} catch (_: IllegalArgumentException) { }
}
}
Box(
modifier = Modifier
Expand Down
Loading