Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2f03a93
feat(android): native BluetoothDevice.getName() bridge for the gen5 r…
DropTabl Aug 25, 2026
95ac27a
fix(ble): the complete official gen5 connection bootstrap, through READY
DropTabl Aug 25, 2026
bb19dfb
test(ble): pin the official gen5 bootstrap — order, gates, counter, f…
DropTabl Aug 25, 2026
bd31920
fix(ble): review round — gen5-first routing, unconditional clock cont…
DropTabl Aug 25, 2026
6f8f9f0
fix(sync): the pairing generation is device-scoped; guard the native …
DropTabl Aug 25, 2026
e805540
fix(ble): memfault counts as liveness; pin the FAILURE-ack clock sema…
DropTabl Aug 25, 2026
ea648fb
test(ble): memfault liveness through the one accounting path; pin the…
DropTabl Aug 25, 2026
6aaea73
chore(deps): repin protocol to the #35 merge — the hello-revision gat…
DropTabl Aug 26, 2026
2672890
fix(ble): one service-discovery and characteristic-validation path
DropTabl Aug 26, 2026
38b7f92
fix(ble): bound the initial bond-state read
DropTabl Aug 26, 2026
563c818
fix(sync): the pairing generation lives in device.adapter_id
DropTabl Aug 26, 2026
7545f14
fix(ble,sync): cross-review round — bound at the seam, and three fals…
DropTabl Aug 26, 2026
8b90ef0
fix(ble,sync): CodeRabbit round — five findings on the gen5 bootstrap
DropTabl Aug 27, 2026
56df30c
Merge remote-tracking branch 'origin/main' into fix/gen5-official-boo…
DropTabl Aug 27, 2026
7414dd6
fix(ble,sync): cross-model review round — quarantine an unknown hello
DropTabl Aug 27, 2026
a1ad77f
fix(sync,ble): review round 3 — load() is a writer, and an unknown
DropTabl Aug 27, 2026
e4b534e
fix pr285 remaining coderabbit findings: unknown-revision identity ga…
abdulsaheel Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package wtf.openstrap.openstrap_edge

import android.Manifest
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.bluetooth.BluetoothManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
Expand Down Expand Up @@ -33,6 +36,7 @@ object NativeChannels {
private const val EDGE_TRACKING_CHANNEL = "openstrap/edge_tracking"
private const val DEVICE_ACTIONS_CHANNEL = "openstrap/device_actions"
private const val ANDROID_BG_CHANNEL = "openstrap/android_background"
private const val BLE_NATIVE_CHANNEL = "openstrap/ble_native"
const val TASKER_CHANNEL = "openstrap/tasker"
const val ACTION_DOUBLE_TAP = "wtf.openstrap.openstrap_edge.DOUBLE_TAP"
private const val TASKER_TOKEN_KEY = "tasker_auth_token"
Expand Down Expand Up @@ -127,6 +131,26 @@ object NativeChannels {
}
}

// Native Bluetooth reads flutter_blue_plus cannot answer. The one
// method here backs the gen5 readiness gate: it must read the
// platform `BluetoothDevice.getName()` (bond/stack-backed), not the
// plugin's in-memory platformName cache, which is empty for a device
// rebuilt from its id on a cold start. See lib/ble/android_native_name.dart.
MethodChannel(engine.dartExecutor.binaryMessenger, BLE_NATIVE_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"remoteDeviceName" -> {
val mac = call.arguments as? String
if (mac.isNullOrEmpty()) {
result.error("bad_args", "expected the remote MAC", null)
} else {
remoteDeviceName(app, mac, result)
}
}
else -> result.notImplemented()
}
}

// OS keep-alive integrations: CompanionDeviceManager association (background
// FGS exemption + device-presence relaunch) and the battery-optimization
// (Doze) exemption. See CompanionBridge.kt / lib/ble/android_background.dart.
Expand Down Expand Up @@ -268,6 +292,33 @@ object NativeChannels {
return token
}

/**
* The platform `BluetoothDevice.getName()` read behind the gen5 readiness
* gate. `getName()` needs BLUETOOTH_CONNECT on API 31+ — the same runtime
* permission every GATT operation already holds by the time a link is
* connected, checked explicitly here so a revoked grant answers as a clean
* error instead of a SecurityException. Lint cannot see that check through
* the early return, hence the targeted suppression; the belt-and-braces
* catch still turns any surprise (invalid MAC, no adapter) into the same
* error, which Dart reads as "no name" — the gate's failing value.
*/
@SuppressLint("MissingPermission")
private fun remoteDeviceName(app: Context, mac: String, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
app.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) !=
android.content.pm.PackageManager.PERMISSION_GRANTED
) {
result.error("name_unavailable", "BLUETOOTH_CONNECT not granted", null)
return
}
try {
val mgr = app.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
result.success(mgr?.adapter?.getRemoteDevice(mac)?.name)
} catch (e: Exception) {
result.error("name_unavailable", e.toString(), null)
}
}

private fun perform(ctx: Context, action: String): Boolean {
return try {
when (action) {
Expand Down
37 changes: 37 additions & 0 deletions lib/ble/android_native_name.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// android_native_name.dart — the Android `BluetoothDevice.getName()` read the
// gen5 readiness gate requires, as a platform-channel call.
//
// Why not flutter_blue_plus's `platformName`: that is an in-memory cache the
// plugin fills from scan results and connection events. A device rebuilt with
// `BluetoothDevice.fromId()` on a cold process start has an EMPTY cache, so
// gating readiness on it would fail every known-device reconnect that skipped
// scanning. The official gate reads the native `BluetoothDevice.getName()`,
// which the Android stack backs with its own bond/cache storage — so this
// channel asks the platform directly.
//
// Native handler: NativeChannels.kt (`BLE_NATIVE_CHANNEL`), registered on the
// long-lived engine so it also answers during headless background syncs.
// Requires BLUETOOTH_CONNECT on API 31+ — the same runtime permission every
// flutter_blue_plus GATT operation already needs, so by the time a link is
// connected the permission is held; a denial surfaces here as null.

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

class AndroidNativeName {
static const MethodChannel _ch = MethodChannel('openstrap/ble_native');

/// The native Android name for [remoteId] (the MAC on Android), or null when
/// the platform has none — which is exactly the value the readiness gate
/// compares against. Errors (missing permission, invalid MAC, no adapter)
/// are logged and reported as null: the gate must never pass on a name we
/// could not actually read.
static Future<String?> of(String remoteId) async {
try {
return await _ch.invokeMethod<String>('remoteDeviceName', remoteId);
} catch (e) {
debugPrint('[ble_native] remoteDeviceName($remoteId) failed: $e');
return null;
}
}
}
Loading
Loading