Mob.Device (mob v0.7.16)

Copy Markdown View Source

Cross-platform device events and queries.

Mob.Device is the single subscription point for OS-level events that exist on both iOS and Android. The native side (iOS NotificationCenter, Android ProcessLifecycleObserver) registers observers once at startup and emits each event as a tagged tuple to a registered dispatcher pid; this GenServer fans the events out to subscribers by category.

Subscribe

Mob.Device.subscribe()             # default categories
Mob.Device.subscribe(:all)         # everything
Mob.Device.subscribe([:app, :power])

Subscribers receive {:mob_device, atom} or {:mob_device, atom, payload} in their mailbox. Default categories are :app, :display, :audio, :memory.

Categories and events

  • :app:will_resign_active, :did_become_active, :did_enter_background, :will_enter_foreground, :will_terminate
  • :display:screen_off, :screen_on, {:orientation_changed, :portrait | :portrait_upside_down | :landscape_left | :landscape_right}
  • :audio:audio_interrupted, :audio_resumed, :audio_route_changed
  • :appearance{:color_scheme_changed, :light | :dark}

  • :power{:battery_state_changed, :unplugged | :charging | :full | :unknown}, {:battery_level_changed, integer}, {:low_power_mode_changed, boolean}

  • :thermal{:thermal_state_changed, :nominal | :fair | :serious | :critical}

  • :memory:memory_warning

Platform-specific events with no cross-platform counterpart go through Mob.Device.IOS / Mob.Device.Android instead.

Queries

Mob.Device.battery_level()      # 0..100 | -1 if unknown
Mob.Device.battery_state()      # :unplugged | :charging | :full | :unknown
Mob.Device.thermal_state()      # :nominal | :fair | :serious | :critical
Mob.Device.low_power_mode?()    # boolean
Mob.Device.foreground?()        # boolean
Mob.Device.os_version()         # binary
Mob.Device.model()              # binary
Mob.Device.orientation()        # :portrait | :landscape_left | ...

Orientation

orientation/0 reports the current interface orientation; subscribe to :display to get {:mob_device, :orientation_changed, orientation} when the device rotates.

Mob.Device.orientation()                 # :portrait
Mob.Device.lock_orientation(:landscape)  # force landscape (either side)
Mob.Device.unlock_orientation()          # follow the sensor again

Locking forces the orientation regardless of the OS auto-rotate-lock setting (Android setRequestedOrientation; iOS supported-orientations + a geometry request). It is global to the app — a screen that wants to be landscape-only should lock_orientation/1 on enter and unlock_orientation/0 on leave.

Summary

Types

A lock request. :landscape / :portrait allow either side of that axis; the four concrete values pin a single orientation.

A connectivity snapshot. online/transport/expensive are cross-platform. validated and constrained are each exposed by only one platform; the other reports :unavailable rather than a misleading false.

A concrete interface orientation.

The transport currently carrying the network path.

A per-platform signal that this platform does not expose — distinct from a known false. e.g. validated on iOS, constrained on Android.

Functions

Current battery level (0..100), or -1 if unknown.

Current battery state — :unplugged | :charging | :full | :unknown.

Returns the configured list of valid categories.

Returns a specification to start this module under a supervisor.

True if the app is currently in the foreground.

Lock the app to orientation, overriding the OS auto-rotate setting.

True if Low Power Mode (iOS) / Power Save Mode (Android) is on.

Device model (e.g. "iPhone", "Pixel 8").

Current network connectivity snapshot.

True if the device currently has a usable network path.

Opens an OS settings screen for this app.

Hands a URL to the OS to open in the default browser/handler.

Current interface orientation — :portrait | :portrait_upside_down | :landscape_left | :landscape_right, or :unknown if it can't be determined.

OS version string (e.g. "17.4").

Start the dispatcher. Called from Mob.Application (or the app supervision tree). After start, the registered NIF dispatcher pid is this GenServer.

Subscribe the calling process to device events.

Current thermal state — :nominal | :fair | :serious | :critical.

Release an orientation lock; the app follows the sensor again.

Unsubscribe the calling process from all categories.

Types

category()

@type category() ::
  :app
  | :display
  | :audio
  | :appearance
  | :power
  | :thermal
  | :memory
  | :network

event()

@type event() :: atom()

lock()

@type lock() ::
  :portrait
  | :portrait_upside_down
  | :landscape
  | :landscape_left
  | :landscape_right

A lock request. :landscape / :portrait allow either side of that axis; the four concrete values pin a single orientation.

network_state()

@type network_state() :: %{
  online: boolean(),
  transport: transport(),
  expensive: boolean(),
  validated: boolean() | unavailable(),
  constrained: boolean() | unavailable()
}

A connectivity snapshot. online/transport/expensive are cross-platform. validated and constrained are each exposed by only one platform; the other reports :unavailable rather than a misleading false.

orientation()

@type orientation() ::
  :portrait
  | :portrait_upside_down
  | :landscape_left
  | :landscape_right
  | :unknown

A concrete interface orientation.

transport()

@type transport() :: :wifi | :cellular | :wired | :other | :none

The transport currently carrying the network path.

unavailable()

@type unavailable() :: :unavailable

A per-platform signal that this platform does not expose — distinct from a known false. e.g. validated on iOS, constrained on Android.

Functions

battery_level()

@spec battery_level() :: integer()

Current battery level (0..100), or -1 if unknown.

battery_state()

@spec battery_state() :: :unplugged | :charging | :full | :unknown

Current battery state — :unplugged | :charging | :full | :unknown.

categories()

@spec categories() :: [category()]

Returns the configured list of valid categories.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

foreground?()

@spec foreground?() :: boolean()

True if the app is currently in the foreground.

lock_orientation(orientation)

@spec lock_orientation(lock()) :: :ok | {:error, :invalid}

Lock the app to orientation, overriding the OS auto-rotate setting.

Accepts :portrait, :portrait_upside_down, :landscape (either side), :landscape_left, or :landscape_right. Returns {:error, :invalid} for anything else. Global to the app; pair with unlock_orientation/0.

low_power_mode?()

@spec low_power_mode?() :: boolean()

True if Low Power Mode (iOS) / Power Save Mode (Android) is on.

model()

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

Device model (e.g. "iPhone", "Pixel 8").

network_state()

@spec network_state() :: network_state()

Current network connectivity snapshot.

online is true when the OS reports a usable default network path is up. It does not guarantee the internet is reachable — a captive-portal or not-yet-validated network reports online: true on both platforms (iOS NWPath satisfied / Android default NetworkCallback). transport names the active interface — :wifi | :cellular | :wired | :other | :none; it is :none exactly when online is false. expensive is true on metered links (cellular, personal hotspot), so defer large transfers when it is set.

validated and constrained are single-platform signals — each platform reports :unavailable (not a misleading false) for the one it can't answer:

  • validatedAndroid only (NET_CAPABILITY_VALIDATED): the OS actively probed and confirmed real internet reachability; false on a captive portal or before validation completes. :unavailable on iOS (NWPath has no probe).

  • constrainediOS only (nw_path_is_constrained): Low Data Mode is on. :unavailable on Android (no per-network equivalent).

    # iOS #=> %{online: true, transport: :wifi, expensive: false, # validated: :unavailable, constrained: false} # Android #=> %{online: true, transport: :wifi, expensive: false, # validated: true, constrained: :unavailable}

Subscribe to the :network category to receive {:mob_device, :connectivity_changed, state} when this changes — state is the same full map shape returned here (all five keys, validated/constrained included).

online?()

@spec online?() :: boolean()

True if the device currently has a usable network path.

open_settings(target \\ :app)

@spec open_settings(:app | :notifications | :exact_alarm) :: :ok | {:error, :invalid}

Opens an OS settings screen for this app.

target is one of:

  • :app — the app's details / permissions page (both platforms). The go-to when a runtime permission was permanently denied and the user must re-enable it by hand.
  • :notifications — the app's notification settings (Android). iOS has no granular deep-link, so it opens the app page.
  • :exact_alarm — the "Alarms & reminders" special-access screen (Android 12+). iOS opens the app page.

iOS exposes only the single app settings page, so target is honored on Android and falls back to the app page on iOS. Fire-and-forget: a valid target returns :ok immediately (an unavailable screen is a no-op, not a raise); an unknown target returns {:error, :invalid} without touching the NIF.

open_url(url)

@spec open_url(String.t()) :: :ok

Hands a URL to the OS to open in the default browser/handler.

Fire-and-forget. Returns :ok immediately; failures (malformed URL, no registered handler) are logged but not raised.

orientation()

@spec orientation() :: orientation()

Current interface orientation — :portrait | :portrait_upside_down | :landscape_left | :landscape_right, or :unknown if it can't be determined.

os_version()

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

OS version string (e.g. "17.4").

start_link(opts \\ [])

@spec start_link(keyword()) :: GenServer.on_start()

Start the dispatcher. Called from Mob.Application (or the app supervision tree). After start, the registered NIF dispatcher pid is this GenServer.

subscribe(categories \\ [:app, :display, :audio, :appearance, :memory])

@spec subscribe(category() | [category()] | :all) :: :ok

Subscribe the calling process to device events.

Accepts a single category atom, a list of categories, or :all. Default is [:app, :display, :audio, :appearance, :memory].

Mob.Device.subscribe()
Mob.Device.subscribe(:thermal)
Mob.Device.subscribe([:app, :power, :thermal])
Mob.Device.subscribe(:all)

thermal_state()

@spec thermal_state() :: :nominal | :fair | :serious | :critical

Current thermal state — :nominal | :fair | :serious | :critical.

unlock_orientation()

@spec unlock_orientation() :: :ok

Release an orientation lock; the app follows the sensor again.

unsubscribe()

@spec unsubscribe() :: :ok

Unsubscribe the calling process from all categories.