Skip to content

Releases: GenericJam/mob

0.6.23

29 May 23:37

Choose a tag to compare

Added

  • Element positions without a screenshot. element_frames/0 NIF surfaced as Mob.Test.element_frames/1 (%{id => {x,y,w,h}}), frame/2, and tap_id/2 (drive by id at real coordinates). Any rendered node given an :id reports its live on-screen frame (logical points iOS / dp Android) to a registry the agent reads over dist — a compact structured map instead of image bytes, with no accessibility activation. The renderer also sets the :id as the element's accessibility identifier (iOS accessibilityIdentifier, Android Compose testTag), so the same tags are visible to XCUITest/Espresso. Opt-in per element: untagged nodes cost nothing (the tracking modifier only attaches when an :id is present). iOS records the full element frame via a GeometryReader background; Android via Modifier.onGloballyPositioned. Verified on iOS sim, Android device, and a physical iPhone. The Android Kotlin side lives in the mob_new MobBridge.kt.eex template.
  • In-process screenshot + scroll control over dist (no adb/xcrun). Three test-harness NIFs (screenshot/3, scroll_info/1, scroll_to/3) surfaced as Mob.Test.screenshot/2, scroll_info/2, scroll_to/4, and screenshot_tour/3. A remotely-connected agent gets pixels and deterministic scroll entirely over Erlang distribution — the capability Sloppy Joe and WireTap need to drive a device an agent can only reach over dist. Capture is in-process (iOS UIGraphicsImageRenderer + drawViewHierarchy; Android PixelCopy against the activity window). Scroll views are addressed by their :id prop; scroll_info reports kind: :pixel (iOS UIScrollView, Android verticalScroll) or :index (Android LazyColumn, where y is an item index and viewport is the visible-item count). Captures the app's own surface only — FLAG_SECURE/secure fields render blank, and a backgrounded app returns {:error, :no_window}. The Android Kotlin side (screenshot/scrollInfo/scrollTo) lives in the mob_new MobBridge.kt.eex template; existing apps pick it up on regeneration. Debug-only (iOS #if !MOB_RELEASE). See decisions/2026-05-29-bridge-nif-screenshot-scroll.md.

Changed

  • Mob.Bt extracted to standalone mob_bluetooth plugin. See plugin_extraction_plan.md Wave 1. Session A moved the Elixir wrappers (Mob.Bt, Mob.Bt.Hfp, Mob.Bt.Hid, Mob.Bt.Spp) out of core into a separate repo as MobBluetooth.*; the Zig NIF (android/jni/mob_nif.zig) and the iOS stubs (ios/mob_nif.m) stay here until Session B promotes the plugin to tier-1. Apps that used Mob.Bt.* should add {:mob_bluetooth, path: "..."} and rename their references to MobBluetooth.* — there is intentionally no compatibility shim.

OTP pre-built runtime 7d46fdd4

29 May 08:49

Choose a tag to compare

Pre-built OTP for Android (aarch64 + arm32), iOS simulator (aarch64-apple-iossimulator), and iOS device (aarch64-apple-ios). OTP source commit: 7d46fdd4.

0.6.22

28 May 19:40

Choose a tag to compare

Added

  • Mob.Certs — load CA certificates from a PEM bundle into Erlang's :public_key cacert store. Android's system trust store lives behind a Java API that :public_key.cacerts_load/0 (no-arg) can't reach, so the first TLS call from Req / Mint / Finch crashes with no_cacerts_found (or FunctionClauseError in some OTP versions). Apps bundle a PEM (conventional source: copy castore's cacerts.pem into priv/ at build time) and call Mob.Certs.load_cacerts!(Application.app_dir(:my_app, "priv/cacerts.pem")) once at boot. iOS and the Android emulator aren't affected; calling unconditionally is harmless there. Verified end-to-end on a Moto G Power 5G 2024 (Android 14): Mix.install([{:req, "~> 0.5"}]) then Req.get!("https://geocoding-api.open-meteo.com/v1/search?name=Vancouver") returns 200.
  • mob_beam.zig exports MOB_NATIVE_LIB_DIR before BEAM start — the absolute path of the app's nativeLibraryDir, which the APK install hash makes unpredictable at compile time. Apps that bundle runtime binaries (escript, rebar3, etc.) as lib*.so need this to set MIX_REBAR3 and locate the bundled escripts.
  • Optional ERTS-extras symlinks (escript / erlexec / erl / beam.smp) in mob_beam.zig. Silent-skips when the lib isn't in nativeLibDir, so non-opting-in apps see no behaviour change. Apps that drop lib<name>.so into android/app/src/main/jniLibs/<abi>/ get a working BINDIR/<name> — enough for runtime Mix.install of rebar3-built deps (telemetry, jose, jiffy, …) to bootstrap a fresh VM. erl and erlexec both target the same liberlexec.so because they are the same binary (erlexec doesn't switch on argv[0]).

