Packages
mob_dev
0.2.3
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mob_dev/discovery/android.ex
defmodule MobDev.Discovery.Android do
@moduledoc "Discovers Android devices and emulators via adb."
alias MobDev.Device
@doc "Returns a list of %Device{} for all adb-visible Android devices."
@spec list_devices() :: [Device.t()]
def list_devices do
case System.find_executable("adb") do
nil -> []
_ -> do_list()
end
end
defp do_list do
case run_adb(["devices", "-l"]) do
{:ok, output} ->
output
|> parse_devices_output()
|> Enum.map(&enrich/1)
{:error, _} -> []
end
end
@doc """
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.
"""
@spec parse_devices_output(String.t()) :: [Device.t()]
def parse_devices_output(output) do
output
|> String.split("\n")
|> Enum.drop(1) # skip "List of devices attached" header
|> Enum.reject(&(String.trim(&1) == ""))
|> Enum.map(&parse_device_line/1)
|> Enum.reject(&is_nil/1)
end
# Parse lines like:
# emulator-5554 device product:sdk_gphone64_arm64 ...
# R5CW3089HVB unauthorized
# 192.168.1.5:5555 device
defp parse_device_line(line) do
case String.split(line, ~r/\s+/, parts: 2) do
[serial, rest] ->
cond do
String.contains?(rest, "unauthorized") ->
%Device{platform: :android, serial: serial, status: :unauthorized,
error: "USB debugging not authorized — check device for prompt"}
String.contains?(rest, "offline") ->
nil
String.starts_with?(rest, "device") or String.starts_with?(rest, "no permissions") ->
type = if String.starts_with?(serial, "emulator"), do: :emulator, else: :physical
%Device{platform: :android, serial: serial, type: type, status: :discovered}
true ->
nil
end
_ -> nil
end
end
defp enrich(%Device{status: :unauthorized} = d), do: d
defp enrich(%Device{serial: serial} = d) do
name = getprop(serial, "ro.product.model")
version = getprop(serial, "ro.build.version.release")
node = Device.node_name(d)
%{d | name: name, version: "Android #{version}", node: node}
end
defp getprop(serial, prop) do
case run_adb(["-s", serial, "shell", "getprop", prop]) do
{:ok, val} -> String.trim(val)
_ -> nil
end
end
@doc """
Check if developer mode is enabled on the device.
Returns :enabled | :disabled | :unknown.
"""
@spec developer_mode(String.t()) :: :enabled | :disabled | :unknown
def developer_mode(serial) do
case run_adb(["-s", serial, "shell", "settings", "get", "global",
"development_settings_enabled"]) do
{:ok, "1\n"} -> :enabled
{:ok, "0\n"} -> :disabled
_ -> :unknown
end
end
@doc """
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 `chcon` copies the correct context from the app's
own `files/` directory (which `installd` always keeps up to date).
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.
"""
@spec restart_app(String.t(), String.t(), String.t(), keyword()) ::
{:ok, String.t()} | {:error, String.t()}
def restart_app(serial, package, activity, opts \\ []) do
dist_port = Keyword.get(opts, :dist_port, 9100)
app_data = "/data/data/#{package}/files"
run_adb(["-s", serial, "shell", "am", "force-stop", package])
# Heal SELinux MCS category mismatch: APK reinstall changes the app's assigned
# category but leaves OTP files with the old label. This causes the BEAM to crash
# on launch (symlink creation fails with EACCES, then erl_start aborts).
run_adb(["-s", serial, "shell",
"chcon -hR $(stat -c %C #{app_data}) #{app_data}/otp"])
:timer.sleep(300)
run_adb(["-s", serial, "shell", "am", "start",
"-n", "#{package}/#{activity}",
"--ei", "mob_dist_port", to_string(dist_port)])
end
defp run_adb(args) do
case System.cmd("adb", args, stderr_to_stdout: true) do
{output, 0} -> {:ok, output}
{output, _} -> {:error, output}
end
end
end