From dffb364089f08c36229213040495b892e5fd288c Mon Sep 17 00:00:00 2001 From: GenericJam Date: Sat, 24 Oct 2026 00:22:16 -0600 Subject: [PATCH] Android NIF builds on both Zig pins; pin Keyboard.dismiss and Android blur in tests The 0.9.26 release gate found that an app keeping mob_dev 0.7.30 (Zig 0.17.0-dev.269) could resolve this mob, whose std.mem.printSentinel the nightly lacks. Format the post-mortem marker paths through a local printZ on std.fmt.bufPrint, which both toolchains have. Compiled for aarch64 Android on both. Mob.Keyboard.dismiss/1's test now runs the call path against a fake NIF, and mob_send_blur is pinned to sendIdentityEvent. --- CHANGELOG.md | 15 +++++++++------ android/jni/mob_nif.zig | 20 ++++++++++++++++---- lib/mob/keyboard.ex | 9 +++++++-- test/mob/keyboard_test.exs | 18 ++++++++++++++++++ test/mob/native_event_handle_test.exs | 9 +++++++++ 5 files changed, 59 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dd076a2..2aa3dded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,12 +37,15 @@ Full module documentation: [hexdocs.pm/mob](https://hexdocs.pm/mob). mob_new, which move with it. ### Fixed -- **Android Zig sources compile on Zig 0.17.0.** `std.fmt.bufPrintZ` was - removed, so the post-mortem marker paths in `mob_nif.zig` use - `std.mem.printSentinel`, and `Allocator.dupeZ` was removed, so - `mob_stored_queue.zig`'s tests use `dupeSentinel`. CI now sets up the - pinned Zig from `.tool-versions`, compiles `mob_nif.zig` for Android and - runs every Android Zig test file. +- **Android Zig sources compile on Zig 0.17.0, and still on the old + nightly.** `std.fmt.bufPrintZ` was removed in 0.17.0, and its replacement + `std.mem.printSentinel` is missing in `0.17.0-dev.269`, so the post-mortem + marker paths in `mob_nif.zig` format through a local `printZ` on + `std.fmt.bufPrint`, which both have. An app that updates mob but keeps + mob_dev 0.7.30 (which pins the nightly) still builds for Android. + `Allocator.dupeZ` was removed, so `mob_stored_queue.zig`'s tests use + `dupeSentinel`. CI now sets up the pinned Zig from `.tool-versions`, + compiles `mob_nif.zig` for Android and runs every Android Zig test file. - **iOS sheets take their identity from `:id` (#115).** A sheet's presentation state was tied to its place in the tree, and a container's child keying (MOB-127) does not reach every sheet, such as one that is the diff --git a/android/jni/mob_nif.zig b/android/jni/mob_nif.zig index 47dac2ad..077eed73 100644 --- a/android/jni/mob_nif.zig +++ b/android/jni/mob_nif.zig @@ -4915,6 +4915,18 @@ fn filesDirPath(env: *jni.JNIEnv, context: jni.JObject) ?[]const u8 { const MARKER_FILENAME = "mob_post_mortem_appexit_marker.txt"; +// Formats into `buf` and NUL-terminates. Built on std.fmt.bufPrint because it +// exists in both Zig toolchains apps may build with: the 0.17.0 stable mob_dev +// pins now and the 0.17.0-dev.269 nightly older mob_dev pins. The sentinel +// helpers differ between them (bufPrintZ is gone in 0.17.0, printSentinel is +// missing in the nightly), and an app can resolve this mob with either. +fn printZ(buf: []u8, comptime fmt: []const u8, args: anytype) ?[:0]u8 { + if (buf.len == 0) return null; + const s = std.fmt.bufPrint(buf[0 .. buf.len - 1], fmt, args) catch return null; + buf[s.len] = 0; + return buf[0..s.len :0]; +} + // Read the persisted last-seen timestamp. Returns 0 on first-ever // sweep (no marker file), on parse failure, or on any I/O error — // treating any of those as "we have not seen anything yet" is the @@ -4922,7 +4934,7 @@ const MARKER_FILENAME = "mob_post_mortem_appexit_marker.txt"; // Registry then dedups within the current BEAM session. fn readMarker(dir: []const u8) i64 { var path_buf: [640]u8 = @splat(0); - const path = std.mem.printSentinel(&path_buf, "{s}/{s}", .{ dir, MARKER_FILENAME }, 0) catch return 0; + const path = printZ(&path_buf, "{s}/{s}", .{ dir, MARKER_FILENAME }) orelse return 0; const f = jni.fopen(path.ptr, "rb") orelse return 0; defer _ = jni.fclose(f); @@ -4952,7 +4964,7 @@ fn readMarker(dir: []const u8) i64 { // console on a permanently-failing path. fn writeMarker(dir: []const u8, ts_ms: i64) void { var path_buf: [640]u8 = @splat(0); - const path = std.mem.printSentinel(&path_buf, "{s}/{s}", .{ dir, MARKER_FILENAME }, 0) catch { + const path = printZ(&path_buf, "{s}/{s}", .{ dir, MARKER_FILENAME }) orelse { logMarkerFailureOnce(.path_too_long); return; }; @@ -4965,7 +4977,7 @@ fn writeMarker(dir: []const u8, ts_ms: i64) void { defer _ = jni.close(fd); var out_buf: [32]u8 = @splat(0); - const out = std.mem.printSentinel(&out_buf, "{d}\n", .{ts_ms}, 0) catch { + const out = printZ(&out_buf, "{d}\n", .{ts_ms}) orelse { logMarkerFailureOnce(.format_failed); return; }; @@ -5001,7 +5013,7 @@ fn logMarkerFailureOnce(kind: MarkerFailure) void { }; var buf: [128]u8 = @splat(0); - const msg = std.mem.printSentinel(&buf, "mob_post_mortem: marker write failed ({s}); cross-boot dedup will re-emit until this clears", .{reason}, 0) catch return; + const msg = printZ(&buf, "mob_post_mortem: marker write failed ({s}); cross-boot dedup will re-emit until this clears", .{reason}) orelse return; jni.logWrite(jni.ANDROID_LOG_ERROR, "MobNIF", "{s}", .{msg}); } diff --git a/lib/mob/keyboard.ex b/lib/mob/keyboard.ex index 4daf4983..0a056cf4 100644 --- a/lib/mob/keyboard.ex +++ b/lib/mob/keyboard.ex @@ -37,11 +37,16 @@ defmodule Mob.Keyboard do `handle_info` / `handle_event` return. Nothing focused is not an error. """ @spec dismiss(Mob.Socket.t()) :: Mob.Socket.t() - def dismiss(socket) do + def dismiss(socket), do: __dismiss__(socket, :mob_nif) + + @doc false + # The NIF module is a parameter so a test can stand in for :mob_nif. + @spec __dismiss__(Mob.Socket.t(), module()) :: Mob.Socket.t() + def __dismiss__(socket, nif) do # Guarded, unlike Mob.Haptic: this sits in handlers Mob.ScreenCase drives # on the host, where no NIF library loads and the call raises — a screen # that chains it has to stay testable there. - if function_exported?(:mob_nif, :dismiss_keyboard, 0), do: :mob_nif.dismiss_keyboard() + if function_exported?(nif, :dismiss_keyboard, 0), do: nif.dismiss_keyboard() socket end end diff --git a/test/mob/keyboard_test.exs b/test/mob/keyboard_test.exs index 941145d1..e9a08468 100644 --- a/test/mob/keyboard_test.exs +++ b/test/mob/keyboard_test.exs @@ -17,4 +17,22 @@ defmodule Mob.KeyboardTest do socket = Mob.Socket.new(Screen, platform: :no_render) assert Mob.Keyboard.dismiss(socket) == socket end + + defmodule FakeNif do + @moduledoc false + # Stands in for :mob_nif: reports the call to the test that set itself up. + def dismiss_keyboard do + send(:persistent_term.get({__MODULE__, :test}), :dismiss_keyboard) + :ok + end + end + + test "dismiss takes the keyboard down through the NIF and returns the socket" do + :persistent_term.put({FakeNif, :test}, self()) + on_exit(fn -> :persistent_term.erase({FakeNif, :test}) end) + socket = Mob.Socket.new(Screen, platform: :no_render) + + assert Mob.Keyboard.__dismiss__(socket, FakeNif) == socket + assert_received :dismiss_keyboard + end end diff --git a/test/mob/native_event_handle_test.exs b/test/mob/native_event_handle_test.exs index a3a45a9a..7095b477 100644 --- a/test/mob/native_event_handle_test.exs +++ b/test/mob/native_event_handle_test.exs @@ -201,6 +201,15 @@ defmodule Mob.NativeEventHandleTest do assert ios_dismiss =~ "mob_send_identity_event(handle, \"dismiss\")" end + # mob#173: Mob.Keyboard.dismiss/1 clears focus in the same handler that may + # re-render, so the blur lands after the new generation committed. Strict + # resolution dropped 6 of 8 of them on a Moto G. + test "Android blur resolves by identity, so a re-render does not drop it" do + [_, android_blur] = String.split(@android_source, "pub export fn mob_send_blur(", parts: 2) + [android_blur, _] = String.split(android_blur, "}", parts: 2) + assert android_blur =~ "sendIdentityEvent(handle, \"blur\")" + end + test "plain taps stay strict; press-node taps and press pairs survive a re-render" do # A plain tap resolves generation-strictly: a stale positional tag such as # Mob.List's {:select, id, index} must be dropped, not routed to whatever