Skip to content

fix(call): restore Bluetooth audio routing - #6567

Open
flake92 wants to merge 5 commits into
nextcloud:masterfrom
flake92:fix/bluetooth-call-routing
Open

fix(call): restore Bluetooth audio routing#6567
flake92 wants to merge 5 commits into
nextcloud:masterfrom
flake92:fix/bluetooth-call-routing

Conversation

@flake92

@flake92 flake92 commented Aug 24, 2026

Copy link
Copy Markdown

Prefer call-capable Bluetooth communication devices and preserve selection across reconnects.

Assisted-by: Codex:gpt-5

🖼️ Screenshots

🏚️ Before 🏡 After
B A

🚧 TODO

  • ...

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔖 Capability is checked or not needed
  • 🔙 Backport requests are created or not needed: /backport to stable-xx.x
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@flake92 flake92 closed this Aug 24, 2026
@flake92
flake92 force-pushed the fix/bluetooth-call-routing branch from 2a60dbe to 97bb606 Compare August 24, 2026 23:44
@flake92
flake92 deleted the fix/bluetooth-call-routing branch August 24, 2026 23:44
@flake92 flake92 reopened this Aug 24, 2026
@mahibi

mahibi commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Thank you for contributing @flake92 🙏

It would be great if the new java class could be converted to kotlin.
Feel free to also have a look at the review from Claude AI.

Just let us know if you want to do this (that would be awesome!) or if we should take over.


Claude AI review:

Correctness (4 findings)

  • No audio route for ~4s on a failed device switch (WebRtcAudioManager.java:366): if the user taps Earpiece while on Bluetooth and the switch transiently fails, the rollback-to-Bluetooth logic can't actually restart Bluetooth (it's mid-teardown), leaving the call with silence until the async disconnect settles.
  • Audio-focus flapping bypasses the retry cap (WebRtcBluetoothManager.java:255): reassertBluetoothAudioAfterFocusGain() resets the SCO attempt counter on every focus-gain event, so MAX_SCO_CONNECTION_ATTEMPTS isn't actually a bound over a call's lifetime if focus flaps repeatedly (e.g. notification sounds).
  • Stale getter can resurrect a route the user just left (WebRtcBluetoothManager.java:1174): a queued Bluetooth callback after an explicit clear can fall back to getCommunicationDevice(), which the code's own comment says is stale on some Samsung devices — risk of Bluetooth "coming back" after the user switched away.
  • Wired-headset detection is inconsistent between two gates (WebRtcAudioManager.java:513): matchesAudioDevice() recognizes more USB/wired types than hasWiredHeadset() does, so the newer, broader matching is currently dead code — a latent trap if either is edited independently later.

Efficiency/simplification/reuse (6 findings) — lower severity: exhausted retries keep re-triggering startScoAudio() harmlessly forever; the legacy vs. Android-12+ paths are interleaved via SDK-version branches across ~11 call sites instead of being split behind an interface (real maintenance risk going forward); some duplicated device-enumeration logic between WebRtcAudioManager and ModernBluetoothRoute; one dead code branch; one needless one-line wrapper.

The two "removed behavior" concerns I'd have flagged up front — un-stickying Bluetooth selection on disconnect, and a possible SCO_DISCONNECTING race in requestBluetoothAudioSelection() — were investigated and ruled out: the first is the intentional point of the fix, the second self-heals within the 4s timeout.

Given this is call-audio-routing code (hard to unit test end-to-end, easy to regress silently), I'd suggest at minimum fixing the first two correctness findings before merging, and manually testing the "switch away from Bluetooth mid-call" and "notification during a Bluetooth call" paths on a real device.

@flake92

flake92 commented Aug 26, 2026

Copy link
Copy Markdown
Author

@mahibi

