Skip to content

Keep Android BLE transcription running when the app is closed#7483

Open
axAilotl wants to merge 4 commits into
BasedHardware:mainfrom
axAilotl:feature/android-ble-background-transcription
Open

Keep Android BLE transcription running when the app is closed#7483
axAilotl wants to merge 4 commits into
BasedHardware:mainfrom
axAilotl:feature/android-ble-background-transcription

Conversation

@axAilotl
Copy link
Copy Markdown

@axAilotl axAilotl commented May 24, 2026

Summary

This came from feedback on a weekly Omi call: an Android user noted that other AI wearables can keep recording and transcribing even after the companion app is closed. This PR makes the stock Android Omi app behave that way for BLE pendant capture, so users do not have to reopen or check the app just to confirm transcription is still running. The pendant on/off state remains the user control point for recording and privacy.

Related issue

What changed

  • keeps the Android BLE foreground service alive when the Flutter task is closed
  • starts a native Android BLE audio streamer that sends pendant audio to the existing /v4/listen websocket while Flutter is not running
  • saves the stock API/auth config and active BLE audio endpoint from Flutter so the native service can reconnect through the same backend path
  • caches recent native transcript websocket messages and drains them into Flutter on app relaunch
  • refreshes the in-progress conversation after relaunch so the live transcript view hydrates after a cold start
  • lets the live transcript card open while capture is active, even before the first visible segment has arrived

Device support note

The native background path does not hardcode an individual device ID. It uses the active device ID and audio service/characteristic saved by Flutter. Packet handling is currently enabled for Omi/OpenGlass-style audio and Friend Pendant audio. Other BLE device families keep the existing foreground app behavior until their packet parsing and start/stop control flows are mirrored natively.

Testing

  • Hardware testing so far was done only with DK2.
  • Manual Android DK2 pendant test: pendant connects, light stays blue after closing the app, and audio continues transcribing while the app task is closed.
  • Manual reopen test: relaunching the app while capture is active hydrates the live transcript state and the transcript view becomes interactive.
  • Repeated close/reopen counting tests: no obvious transcript gap in the tested warm path.
  • flutter build apk --debug --flavor dev passed.
  • bash test.sh ran; the current app suite reports 465 passing and 7 unrelated baseline failures:
    • AudioWavePainter shouldRepaint returns false when level differs by 0.01 or less
    • AudioWavePainter shouldRepaint returns false when level differs by clearly less than 0.01
    • env_test.dart, env_staging_test.dart, and env_empty_staging_test.dart fail to compile because test EnvFields stubs do not implement posthogApiKey
    • RingProtocol.parseAudioPayload parses a frame that exactly fills the buffer (boundary)
    • RingProtocol.parseAudioPayload parses tightly-packed frames with no trailing padding (440B exactly)

Additional tester request

This touches Android foreground service, BLE, Flutter lifecycle behavior, and has only been tested on DK2 so far. Please test on additional Android models, OS versions, and supported Omi/OpenGlass/Friend Pendant devices by starting pendant capture, closing the app task, speaking for a while, reopening the app, and confirming the pendant stays connected and the transcript catches up without losing audio.

@axAilotl axAilotl changed the title Keep Android BLE transcription alive after app task close Keep Android BLE transcription running when the app is closed May 24, 2026
@axAilotl axAilotl marked this pull request as ready for review May 24, 2026 17:04
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 24, 2026

Greptile Summary

This PR keeps the Android BLE foreground service alive after the app task is removed, so pendant audio continues transcribing without the user needing to reopen the app. A new native Kotlin WebSocket streamer (OmiBackgroundAudioStreamer) takes over audio delivery when Flutter is not running, caches transcript messages, and drains them back into Flutter on relaunch.

  • OmiBleForegroundService becomes START_STICKY, survives task removal, and restores its managed device from SharedPreferences; a new CharacteristicValueListener feeds raw BLE frames to the native streamer when Flutter is not alive.
  • CaptureProvider saves BLE config to SharedPreferences before streaming, drains the native transcript cache on relaunch, and starts an in-progress conversation refresh timer so the live transcript view hydrates after a cold start.
  • processing_capture.dart unblocks tapping the live transcript card while capture is active even before the first segment arrives.

Confidence Score: 3/5

The BLE background path works but stopWithTask=false was accidentally applied to the phone-mic foreground service as well, which could cause the microphone to keep recording after the user closes the app.

The core BLE-keeps-alive mechanism is well-structured and the Flutter-side state management is careful. The unintended stopWithTask=false on the Flutter background service (used for phone-mic recording) is the most concerning change: users who record via phone mic could have their microphone continue running silently after swiping away the app. The native streamer also has a few small thread-safety gaps and a URL normalisation edge case that could silently break background transcription for non-standard API base URLs.

AndroidManifest.xml (phone-mic service stopWithTask flag), OmiBackgroundAudioStreamer.kt (thread-safety and URL normalisation)

Important Files Changed

Filename Overview
app/android/app/src/main/AndroidManifest.xml Adds android:stopWithTask="false" to both OmiBleForegroundService (intentional) and flutter_background_service.BackgroundService (phone-mic service, likely unintentional side effect).
app/android/app/src/main/kotlin/com/friend/ios/OmiBackgroundAudioStreamer.kt New file: native WebSocket audio streamer for background BLE transcription. Has thread-safety gaps (sentFrames not volatile, lastFailureAtMs read outside lock) and a URL normalisation bug for wss:// inputs.
app/android/app/src/main/kotlin/com/friend/ios/OmiBleForegroundService.kt Adds background audio subscription, START_STICKY restart, saved-device restore, and CharacteristicValueListener hookup. Logic looks correct; UUID constants are duplicated with OmiBackgroundAudioStreamer but that is cosmetic.
app/android/app/src/main/kotlin/com/example/my_project/MainActivity.kt Adds drain MethodChannel for native BLE transcript cache and resets nativeBleForegroundReady flag on engine creation. Removes explicit service stop on task close. Changes look correct.
app/android/app/src/main/kotlin/com/friend/ios/BleCompanionService.kt Removes the isAppAlive() guard so the companion service can start the BLE foreground service even when Flutter is not running. Intentional for background mode.
app/android/app/src/main/kotlin/com/friend/ios/OmiBleManager.kt Adds CharacteristicValueListener interface and fires it alongside the Flutter API callback (guarded by isFlutterAlive). Also fixes a missing completeCommand() call on duplicate onServicesDiscovered.
app/lib/providers/capture_provider.dart Adds native BLE transcript drain on app relaunch, in-progress conversation refresh timer, nativeBleStreamingEnabled/nativeBleForegroundReady flag management, and changes _processNewSegmentReceived to async. The logic is generally sound.
app/lib/pages/conversations/widgets/processing_capture.dart Allows the live transcript card to be tapped when capture is active (even before any segments arrive), unblocking cold-start UX after background recording.

Sequence Diagram

sequenceDiagram
    participant Flutter
    participant OmiBleFgService
    participant BgAudioStreamer
    participant BackendWS as /v4/listen WS

    Flutter->>OmiBleFgService: startService(device, requiresBond)
    Flutter->>SharedPrefs: "save nativeBleStreamConfig + nativeBleStreamingEnabled=true"
    Flutter->>OmiBleFgService: BLE audio frames (via CharacteristicValueListener)
    Flutter->>SharedPrefs: "nativeBleForegroundReady=true"
    note over BgAudioStreamer: Flutter alive — streamer idle

    note over Flutter: User closes app (task removed)
    Flutter->>SharedPrefs: "nativeBleForegroundReady=false"
    OmiBleFgService->>OmiBleFgService: onTaskRemoved (service survives, START_STICKY)
    OmiBleFgService->>BgAudioStreamer: handleCharacteristic (BLE frames)
    BgAudioStreamer->>BackendWS: WebSocket connect + stream audio
    BackendWS-->>BgAudioStreamer: transcript messages (cached in memory)

    note over Flutter: User reopens app
    Flutter->>SharedPrefs: "nativeBleForegroundReady=false (reset)"
    Flutter->>BgAudioStreamer: drain() via MethodChannel
    BgAudioStreamer-->>Flutter: cached transcript messages
    Flutter->>Flutter: _processNewSegmentReceived (hydrate UI)
    Flutter->>SharedPrefs: "nativeBleForegroundReady=true"
    BgAudioStreamer->>BgAudioStreamer: stop("foreground_ready")
Loading

Reviews (1): Last reviewed commit: "fix(android): hydrate cold-start BLE tra..." | Re-trigger Greptile

Comment thread app/android/app/src/main/AndroidManifest.xml
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.

2 participants