also looking at this
feat(android): the Bluetooth scan pair in Zig — ratchet 56 → 54
#186
7 files
+377
-3
| @@ -1,6 +1,12 @@ | ||
| 1 | 1 | package com.craft.runtime |
| 2 | 2 | |
| 3 | 3 | import android.app.Activity |
| 4 | import androidx.core.app.ActivityCompat | |
| 5 | import android.os.Build | |
| 6 | import android.bluetooth.le.ScanResult | |
| 7 | import android.bluetooth.le.ScanCallback | |
| 8 | import android.bluetooth.le.BluetoothLeScanner | |
| 9 | import android.bluetooth.BluetoothManager | |
| 4 | 10 | import androidx.lifecycle.ProcessLifecycleOwner |
| 5 | 11 | import androidx.lifecycle.LifecycleEventObserver |
| 6 | 12 | import android.net.NetworkRequest |
| @@ -153,6 +159,9 @@ object CraftNative { | ||
| 153 | 159 | private external fun nativeStartAppStateMonitoring(activity: Activity): Boolean |
| 154 | 160 | private external fun nativeStopAppStateMonitoring(activity: Activity): Boolean |
| 155 | 161 | 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 | |
| 156 | 165 | |
| 157 | 166 | /** |
| 158 | 167 | * `getDeviceInfo`, or null to use the Kotlin implementation. |
| @@ -917,4 +926,95 @@ object CraftNative { | ||
| 917 | 926 | null |
| 918 | 927 | } |
| 919 | 928 | } |
| 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 | } | |
| 920 | 1020 | } |