fix(android): support non-rooted OnePlus 12 / OxygenOS 16#453
Open
teng-lin wants to merge 17 commits intokavishdevar:mainfrom
Open
fix(android): support non-rooted OnePlus 12 / OxygenOS 16#453teng-lin wants to merge 17 commits intokavishdevar:mainfrom
teng-lin wants to merge 17 commits intokavishdevar:mainfrom
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OxygenOS 16's QTI Bluetooth stack handles L2CAP natively without root hooks. This commit: - Detect OxygenOS/ColorOS 16+ (OnePlus/OPPO/Realme on SDK 36) and skip root/radare2 setup in RadareOffsetFinder - Start service via startForegroundService() so it survives activity lifecycle (onStop unbind no longer kills the service) - Auto-reconnect L2CAP in onStartCommand() when service restarts via START_STICKY with a saved MAC address - Guard lateinit connectionStatusReceiver/serviceConnection with isInitialized checks to prevent UninitializedPropertyAccessException - Skip BLUETOOTH_PRIVILEGED setBatteryMetadata() calls on non-rooted devices to eliminate SecurityException log spam Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The settings screen rendered nothing when airPodsService was null, causing a black screen on startup until the service bind completed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two callers (onStartCommand reconnect + BLE/A2DP callback) can race into connectToSocket simultaneously. The first wins the L2CAP channel; the second fails with "Message too long" and shows a spurious error notification. Add AtomicBoolean guard to serialize connection attempts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ions The foreground service notification (ID 1) cannot be cancelled via notificationManager.cancel(). Use ID 1 for both connected and disconnected states so the battery notification replaces the "Background Service Running" one instead of showing alongside it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use CopyOnWriteArrayList/ConcurrentHashMap for AACP control command collections to prevent ConcurrentModificationException - Wrap NoiseControlSettings BroadcastReceiver in DisposableEffect to properly unregister on composable disposal (IntentReceiverLeaked) - Reset isConnectedLocally and isConnecting on bytesRead==-1 disconnect so auto-reconnect can trigger via onStartCommand Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only Bluetooth and location permissions are required to proceed past the permission screen. Notification (POST_NOTIFICATIONS) and phone (READ_PHONE_STATE, ANSWER_PHONE_CALLS) permissions are still requested but no longer block the main settings screen. The foreground service notification is exempt from POST_NOTIFICATIONS on Android 13+. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When AirPods report audio source switched back to the local device, send OWNS_CONNECTION=0x01 to reclaim control. Previously the app only gave up control but never took it back, causing ANC/transparency switching to stop working after switching audio between devices. Also guard audio source checks with localMac.isNotEmpty(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When AirPods switch audio to another device (e.g. Mac), the L2CAP AACP socket gets dropped. When audio returns to the phone, the A2DP PLAYING_STATE_CHANGED broadcast fires but the bluetoothReceiver only handled ACL_CONNECTED. Now also handle PLAYING_STATE_CHANGED to re-trigger L2CAP connection when A2DP starts playing on the AirPods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
isConnectedLocally can be stale after a remote disconnect because connectionReceiver sets it true on ACL_CONNECTED before connectToSocket runs. Now verify the socket is actually alive by probing inputStream before skipping reconnection. If the socket is dead, reset the flag and proceed with a fresh connection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
inputStream.available() returns 0 on dead sockets instead of throwing, so it can't detect stale connections. Use aacpManager.connectedDevices which is cleared on disconnect and only populated after successful AACP handshake - a reliable indicator of actual socket health. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The service is already unbound in onStop(), so calling unbindService() again in onDestroy() causes "Service not registered" error. Remove the duplicate unbind call since onStop() is called before onDestroy(). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Add documentation for the MAC address injection workaround needed on non-rooted SDK 36 devices where the system's bluetooth_address is not accessible to user apps. Include step-by-step instructions using adb. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The socketActuallyAlive check required aacpManager.connectedDevices to be non-empty, but during the AACP handshake window (~seconds after socket.connect() succeeds) this list is still empty. A second connectToSocket call from the A2DP profile proxy callback would see the socket as "stale", tear it down, and fail to reconnect. - Add 10-second handshake grace period to socketActuallyAlive check - Remove premature isConnectedLocally=true from connectionReceiver and takeOver (connectToSocket sets it internally on success) - Wrap socket read loop in try/catch/finally to properly handle IOException from remote disconnect, preventing stale socket state
Address PR review comment: run-as only works on debuggable builds. Add prerequisite note directing users to install the debug/nightly APK. Remove the alternative adb settings method which also requires elevated privileges.
d090728 to
aa3db92
Compare
4bbaa29 to
cb246d1
Compare
…12-oxygenos16 # Conflicts: # .gitignore # README.md # android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt # android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NoiseControlSettings.kt # android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt
Owner
|
Could you please another PR for the CI fix? Thanks! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR enables LibrePods to work on non-rooted OnePlus 12 phones with OxygenOS 16, bringing full ANC, transparency, audio control, and other core features without requiring root or Xposed.
Changes
Testing
Compatibility
Device Tested
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes / Improvements
Documentation