ReviewOS

also looking at this

craft-native/craft

feat(android): the Bluetooth scan pair in Zig — ratchet 56 → 54

#186
Merged glennmichael123 wants to merge feat/android-bluetooth into main
7 files +377 -3
packages/android/templates/CraftNative.kt.templatemodified+100-0
Changes to packages/android/templates/CraftNative.kt.template
@@ -1,6 +1,12 @@
11package com.craft.runtime
22
33import android.app.Activity
4import androidx.core.app.ActivityCompat
5import android.os.Build
6import android.bluetooth.le.ScanResult
7import android.bluetooth.le.ScanCallback
8import android.bluetooth.le.BluetoothLeScanner
9import android.bluetooth.BluetoothManager
410import androidx.lifecycle.ProcessLifecycleOwner
511import androidx.lifecycle.LifecycleEventObserver
612import android.net.NetworkRequest
@@ -153,6 +159,9 @@ object CraftNative {
153159 private external fun nativeStartAppStateMonitoring(activity: Activity): Boolean
154160 private external fun nativeStopAppStateMonitoring(activity: Activity): Boolean
155161 private external fun nativeGetAppState(): String?
162 private external fun nativeBluetoothDevice(address: String, name: String?, rssi: Int)
163 private external fun nativeStartBluetoothScan(activity: Activity): Boolean
164 private external fun nativeStopBluetoothScan(activity: Activity): Boolean
156165
157166 /**
158167 * `getDeviceInfo`, or null to use the Kotlin implementation.
@@ -917,4 +926,95 @@ object CraftNative {
917926 null
918927 }
919928 }
929
930 // ==================== Bluetooth scanning ====================
931 //
932 // The third listener shim. `ScanCallback` is an abstract Java class, so
933 // the object is here and every result forwards to Zig — with the device
934 // name still nullable, so the "Unknown" default stays one decision in one
935 // place rather than two that have to agree.
936
937 private var bleScanner: BluetoothLeScanner? = null
938 private var bleScanCallback: ScanCallback? = null
939
940 /**
941 * Start scanning, including the part that does nothing.
942 *
943 * `bleScanner` is null when the device has no adapter or Bluetooth is off,
944 * and the `?.` then skips the scan — while the caller resolves true
945 * regardless, exactly as `CraftBridge` does. Reproduced rather than
946 * guarded, because the reply is what a page acts on. See #185.
947 */
948 @JvmStatic
949 fun startBluetoothWatch(activity: Activity) {
950 val manager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
951 bleScanner = manager.adapter?.bluetoothLeScanner
952
953 val callback = object : ScanCallback() {
954 override fun onScanResult(callbackType: Int, result: ScanResult) {
955 try {
956 nativeBluetoothDevice(
957 result.device.address,
958 result.device.name,
959 result.rssi
960 )
961 } catch (e: UnsatisfiedLinkError) {
962 // The library went away. Nothing is waiting on this.
963 } catch (e: SecurityException) {
964 // `device.name` needs BLUETOOTH_CONNECT on API 31+, which
965 // this scan does not require. The shim would throw out of
966 // the callback here; dropping the result is the smaller
967 // difference, and it is the only one.
968 }
969 }
970 }
971
972 bleScanner?.startScan(callback)
973 bleScanCallback = callback
974 }
975
976 @JvmStatic
977 fun stopBluetoothWatch(activity: Activity) {
978 bleScanCallback?.let { bleScanner?.stopScan(it) }
979 bleScanCallback = null
980 }
981
982 /**
983 * Ask for BLUETOOTH_SCAN, but only where it exists.
984 *
985 * The permission arrived in API 31. Below that the shim does not ask at
986 * all — it rejects and leaves it — so the version check lives here, next
987 * to the call that would throw without it.
988 */
989 @JvmStatic
990 fun requestBluetoothScanPermission(activity: Activity) {
991 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
992 ActivityCompat.requestPermissions(
993 activity,
994 arrayOf(android.Manifest.permission.BLUETOOTH_SCAN),
995 REQUEST_BLUETOOTH
996 )
997 }
998 }
999
1000 /** `CraftBridge.REQUEST_BLUETOOTH`, which this has to match. */
1001 private const val REQUEST_BLUETOOTH = 1009
1002
1003 fun startBluetoothScan(activity: Activity): Boolean {
1004 if (!isAvailable) return false
1005 return try {
1006 nativeStartBluetoothScan(activity)
1007 } catch (e: UnsatisfiedLinkError) {
1008 false
1009 }
1010 }
1011
1012 fun stopBluetoothScan(activity: Activity): Boolean {
1013 if (!isAvailable) return false
1014 return try {
1015 nativeStopBluetoothScan(activity)
1016 } catch (e: UnsatisfiedLinkError) {
1017 false
1018 }
1019 }
9201020}