Current section

Files

Jump to
mob_dev lib mix tasks mob.devices.ex
Raw

lib/mix/tasks/mob.devices.ex

defmodule Mix.Tasks.Mob.Devices do
use Mix.Task
@shortdoc "List all connected Android and iOS devices"
@moduledoc """
Scans for connected Android devices (via adb) and iOS simulators/physical
devices (via xcrun simctl / ideviceinfo) and prints their status.
mix mob.devices
Each device is shown with a short **ID** you can pass to `--device`:
mix mob.deploy --device emulator-5554
mix mob.deploy --native --device 78354490
Gracefully skips platforms whose tools are not installed (adb / xcrun).
## Under the hood
# Android
adb devices -l
# → parses serial numbers, device/emulator state, and manufacturer/model
# iOS (macOS only)
xcrun simctl list devices booted --json
ideviceinfo -k UniqueDeviceID (if libimobiledevice is installed)
"""
alias MobDev.Discovery.{Android, IOS}
alias MobDev.Device
@impl Mix.Task
def run(_args) do
Mix.Task.run("app.config")
android = list_android()
ios = list_ios()
IO.puts("")
print_section("Android", android)
IO.puts("")
print_section("iOS", ios)
all = device_list(android) ++ device_list(ios)
if all != [] do
IO.puts("")
IO.puts("Pass the ID to --device to target a specific device:")
IO.puts(" mix mob.deploy --device #{Device.display_id(hd(all))}")
print_bench_hints(all)
end
IO.puts("")
end
defp print_bench_hints(all) do
physical_ios_with_ip =
Enum.find(all, fn d ->
d.platform == :ios and d.type == :physical and d.host_ip
end)
physical_android =
Enum.find(all, fn d -> d.platform == :android and d.type == :physical end)
if physical_ios_with_ip do
IO.puts("")
IO.puts("For an iOS battery bench, use --wifi-ip with the device IP:")
IO.puts(" mix mob.battery_bench_ios --no-build --wifi-ip #{physical_ios_with_ip.host_ip}")
end
if physical_android do
IO.puts("")
IO.puts("For an Android battery bench, target the device by serial:")
IO.puts(" mix mob.battery_bench_android --no-build --device #{physical_android.serial}")
end
end
# ── Device discovery (returns tagged list or a reason atom) ──────────────────
defp list_android do
case System.find_executable("adb") do
nil -> {:unavailable, "adb not found — install Android platform-tools"}
_ -> {:ok, Android.list_devices()}
end
end
defp list_ios do
cond do
not macos?() ->
{:unavailable, "iOS deployment requires macOS"}
System.find_executable("xcrun") == nil ->
{:unavailable, "xcrun not found — install Xcode command-line tools"}
true ->
{:ok, IOS.list_devices()}
end
end
# ── Output ───────────────────────────────────────────────────────────────────
defp print_section(title, result) do
IO.puts("#{IO.ANSI.cyan()}#{title}#{IO.ANSI.reset()}")
case result do
{:unavailable, reason} ->
IO.puts(" (#{reason})")
{:ok, []} ->
IO.puts(" (none)")
{:ok, devices} ->
print_table(devices)
Enum.each(devices, &print_hints/1)
end
end
defp print_table(devices) do
max_name = devices |> Enum.map(&name_len/1) |> Enum.max()
max_ver = devices |> Enum.map(&ver_len/1) |> Enum.max()
max_type = devices |> Enum.map(&type_len/1) |> Enum.max()
max_id = devices |> Enum.map(&id_len/1) |> Enum.max()
any_ip = Enum.any?(devices, & &1.host_ip)
Enum.each(devices, fn d ->
icon = status_icon(d)
name = pad(d.name || d.serial, max_name)
ver = pad(d.version || "", max_ver)
type = pad(type_label(d), max_type)
id_str = Device.display_id(d)
id_padded = pad(id_str, max_id)
id = IO.ANSI.bright() <> id_padded <> IO.ANSI.reset()
ip_part =
cond do
d.host_ip -> " " <> IO.ANSI.faint() <> d.host_ip <> IO.ANSI.reset()
any_ip -> " " <> IO.ANSI.faint() <> "(no IP)" <> IO.ANSI.reset()
true -> ""
end
IO.puts(" #{icon} #{name} #{ver} #{type} #{id}#{ip_part}")
end)
end
defp name_len(d), do: String.length(d.name || d.serial)
defp ver_len(d), do: String.length(d.version || "")
defp type_len(d), do: String.length(type_label(d))
defp id_len(d), do: String.length(Device.display_id(d))
defp type_label(%{type: :emulator}), do: "emulator"
defp type_label(%{type: :simulator}), do: "simulator"
defp type_label(%{type: :physical}), do: "physical"
defp type_label(_), do: "device"
defp status_icon(%{status: :connected}), do: "✓"
defp status_icon(%{status: :booted}), do: "·"
defp status_icon(%{status: :discovered}), do: "·"
defp status_icon(%{status: :unauthorized}), do: "✗"
defp status_icon(%{status: :error}), do: "!"
defp status_icon(_), do: "·"
defp pad(str, width), do: String.pad_trailing(str, width)
# ── Hints ────────────────────────────────────────────────────────────────────
defp print_hints(%{status: :unauthorized}) do
IO.puts(
" #{IO.ANSI.yellow()}→ Check device for 'Allow USB debugging?' prompt#{IO.ANSI.reset()}"
)
end
defp print_hints(%{platform: :android, serial: serial}) do
case Android.developer_mode(serial) do
:disabled ->
IO.puts(
" #{IO.ANSI.yellow()}→ Enable Developer Mode: Settings → About → tap Build Number 7×#{IO.ANSI.reset()}"
)
_ ->
:ok
end
end
defp print_hints(_), do: :ok
defp device_list({:ok, devices}), do: devices
defp device_list({:unavailable, _}), do: []
defp macos?, do: match?({:unix, :darwin}, :os.type())
end