ReviewOS

craft-native/craft

startBluetoothScan resolves true when no scan started

#185
Closed glennmichael123 opened this 9 days ago ·
9 days ago

CraftBridge.startBluetoothScan starts the scan through a safe call and resolves unconditionally:

val bluetoothManager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
bluetoothScanner = bluetoothAdapter?.bluetoothLeScanner

bleScanCallback = object : ScanCallback() { ... }

bluetoothScanner?.startScan(bleScanCallback)
activity.runOnUiThread {
    webView.evaluateJavascript("window._craftBleResolve && window._craftBleResolve(true)", null)
}

bluetoothScanner is null in two ordinary situations:

  • the device has no Bluetooth adapter — bluetoothManager.adapter is null;
  • Bluetooth is switched off — adapter.bluetoothLeScanner returns null until it is on.

In both, the ?. skips startScan and the resolve fires anyway. The page is told scanning started, no craftBluetoothDevice events ever arrive, and there is nothing to distinguish that from a room with no devices in it.

The second case is the common one. A user with Bluetooth off is not an edge case, and the app has no way to tell them to turn it on, because as far as the page knows the scan is running.

Suggested fix

The information is right there:

val scanner = bluetoothAdapter?.bluetoothLeScanner
if (scanner == null) {
    activity.runOnUiThread {
        webView.evaluateJavascript(
            "window._craftBleReject && window._craftBleReject(${jsQuote("Bluetooth is unavailable or switched off")})", null)
    }
    return
}

_craftBleReject is already assigned by the injected JS — the permission path uses it — so there is somewhere honest to send this.

Distinguishing "no adapter" from "adapter off" would be better still, since only the second is something the user can act on. BluetoothAdapter.isEnabled is what tells them apart.

Zig reproduces it deliberately

nativeStartBluetoothScan resolves true on the same path, and CraftNative.startBluetoothWatch skips the scan the same way. Rejecting on the Zig path alone would mean the same call succeeds or fails depending on which implementation served it, and the difference would only show up as a missing rejection.

Related: #148, #157, #159, #175, #178 — replies that are sent but do not mean what they appear to.

9 days ago

Fixed and shipped on main in 5c3fd3c (fix(android): reject Bluetooth scans that do not start). Both the native-holder and Kotlin fallback paths now reject when no scan reaches Android's BluetoothLeScanner, with distinct unavailable, powered-off, permission, scanner, and runtime failures. Regression coverage was added; CI, SBOM, Binary Size, and Benchmark all passed.

Sign in to comment on this issue.