MobDev.Discovery.Android (mob_dev v0.7.7)

Copy Markdown View Source

Discovers Android devices and emulators via adb.

Summary

Functions

Check if developer mode is enabled on the device. Returns :enabled | :disabled | :unknown.

Returns the Mob node-name suffix for the device reachable via the given adb identifier. The suffix is derived from the device's hardware serial (ro.serialno), which is stable across USB and WiFi-adb identifiers for the same physical phone — so a deploy that targets ZY22K6BSJM (USB) and a bench that targets 10.0.0.17:5555 (WiFi) both end up using the same node name.

Returns a list of %Device{} for all adb-visible Android devices.

Sanitizes a string into a Mob node-name suffix. Pure — no adb calls.

Parses the raw output of adb devices -l into a list of %Device{}. Does not perform enrichment (no adb calls for name/version). Exposed for testing.

The first of candidates (node names without @host, in preference order) that registered (EPMD's {name, port} pairs) lists, with its port, or nil.

Restarts the app on the device, optionally passing a dist_port intent extra.

Writes the app's private distribution cookie to /data/data/<package>/files/otp/<app>/mob_dist_cookie, where Mob.Dist reads it when the app starts (MOB-49).

Functions

developer_mode(serial)

@spec developer_mode(String.t()) :: :enabled | :disabled | :unknown

Check if developer mode is enabled on the device. Returns :enabled | :disabled | :unknown.

device_node_suffix(adb_id)

@spec device_node_suffix(String.t()) :: String.t()

Returns the Mob node-name suffix for the device reachable via the given adb identifier. The suffix is derived from the device's hardware serial (ro.serialno), which is stable across USB and WiFi-adb identifiers for the same physical phone — so a deploy that targets ZY22K6BSJM (USB) and a bench that targets 10.0.0.17:5555 (WiFi) both end up using the same node name.

Falls back to sanitizing the adb identifier itself when getprop fails (e.g. unrooted device, missing executable, dead transport). The fallback is the legacy behaviour, kept so a bench against a pre-suffix-aware deploy still converges on some deterministic name.

Single adb shell call (~100–300 ms). Suitable for once-per-launch use by the deployer and bench.

emulator_adb_id?(adb_id)

@spec emulator_adb_id?(String.t()) :: boolean()

emulator_serial?(serial)

@spec emulator_serial?(String.t()) :: boolean()

list_devices()

@spec list_devices() :: [MobDev.Device.t()]

Returns a list of %Device{} for all adb-visible Android devices.

node_suffix_for(serial)

@spec node_suffix_for(String.t()) :: String.t()

Sanitizes a string into a Mob node-name suffix. Pure — no adb calls.

node_suffix_for("ZY22CRLMWK")        → "zy22crlmwk"
node_suffix_for("10.0.0.82:5555")    → "10_0_0_82"
node_suffix_for("emulator-5554")     → "emulator_5554"

Used as the final transformation step by device_node_suffix/1 (which asks the device for a stable hardware serial and runs it through here). Tests use it directly to verify the sanitization rules.

parse_devices_output(output)

@spec parse_devices_output(String.t()) :: [MobDev.Device.t()]

Parses the raw output of adb devices -l into a list of %Device{}. Does not perform enrichment (no adb calls for name/version). Exposed for testing.

pick_registered_node(registered, candidates)

@spec pick_registered_node([{String.t(), pos_integer()}], [String.t()]) ::
  {String.t(), pos_integer()} | nil

The first of candidates (node names without @host, in preference order) that registered (EPMD's {name, port} pairs) lists, with its port, or nil.

An app started by mix mob.deploy registers <app>_android_<suffix> on the port mob_dev chose; one started from the launcher, with no intent extras, registers the bare <app>_android on 9100. Attaching has to find whichever is running.

restart_app(serial, package, activity, opts \\ [])

@spec restart_app(String.t(), String.t(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, String.t()}

Restarts the app on the device, optionally passing a dist_port intent extra.

Runs chcon before am start to heal any SELinux MCS category mismatch on OTP files. This mismatch happens when the APK is reinstalled and Android assigns a new MCS category to the package — files pushed via adb push retain the old label and the BEAM can't access them.

The label is copied from the app's cache/ directory, not files/. On Android 15 the files/ directory itself lacks MCS categories (s0 only), whereas cache/ always carries the full s0:cXXX,cYYY label that installd assigns to the package.

The chcon requires root (adb root) — it's silently skipped on non-rooted devices where the OTP files were pushed with the correct label to begin with.

write_dist_cookie(serial, package, app, cookie)

@spec write_dist_cookie(String.t(), String.t(), atom() | String.t(), atom()) ::
  :ok | {:error, String.t()}

Writes the app's private distribution cookie to /data/data/<package>/files/otp/<app>/mob_dist_cookie, where Mob.Dist reads it when the app starts (MOB-49).

run-as writes it with the app's owner, label and a 077 umask, so other apps on the phone can't read it. The cookie travels on adb's stdin rather than in an argument, which ps would show to every user on the Mac.