Releases: GenericJam/mob
0.6.23
Added
- Element positions without a screenshot.
element_frames/0NIF surfaced asMob.Test.element_frames/1(%{id => {x,y,w,h}}),frame/2, andtap_id/2(drive by id at real coordinates). Any rendered node given an:idreports 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:idas the element's accessibility identifier (iOSaccessibilityIdentifier, Android ComposetestTag), so the same tags are visible to XCUITest/Espresso. Opt-in per element: untagged nodes cost nothing (the tracking modifier only attaches when an:idis present). iOS records the full element frame via aGeometryReaderbackground; Android viaModifier.onGloballyPositioned. Verified on iOS sim, Android device, and a physical iPhone. The Android Kotlin side lives in themob_newMobBridge.kt.eextemplate. - In-process screenshot + scroll control over dist (no adb/xcrun). Three test-harness NIFs (
screenshot/3,scroll_info/1,scroll_to/3) surfaced asMob.Test.screenshot/2,scroll_info/2,scroll_to/4, andscreenshot_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 (iOSUIGraphicsImageRenderer+drawViewHierarchy; AndroidPixelCopyagainst the activity window). Scroll views are addressed by their:idprop;scroll_inforeportskind: :pixel(iOSUIScrollView, AndroidverticalScroll) or:index(AndroidLazyColumn, 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 themob_newMobBridge.kt.eextemplate; existing apps pick it up on regeneration. Debug-only (iOS#if !MOB_RELEASE). Seedecisions/2026-05-29-bridge-nif-screenshot-scroll.md.
Changed
Mob.Btextracted to standalonemob_bluetoothplugin. Seeplugin_extraction_plan.mdWave 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 asMobBluetooth.*; 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 usedMob.Bt.*should add{:mob_bluetooth, path: "..."}and rename their references toMobBluetooth.*— there is intentionally no compatibility shim.
OTP pre-built runtime 7d46fdd4
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
Added
Mob.Certs— load CA certificates from a PEM bundle into Erlang's:public_keycacert 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 withno_cacerts_found(orFunctionClauseErrorin some OTP versions). Apps bundle a PEM (conventional source: copycastore'scacerts.pemintopriv/at build time) and callMob.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"}])thenReq.get!("https://geocoding-api.open-meteo.com/v1/search?name=Vancouver")returns200.mob_beam.zigexportsMOB_NATIVE_LIB_DIRbefore 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.) aslib*.soneed this to setMIX_REBAR3and locate the bundled escripts.- Optional ERTS-extras symlinks (
escript/erlexec/erl/beam.smp) inmob_beam.zig. Silent-skips when the lib isn't in nativeLibDir, so non-opting-in apps see no behaviour change. Apps that droplib<name>.sointoandroid/app/src/main/jniLibs/<abi>/get a workingBINDIR/<name>— enough for runtimeMix.installof rebar3-built deps (telemetry, jose, jiffy, …) to bootstrap a fresh VM.erlanderlexecboth target the sameliberlexec.sobecause they are the same binary (erlexec doesn't switch onargv[0]).
Changed
extra_applications: [:logger, :public_key]— Elixir 1.19+ strips unused OTP applications from the code path;Mob.Certscalls:public_key.cacerts_load/1at runtime, so its.beammust be in the path even though mob doesn't start:public_keyitself.
Fixed
mix.exs— collapsed duplicatebefore_closing_body_tag/1clauses 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 duplicatedocs/0keyword 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/*.bootmaterialization) for apps that opt into runtime rebar3.
0.6.21
Added
Mob.DNS.resolve/1now works on Android.nif_resolve_ipv4(android/jni/mob_nif.zig) calls Bionic'sgetaddrinfoin-process and seeds:inet_db's:filetable, mirroring the iOS NIF added in #32. Physical Android devices return:nxdomainfrom BEAM's default DNS path (forkinginet_gethostas 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/2then succeeds via the seeded entry, andMix.install([{:dep, "~> ..."}])from a notebook setup cell resolves, fetches, and compiles on-device. Bionicaddrinfo/sockaddr_in/getaddrinfo/freeaddrinfo/EAI_*bindings added toandroid/jni/mob_zig.zig. Suspected root cause islibnetd_client.so's netd routing not surviving execve; the NIF sidesteps it by running in the app's own process.
Changed
Mob.DNSmoduledoc — 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/:timeouton 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:nxdomainsymptom on physical Android, the foreground-app caveat, and the fix.
0.6.20
Full Changelog: 0.6.19...0.6.20
0.6.19
What's Changed
- DNS: document preresolve as the robust iOS path (cellular-safe) by @GenericJam in #32
- docs: mark build_system_migration as a historical record by @GenericJam in #33
- Add Mob.Speech text-to-speech capability by @GenericJam in #34
- removed .DS_Store by @clsource in #30
- docs: update Android runtime support matrix by @dl-alexandre in #23
- docs: improved readme with mermaid and logo by @clsource in #31
- docs: document background execution model by @dl-alexandre in #19
- docs: explain background execution limits by @dl-alexandre in #24
New Contributors
- @clsource made their first contribution in #30
- @dl-alexandre made their first contribution in #23
Full Changelog: 0.6.18...0.6.19
0.6.18
Changed
RUSTLER_NIF_LIB_PATH→RUSTLER_BEAM_LIBRARY_PATHinmob_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
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 viaProcess.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 existingMediaPlayerpath (port to AAudio is pending).- iOS:
nif_audio_play_at(Path, OptsJson, AtWallMs)backed by a dedicatedAVAudioEngine+AVAudioPlayerNode. The wall-time target is converted to anAVAudioTimehostTimeviamach_absolute_time+mach_timebase_info, then handed to-[AVAudioPlayerNode scheduleBuffer:atTime:options:completionHandler:]. Past targets schedule ASAP. Multipleplay_atcalls accumulate on the player's timeline — useaudio_stop_playbackto flush. audio_set_volumeandaudio_stop_playbacknow 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
Added
mob_beam.zigexportsRUSTLER_NIF_LIB_PATHbefore BEAM start. Callsdladdr(&mob_start_beam)to discover the absolute path of the host.so(e.g.lib<app>.so) andsetenv()s it asRUSTLER_NIF_LIB_PATH. Pairs with the matching upstream rustler change (rusterlium/rustler#726): rustler'sDlsymNifFiller::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.sonow resolveenif_*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.zigexposesdladdr+DlInfoto other Zig consumers underjni.dladdr/jni.DlInfo. Hand-declared to match the libc/Bionic surface; same hand-declared FFI policy as the rest ofmob_zig.zig(we don't use@cImporthere).
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)→dlsymallenif_*exports → Rust NIFgreet/0executes and returns"Hello from Rust!"to BEAM.
0.6.15
Added
-
text_fieldnow accepts asecure: trueprop. iOS renders the field
as a SwiftUISecureField(masked input) instead of the plain
TextField. The prop flows through the existing renderer
passthrough; cleartext still reaches the BEAM viaon_changeso apps
can hash/store the value as normal. Android consumes the same prop
viaPasswordVisualTransformationoncemob_new'sMobBridge.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 aZStack-and-opacity
rebuild ofMobTextFieldand warrants its own change.
Fixed
- iOS:
Mob.App.start/0now switches:inet_dbto file-only lookup and seedslocalhostbefore any user code runs — BEAM's default:nativelookup tries toexecvetheinet_gethostport program, which the iOS sandbox refuses, crashing the firstNode.connect/:erpc.call/gen_tcp.connect/3with:badarg. Apps no longer need to set the lookup chain themselves;Mob.DNS.configure_pure_beam/1still composes on top for outbound DNS. Seeguides/dns_on_ios.md. - iOS:
Columnnow honoursfill_height: true. The.columncase inMobRootViewonly setmaxWidth, so aColumnwithfill_height: truewould collapse to its children's natural height — breaking the canonical<Column fill_width fill_height>header/flex/footer pattern. Now setsmaxHeight: .infinitywhen the prop is set and switches alignment to.topLeadingso children anchor at the top when the column flexes. Default (nofill_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 speculativeplugin_spec_version: 3. Companionagent_briefs/rustler_env_var_test.mdcovers filmor's env-var-based fix inrusterlium/rustler#726.