also looking at this
feat(android): setKeepAwake through the trampoline — ratchet 70 → 69
#174
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.
| @@ -2577,6 +2577,11 @@ class CraftBridge( | ||
| 2577 | 2577 | |
| 2578 | 2578 | @JavascriptInterface |
| 2579 | 2579 | 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 | ||
| 2580 | 2585 | activity.runOnUiThread { |
| 2581 | 2586 | if (enabled) { |
| 2582 | 2587 | activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) |
| @@ -128,6 +128,7 @@ object CraftNative { | ||
| 128 | 128 | private external fun nativeRunTask(activity: Activity, token: Long) |
| 129 | 129 | private external fun nativeLockOrientation(activity: Activity, orientation: String): Boolean |
| 130 | 130 | private external fun nativeUnlockOrientation(activity: Activity): Boolean |
| 131 | private external fun nativeSetKeepAwake(activity: Activity, enabled: Boolean): Boolean | |
| 131 | 132 | |
| 132 | 133 | /** |
| 133 | 134 | * `getDeviceInfo`, or null to use the Kotlin implementation. |
| @@ -619,4 +620,13 @@ object CraftNative { | ||
| 619 | 620 | false |
| 620 | 621 | } |
| 621 | 622 | } |
| 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 | } | |
| 622 | 632 | } |
| @@ -473,8 +473,8 @@ pub fn build(b: *std.Build) void { | ||
| 473 | 473 | android_conformance_tests.root_module.addAnonymousImport("src/bridge_android_shortcuts.zig", .{ |
| 474 | 474 | .root_source_file = b.path("src/bridge_android_shortcuts.zig"), |
| 475 | 475 | }); |
| 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"), | |
| 478 | 478 | }); |
| 479 | 479 | const run_android_conformance_tests = b.addRunArtifact(android_conformance_tests); |
| 480 | 480 | |
| @@ -52,7 +52,7 @@ const shareditem = ; | ||
| 52 | 52 | const contacts = ; |
| 53 | 53 | const widgets = ; |
| 54 | 54 | const shortcuts = ; |
| 55 | const orientation = ; | |
| 55 | const screen = ; | |
| 56 | 56 | const main_thread = ; |
| 57 | 57 | |
| 58 | 58 | const Jni = jni.Jni; |
| @@ -342,6 +342,11 @@ const natives = [_]jni.JNINativeMethod{ | ||
| 342 | 342 | .signature = "(Landroid/app/Activity;)Z", |
| 343 | 343 | .fnPtr = @ptrCast(&nativeUnlockOrientation), |
| 344 | 344 | }, |
| 345 | .{ | |
| 346 | .name = "nativeSetKeepAwake", | |
| 347 | .signature = "(Landroid/app/Activity;Z)Z", | |
| 348 | .fnPtr = @ptrCast(&nativeSetKeepAwake), | |
| 349 | }, | |
| 345 | 350 | }; |
| 346 | 351 | |
| 347 | 352 | /// How binding went. A value rather than a log line, so every path is |
| @@ -1354,7 +1359,7 @@ fn nativeLockOrientation( | ||
| 1354 | 1359 | |
| 1355 | 1360 | const text = j.stringToUtf8(arena.allocator(), name) catch return jni.JNI_FALSE; |
| 1356 | 1361 | |
| 1357 | orientation.apply(j, activity, orientation.modeFor(text)) catch |err| { | |
| 1362 | screen.apply(j, activity, screen.modeFor(text)) catch |err| { | |
| 1358 | 1363 | std.log.warn("craft: lockOrientation fell through to the shim ({s})", .{@errorName(err)}); |
| 1359 | 1364 | return jni.JNI_FALSE; |
| 1360 | 1365 | }; |
| @@ -1369,13 +1374,33 @@ fn nativeUnlockOrientation( | ||
| 1369 | 1374 | ) callconv(.c) jni.jboolean { |
| 1370 | 1375 | const j = Jni.init(env); |
| 1371 | 1376 | |
| 1372 | orientation.apply(j, activity, .unspecified) catch |err| { | |
| 1377 | screen.apply(j, activity, .unspecified) catch |err| { | |
| 1373 | 1378 | std.log.warn("craft: unlockOrientation fell through to the shim ({s})", .{@errorName(err)}); |
| 1374 | 1379 | return jni.JNI_FALSE; |
| 1375 | 1380 | }; |
| 1376 | 1381 | return jni.JNI_TRUE; |
| 1377 | 1382 | } |
| 1378 | 1383 | |
| 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. | |
| 1389 | fn 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 | ||
| 1379 | 1404 | // ============================================================================= |
| 1380 | 1405 | // Tests |
| 1381 | 1406 | // ============================================================================= |
| @@ -1487,7 +1512,7 @@ test "the registered natives name methods the Kotlin actually declares" { | ||
| 1487 | 1512 | // A descriptor is checked by the JVM at registration, so a wrong one fails |
| 1488 | 1513 | // at load rather than at call — but only if the *name* matches something. |
| 1489 | 1514 | // 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); | |
| 1491 | 1516 | try testing.expectEqualStrings("nativeGetDeviceInfo", std.mem.span(natives[0].name)); |
| 1492 | 1517 | |
| 1493 | 1518 | // And the class they bind to is the fixed one, not the templated bridge. |
| @@ -48,6 +48,8 @@ const jobject = jni.jobject; | ||
| 48 | 48 | pub const Task = union(enum) { |
| 49 | 49 | /// `activity.setRequestedOrientation(mode)`. |
| 50 | 50 | set_requested_orientation: i32, |
| 51 | /// `activity.window.addFlags(flags)` or `clearFlags(flags)`. | |
| 52 | set_window_flags: struct { flags: i32, add: bool }, | |
| 51 | 53 | }; |
| 52 | 54 | |
| 53 | 55 | const slot_count = 32; |
| @@ -160,9 +162,36 @@ pub fn run(j: Jni, activity: jobject, token: u64) void { | ||
| 160 | 162 | const task = claim(token) orelse return; |
| 161 | 163 | switch (task) { |
| 162 | 164 | .set_requested_orientation => |mode| setRequestedOrientation(j, activity, mode) catch {}, |
| 165 | .set_window_flags => |request| setWindowFlags(j, activity, request.flags, request.add) catch {}, | |
| 163 | 166 | } |
| 164 | 167 | } |
| 165 | 168 | |
| 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. | |
| 173 | pub fn windowFlagMethod(add: bool) [:0]const u8 { | |
| 174 | return if (add) "addFlags" else "clearFlags"; | |
| 175 | } | |
| 176 | ||
| 177 | /// `activity.window.addFlags(flags)` / `clearFlags(flags)`. | |
| 178 | fn 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 | ||
| 166 | 195 | fn setRequestedOrientation(j: Jni, activity: jobject, mode: i32) !void { |
| 167 | 196 | try j.pushLocalFrame(4); |
| 168 | 197 | defer _ = j.popLocalFrame(null); |
| @@ -257,6 +286,22 @@ test "releasing a token a caller could not schedule frees its slot" { | ||
| 257 | 286 | } |
| 258 | 287 | } |
| 259 | 288 | |
| 289 | test "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 | ||
| 298 | test "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 | ||
| 260 | 305 | test "the holder class is the fixed one, not the templated bridge" { |
| 261 | 306 | // A `{{` here would mean the library had been made app-specific, which is |
| 262 | 307 | // the thing RegisterNatives exists to avoid. |
| @@ -1,8 +1,8 @@ | ||
| 1 | //! `lockOrientation` and `unlockOrientation` on Android. | |
| 1 | //! The screen: `lockOrientation`, `unlockOrientation` and `setKeepAwake`. | |
| 2 | 2 | //! |
| 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 { ... }`. | |
| 6 | 6 | //! |
| 7 | 7 | //! ## They answer by returning, not through the reply channel |
| 8 | 8 | //! |
| @@ -41,6 +41,7 @@ const jobject = jni.jobject; | ||
| 41 | 41 | pub const A = struct { |
| 42 | 42 | pub const lock_orientation = "lockOrientation"; |
| 43 | 43 | pub const unlock_orientation = "unlockOrientation"; |
| 44 | pub const set_keep_awake = "setKeepAwake"; | |
| 44 | 45 | }; |
| 45 | 46 | |
| 46 | 47 | /// Which `ActivityInfo` constant a page's string names. |
| @@ -85,6 +86,45 @@ pub fn apply(j: Jni, activity: jobject, mode: Mode) !void { | ||
| 85 | 86 | try main_thread.post(j, activity, .{ .set_requested_orientation = constant }, 0); |
| 86 | 87 | } |
| 87 | 88 | |
| 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. | |
| 108 | pub 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. | |
| 120 | pub 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 | ||
| 88 | 128 | // ============================================================================= |
| 89 | 129 | // Tests |
| 90 | 130 | // ============================================================================= |
| @@ -146,4 +186,5 @@ test "each mode names a real ActivityInfo field" { | ||
| 146 | 186 | test "the actions match the shim exactly" { |
| 147 | 187 | try testing.expectEqualStrings("lockOrientation", A.lock_orientation); |
| 148 | 188 | try testing.expectEqualStrings("unlockOrientation", A.unlock_orientation); |
| 189 | try testing.expectEqualStrings("setKeepAwake", A.set_keep_awake); | |
| 149 | 190 | } |
| @@ -53,7 +53,7 @@ const zig_sources = [_][]const u8{ | ||
| 53 | 53 | @embedFile("src/bridge_android_contacts.zig"), |
| 54 | 54 | @embedFile("src/bridge_android_widgets.zig"), |
| 55 | 55 | @embedFile("src/bridge_android_shortcuts.zig"), |
| 56 | @embedFile("src/bridge_android_orientation.zig"), | |
| 56 | @embedFile("src/bridge_android_screen.zig"), | |
| 57 | 57 | }; |
| 58 | 58 | |
| 59 | 59 | /// How many spec actions Zig does not serve yet. |
| @@ -80,8 +80,9 @@ const zig_sources = [_][]const u8{ | ||
| 80 | 80 | /// API 25 and are ported disagreeing; 72 with the immediate half of |
| 81 | 81 | /// scheduleNotification, whose delayed half needs a Runnable; 70 with the |
| 82 | 82 | /// 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. | |
| 84 | const 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. | |
| 85 | const max_not_yet_migrated: usize = 69; | |
| 85 | 86 | |
| 86 | 87 | /// Every `@JavascriptInterface fun <name>(` in the Kotlin bridge. |
| 87 | 88 | /// |