Skip to content

fix: BLE shutdown cleanup + battery-tunable power settings#609

Merged
torlando-tech merged 11 commits intomainfrom
fix/ble-shutdown-and-power-settings
Mar 7, 2026
Merged

fix: BLE shutdown cleanup + battery-tunable power settings#609
torlando-tech merged 11 commits intomainfrom
fix/ble-shutdown-and-power-settings

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

Summary

  • BLE Shutdown Fix: Add synchronous stopImmediate() methods across the BLE stack (Scanner, Advertiser, GATT Server, Bridge, Coordinator) called before System.exit(0) to ensure BLE resources are properly released during service shutdown. Fixes persistent BLE connections after disabling the interface.

  • Battery-Tunable BLE Settings: Add user-configurable power presets (Performance / Balanced / Battery Saver / Custom) with granular control over scan intervals, scan duration, and advertising refresh interval. Settings flow through the full stack: UI → Room DB → Python config → Kotlin BLE bridge → Scanner/Advertiser.

  • Default device name: Changed to empty string so BLE advertising doesn't rename the device unless explicitly configured.

Test plan

  • Enable BLE interface → Apply → verify scanning/advertising in logcat → Disable → Apply → verify stopImmediate logs appear before System.exit(0) → verify no lingering BLE connections
  • Open AndroidBLE config → verify preset selector works → switch presets → verify slider values update
  • Select Custom → verify sliders become interactive → adjust values → Apply → verify logcat shows new intervals
  • Existing BLE interfaces without power settings should default to Balanced (backward compat)
  • Build succeeds: ./gradlew assembleDebug
  • Tests pass: ./gradlew :reticulum:test

🤖 Generated with Claude Code

Fix timing bug where BLE scanner/advertiser/GATT connections persist after
service restart because System.exit(0) kills the process before the async
Python shutdown chain can complete. Add synchronous stopImmediate() methods
that run directly on Main thread before process exit.

Add user-configurable BLE power settings (Performance/Balanced/Battery Saver/
Custom presets) with per-interface controls for scan interval, scan duration,
and advertising refresh interval. Settings flow through the full stack:
UI → Room DB → Python config → Kotlin BLE bridge → Scanner/Advertiser.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR delivers two cohesive improvements to the Android BLE stack: a synchronous stopImmediate() shutdown path across all four BLE components (Scanner, Advertiser, GATT Server, GATT Client) that runs before System.exit(0), and a user-configurable power-preset system (Performance / Balanced / Battery Saver / Custom) with granular scan/advertising interval controls that flow end-to-end from the Compose UI through Room DB, Python config, and the Kotlin BLE bridge.

Key changes:

  • BleScanner, BleAdvertiser, BleGattServer, and BleGattClient each gain a stopImmediate() / closeImmediate() method that shuts down BLE resources synchronously without relying on coroutines.
  • KotlinBLEBridge.stopImmediate() orchestrates all four closures with isolated try blocks, then cancels the coroutine scope, ensuring GATT server/client closure is not blocked by a ConcurrentModificationException in keepalive-job cleanup.
  • BleScanner scan-interval constants are promoted to @Volatile instance fields; currentScanInterval is also correctly marked @Volatile and reset in updatePowerSettings().
  • BleAdvertiser.updatePowerSettings() restarts the refresh job (via startRefreshJob(), which already cancels the existing job before launching a new one) so the new interval takes effect immediately.
  • BlePowerSettings / BlePowerPreset introduce a clean model layer; the PERFORMANCE preset's intentional high-duty-cycle scan window is now documented inline.
  • Warning text in InterfaceConfigDialog was corrected from "scans may overlap" to the accurate "high duty-cycle scanning will increase battery usage".
  • Default device_name changed to empty string so BLE advertising no longer renames the device unless the user explicitly sets one.

