ReviewOS

also looking at this

craft-native/craft

feat(android): setKeepAwake through the trampoline — ratchet 70 → 69

#174
Merged glennmichael123 wants to merge feat/android-keep-awake into main
7 files +140 -13

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/android/templates/CraftBridge.kt.templatemodified+5-0
Changes to packages/android/templates/CraftBridge.kt.template
@@ -2577,6 +2577,11 @@ class CraftBridge(
25772577
25782578 @JavascriptInterface
25792579 fun setKeepAwake(enabled: Boolean): Boolean {
2580 // Zig queues the same window call on the same looper. isKeepingAwake
2581 // below is written here and read nowhere, so the Kotlin body not
2582 // running leaves nothing stale — unlike the flashlight pair.
2583 if (CraftNative.setKeepAwake(activity, enabled)) return true
2584
25802585 activity.runOnUiThread {
25812586 if (enabled) {
25822587 activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
packages/android/templates/CraftNative.kt.templatemodified+10-0
Changes to packages/android/templates/CraftNative.kt.template
@@ -128,6 +128,7 @@ object CraftNative {
128128 private external fun nativeRunTask(activity: Activity, token: Long)
129129 private external fun nativeLockOrientation(activity: Activity, orientation: String): Boolean
130130 private external fun nativeUnlockOrientation(activity: Activity): Boolean
131 private external fun nativeSetKeepAwake(activity: Activity, enabled: Boolean): Boolean
131132
132133 /**
133134 * `getDeviceInfo`, or null to use the Kotlin implementation.
@@ -619,4 +620,13 @@ object CraftNative {
619620 false
620621 }
621622 }
623
624 fun setKeepAwake(activity: Activity, enabled: Boolean): Boolean {
625 if (!isAvailable) return false
626 return try {
627 nativeSetKeepAwake(activity, enabled)
628 } catch (e: UnsatisfiedLinkError) {
629 false
630 }
631 }
622632}
packages/zig/build.zigmodifiedrenamed throughout+2-2
Changes to packages/zig/build.zig
@@ -473,8 +473,8 @@ pub fn build(b: *std.Build) void {
473473 android_conformance_tests.root_module.addAnonymousImport("src/bridge_android_shortcuts.zig", .{
474474 .root_source_file = b.path("src/bridge_android_shortcuts.zig"),
475475 });
476 android_conformance_tests.root_module.addAnonymousImport("src/bridge_android_orientation.zig", .{
477 .root_source_file = b.path("src/bridge_android_orientation.zig"),
476 android_conformance_tests.root_module.addAnonymousImport("src/bridge_android_screen.zig", .{
477 .root_source_file = b.path("src/bridge_android_screen.zig"),
478478 });
479479 const run_android_conformance_tests = b.addRunArtifact(android_conformance_tests);
480480
packages/zig/src/android_dispatch.zigmodified+29-4
Changes to packages/zig/src/android_dispatch.zig
@@ -52,7 +52,7 @@ const shareditem = ;
5252const contacts = ;
5353const widgets = ;
5454const shortcuts = ;
55const orientation = ;
55const screen = ;
5656const main_thread = ;
5757
5858const Jni = jni.Jni;
@@ -342,6 +342,11 @@ const natives = [_]jni.JNINativeMethod{
342342 .signature = "(Landroid/app/Activity;)Z",
343343 .fnPtr = @ptrCast(&nativeUnlockOrientation),
344344 },
345 .{
346 .name = "nativeSetKeepAwake",
347 .signature = "(Landroid/app/Activity;Z)Z",
348 .fnPtr = @ptrCast(&nativeSetKeepAwake),
349 },
345350};
346351
347352/// How binding went. A value rather than a log line, so every path is
@@ -1354,7 +1359,7 @@ fn nativeLockOrientation(
13541359
13551360 const text = j.stringToUtf8(arena.allocator(), name) catch return jni.JNI_FALSE;
13561361
1357 orientation.apply(j, activity, orientation.modeFor(text)) catch |err| {
1362 screen.apply(j, activity, screen.modeFor(text)) catch |err| {
13581363 std.log.warn("craft: lockOrientation fell through to the shim ({s})", .{@errorName(err)});
13591364 return jni.JNI_FALSE;
13601365 };
@@ -1369,13 +1374,33 @@ fn nativeUnlockOrientation(
13691374) callconv(.c) jni.jboolean {
13701375 const j = Jni.init(env);
13711376
1372 orientation.apply(j, activity, .unspecified) catch |err| {
1377 screen.apply(j, activity, .unspecified) catch |err| {
13731378 std.log.warn("craft: unlockOrientation fell through to the shim ({s})", .{@errorName(err)});
13741379 return jni.JNI_FALSE;
13751380 };
13761381 return jni.JNI_TRUE;
13771382}
13781383
1384/// `nativeSetKeepAwake(activity, enabled)`.
1385///
1386/// Safe to serve where the flashlight pair is not: the shim's block ends
1387/// `isKeepingAwake = enabled`, and nothing in `CraftBridge.kt` ever reads that
1388/// field — so the Kotlin body not running leaves nothing stale behind.
1389fn nativeSetKeepAwake(
1390 env: jni.JNIEnv,
1391 _: jni.jobject,
1392 activity: jni.jobject,
1393 enabled: jni.jboolean,
1394) callconv(.c) jni.jboolean {
1395 const j = Jni.init(env);
1396
1397 screen.keepAwake(j, activity, enabled != jni.JNI_FALSE) catch |err| {
1398 std.log.warn("craft: setKeepAwake fell through to the shim ({s})", .{@errorName(err)});
1399 return jni.JNI_FALSE;
1400 };
1401 return jni.JNI_TRUE;
1402}
1403
13791404// =============================================================================
13801405// Tests
13811406// =============================================================================
@@ -1487,7 +1512,7 @@ test "the registered natives name methods the Kotlin actually declares" {
14871512 // A descriptor is checked by the JVM at registration, so a wrong one fails
14881513 // at load rather than at call — but only if the *name* matches something.
14891514 // These two strings are the contract with CraftBridge.kt.
1490 try testing.expectEqual(@as(usize, 34), natives.len);
1515 try testing.expectEqual(@as(usize, 35), natives.len);
14911516 try testing.expectEqualStrings("nativeGetDeviceInfo", std.mem.span(natives[0].name));
14921517
14931518 // And the class they bind to is the fixed one, not the templated bridge.
packages/zig/src/android_main_thread.zigmodified+45-0
Changes to packages/zig/src/android_main_thread.zig
@@ -48,6 +48,8 @@ const jobject = jni.jobject;
4848pub const Task = union(enum) {
4949 /// `activity.setRequestedOrientation(mode)`.
5050 set_requested_orientation: i32,
51 /// `activity.window.addFlags(flags)` or `clearFlags(flags)`.
52 set_window_flags: struct { flags: i32, add: bool },
5153};
5254
5355const slot_count = 32;
@@ -160,9 +162,36 @@ pub fn run(j: Jni, activity: jobject, token: u64) void {
160162 const task = claim(token) orelse return;
161163 switch (task) {
162164 .set_requested_orientation => |mode| setRequestedOrientation(j, activity, mode) catch {},
165 .set_window_flags => |request| setWindowFlags(j, activity, request.flags, request.add) catch {},
163166 }
164167}
165168
169/// Which `Window` method a request names.
170///
171/// Split out because it is the one place the two directions can be swapped,
172/// and swapping them turns the screen timeout back on while reporting success.
173pub fn windowFlagMethod(add: bool) [:0]const u8 {
174 return if (add) "addFlags" else "clearFlags";
175}
176
177/// `activity.window.addFlags(flags)` / `clearFlags(flags)`.
178fn setWindowFlags(j: Jni, activity: jobject, flags: i32, add: bool) !void {
179 try j.pushLocalFrame(8);
180 defer _ = j.popLocalFrame(null);
181
182 const window = try j.callObjectMethod(
183 activity,
184 try j.methodId(try j.objectClass(activity), "getWindow", "()Landroid/view/Window;"),
185 );
186
187 const window_cls = try j.objectClass(window);
188 try j.callVoidMethodA(
189 window,
190 try j.methodId(window_cls, windowFlagMethod(add), "(I)V"),
191 &.{.{ .i = flags }},
192 );
193}
194
166195fn setRequestedOrientation(j: Jni, activity: jobject, mode: i32) !void {
167196 try j.pushLocalFrame(4);
168197 defer _ = j.popLocalFrame(null);
@@ -257,6 +286,22 @@ test "releasing a token a caller could not schedule frees its slot" {
257286 }
258287}
259288
289test "a window-flag task carries both halves of the request" {
290 clearTable();
291
292 const token = try reserve(.{ .set_window_flags = .{ .flags = 128, .add = true } });
293 const claimed = claim(token).?;
294 try testing.expectEqual(@as(i32, 128), claimed.set_window_flags.flags);
295 try testing.expect(claimed.set_window_flags.add);
296}
297
298test "adding and clearing name different Window methods" {
299 // The swap that would leave the screen timing out while every layer
300 // reported success.
301 try testing.expectEqualStrings("addFlags", windowFlagMethod(true));
302 try testing.expectEqualStrings("clearFlags", windowFlagMethod(false));
303}
304
260305test "the holder class is the fixed one, not the templated bridge" {
261306 // A `{{` here would mean the library had been made app-specific, which is
262307 // the thing RegisterNatives exists to avoid.
packages/zig/src/bridge_android_orientation.zig packages/zig/src/bridge_android_screen.zigrenamed+45-4
Changes to packages/zig/src/bridge_android_screen.zig
@@ -1,8 +1,8 @@
1//! `lockOrientation` and `unlockOrientation` on Android.
1//! The screen: `lockOrientation`, `unlockOrientation` and `setKeepAwake`.
22//!
3//! The first pair to go through `android_main_thread`: both do their whole job
4//! on the main looper and neither touches state the shim keeps, which is what
5//! makes them the right thing to prove the trampoline with.
3//! Everything here goes through `android_main_thread`, because everything here
4//! touches the Activity's window and the shim wraps each one in
5//! `runOnUiThread { ... }`.
66//!
77//! ## They answer by returning, not through the reply channel
88//!
@@ -41,6 +41,7 @@ const jobject = jni.jobject;
4141pub const A = struct {
4242 pub const lock_orientation = "lockOrientation";
4343 pub const unlock_orientation = "unlockOrientation";
44 pub const set_keep_awake = "setKeepAwake";
4445};
4546
4647/// Which `ActivityInfo` constant a page's string names.
@@ -85,6 +86,45 @@ pub fn apply(j: Jni, activity: jobject, mode: Mode) !void {
8586 try main_thread.post(j, activity, .{ .set_requested_orientation = constant }, 0);
8687}
8788
89// =============================================================================
90// setKeepAwake
91// =============================================================================
92//
93// ## The field it writes is read by nothing
94//
95// The shim's block ends `isKeepingAwake = enabled`, and that field is written
96// there and read nowhere else in `CraftBridge.kt`. So unlike the flashlight
97// pair — where `toggleFlashlight` reads what `setFlashlight` wrote, and Zig
98// serving one would leave the other inverting a stale value — there is no
99// state to keep in step here. Worth saying, because the two look identical
100// from the outside and only one of them is safe to take.
101
102/// `WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON`.
103///
104/// Read through JNI rather than written down. A wrong column name throws and a
105/// wrong flag does not: the window would quietly get `FLAG_DIM_BEHIND` or
106/// `FLAG_BLUR_BEHIND` instead, and the screen would keep timing out while
107/// everything reported success.
108pub fn keepScreenOnFlag(j: Jni) !i32 {
109 try j.pushLocalFrame(4);
110 defer _ = j.popLocalFrame(null);
111
112 const params_cls = try j.findClass("android/view/WindowManager$LayoutParams");
113 return j.staticIntField(
114 params_cls,
115 try j.staticFieldId(params_cls, "FLAG_KEEP_SCREEN_ON", "I"),
116 );
117}
118
119/// Queue `window.addFlags(...)` or `window.clearFlags(...)` for the main thread.
120pub fn keepAwake(j: Jni, activity: jobject, enabled: bool) !void {
121 const flag = try keepScreenOnFlag(j);
122 try main_thread.post(j, activity, .{ .set_window_flags = .{
123 .flags = flag,
124 .add = enabled,
125 } }, 0);
126}
127
88128// =============================================================================
89129// Tests
90130// =============================================================================
@@ -146,4 +186,5 @@ test "each mode names a real ActivityInfo field" {
146186test "the actions match the shim exactly" {
147187 try testing.expectEqualStrings("lockOrientation", A.lock_orientation);
148188 try testing.expectEqualStrings("unlockOrientation", A.unlock_orientation);
189 try testing.expectEqualStrings("setKeepAwake", A.set_keep_awake);
149190}
packages/zig/test/android_conformance_test.zigmodified+4-3
Changes to packages/zig/test/android_conformance_test.zig
@@ -53,7 +53,7 @@ const zig_sources = [_][]const u8{
5353 @embedFile("src/bridge_android_contacts.zig"),
5454 @embedFile("src/bridge_android_widgets.zig"),
5555 @embedFile("src/bridge_android_shortcuts.zig"),
56 @embedFile("src/bridge_android_orientation.zig"),
56 @embedFile("src/bridge_android_screen.zig"),
5757};
5858
5959/// How many spec actions Zig does not serve yet.
@@ -80,8 +80,9 @@ const zig_sources = [_][]const u8{
8080/// API 25 and are ported disagreeing; 72 with the immediate half of
8181/// scheduleNotification, whose delayed half needs a Runnable; 70 with the
8282/// orientation pair, the first to run on the main looper — through a Runnable
83/// the Kotlin holder owns and a token that says what to do.
84const max_not_yet_migrated: usize = 70;
83/// the Kotlin holder owns and a token that says what to do; 69 with
84/// setKeepAwake, whose Kotlin field turns out to be written and never read.
85const max_not_yet_migrated: usize = 69;
8586
8687/// Every `@JavascriptInterface fun <name>(` in the Kotlin bridge.
8788///