Skip to content

Add Android route manager backend in C# (PAR-020) - #371

Draft
MaxHeimbrock wants to merge 1 commit into
max/par-019-routing-api-surfacefrom
max/par-020-unity-android-route-manager
Draft

Add Android route manager backend in C# (PAR-020)#371
MaxHeimbrock wants to merge 1 commit into
max/par-019-routing-api-surfacefrom
max/par-020-unity-android-route-manager

Conversation

@MaxHeimbrock

Copy link
Copy Markdown
Contributor

Background

PAR-019 (#370) defined the public audio output routing API, but on Android it was wired to a documented unsupported placeholder — OutputPreference round-tripped without effect, SelectOutput threw, and DevicesChanged never fired. The two customer-reported routing bugs (a headset connected mid-call is ignored; a headset disconnect falls back to the earpiece instead of the loudspeaker) were so far only addressed by disposable sample-level code (PR #364). This PR moves that routing into the SDK as the PAR-019 Android backend, hardened with the device findings from the hotfix work, and kept behind the internal IRouteController seam so phase B can later replace it with the FFI-driven router mechanically.

Stacked on #370 (max/par-019-routing-api-surface) — merge that first.

Changes

  • New AndroidRouteController (#if UNITY_ANDROID && !UNITY_EDITOR, Android 12/API 31+), all JNI contained in the class:
    • owns the voice-communication session for its lifetime: MODE_IN_COMMUNICATION with save/restore of the prior mode, communication-device pin via setCommunicationDevice/clearCommunicationDevice
    • policy: sticky SelectOutput override while its device is available (dropped once it disappears), else the highest-ranked available kind per OutputPreference; unranked kinds are never auto-selected (pin released, OS default applies)
    • change detection: OnCommunicationDeviceChangedListener plus a 1.5 s poll on a JVM-attached background thread — the poll covers the device-verified transitions that fire no OS event (device added while pinned; a powered-off BT headset leaving the available list ~10 s after the route already fell back). A no-op re-pin guard stops the feedback loop from our own pin firing the listener; teardown unregisters the listener before clearing the pin
    • GetDevices().Playout and DevicesChanged now report real communication devices with Kind and IsSelected (recording stays the FFI default-input placeholder); events are marshalled to the Unity main thread by PlatformAudio
    • optional audio-focus request (AUDIOFOCUS_GAIN, voice-communication attributes), default off and internal — the PAR-019 public surface stays frozen
    • pre-API-31 remains a documented unsupported placeholder (same gate as the hotfix)
  • PlatformAudio.StartRecording re-asserts the routing policy once capture is active: since Android 13 the mode request — and with it the pin — is only honored while the app has active voice-communication capture
  • Platform notes in the public XML docs updated truthfully (Android now implemented; iOS/desktop unchanged)
  • Note: the default OutputPreference (BT > wired > speaker > earpiece, defined in PAR-019) does not rank Usb/HearingAid, so unlike the hotfix's fixed ranking those are not auto-pinned — apps can add them to the list; when nothing ranked is available the OS default route applies

Testing

  • csc compile checks on the Bee LiveKit.rsp under editor and Android-player define sets, plus the Meet sample assembly
  • [HUMAN, done] Device test on Android with a BT headset (2026-08-14): headset connect mid-call moves audio; headset power-off lands on the loudspeaker, never the earpiece; sticky SelectOutput(earpiece) survives a BT connect until cleared; DevicesChanged delivered kinds/selection on the Unity main thread
  • Known, expected limitation (out of scope): joining muted and never unmuting leaves the pin un-owned per the Android 13 mode-activity rule — addressed app-side by the capture keepalive from PR Samples: Android routing to headset over speaker over earpiece #364 and carried into the samples by PAR-011

🤖 Generated with Claude Code

Implement the PAR-019 routing seam on Android as an SDK-owned backend,
hardened from the device-validated sample hotfix (PR #364):

- AndroidRouteController (API 31+) pins the communication device to the
  sticky SelectOutput override while its device is available, else the
  highest-ranked available kind per OutputPreference; kinds missing from
  the ranking are never auto-selected (pin released, OS default applies)
- session-scoped MODE_IN_COMMUNICATION with save/restore of the prior
  mode; optional audio-focus request (internal, default off)
- change detection via OnCommunicationDeviceChangedListener plus a 1.5 s
  poll thread for the trace-verified transitions that fire no OS event
  (device added while pinned; BT headset leaving the available list ~10 s
  after the route already fell back); no-op re-pin guard stops the
  feedback loop from our own setCommunicationDevice
- GetDevices playout list and DevicesChanged now report real Android
  communication devices with Kind and IsSelected; recording stays the
  FFI default-input placeholder
- StartRecording re-asserts the policy: since Android 13 the mode
  request is only honored while voice-communication capture is active
- pre-API-31 stays a documented unsupported placeholder, matching the
  hotfix gate; platform notes on the public API updated accordingly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant