Skip to content

Android: a call still ends the recording — no audio focus handling, and the reconnect gap is thrown away #284

Description

@Lanznx

Split out of #283, whose iOS half landed in the relay-reconnect PR. Two Android gaps remain, and neither can be verified from a machine without an Android SDK — CI skips android/** on pull requests, so this needs someone who can build and run it on a device.

1. No audio focus, no interruption handling at all

There is still no AudioFocusRequest, onAudioFocusChange or AudioDeviceCallback anywhere under android/app/src/main/kotlin. A call takes the microphone with it and nothing tries to bring it back:

  • MicCapture opens one AudioRecord for the life of the flow. From Android 10 on, an app that loses the microphone to a call is fed silence rather than an error (MicCapture's own class doc says so), so read() keeps succeeding and the meeting records nothing.
  • MeetingSession.startMicMonitor already watches AudioRecordingConfiguration.isClientSilenced and surfaces micSilenced as a banner — that is detection, not recovery. Nothing reopens the AudioRecord when the call ends, and on devices where the record object is invalidated instead, read() returns ERROR_DEAD_OBJECT and the whole meeting fails.

What iOS does (ios/App/Parley/AudioCapture.swift, landed in #272) is the shape to mirror: treat capture as a thing to be held, rebuild it on every interruption/route change, retry with backoff, keep a watchdog for what the platform does not announce, and report each transition so the UI can say what is going on.

Concretely:

  • Request audio focus (AudioFocusRequest with AUDIOFOCUS_GAIN) while a meeting runs and abandon it on stop.
  • AUDIOFOCUS_LOSS_TRANSIENT / ..._TRANSIENT_CAN_DUCK → mark interrupted, keep the session alive, keep the encoder running.
  • AUDIOFOCUS_GAIN after a loss → reopen the AudioRecord, do not just resume reading the old one. The mic.start() collection in MeetingSession.runCapture needs to become a loop that can be restarted, with its own retry ceiling before it gives up as MeetingFailure.MIC_UNAVAILABLE.
  • AUDIOFOCUS_LOSS (permanent) → the mic is gone for good; end the meeting the way a mic failure ends it, which still saves and uploads what was captured.
  • Route changes via AudioManager.registerAudioDeviceCallback — a headset or Bluetooth mic disappearing mid-meeting is the same "reopen it" path.
  • micSilenced staying true past the end of a call is a signal to reopen too, not only to show a banner.

Strings for every new user-facing state go in values/strings.xml and values-zh-rTW/strings.xml.

2. Android throws the reconnect gap away

MeetingSession.scheduleReconnect already redials with backoff and per-leg id prefixes, but like iOS did before this week it computes the new leg's offset as elapsedRealtime() - captureStartedAt and simply stops feeding the relay in between — so the words spoken during the gap never reach it.

The iOS fix is RelayAudioBridge in ParleyKit (ios/ParleyKit/Sources/ParleyKit/RelayAudioBridge.swift), with tests in ios/ParleyKit/Tests/ParleyKitTests/RelayAudioBridgeTests.swift. The Kotlin port is small and belongs in android/parleykit:

  • one object between MicCapture and SttRelayClient, holding chunks while there is no leg,
  • bounded by milliseconds with drop-oldest,
  • handing the next leg the position of its first held chunk as timeOffsetMs — not "now", which files the gap after the audio that followed it,
  • flushing the held chunks into the new client before live audio resumes.

android/parleykit/src/test/kotlin/… already has the test patterns (SttRelayClientTest, SegmentBuilderTest).

Worth porting ReconnectPolicy at the same time so the two platforms' ladders cannot drift.

Verification (device only)

Per the checklist in #283, on Android:

  • incoming call answered then ended → recording continues, transcript continues, no gap beyond the call
  • incoming call declined → same
  • Wi-Fi → cellular handover mid-recording
  • airplane mode for ~10 s mid-recording → reconnects, and the words spoken during the gap arrive (that is what the bridge buys)
  • Siri/Assistant, an alarm, and another app opening a recording session
  • the relay restarted under a live session
  • a Bluetooth headset connected and disconnected mid-recording

Activity

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions