# `Mob.Device`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/device.ex#L1)

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.

# `category`

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

# `event`

```elixir
@type event() :: atom()
```

# `lock`

```elixir
@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.

# `orientation`

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

A concrete interface orientation.

# `battery_level`

```elixir
@spec battery_level() :: integer()
```

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

# `battery_state`

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

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

# `categories`

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

Returns the configured list of valid categories.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `foreground?`

```elixir
@spec foreground?() :: boolean()
```

True if the app is currently in the foreground.

# `lock_orientation`

```elixir
@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?`

```elixir
@spec low_power_mode?() :: boolean()
```

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

# `model`

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

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

# `open_settings`

```elixir
@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`

```elixir
@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`

```elixir
@spec orientation() :: orientation()
```

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

# `os_version`

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

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

# `start_link`

```elixir
@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`

```elixir
@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`

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

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

# `unlock_orientation`

```elixir
@spec unlock_orientation() :: :ok
```

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

# `unsubscribe`

```elixir
@spec unsubscribe() :: :ok
```

Unsubscribe the calling process from all categories.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