Minor concerns:

  • updatePowerSettings() in BleScanner unconditionally resets currentScanInterval to the active value, discarding idle state — see inline comment.
  • The safety-net stopImmediate() call in ReticulumService's cleanup block lacks the isInitialized guard present on the other two call sites — see inline comment.
  • configurePower() in KotlinBLEBridge propagates settings to scanner and advertiser but not to gattClient; a comment noting the intentional omission would help future readers.

Confidence Score: 4/5

  • Safe to merge with minor follow-up; the shutdown path is materially more correct than before and the power-settings plumbing is solid end-to-end.
  • All critical issues raised in the previous review round (missing @volatile on currentScanInterval, GATT client not cleaned up, incorrect warning text, refresh-job interval not applied live, CME-resistant try blocks) are addressed in this PR. The remaining open items are low-risk: the unguarded safety-net stopImmediate() call is a latent crash only on a code path that is unlikely to be reached before managers is initialised; the idle-state reset in updatePowerSettings() causes at most one extra active scan cycle; and the missing configurePower() propagation to gattClient is a no-op today.
  • Pay close attention to app/src/main/java/com/lxmf/messenger/service/ReticulumService.kt (missing isInitialized guard on the safety-net stopImmediate call) and reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/client/BleScanner.kt (idle-state reset behaviour in updatePowerSettings).

Important Files Changed

Filename Overview
reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/bridge/KotlinBLEBridge.kt Adds stopImmediate() and configurePower() methods; gattClient is now also cleaned up via closeImmediate(); scope is correctly cancelled after all component closures.
reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/client/BleScanner.kt Converts hard-coded scan constants to @volatile instance fields; updatePowerSettings() now resets currentScanInterval and adds @volatile; adds stopImmediate().
reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/server/BleAdvertiser.kt Makes advertising refresh interval power-tunable; updatePowerSettings() restarts the refresh job (startRefreshJob already cancels the old job before launching); adds stopImmediate().
reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/server/BleGattServer.kt Adds closeImmediate() with separate try blocks ensuring gattServer.close() is not blocked by a CME in keepalive job cleanup; no main-thread guard (consistent with how close() is thread-safe).
reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/client/BleGattClient.kt Adds closeImmediate() with a separate try block for the connections snapshot; if CME occurs during toMap(), an emptyMap() is returned and connections go unclosed (acceptable for forced shutdown since scope.cancel() follows shortly after).
reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/model/BlePowerSettings.kt New file defining BlePowerPreset enum and BlePowerSettings data class; PERFORMANCE preset's intentional scanDuration > discoveryInterval is now documented with a comment; CUSTOM fallback branch also commented.
app/src/main/java/com/lxmf/messenger/service/ReticulumService.kt Adds stopImmediate() calls in three shutdown paths (ACTION_STOP, restart, onDestroy); first call has isInitialized guard, safety-net call does not, but it is inside the same block that already accesses managers.
app/src/main/java/com/lxmf/messenger/ui/components/InterfaceConfigDialog.kt Adds segmented preset selector and custom sliders for power settings; warning text now correctly describes high duty-cycle battery impact (not "scans may overlap"); slider step counts are mathematically consistent.
python/ble_modules/android_ble_interface.py Calls configure_power() after super().init(); scanner/advertiser are created in KotlinBLEBridge's constructor so they exist at call time; power settings are correctly applied before startAsync().
python/reticulum_wrapper.py Writes new BLE power config keys; numeric values are safe, ble_power_preset string is written unquoted (existing pattern in this file); default device_name changed to empty string.

Sequence Diagram