Changed

  • extra_applications: [:logger, :public_key] — Elixir 1.19+ strips unused OTP applications from the code path; Mob.Certs calls :public_key.cacerts_load/1 at runtime, so its .beam must be in the path even though mob doesn't start :public_key itself.

Fixed

  • mix.exs — collapsed duplicate before_closing_body_tag/1 clauses introduced in 0.6.20. The mermaid clause's _ catchall shadowed an older language-elixir highlighter clause, leaving it as dead code (and emitting compile warnings). The unified clause emits both scripts; the duplicate docs/0 keyword entry was removed.

Docs

  • common_fixes.md — new section documenting the Android cacerts symptom (no_cacerts_found / FunctionClauseError) and the load-PEM-at-boot fix; also the bundled-OTP-extras pattern (wrapper script, rebar3 module-name derivation, $ROOTDIR/bin/*.boot materialization) for apps that opt into runtime rebar3.

0.6.21

28 May 15:01

Choose a tag to compare

Added

  • Mob.DNS.resolve/1 now works on Android. nif_resolve_ipv4 (android/jni/mob_nif.zig) calls Bionic's getaddrinfo in-process and seeds :inet_db's :file table, mirroring the iOS NIF added in #32. Physical Android devices return :nxdomain from BEAM's default DNS path (forking inet_gethost as a port program) even when the same app's in-process HTTPS stack resolves the hostname fine — the emulator masks this. Verified end-to-end on a Moto G Power 5G 2024 (Android 14): Mob.DNS.resolve("repo.hex.pm") returns the right IP, :inet.getaddr/2 then succeeds via the seeded entry, and Mix.install([{:dep, "~> ..."}]) from a notebook setup cell resolves, fetches, and compiles on-device. Bionic addrinfo / sockaddr_in / getaddrinfo / freeaddrinfo / EAI_* bindings added to android/jni/mob_zig.zig. Suspected root cause is libnetd_client.so's netd routing not surviving execve; the NIF sidesteps it by running in the app's own process.

Changed

  • Mob.DNS moduledoc — dropped the "Android isn't affected" claim. Added a background-app caveat: Android App Standby blocks all outbound network from a backgrounded mob app (TCP-by-IP, not just DNS — surfaces as :closed / :timeout on any socket attempt). Fix is a foreground service or keep the app foregrounded; not a mob bug.

Docs

  • common_fixes.md — new section documenting the :nxdomain symptom on physical Android, the foreground-app caveat, and the fix.

0.6.20

27 May 15:17

Choose a tag to compare

Full Changelog: 0.6.19...0.6.20

0.6.19

27 May 13:57

Choose a tag to compare

What's Changed

New Contributors

Full Changelog: 0.6.18...0.6.19

0.6.18

21 May 10:07

Choose a tag to compare

Changed

  • RUSTLER_NIF_LIB_PATHRUSTLER_BEAM_LIBRARY_PATH in mob_beam.zig's host setenv block. Matches the env var name filmor chose for the alternative upstream rustler PR (rusterlium/rustler#733), which is what'll land upstream instead of our #726. End-to-end tested on physical arm64 Android with filmor's branch: Mob sets the env var → rustler reads it → Rust NIF resolves and executes. Mob users on rustler 0.37 Hex release (no patch) see no change; users on the GenericJam fork OR on whatever rustler version eventually ships #733 get matching behaviour.

0.6.17

21 May 07:52

Choose a tag to compare

Added

  • Mob.Audio.play_at/4 — sample-accurate scheduled audio playback. Takes an absolute local wall-clock target (System.system_time(:millisecond) ms-since-epoch) and hands it to the audio hardware clock for firing, rather than waking the BEAM via Process.send_after. The hardware-clock path eliminates timer-wheel + scheduler jitter from the end-to-end sync error, leaving per-device first-sample latency (~30–80 ms, calibratable) as the dominant remaining term. iOS only in this release; Android still falls through to the existing MediaPlayer path (port to AAudio is pending).
  • iOS: nif_audio_play_at(Path, OptsJson, AtWallMs) backed by a dedicated AVAudioEngine + AVAudioPlayerNode. The wall-time target is converted to an AVAudioTime hostTime via mach_absolute_time + mach_timebase_info, then handed to -[AVAudioPlayerNode scheduleBuffer:atTime:options:completionHandler:]. Past targets schedule ASAP. Multiple play_at calls accumulate on the player's timeline — use audio_stop_playback to flush.
  • audio_set_volume and audio_stop_playback now also reach the scheduled-engine player so cross-API mixing behaves sanely.

Use case

  • Distributed orchestra / multi-device musical performance where every phone must start the same sample at the same wall-clock instant. Pair with an NTP-style server-clock-sync helper on the caller side; this API takes the converted local-clock target.

0.6.16

21 May 02:13

Choose a tag to compare

Added

  • mob_beam.zig exports RUSTLER_NIF_LIB_PATH before BEAM start. Calls dladdr(&mob_start_beam) to discover the absolute path of the host .so (e.g. lib<app>.so) and setenv()s it as RUSTLER_NIF_LIB_PATH. Pairs with the matching upstream rustler change (rusterlium/rustler#726): rustler's DlsymNifFiller::new() on Android reads the env var first, falls back to its existing dladdr-self probe when unset. End result: rustler-based Rust NIFs statically linked into Mob's main .so now resolve enif_* symbols correctly on Bionic without any per-app patching. Existing rustler users on Android who don't run inside Mob see no change — the dladdr fallback covers them.
  • mob_zig.zig exposes dladdr + DlInfo to other Zig consumers under jni.dladdr / jni.DlInfo. Hand-declared to match the libc/Bionic surface; same hand-declared FFI policy as the rest of mob_zig.zig (we don't use @cImport here).

Notes

  • The setenv runs unconditionally — even apps that don't ship a rustler NIF get the env var set. Harmless. The env var only affects rustler's own startup logic when a rustler-built NIF loads.
  • Verified end-to-end on a physical arm64 Android device (moto g power 2021): host sets path → rustler reads env var → dlopen(path, RTLD_NOW | RTLD_NOLOAD)dlsym all enif_* exports → Rust NIF greet/0 executes and returns "Hello from Rust!" to BEAM.

0.6.15

20 May 17:39

Choose a tag to compare

Added

  • text_field now accepts a secure: true prop. iOS renders the field
    as a SwiftUI SecureField (masked input) instead of the plain
    TextField. The prop flows through the existing renderer
    passthrough; cleartext still reaches the BEAM via on_change so apps
    can hash/store the value as normal. Android consumes the same prop
    via PasswordVisualTransformation once mob_new's MobBridge.kt.eex
    template is updated in a companion PR — until then the prop is a
    graceful no-op on Android (renders as a regular field), no breakage.

    Reveal-toggle ("eye" button) is intentionally deferred — its
    interaction with SwiftUI focus retention requires a ZStack-and-opacity
    rebuild of MobTextField and warrants its own change.

Fixed

  • iOS: Mob.App.start/0 now switches :inet_db to file-only lookup and seeds localhost before any user code runs — BEAM's default :native lookup tries to execve the inet_gethost port program, which the iOS sandbox refuses, crashing the first Node.connect / :erpc.call / gen_tcp.connect/3 with :badarg. Apps no longer need to set the lookup chain themselves; Mob.DNS.configure_pure_beam/1 still composes on top for outbound DNS. See guides/dns_on_ios.md.
  • iOS: Column now honours fill_height: true. The .column case in MobRootView only set maxWidth, so a Column with fill_height: true would collapse to its children's natural height — breaking the canonical <Column fill_width fill_height> header/flex/footer pattern. Now sets maxHeight: .infinity when the prop is set and switches alignment to .topLeading so children anchor at the top when the column flexes. Default (no fill_height) behavior is unchanged.

Docs

  • Plugin system design corpus: MOB_PLUGINS.md (capability-plugin manifest, tiers 0-4, spec-v2 code-generated plugins), MOB_STYLES.md (style preset system, namespaced cherry-pick, stable per-primitive prop contract), MOB_PLUGIN_SECURITY.md (three-layer trust model, dev-mode escape hatches, :acknowledge_unsafe_plugins), plugin_extraction_plan.md (Phase 0 → Phase 3 + risk register + kickoff checklist). Locks scope to Elixir-first, BEAM-native, Gen-AI-enabled; parks full-language non-BEAM frontends at speculative plugin_spec_version: 3. Companion agent_briefs/rustler_env_var_test.md covers filmor's env-var-based fix in rusterlium/rustler#726.