14 KiB
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 process‑wide 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/over‑engineering hotspots. Below are concrete issues and recommendations with file references.
Key Risks and Issues
-
Foreground toggle doesn’t 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 doesn’t 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 callmeshService.stopServices())
-
Service destroy doesn’t 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
BluetoothPacketBroadcastercreates its ownbroadcasterScopewith a freshSupervisorJob()instead of using the connection scope passed into the manager. It’s never cancelled instopServices().- 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 newbroadcasterScopeignoring 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 isn’t granted,
MeshForegroundService.start()won’t start (correct), but there’s 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-19app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:37-63(strict start gating on notif permission)
-
Multiple starters and race potential
- Both
BitchatApplicationandMainActivityattempt 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-46app/src/main/java/com/bitchat/android/MainActivity.kt:47-67
- Both
-
Mesh can run without an attached UI delegate
- Intentional, and DM notifications are handled via
serviceNotificationManagerif no UI delegate is present. - Risk: Good for background DMs, but be sure delivery acks/read receipts don’t depend on UI-only state.
- Where:
app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt:391-411(use ofserviceNotificationManagerwhen delegate is null)
- Intentional, and DM notifications are handled via
-
Android 14 FGS restrictions timing window
startForegroundService()requires callingstartForeground()within ~5 seconds. Primary start path builds the notification right away, but the retry/promotion logic in the update loop could callstartForeground()multiple times or after a delay.- Impact: While caught in try/catch, repeated
startForeground()attempts aren’t 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 opt‑in.
- Where:
app/src/main/java/com/bitchat/android/service/MeshServicePreferences.kt:20-32
-
Minor: Notification permission “grant” handling depends on an in‑app broadcast
NotificationPermissionChangedReceiverlistens forACTION_NOTIFICATION_PERMISSION_GRANTEDbut depends on some component actually broadcasting it.- Impact: If no component sends the broadcast after permission grant, the FGS won’t auto‑promote.
- 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.
- If background preference is disabled or notification permission is missing, the mesh can still be started by
-
“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. There’s 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 re‑invoke
startForeground()unnecessarily.
- Promotion attempts in the periodic loop could miss the 5s window or re‑invoke
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 requireMeshServicePreferences.isBackgroundEnabled(true)andhasNotificationPermission()in addition to BT perms before starting the mesh. Also add a symmetricensureMeshStopped()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
- Update
- Start/stop mesh strictly based on foreground eligibility and user preference:
-
Fix STOP and onDestroy behavior
- STOP action should stop the mesh and clear the holder.
onDestroy()should best‑effort stop mesh if this service is the owner.- Where/How:
app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:133— callmeshService?.stopServices()andMeshServiceHolder.clear()beforestopSelf().app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:234— on destroy, if foreground eligibility is off, callmeshService?.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
MeshServiceHolderwith simple ref counts or flags to indicate active owners; addattach(type)/detach(type).app/src/main/java/com/bitchat/android/service/MeshServiceHolder.kt
- In
MainActivity.onStart/onStopattach/detach the UI owner; inMeshForegroundService.onCreate/onDestroyattach/detach the service owner.
- Extend
- Avoid ambiguous ownership between Activity and FGS:
-
Use a single, parent scope to avoid leaks
- Make
BluetoothPacketBroadcasteruse the injected connection scope instead of creating its ownSupervisorJob(). - Where/How:
app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt:98— replacebroadcasterScopewith the passedconnectionScopeand remove the extra job. Also ensure any actors/jobs are cancelled whenBluetoothConnectionManager.stopServices()cancels its scope.
- Make
-
Tighten foreground promotion logic
- Avoid repeated
startForeground()calls in the update loop. Promote once on start when eligible; afterwards only callNotificationManagerCompat.notify(). - Where/How:
app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:176-187— keep a localisInForegroundflag; only callstartForeground()on the transition from not‑in‑FG to in‑FG. Usenotify()for updates.
- Avoid repeated
-
Improve boot/permission retry behavior
- When auto-start is enabled but notification permission is missing:
- Consider scheduling a WorkManager task that re‑evaluates eligibility on unlock or app launch and then promotes the FGS, or rely on an in‑app 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_GRANTEDbroadcast upon grant soNotificationPermissionChangedReceivercan promote immediately.
- When auto-start is enabled but notification permission is missing:
-
Default background OFF on fresh installs
- Reduce surprises and align with Play policy expectations by requiring explicit opt‑in to run in background.
- Where/How:
- Change defaults in
MeshServicePreferences:app/src/main/java/com/bitchat/android/service/MeshServicePreferences.kt:20-32— set defaultfalseforBACKGROUND_ENABLEDand possiblyAUTO_START.
- Change defaults in
-
Optional: Consolidate service/mesh creation path
- To reduce races and complexity:
- Let the foreground service own
BluetoothMeshServicecreation, and have the UI bind to it via the holder. - Remove the eager start from
BitchatApplication, and letMainActivityonly request start of the FGS (which in turn creates/adopts the mesh).
- Let the foreground service own
- 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.
- To reduce races and complexity:
-
Validate read receipt/DM flows without UI
- Current design shows DM notifications via the service when
delegate == null; that’s good. - Ensure delivery acks and handshake retries don’t rely on UI-only paths.
- Where/How:
- Review and, if needed, mirror “UI-triggered” handshake initiation logic inside
BluetoothMeshServicefor background-only contexts.app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt:789, 873, 1086-1090
- Review and, if needed, mirror “UI-triggered” handshake initiation logic inside
- Current design shows DM notifications via the service when
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
- Where:
-
Consider scoping
updateJobtoserviceJoband guarding multiple starters.- Where:
app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt:27, 158-196
- Where:
-
Revisit scan/advertise settings in Doze with FGS active
- PowerManager duty cycle is good; validate real‑world 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 addingensureMeshStopped(), - fixing
BluetoothPacketBroadcasterto use the injected scope, - tightening
startForeground()promotion logic.