ReviewOS

also looking at this

craft-native/craft

feat(android): the motion pair, and org.json's numbers — ratchet 54 → 52

#187
Merged glennmichael123 wants to merge feat/android-motion into main
7 files +577 -3
packages/zig/src/android_dispatch.zigmodified+122-1
Changes to packages/zig/src/android_dispatch.zig
@@ -44,6 +44,8 @@ const intents = ;
4444const network = ;
4545const appstate = ;
4646const bluetooth = ;
47const motion = ;
48const json_number = ;
4749const securestore = ;
4850const haptics = ;
4951const notifications = ;
@@ -449,6 +451,22 @@ const natives = [_]jni.JNINativeMethod{
449451 .signature = "(Landroid/app/Activity;)Z",
450452 .fnPtr = @ptrCast(&nativeStopBluetoothScan),
451453 },
454 // Not an action: the other end of the SensorEventListener the holder owns.
455 .{
456 .name = "nativeMotionSample",
457 .signature = "(ZFFF)V",
458 .fnPtr = @ptrCast(&nativeMotionSample),
459 },
460 .{
461 .name = "nativeStartMotionUpdates",
462 .signature = "(Landroid/app/Activity;I)Z",
463 .fnPtr = @ptrCast(&nativeStartMotionUpdates),
464 },
465 .{
466 .name = "nativeStopMotionUpdates",
467 .signature = "(Landroid/app/Activity;)Z",
468 .fnPtr = @ptrCast(&nativeStopMotionUpdates),
469 },
452470};
453471
454472/// How binding went. A value rather than a log line, so every path is
@@ -1975,6 +1993,109 @@ fn nativeStopBluetoothScan(
19751993 return jni.JNI_TRUE;
19761994}
19771995
1996/// `nativeMotionSample(isAccelerometer, x, y, z)` — one sensor reading.
1997///
1998/// Every sample sends a complete event: the sensor that just reported and the
1999/// last value the other one gave, which is what the shim does and is why a
2000/// device with both sensors produces two events per cycle.
2001fn nativeMotionSample(
2002 env: jni.JNIEnv,
2003 _: jni.jobject,
2004 is_accelerometer: jni.jboolean,
2005 x: jni.jfloat,
2006 y: jni.jfloat,
2007 z: jni.jfloat,
2008) callconv(.c) void {
2009 const j = Jni.init(env);
2010 var arena = std.heap.ArenaAllocator.init(backing);
2011 defer arena.deinit();
2012 const allocator = arena.allocator();
2013
2014 motion.record(is_accelerometer != jni.JNI_FALSE, .{ x, y, z });
2015
2016 const acceleration = motion.lastAcceleration();
2017 const rotation = motion.lastRotation();
2018
2019 // Formatted by org.json, because these are the numbers a page reads and
2020 // `Float.toString` is not a format string — see `android_json_number`.
2021 var printed: [6][]const u8 = undefined;
2022 inline for (0..3) |i| {
2023 printed[i] = json_number.fromFloat(j, allocator, acceleration[i]) catch return;
2024 printed[i + 3] = json_number.fromFloat(j, allocator, rotation[i]) catch return;
2025 }
2026
2027 motion.announce(
2028 allocator,
2029 .{ printed[0], printed[1], printed[2] },
2030 .{ printed[3], printed[4], printed[5] },
2031 ) catch {};
2032}
2033
2034/// `nativeStartMotionUpdates(activity, intervalMs)`.
2035///
2036/// The delay constant is read here and handed across, so the shim's `when`
2037/// stays a Zig decision while the field it names stays the platform's number.
2038fn nativeStartMotionUpdates(
2039 env: jni.JNIEnv,
2040 _: jni.jobject,
2041 activity: jni.jobject,
2042 interval_ms: jni.jint,
2043) callconv(.c) jni.jboolean {
2044 const j = Jni.init(env);
2045
2046 const delay = sensorDelay(j, motion.delayFor(interval_ms)) catch |err| {
2047 std.log.warn("craft: startMotionUpdates fell through to the shim ({s})", .{@errorName(err)});
2048 return jni.JNI_FALSE;
2049 };
2050
2051 // The shim builds a new listener object, whose lastAccel and lastGyro
2052 // start null — so a restart forgets what the last one saw.
2053 motion.reset();
2054
2055 callHolderInt(j, "startMotionWatch", activity, delay) catch |err| {
2056 std.log.warn("craft: startMotionUpdates fell through to the shim ({s})", .{@errorName(err)});
2057 return jni.JNI_FALSE;
2058 };
2059 return jni.JNI_TRUE;
2060}
2061
2062/// `nativeStopMotionUpdates(activity)`.
2063fn nativeStopMotionUpdates(
2064 env: jni.JNIEnv,
2065 _: jni.jobject,
2066 activity: jni.jobject,
2067) callconv(.c) jni.jboolean {
2068 const j = Jni.init(env);
2069
2070 callHolderVoid(j, "stopMotionWatch", activity) catch |err| {
2071 std.log.warn("craft: stopMotionUpdates failed ({s})", .{@errorName(err)});
2072 return jni.JNI_FALSE;
2073 };
2074 return jni.JNI_TRUE;
2075}
2076
2077/// `SensorManager.<field>`.
2078fn sensorDelay(j: Jni, delay: motion.Delay) !jni.jint {
2079 try j.pushLocalFrame(4);
2080 defer _ = j.popLocalFrame(null);
2081
2082 const manager_cls = try j.findClass("android/hardware/SensorManager");
2083 return j.staticIntField(manager_cls, try j.staticFieldId(manager_cls, delay.field(), "I"));
2084}
2085
2086/// Call an `(Activity, int) -> void` static on the holder.
2087fn callHolderInt(j: Jni, comptime name: [:0]const u8, activity: jni.jobject, value: jni.jint) !void {
2088 try j.pushLocalFrame(8);
2089 defer _ = j.popLocalFrame(null);
2090
2091 const holder = try j.findClass(holder_class);
2092 try j.callStaticVoidMethodA(
2093 holder,
2094 try j.staticMethodId(holder, name, "(Landroid/app/Activity;I)V"),
2095 &.{ .{ .l = activity }, .{ .i = value } },
2096 );
2097}
2098
19782099// =============================================================================
19792100// Tests
19802101// =============================================================================
@@ -2086,7 +2207,7 @@ test "the registered natives name methods the Kotlin actually declares" {
20862207 // A descriptor is checked by the JVM at registration, so a wrong one fails
20872208 // at load rather than at call — but only if the *name* matches something.
20882209 // These two strings are the contract with CraftBridge.kt.
2089 try testing.expectEqual(@as(usize, 53), natives.len);
2210 try testing.expectEqual(@as(usize, 56), natives.len);
20902211 try testing.expectEqualStrings("nativeGetDeviceInfo", std.mem.span(natives[0].name));
20912212
20922213 // And the class they bind to is the fixed one, not the templated bridge.