also looking at this
feat(android): the Bluetooth scan pair in Zig — ratchet 56 → 54
#186
7 files
+377
-3
| @@ -43,6 +43,7 @@ const clipboard = ; | ||
| 43 | 43 | const intents = ; |
| 44 | 44 | const network = ; |
| 45 | 45 | const appstate = ; |
| 46 | const bluetooth = ; | |
| 46 | 47 | const securestore = ; |
| 47 | 48 | const haptics = ; |
| 48 | 49 | const notifications = ; |
| @@ -431,6 +432,23 @@ const natives = [_]jni.JNINativeMethod{ | ||
| 431 | 432 | .signature = "()Ljava/lang/String;", |
| 432 | 433 | .fnPtr = @ptrCast(&nativeGetAppState), |
| 433 | 434 | }, |
| 435 | // Not an action: the other end of the ScanCallback the holder owns. The | |
| 436 | // name arrives nullable, so the "Unknown" default stays a Zig decision. | |
| 437 | .{ | |
| 438 | .name = "nativeBluetoothDevice", | |
| 439 | .signature = "(Ljava/lang/String;Ljava/lang/String;I)V", | |
| 440 | .fnPtr = @ptrCast(&nativeBluetoothDevice), | |
| 441 | }, | |
| 442 | .{ | |
| 443 | .name = "nativeStartBluetoothScan", | |
| 444 | .signature = "(Landroid/app/Activity;)Z", | |
| 445 | .fnPtr = @ptrCast(&nativeStartBluetoothScan), | |
| 446 | }, | |
| 447 | .{ | |
| 448 | .name = "nativeStopBluetoothScan", | |
| 449 | .signature = "(Landroid/app/Activity;)Z", | |
| 450 | .fnPtr = @ptrCast(&nativeStopBluetoothScan), | |
| 451 | }, | |
| 434 | 452 | }; |
| 435 | 453 | |
| 436 | 454 | /// How binding went. A value rather than a log line, so every path is |
| @@ -1872,6 +1890,91 @@ fn nativeGetAppState(env: jni.JNIEnv, _: jni.jobject) callconv(.c) jni.jstring { | ||
| 1872 | 1890 | return j.newStringUtf8(arena.allocator(), appstate.state().name()) catch null; |
| 1873 | 1891 | } |
| 1874 | 1892 | |
| 1893 | /// `nativeBluetoothDevice(address, name, rssi)` — one scan result. | |
| 1894 | fn nativeBluetoothDevice( | |
| 1895 | env: jni.JNIEnv, | |
| 1896 | _: jni.jobject, | |
| 1897 | address: jni.jstring, | |
| 1898 | name: jni.jstring, | |
| 1899 | rssi: jni.jint, | |
| 1900 | ) callconv(.c) void { | |
| 1901 | const j = Jni.init(env); | |
| 1902 | var arena = std.heap.ArenaAllocator.init(backing); | |
| 1903 | defer arena.deinit(); | |
| 1904 | const allocator = arena.allocator(); | |
| 1905 | ||
| 1906 | const address_text = j.stringToUtf8(allocator, address) catch return; | |
| 1907 | ||
| 1908 | // Null is a device advertising no name, which is the common case rather | |
| 1909 | // than the edge one — `bluetooth.announce` applies the shim's "Unknown". | |
| 1910 | const name_text: ?[]const u8 = if (name == null) | |
| 1911 | null | |
| 1912 | else | |
| 1913 | j.stringToUtf8(allocator, name) catch return; | |
| 1914 | ||
| 1915 | bluetooth.announce(allocator, address_text, name_text, rssi) catch {}; | |
| 1916 | } | |
| 1917 | ||
| 1918 | /// `nativeStartBluetoothScan(activity)`. | |
| 1919 | /// | |
| 1920 | /// Resolves `true` whether or not a scan actually started, because the shim | |
| 1921 | /// does: `bluetoothScanner?.startScan(...)` is skipped when there is no | |
| 1922 | /// adapter or Bluetooth is off, and the resolve fires regardless. See #185. | |
| 1923 | fn nativeStartBluetoothScan( | |
| 1924 | env: jni.JNIEnv, | |
| 1925 | _: jni.jobject, | |
| 1926 | activity: jni.jobject, | |
| 1927 | ) callconv(.c) jni.jboolean { | |
| 1928 | const j = Jni.init(env); | |
| 1929 | var arena = std.heap.ArenaAllocator.init(backing); | |
| 1930 | defer arena.deinit(); | |
| 1931 | const allocator = arena.allocator(); | |
| 1932 | ||
| 1933 | const granted = permissions.isGranted(j, activity, permissions.bluetooth_scan) catch |err| { | |
| 1934 | std.log.warn("craft: startBluetoothScan fell through to the shim ({s})", .{@errorName(err)}); | |
| 1935 | return jni.JNI_FALSE; | |
| 1936 | }; | |
| 1937 | ||
| 1938 | if (!granted) { | |
| 1939 | // The shim only asks on API 31+, where the permission exists at all; | |
| 1940 | // the holder makes that check, because it is the one that knows how to | |
| 1941 | // ask without throwing on older releases. | |
| 1942 | callHolderVoid(j, "requestBluetoothScanPermission", activity) catch |err| { | |
| 1943 | std.log.warn("craft: startBluetoothScan fell through to the shim ({s})", .{@errorName(err)}); | |
| 1944 | return jni.JNI_FALSE; | |
| 1945 | }; | |
| 1946 | calendar.rejectOn(allocator, bluetooth.reject_global, "Permission denied") catch {}; | |
| 1947 | return jni.JNI_TRUE; | |
| 1948 | } | |
| 1949 | ||
| 1950 | callHolderVoid(j, "startBluetoothWatch", activity) catch |err| { | |
| 1951 | std.log.warn("craft: startBluetoothScan fell through to the shim ({s})", .{@errorName(err)}); | |
| 1952 | return jni.JNI_FALSE; | |
| 1953 | }; | |
| 1954 | ||
| 1955 | events.settle(allocator, bluetooth.resolve_global, "true") catch return jni.JNI_FALSE; | |
| 1956 | return jni.JNI_TRUE; | |
| 1957 | } | |
| 1958 | ||
| 1959 | /// `nativeStopBluetoothScan(activity)`. | |
| 1960 | /// | |
| 1961 | /// Cannot decline after a successful start, for the reason the network watch | |
| 1962 | /// cannot: the shim's own `bleScanCallback` is null, so its stop finds nothing | |
| 1963 | /// to hand to `stopScan`. | |
| 1964 | fn nativeStopBluetoothScan( | |
| 1965 | env: jni.JNIEnv, | |
| 1966 | _: jni.jobject, | |
| 1967 | activity: jni.jobject, | |
| 1968 | ) callconv(.c) jni.jboolean { | |
| 1969 | const j = Jni.init(env); | |
| 1970 | ||
| 1971 | callHolderVoid(j, "stopBluetoothWatch", activity) catch |err| { | |
| 1972 | std.log.warn("craft: stopBluetoothScan failed ({s})", .{@errorName(err)}); | |
| 1973 | return jni.JNI_FALSE; | |
| 1974 | }; | |
| 1975 | return jni.JNI_TRUE; | |
| 1976 | } | |
| 1977 | ||
| 1875 | 1978 | // ============================================================================= |
| 1876 | 1979 | // Tests |
| 1877 | 1980 | // ============================================================================= |
| @@ -1983,7 +2086,7 @@ test "the registered natives name methods the Kotlin actually declares" { | ||
| 1983 | 2086 | // A descriptor is checked by the JVM at registration, so a wrong one fails |
| 1984 | 2087 | // at load rather than at call — but only if the *name* matches something. |
| 1985 | 2088 | // These two strings are the contract with CraftBridge.kt. |
| 1986 | try testing.expectEqual(@as(usize, 50), natives.len); | |
| 2089 | try testing.expectEqual(@as(usize, 53), natives.len); | |
| 1987 | 2090 | try testing.expectEqualStrings("nativeGetDeviceInfo", std.mem.span(natives[0].name)); |
| 1988 | 2091 | |
| 1989 | 2092 | // And the class they bind to is the fixed one, not the templated bridge. |