Changes:

  • Converted the new production policy class and all newly added tests from Java to Kotlin.
  • Preserved the current working audio route when setCommunicationDevice() fails.
  • Confirmed a new non-Bluetooth route before ending Bluetooth routing state.
  • Preserved the Bluetooth retry counter across audio-focus loss and recovery.
  • Enforced retry limits for Bluetooth auto-start and route reassertion.
  • Prevented further automatic retries after the retry budget is exhausted.
  • Ignored stale communication-device getters and callbacks.
  • Rejected delayed Bluetooth callbacks after the user selected a non-Bluetooth output.
  • Unified wired headset, headphone, and USB-device detection.
  • Added tests for failed route changes, focus recovery, bounded retries, stale callbacks, late Bluetooth activation, wired-device detection, and exhausted retries.
  • Added no new .java files relative to the PR base.
  • Passed ktlint, detekt, targeted WebRTC tests, and the complete GPlay debug unit-test suite.
    Pending manual hardware checks:
  • Switching from Bluetooth to another output during an active call.
  • Receiving a notification during a Bluetooth call and confirming correct route recovery.

flake92 and others added 3 commits August 27, 2026 00:20
Prefer call-capable Bluetooth communication devices and preserve selection across reconnects.

Assisted-by: Codex:gpt-5
Signed-off-by: Oleg Cherry <80347136+flake92@users.noreply.github.com>
Assisted-by: Codex:gpt-5
Signed-off-by: Oleg Cherry <80347136+flake92@users.noreply.github.com>
Keep the working route when a device switch is rejected, preserve bounded retry state across focus changes, reject stale callbacks, and share wired-device detection. Convert the new policy and tests to Kotlin.

Assisted-by: Codex:gpt-5
Signed-off-by: flake92 <flake92@live.com>
@flake92
flake92 force-pushed the fix/bluetooth-call-routing branch from 98c2aba to ef84967 Compare August 26, 2026 21:20
@mahibi

mahibi commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Thank you @flake92 👍

As there are quite some changes here, this PR won't make it into v25.0.x (we are already in RC phase).
As we are quite busy with the RC's, the review might be delayed a bit.
Looking forward to get this reviewed and merged for v25.1

Preserve the active route until a non-Bluetooth replacement is confirmed.

Keep automatic retry limits across focus recovery, reject stale callbacks, and reconcile wired and USB devices consistently.

Assisted-by: OpenAI Codex:gpt-5.6-sol
Signed-off-by: flake92 <flake92@live.com>
@flake92

flake92 commented Aug 27, 2026

Copy link
Copy Markdown
Author

@mahibi
Thank you for the update! I completely understand that the RCs are the priority and that the review may take some time.

For now, I’m using my own custom builds because I make calls frequently, and this bug is especially frustrating in quiet public places, such as a cinema. With my changes, video calls now work perfectly. However, when starting an audio call, the ringback tone still briefly comes through the device speaker instead of the Bluetooth headset. I’ll continue working on that and get this PR back to green.

I’m looking forward to seeing these changes included in a future release, but there’s no need for them to land specifically in v25.1—I’ll be happy to have them accepted for any later version. Thanks again!

Extract modern Bluetooth focus recovery into a dedicated helper to keep the public dispatcher below the Codacy PMD NPath threshold without changing behavior.

Assisted-by: OpenAI Codex:gpt-5.6-sol
Signed-off-by: flake92 <flake92@live.com>
@flake92

flake92 commented Aug 27, 2026

Copy link
Copy Markdown
Author

Changes:

Added a Kotlin audio-route policy and Kotlin regression tests.

Preserved the active Bluetooth route until a replacement output is successfully confirmed.

Prevented route loss when switching from Bluetooth fails.

Kept Bluetooth retry limits intact across audio-focus recovery.

Added a safe non-Bluetooth fallback after retry exhaustion.

Prevented stale getters and delayed callbacks from re-enabling Bluetooth.

Improved modern and legacy Bluetooth recovery after transient audio-focus loss.

Made wired headset and USB output detection consistent.

Added device callbacks to handle wired/USB replacement correctly.

Prevented automatic Bluetooth restarts during transient focus loss.

Reset focus state correctly between calls.

Added regression coverage for routing failures, retry exhaustion, stale callbacks, focus recovery, and wired/USB devices.

Reduced PMD NPath complexity by extracting modern focus recovery into a dedicated helper.

Backported the final result to Nextcloud Talk v24.0.4.

Built and signed the update-compatible 24.0.4-privacy.3 GPlay APK.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review Waiting for reviews AI assisted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants