Files
bitchat-android/docs/REVIEW_FOREGROUND_SERVICE.md
2026-06-10 14:14:28 +02:00

190 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
**Summary**
I reviewed the background/foreground service branch against `main` focusing on mesh, relay, DMs, handshake and lifecycle. Overall the architecture moves the mesh into a persistent Foreground Service with a notification and centralizes a single `BluetoothMeshService` via a processwide holder. The BLE layer and power manager refactors look solid.
I found several lifecycle and policy gaps that can break behavior when the app is in the background or is killed, and a couple of leaks/overengineering hotspots. Below are concrete issues and recommendations with file references.
**Key Risks and Issues**
- Foreground toggle doesnt control mesh start/stop
- `MeshForegroundService.ensureMeshStarted()` starts the mesh regardless of the “background enabled” preference or notification permission.
- Impact: Mesh can run without a visible foreground notification (policy risk) or continue running after the user disables background, causing hidden BLE activity.
- Where:
- `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:195` (ensureMeshStarted only checks BT perms)
- `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:176` (loop updates notif but never stops mesh when disabled/perms missing)
- ACTION_STOP doesnt stop the mesh
- The STOP action stops only the foreground service and not the BLE mesh.
- Impact: BLE continues to scan/advertise without a notification, violating FGS expectations and confusing users.
- Where: `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:133` (STOP path does not call `meshService.stopServices()`)
- Service destroy doesnt clean up the mesh
- `onDestroy()` cancels a job but does not stop the mesh or clear the holder.
- Impact: Potentially leaves the mesh running if the FGS is torn down (e.g., the system stops the service), and can lead to a stale singleton.
- Where: `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:234`
- Possible foreground service policy violation window
- In the periodic loop, if background is disabled or POST_NOTIFICATIONS is missing, the code stops the foreground state and cancels the notification, but it does not stop the mesh.
- Impact: Mesh continues running in background with no notification. This is high risk for Play policy and Android OS behavior (more aggressive kills).
- Where: `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:182-191`
- BluetoothPacketBroadcaster actor scope leak
- `BluetoothPacketBroadcaster` creates its own `broadcasterScope` with a fresh `SupervisorJob()` instead of using the connection scope passed into the manager. Its never cancelled in `stopServices()`.
- Impact: Actor and jobs can outlive the connection manager/service, causing leaks and unexpected broadcast attempts.
- Where:
- `app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt:55` (ctor receives a scope)
- `app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt:98` (creates new `broadcasterScope` ignoring the injected scope)
- No corresponding cancellation on stop in `BluetoothConnectionManager.stopServices()`.
- Boot completed auto-start edge cases
- Auto-start runs on BOOT_COMPLETED and LOCKED_BOOT_COMPLETED.
- On Android 13+, if POST_NOTIFICATIONS isnt granted, `MeshForegroundService.start()` wont start (correct), but theres no fallback scheduling (e.g., WorkManager) to retry foreground promotion once the user unlocks or grants permission later.
- Impact: Users expecting auto-start may never get the mesh until they open the app once or grant notifications via a separate action.
- Where:
- `app/src/main/java/com/bitchat/android/service/BootCompletedReceiver.kt:10-19`
- `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:37-63` (strict start gating on notif permission)
- Multiple starters and race potential
- Both `BitchatApplication` and `MainActivity` attempt to start the foreground service and/or get/create the mesh.
- Impact: It works because the holder dedupes instances and `startServices()` is idempotent, but the responsibility boundaries are muddled and harder to reason about; risk of start during missing perms or unwanted background.
- Where:
- `app/src/main/java/com/bitchat/android/BitchatApplication.kt:42-46`
- `app/src/main/java/com/bitchat/android/MainActivity.kt:47-67`
- Mesh can run without an attached UI delegate
- Intentional, and DM notifications are handled via `serviceNotificationManager` if no UI delegate is present.
- Risk: Good for background DMs, but be sure delivery acks/read receipts dont depend on UI-only state.
- Where:
- `app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt:391-411` (use of `serviceNotificationManager` when delegate is null)
- Android 14 FGS restrictions timing window
- `startForegroundService()` requires calling `startForeground()` within ~5 seconds. Primary start path builds the notification right away, but the retry/promotion logic in the update loop could call `startForeground()` multiple times or after a delay.
- Impact: While caught in try/catch, repeated `startForeground()` attempts arent ideal; more deterministic promotion improves reliability.
- Where:
- `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:168-174` (initial call OK)
- `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:178-187` (repeated attempts in loop)
- Over-permissive defaults
- Defaults enable background and auto-start. Without an explicit UX flow, users may unexpectedly run BLE activity until they disable background.
- Recommendation: Default background OFF on fresh installs, and require explicit optin.
- Where:
- `app/src/main/java/com/bitchat/android/service/MeshServicePreferences.kt:20-32`
- Minor: Notification permission “grant” handling depends on an inapp broadcast
- `NotificationPermissionChangedReceiver` listens for `ACTION_NOTIFICATION_PERMISSION_GRANTED` but depends on some component actually broadcasting it.
- Impact: If no component sends the broadcast after permission grant, the FGS wont autopromote.
- Where:
- `app/src/main/java/com/bitchat/android/service/NotificationPermissionChangedReceiver.kt:10-18`
**What Might Not Work Reliably In Background**
- Packet relay and scanning without a persistent FGS
- If background preference is disabled or notification permission is missing, the mesh can still be started by `ensureMeshStarted()` and keep running without a foreground notification. Android may restrict scan/advertise in Doze, leading to reduced connectivity, delayed DMs, and poor relay propagation.
- “Stop service” paths
- Using the STOP notification action does not stop the mesh; users think they stopped the app, but BLE stays active. This can cause unexpected background behavior and policy problems.
- Restart after process kill or reboot
- Reboot auto-start requires notification permission; otherwise it silently does nothing. Theres no queued retry to promote the service after the user unlocks/changes permission without opening the app.
- Actor/broadcaster lifecycle
- The broadcaster actor may outlive the connection manager/service and could keep jobs alive, causing unexpected logs or work after shutdown.
- Foreground service promotion timing/retries
- Promotion attempts in the periodic loop could miss the 5s window or reinvoke `startForeground()` unnecessarily.
**Recommendations**
- Enforce mesh lifecycle to foreground state
- Start/stop mesh strictly based on foreground eligibility and user preference:
- Only start mesh when both background is enabled and all required permissions are present.
- Stop mesh immediately when background is disabled or notification permission is revoked.
- Where/How:
- Update `ensureMeshStarted()` to require `MeshServicePreferences.isBackgroundEnabled(true)` and `hasNotificationPermission()` in addition to BT perms before starting the mesh. Also add a symmetric `ensureMeshStopped()` when not eligible.
- `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:192`
- In the periodic loop, if background is disabled or permissions are missing, also stop the mesh, not just the foreground/notification.
- `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:182-191`
- Fix STOP and onDestroy behavior
- STOP action should stop the mesh and clear the holder.
- `onDestroy()` should besteffort stop mesh if this service is the owner.
- Where/How:
- `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:133` — call `meshService?.stopServices()` and `MeshServiceHolder.clear()` before `stopSelf()`.
- `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:234` — on destroy, if foreground eligibility is off, call `meshService?.stopServices()`; consider a guard not to kill if another visible Activity is using it (see next).
- Introduce simple ownership/reference accounting for `MeshServiceHolder`
- Avoid ambiguous ownership between Activity and FGS:
- Track “UI attached” vs “FGS attached”; only stop underlying mesh when both detach, or when policy requires it (user disabled background).
- Where/How:
- Extend `MeshServiceHolder` with simple ref counts or flags to indicate active owners; add `attach(type)`/`detach(type)`.
- `app/src/main/java/com/bitchat/android/service/MeshServiceHolder.kt`
- In `MainActivity.onStart/onStop` attach/detach the UI owner; in `MeshForegroundService.onCreate/onDestroy` attach/detach the service owner.
- Use a single, parent scope to avoid leaks
- Make `BluetoothPacketBroadcaster` use the injected connection scope instead of creating its own `SupervisorJob()`.
- Where/How:
- `app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt:98` — replace `broadcasterScope` with the passed `connectionScope` and remove the extra job. Also ensure any actors/jobs are cancelled when `BluetoothConnectionManager.stopServices()` cancels its scope.
- Tighten foreground promotion logic
- Avoid repeated `startForeground()` calls in the update loop. Promote once on start when eligible; afterwards only call `NotificationManagerCompat.notify()`.
- Where/How:
- `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:176-187` — keep a local `isInForeground` flag; only call `startForeground()` on the transition from notinFG to inFG. Use `notify()` for updates.
- Improve boot/permission retry behavior
- When auto-start is enabled but notification permission is missing:
- Consider scheduling a WorkManager task that reevaluates eligibility on unlock or app launch and then promotes the FGS, or rely on an inapp signal sent by permission request flows.
- Where/How:
- `app/src/main/java/com/bitchat/android/service/BootCompletedReceiver.kt:10-19`
- Ensure the onboarding/permission flow emits `ACTION_NOTIFICATION_PERMISSION_GRANTED` broadcast upon grant so `NotificationPermissionChangedReceiver` can promote immediately.
- Default background OFF on fresh installs
- Reduce surprises and align with Play policy expectations by requiring explicit optin to run in background.
- Where/How:
- Change defaults in `MeshServicePreferences`:
- `app/src/main/java/com/bitchat/android/service/MeshServicePreferences.kt:20-32` — set default `false` for `BACKGROUND_ENABLED` and possibly `AUTO_START`.
- Optional: Consolidate service/mesh creation path
- To reduce races and complexity:
- Let the foreground service own `BluetoothMeshService` creation, and have the UI bind to it via the holder.
- Remove the eager start from `BitchatApplication`, and let `MainActivity` only request start of the FGS (which in turn creates/adopts the mesh).
- Where/How:
- `app/src/main/java/com/bitchat/android/BitchatApplication.kt:42-46` — consider removing the proactive start or guard behind explicit background enabled + permission checks.
- Validate read receipt/DM flows without UI
- Current design shows DM notifications via the service when `delegate == null`; thats good.
- Ensure delivery acks and handshake retries dont rely on UI-only paths.
- Where/How:
- Review and, if needed, mirror “UI-triggered” handshake initiation logic inside `BluetoothMeshService` for background-only contexts.
- `app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt:789, 873, 1086-1090`
**Minor Polish**
- Consider adding a user-visible state in the persistent notification when background is disabled or permissions are missing, so the user can tap to enable.
- Where: `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:208-240`
- Consider scoping `updateJob` to `serviceJob` and guarding multiple starters.
- Where: `app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:27, 158-196`
- Revisit scan/advertise settings in Doze with FGS active
- PowerManager duty cycle is good; validate realworld scan rates under Doze while FGS is active and adjust thresholds if relay connectivity drops.
- Where: `app/src/main/java/com/bitchat/android/mesh/PowerManager.kt`
**Suggested Acceptance Criteria After Fixes**
- When background is ON and notifications are granted:
- Mesh starts, persistent notification is visible, scanning/advertising work after swiping the app away.
- When background is OFF or notifications are revoked:
- Foreground service and mesh both stop; no BLE activity remains.
- STOP action fully stops mesh and closes the foreground service.
- After reboot, if background is ON and notification permission is granted:
- Foreground service starts and mesh runs. If not granted, it defers cleanly until the user grants.
- No coroutine/actor leaks after stop/restart cycles:
- Logs cleanly shut down and no lingering “broadcaster actor” messages after stopping.
If you want, I can implement the specific code changes for:
- stopping mesh on STOP/onDestroy,
- gating `ensureMeshStarted()` and adding `ensureMeshStopped()`,
- fixing `BluetoothPacketBroadcaster` to use the injected scope,
- tightening `startForeground()` promotion logic.