sequenceDiagram
    participant UI as InterfaceConfigDialog
    participant VM as InterfaceManagementViewModel
    participant DB as Room DB (InterfaceConfig)
    participant PY as reticulum_wrapper.py
    participant DRV as AndroidBLEDriver
    participant IFACE as AndroidBLEInterface
    participant BRIDGE as KotlinBLEBridge
    participant SCAN as BleScanner
    participant ADV as BleAdvertiser

    UI->>VM: onConfigUpdate(blePowerPreset, intervals)
    VM->>DB: save InterfaceConfig (JSON with power fields)

    Note over PY: On service start
    DB->>PY: iface dict with ble_power_preset + intervals
    PY->>PY: _create_config_file() writes ble_power_preset,\nble_discovery_interval_ms, etc.

    Note over IFACE: Interface init
    IFACE->>DRV: super().__init__() → driver.start() → KotlinBLEBridge created
    IFACE->>DRV: configure_power(preset, intervals)
    DRV->>BRIDGE: configurePower(preset, discoveryIntervalMs, …)
    BRIDGE->>BRIDGE: powerSettings = BlePowerPreset.getSettings(preset)\n  or custom BlePowerSettings
    BRIDGE->>SCAN: updatePowerSettings(powerSettings)
    BRIDGE->>ADV: updatePowerSettings(powerSettings)

    Note over BRIDGE,ADV: On forced shutdown
    BRIDGE->>SCAN: stopImmediate()
    BRIDGE->>ADV: stopImmediate()
    BRIDGE->>BRIDGE: gattServer?.closeImmediate()
    BRIDGE->>BRIDGE: gattClient?.closeImmediate()
    BRIDGE->>BRIDGE: scope.cancel()
Loading

Comments Outside Diff (2)

  1. reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/client/BleScanner.kt, line 877-883 (link)

    updatePowerSettings() resets currentScanInterval unconditionally, discarding idle state

    currentScanInterval = activeScanIntervalMs is set regardless of whether the scanner was previously in idle mode (i.e. currentScanInterval == idleScanIntervalMs). After a power-settings update the scanner immediately switches to active-interval polling, even in a stable environment with no new devices, and won't return to idle until IDLE_SCANS_THRESHOLD consecutive empty scans have accumulated again.

    For most use cases this is harmless (one extra active cycle before re-entering idle), but if you want to preserve the current polling mode across a settings change, you could map the old interval ratio to the new one:

    fun updatePowerSettings(settings: BlePowerSettings) {
        val wasIdle = currentScanInterval == idleScanIntervalMs
        activeScanIntervalMs = settings.discoveryIntervalMs
        idleScanIntervalMs = settings.discoveryIntervalIdleMs
        scanDurationMs = settings.scanDurationMs
        currentScanInterval = if (wasIdle) idleScanIntervalMs else activeScanIntervalMs
        Log.d(TAG, "Power settings updated: active=${activeScanIntervalMs}ms, idle=${idleScanIntervalMs}ms, duration=${scanDurationMs}ms")
    }
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/client/BleScanner.kt
    Line: 877-883
    
    Comment:
    **`updatePowerSettings()` resets `currentScanInterval` unconditionally, discarding idle state**
    
    `currentScanInterval = activeScanIntervalMs` is set regardless of whether the scanner was previously in idle mode (i.e. `currentScanInterval == idleScanIntervalMs`). After a power-settings update the scanner immediately switches to active-interval polling, even in a stable environment with no new devices, and won't return to idle until `IDLE_SCANS_THRESHOLD` consecutive empty scans have accumulated again.
    
    For most use cases this is harmless (one extra active cycle before re-entering idle), but if you want to preserve the current polling mode across a settings change, you could map the old interval ratio to the new one:
    
    ```kotlin
    fun updatePowerSettings(settings: BlePowerSettings) {
        val wasIdle = currentScanInterval == idleScanIntervalMs
        activeScanIntervalMs = settings.discoveryIntervalMs
        idleScanIntervalMs = settings.discoveryIntervalIdleMs
        scanDurationMs = settings.scanDurationMs
        currentScanInterval = if (wasIdle) idleScanIntervalMs else activeScanIntervalMs
        Log.d(TAG, "Power settings updated: active=${activeScanIntervalMs}ms, idle=${idleScanIntervalMs}ms, duration=${scanDurationMs}ms")
    }
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/bridge/KotlinBLEBridge.kt, line 758-773 (link)

    configurePower() does not apply settings to gattClient

    scanner and advertiser both receive the new BlePowerSettings via updatePowerSettings(), but gattClient is not notified. BleGattClient doesn't currently expose any power-tunable parameters, so this is a no-op difference today — but if connection-keepalive timers or retry backoffs become configurable in the future, gattClient would silently miss updates. A brief comment here would document the intentional omission:

    // Propagate to scanner and advertiser if they exist
    // (gattClient has no power-tunable parameters currently)
    scanner?.updatePowerSettings(powerSettings)
    advertiser?.updatePowerSettings(powerSettings)
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/bridge/KotlinBLEBridge.kt
    Line: 758-773
    
    Comment:
    **`configurePower()` does not apply settings to `gattClient`**
    
    `scanner` and `advertiser` both receive the new `BlePowerSettings` via `updatePowerSettings()`, but `gattClient` is not notified. `BleGattClient` doesn't currently expose any power-tunable parameters, so this is a no-op difference today — but if connection-keepalive timers or retry backoffs become configurable in the future, `gattClient` would silently miss updates. A brief comment here would document the intentional omission:
    
    ```kotlin
    // Propagate to scanner and advertiser if they exist
    // (gattClient has no power-tunable parameters currently)
    scanner?.updatePowerSettings(powerSettings)
    advertiser?.updatePowerSettings(powerSettings)
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 28ce91a

torlando-tech and others added 3 commits March 6, 2026 20:10
…up, overlap warning

- Add @volatile to cross-thread power settings fields (KotlinBLEBridge,
  BleScanner, BleAdvertiser) for JMM visibility between Python/Kotlin threads
- Remove dead **power_kwargs from AndroidBLEDriver.start()
- Add ordering-safety comment in AndroidBLEInterface.__init__()
- Upgrade closeImmediate() off-thread log from Log.w to Log.e
- Deduplicate preset values in InterfaceConfigDialog using BlePowerPreset.getSettings()
- Add scan overlap warning when scan duration >= active scan interval

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Suppress TooManyFunctions on BleScanner (cohesive BLE component)
- Suppress SwallowedException in BlePowerPreset.fromString() (intentional fallback)
- Suppress CyclomaticComplexMethod on configStateToInterfaceConfig (one branch per type)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Mar 7, 2026

Codecov Report

❌ Patch coverage is 44.44444% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
python/ble_modules/android_ble_driver.py 12.50% 7 Missing ⚠️
python/ble_modules/android_ble_interface.py 0.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

companion object {
fun getSettings(preset: BlePowerPreset): BlePowerSettings =
when (preset) {
PERFORMANCE -> BlePowerSettings(PERFORMANCE, 3000L, 15000L, 5000L, 30_000L)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PERFORMANCE preset has scanDurationMs (5s) > discoveryIntervalMs (3s) — add intent comment

The PERFORMANCE preset intentionally sets a scan window (5 s) that is longer than the inter-scan delay (3 s), yielding a high-duty-cycle pattern (scan 5 s → wait 3 s → repeat). This is valid because the scan loop in BleScanner is sequential (scan completes, then waits currentScanInterval), so scans never literally overlap.

However, the UI in InterfaceConfigDialog.kt warns users when scanDuration ≥ activeInterval while in Custom mode. A reader browsing BlePowerSettings.kt alone could conclude this is an accidental configuration or a bug. A short inline note would prevent that confusion:

Suggested change
PERFORMANCE -> BlePowerSettings(PERFORMANCE, 3000L, 15000L, 5000L, 30_000L)
PERFORMANCE -> BlePowerSettings(PERFORMANCE, 3000L, 15000L, 5000L, 30_000L) // scanDuration(5s) > interval(3s) intentional — high duty-cycle sequential scanning
Prompt To Fix With AI
This is a comment left during a code review.
Path: reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/model/BlePowerSettings.kt
Line: 21

Comment:
**PERFORMANCE preset has `scanDurationMs (5s) > discoveryIntervalMs (3s)` — add intent comment**

The PERFORMANCE preset intentionally sets a scan window (5 s) that is longer than the inter-scan delay (3 s), yielding a high-duty-cycle pattern (scan 5 s → wait 3 s → repeat). This is valid because the scan loop in `BleScanner` is sequential (scan completes, then waits `currentScanInterval`), so scans never literally overlap.

However, the UI in `InterfaceConfigDialog.kt` warns users when `scanDuration ≥ activeInterval` while in Custom mode. A reader browsing `BlePowerSettings.kt` alone could conclude this is an accidental configuration or a bug. A short inline note would prevent that confusion:

```suggestion
                PERFORMANCE -> BlePowerSettings(PERFORMANCE, 3000L, 15000L, 5000L, 30_000L) // scanDuration(5s) > interval(3s) intentional — high duty-cycle sequential scanning
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…preset comment

- BluetoothGattServer.close() is an IPC call that is thread-safe; the
  guard was silently preventing GATT cleanup when shutdown ran off-Main
- Add comment clarifying PERFORMANCE preset's intentional high duty-cycle
  scan config (scanDuration 5s > interval 3s)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

…n power update

- Add BleGattClient.closeImmediate() to synchronously close all outgoing
  GATT connections during forced shutdown, matching the pattern used by
  BleGattServer, BleScanner, and BleAdvertiser
- Wire gattClient.closeImmediate() into KotlinBLEBridge.stopImmediate()
- Reset currentScanInterval in updatePowerSettings() so new intervals
  take effect immediately instead of waiting for the next adaptive cycle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

…currency

Scans are sequential in BleScanner — they can't overlap. The real
concern is high duty-cycle scanning increasing battery usage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

config_lines.append(f" mode = {mode}")

ble_power_preset = iface.get("ble_power_preset", "balanced")
config_lines.append(f" ble_power_preset = {ble_power_preset}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config values written without validation

Power preset values are written directly to the Reticulum config file with no sanitisation:

config_lines.append(f"    ble_power_preset = {ble_power_preset}")

If a user somehow injects a newline into ble_power_preset (e.g., via a crafted database entry), this would corrupt the generated config file. Consider quoting or stripping newlines, consistent with how other string values are handled elsewhere in the config writer:

Suggested change
config_lines.append(f" ble_power_preset = {ble_power_preset}")
config_lines.append(f" ble_power_preset = {str(ble_power_preset).strip()}")
Prompt To Fix With AI
This is a comment left during a code review.
Path: python/reticulum_wrapper.py
Line: 1630

Comment:
**Config values written without validation**

Power preset values are written directly to the Reticulum config file with no sanitisation:

```python
config_lines.append(f"    ble_power_preset = {ble_power_preset}")
```

If a user somehow injects a newline into `ble_power_preset` (e.g., via a crafted database entry), this would corrupt the generated config file. Consider quoting or stripping newlines, consistent with how other string values are handled elsewhere in the config writer:

```suggestion
                config_lines.append(f"    ble_power_preset = {str(ble_power_preset).strip()}")
```

How can I resolve this? If you propose a fix, please make it concise.

Without this, switching from BATTERY_SAVER (180s) to PERFORMANCE (30s)
while advertising is active would wait up to 3 minutes for the old
delay() to complete before the new interval takes effect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Written from Python thread via updatePowerSettings(), read from
Dispatchers.Default in the scan loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Snapshot the keepalive jobs before clearing to avoid CME, and use
separate try blocks so a failure in job cancellation can't prevent
gattServer.close() from running.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +71 to +79
if config and hasattr(self, 'driver') and self.driver is not None:
power_preset = config.get("ble_power_preset", "balanced")
self.driver.configure_power(
preset=power_preset,
discovery_interval_ms=int(config.get("ble_discovery_interval_ms", 5000)),
discovery_interval_idle_ms=int(config.get("ble_discovery_interval_idle_ms", 30000)),
scan_duration_ms=int(config.get("ble_scan_duration_ms", 10000)),
advertising_refresh_interval_ms=int(config.get("ble_advertising_refresh_interval_ms", 60000)),
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: configure_power() is called before the kotlin_bridge is initialized, causing all user-configured BLE power settings to be silently ignored.
Severity: CRITICAL

Suggested Fix

The call to configure_power() should be moved from AndroidBLEInterface.__init__() to a point after the kotlin_bridge has been successfully initialized in the start() method. Alternatively, configure_power() could be called again after the bridge is ready.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: python/ble_modules/android_ble_interface.py#L71-L79

Potential issue: The `AndroidBLEInterface.__init__()` method calls
`self.driver.configure_power()` immediately after the driver is instantiated. However,
the `kotlin_bridge` attribute on the driver is only initialized later when the `start()`
method is called. As a result, the `configure_power()` method finds `self.kotlin_bridge`
is `None`, logs a warning, and does not apply the power settings. Since there is no
subsequent call to configure power after the bridge is initialized, the entire power
settings feature is non-functional.

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

…seImmediate()

Same pattern as BleGattServer — snapshot the connections map in its own
try block so a CME can't prevent individual gatt.close() calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +303 to +309
// Stop BLE immediately before process exit
if (::managers.isInitialized) {
try {
managers.bleCoordinator.stopImmediate()
} catch (e: Exception) {
Log.w(TAG, "Error during BLE immediate shutdown in restart", e)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: triggerServiceRestart() calls BLE shutdown methods from a background thread via the onStaleHeartbeat callback, but these operations are expected to run on the Main thread.
Severity: MEDIUM

Suggested Fix

In the triggerServiceRestart() function, dispatch the call to managers.bleCoordinator.stopImmediate() to the main thread. This can be achieved by wrapping the call in a withContext(Dispatchers.Main) block to ensure BLE operations execute on the correct thread.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: app/src/main/java/com/lxmf/messenger/service/ReticulumService.kt#L303-L309

Potential issue: The `onStaleHeartbeat` callback, executed on a background thread
(`Dispatchers.Default`), calls `triggerServiceRestart()`. This function then directly
invokes `managers.bleCoordinator.stopImmediate()`, which results in Android BLE APIs
like `stopScan()` and `stopAdvertising()` being called from a background thread. This
violates the documented contract within the BLE methods, which state they expect to be
called from the Main thread. This deviation from best practices and the code's own
intent can lead to improper BLE resource cleanup and unstable behavior.

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Comment on lines 272 to 274
managers.eventHandler.stopAll()
managers.lockManager.releaseAll()
managers.broadcaster.kill()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safety-net stopImmediate() call missing isInitialized guard

The ACTION_STOP handler (earlier in the same function) wraps its stopImmediate() call in an if (::managers.isInitialized) check. This safety-net call does not, which would throw UninitializedPropertyAccessException if onStartCommand() is reached via the onDestroy / service-cleanup path before managers is ever initialised.

// Safety net: stop BLE if not already stopped by ACTION_STOP
if (::managers.isInitialized) managers.bleCoordinator.stopImmediate()
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/service/ReticulumService.kt
Line: 272-274

Comment:
**Safety-net `stopImmediate()` call missing `isInitialized` guard**

The `ACTION_STOP` handler (earlier in the same function) wraps its `stopImmediate()` call in an `if (::managers.isInitialized)` check. This safety-net call does not, which would throw `UninitializedPropertyAccessException` if `onStartCommand()` is reached via the `onDestroy` / service-cleanup path before `managers` is ever initialised.

```kotlin
// Safety net: stop BLE if not already stopped by ACTION_STOP
if (::managers.isInitialized) managers.bleCoordinator.stopImmediate()
```

How can I resolve this? If you propose a fix, please make it concise.

@torlando-tech torlando-tech merged commit b469f49 into main Mar 7, 2026
13 of 14 checks passed
@torlando-tech torlando-tech deleted the fix/ble-shutdown-and-power-settings branch March 7, 2026 06:14
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