Packages
mob_dev
0.4.0
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/server/device_poller.ex
defmodule MobDev.Server.DevicePoller do
@moduledoc """
Polls adb and simctl every 2 seconds, broadcasting device state changes via PubSub.
"""
use GenServer
alias MobDev.Discovery.{Android, IOS}
@poll_ms 2_000
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc "Returns the current device list synchronously."
@spec get_devices() :: [map()]
def get_devices do
GenServer.call(__MODULE__, :get_devices)
end
# ── GenServer ────────────────────────────────────────────────────────────────
@impl GenServer
@spec init(term()) :: {:ok, map()}
def init(_opts) do
schedule_poll()
devices = poll_devices()
{:ok, %{devices: devices}}
end
@impl GenServer
@spec handle_call(:get_devices, GenServer.from(), map()) :: {:reply, [map()], map()}
def handle_call(:get_devices, _from, state) do
{:reply, state.devices, state}
end
@impl GenServer
@spec handle_info(:poll, map()) :: {:noreply, map()}
def handle_info(:poll, state) do
schedule_poll()
devices = poll_devices()
if devices != state.devices do
Phoenix.PubSub.broadcast(MobDev.PubSub, "devices", {:devices_updated, devices})
end
{:noreply, %{state | devices: devices}}
end
# ── Internals ────────────────────────────────────────────────────────────────
defp schedule_poll, do: Process.send_after(self(), :poll, @poll_ms)
defp poll_devices do
android =
try do
Android.list_devices()
|> Enum.reject(&(&1.status == :unauthorized))
|> Enum.map(&enrich_android/1)
rescue
_ -> []
end
ios =
if macos?() do
try do
IOS.list_simulators()
|> Enum.filter(&(&1.status == :booted))
|> Enum.map(&enrich_ios/1)
rescue
_ -> []
end
else
[]
end
android ++ ios
end
defp enrich_android(device) do
battery = read_android_battery(device.serial)
beam_running = beam_running_android?(device.serial)
Map.merge(device, %{battery: battery, beam_running: beam_running})
end
defp enrich_ios(device) do
Map.merge(device, %{battery: nil, beam_running: nil})
end
defp read_android_battery(serial) do
case System.cmd("adb", ["-s", serial, "shell", "dumpsys battery"], stderr_to_stdout: true) do
{out, 0} ->
case Regex.run(Regex.compile!("level:\\s*(\\d+)"), out) do
[_, pct] -> String.to_integer(pct)
nil -> nil
end
_ ->
nil
end
end
defp beam_running_android?(serial) do
# Use pm list packages to find the installed third-party app rather than
# hardcoding a bundle ID — works for any app name.
case System.cmd(
"adb",
["-s", serial, "shell", "pidof $(pm list packages -3 | head -1 | cut -d: -f2)"],
stderr_to_stdout: true
) do
{out, 0} -> String.trim(out) != ""
_ -> false
end
end
defp macos?, do: match?({:unix, :darwin}, :os.type())
end