# Device

`Device` is one attached device: keys, the screen, rotation, device conditions, files,
notifications, screenshots and waits of your own. `device.app(...)` and `device.screen` find
elements. Guide: [Keys, screen and rotation](../../guide/device-control.md),
[Device conditions](../../guide/device-conditions.md).

## <kbd>class</kbd> `Device`
All calls block. The client does not serialize calls on one device: issue them from one thread, and use one thread per Device for concurrency across devices. Nothing here caches UI state: ``App.element`` / ``Screen.element`` return a lazy selector that every action resolves again, and mutations fail with AMBIGUOUS/NOT_FOUND before any input when the selector does not match exactly one node. 

### <kbd>method</kbd> `Device.__init__`

```python
__init__(
    owner_connection: 'TapConnection',
    response: 'pb.AttachResponse',
    timeouts: 'Timeouts'
)
```

---

#### <kbd>property</kbd> Device.detached

True once ``detach()`` ran; every later call on this device is rejected. 

---

### <kbd>method</kbd> `Device.add_media`

```python
add_media(
    source: 'bytes | bytearray | memoryview | str | os.PathLike[str]',
    file_name: 'str | None' = None
) → str
```

Add a photo or video to the device's gallery until ``detach()``; return its device path. 

``source`` is the content (``bytes``, which needs ``file_name``) or a local file (``file_name`` defaults to its name). ``file_name`` is letters, digits, ``.``, ``_``, ``-`` or spaces with a photo extension (jpg, jpeg, png, gif, webp, heic, heif, bmp) or a video one (mp4, 3gp, webm, mkv, mov): the file goes to ``/sdcard/Pictures/Tap/`` or ``/sdcard/Movies/Tap/`` and the media scanner indexes it, so gallery apps and photo pickers list it (read back). A name already there is refused (``ServerError``, reason ``DEVICE_FILE``) unless this device handle added it. Deleted, and dropped from the index, on ``detach()``; nothing else in the gallery is touched. 

---

### <kbd>method</kbd> `Device.app`

```python
app(package_name: 'str') → App
```

Bind lifecycle and selectors to ``package_name``. 

---

### <kbd>method</kbd> `Device.await_notification`

```python
await_notification(
    title: 'str | None' = None,
    text: 'str | None' = None,
    mode: 'MatchMode' = <MatchMode.EXACT: 'EXACT'>,
    package_name: 'str | None' = None,
    timeout: 'float | None' = None
) → Notification
```

Wait until a matching notification is active (one already posted counts) and return the newest, within ``timeout`` (default ``timeouts.wait``). 

``title`` and ``text`` match with ``mode`` (any when ``None``); ``package_name`` narrows it to one app's notifications (``App.await_notification`` passes its own). Notifications are read as data from a notification listener in the Tap driver app, not from the shade, so nothing opens on screen; the server gives the driver notification access for the session (taken back on ``detach()``). Group summaries are left out. Raises ``WaitTimeoutError`` (reason ``NO_NOTIFICATION``) when none matches. 

---

### <kbd>method</kbd> `Device.await_permission_prompt`

```python
await_permission_prompt(timeout: 'float | None' = None) → PermissionPrompt
```

Wait until a runtime-permission dialog shows and return the choices it offers. 

Buttons are recognised by the permission controller's resource ids, never by label or position. Raises ``WaitTimeoutError`` (reason ``NO_PERMISSION_PROMPT``) when none appears within ``timeout`` (default ``timeouts.wait``). 

---

### <kbd>method</kbd> `Device.await_toast`

```python
await_toast(
    text: 'str | None' = None,
    mode: 'MatchMode' = <MatchMode.EXACT: 'EXACT'>,
    package_name: 'str | None' = None,
    timeout: 'float | None' = None
) → Toast
```

Wait for a toast and return it: one shown in the last 3.5 s (the longest a toast stays up) or arriving within ``timeout`` (default ``timeouts.wait``). 

``text`` matches with ``mode`` (any text when ``None``); a toast from any package counts unless ``package_name`` names one (``App.await_toast`` passes its own). Not consuming: the same toast can satisfy two calls in a row. Raises ``WaitTimeoutError`` (reason ``NO_TOAST``) when none matches. 

---

### <kbd>method</kbd> `Device.await_until`

```python
await_until(
    description: 'str',
    condition: 'Callable[[], bool]',
    timeout: 'float | None' = None,
    poll_interval: 'float | None' = None,
    observe: 'Callable[[], str | None] | None' = None
) → None
```

Host-side polling for conditions the driver cannot evaluate in one command (cross-device, backend state). Prefer ``wait`` for UI conditions: it polls on the device. 

---

### <kbd>method</kbd> `Device.capture`

```python
capture(timeout: 'float' = 30.0) → Capture
```

Takes a screenshot, the hierarchy, the device info and the driver log at once, each within ``timeout`` seconds, and returns them as one ``Capture``. Never raises for the device: a part that fails or runs out of time is None in the result, with its cause in ``Capture.failures``. Use it wherever a test wants evidence (after a step, in an ``except``); the pytest plugin calls it for every failed test. 

---

### <kbd>method</kbd> `Device.choose_permission`

```python
choose_permission(
    choice: 'PermissionChoice',
    accuracy: 'LocationAccuracy | None' = None,
    timeout: 'float | None' = None
) → None
```

Tap the permission dialog's button for ``choice`` (``NOT_FOUND`` before any input when the dialog does not offer it). With ``accuracy``, the location dialog's Precise / Approximate radio is selected first; it must be offered too (``PermissionPrompt.accuracies``, API 31+ when the app asks for fine location), or nothing is tapped. Only the taps are reported: assert the outcome, e.g. the dialog gone and the app's state. 

---

### <kbd>method</kbd> `Device.clipboard`

```python
clipboard(timeout: 'float | None' = None) → str
```

The device clipboard as text; ``""`` when it is empty or holds nothing that reads as text. 

---

### <kbd>method</kbd> `Device.detach`

```python
detach() → str | None
```

Detaches the device. Returns the quarantine detail when the device could not be left clean (the server keeps it out of circulation), else None. Idempotent once it succeeded; a failed detach may be retried. 

---

### <kbd>method</kbd> `Device.dismiss_keyguard`

```python
dismiss_keyguard() → None
```

Dismiss a keyguard that has no PIN, pattern or password (``wm dismiss-keyguard``); with no keyguard showing nothing is sent. A secure keyguard raises ``CommandError`` (``ACTION_REJECTED`` / ``KEYGUARD_SECURE``) before any input: Tap never unlocks one. ``info()`` reports the state. 

---

### <kbd>method</kbd> `Device.dismiss_notification`

```python
dismiss_notification(
    title: 'str | None' = None,
    text: 'str | None' = None,
    mode: 'MatchMode' = <MatchMode.EXACT: 'EXACT'>,
    package_name: 'str | None' = None,
    timeout: 'float | None' = None
) → None
```

Dismiss the one active notification that matches (as for ``await_notification``) as a swipe does. No match or several matches raise ``CommandError`` (``NOT_FOUND`` / ``AMBIGUOUS``), and an ongoing notification ``ACTION_REJECTED`` (``NOT_CLEARABLE``), before anything changes. 

---

### <kbd>method</kbd> `Device.driver_log`

```python
driver_log() → DriverLog
```

The driver instrumentation's recent output. 

---

### <kbd>method</kbd> `Device.dump_hierarchy`

```python
dump_hierarchy(timeout: 'float | None' = None) → Hierarchy
```

The diagnostic accessibility ``Hierarchy``. Never used by selectors; keep it out of assertions. 

---

### <kbd>method</kbd> `Device.foreground_activity`

```python
foreground_activity() → ForegroundActivity | None
```

The activity on top of the screen (the resumed one, the focused one in multi-window), or ``None`` when none is resumed: the keyguard is showing, or an activity is starting. Read on the host from ``dumpsys activity``; changes nothing. Use it to check that a deep link or a notification opened the right screen. 

---

### <kbd>method</kbd> `Device.hide_keyboard`

```python
hide_keyboard(timeout: 'float | None' = None) → None
```

Hide the soft keyboard with one Back key, which the keyboard consumes; with no keyboard showing nothing is sent, so Back never reaches the app. Only the key is reported: assert ``keyboard_shown()`` when it matters. 

---

### <kbd>method</kbd> `Device.info`

```python
info() → DeviceInfo
```

API level, model, display size and the package owning the focused window. 

---

### <kbd>method</kbd> `Device.keyboard_shown`

```python
keyboard_shown() → bool
```

Whether a soft keyboard (any input method's window) is on screen; ``info()`` reports the same. 

---

### <kbd>method</kbd> `Device.notifications`

```python
notifications(timeout: 'float | None' = None) → list[Notification]
```

The active notifications, newest first (group summaries left out), as ``await_notification`` reads them. 

---

### <kbd>method</kbd> `Device.open_notification`

```python
open_notification(
    title: 'str | None' = None,
    text: 'str | None' = None,
    mode: 'MatchMode' = <MatchMode.EXACT: 'EXACT'>,
    package_name: 'str | None' = None,
    action: 'str | None' = None,
    timeout: 'float | None' = None
) → None
```

Open the one active notification that matches (as for ``await_notification``) as a tap on it in the shade does: send its content intent and, when it auto-cancels, remove it. 

With ``action``, press its action button with exactly that title instead; the notification stays. No match or several matches raise ``CommandError`` (``NOT_FOUND`` / ``AMBIGUOUS``), and a notification that opens nothing or has no such action ``ACTION_REJECTED`` (``ACTION_NOT_OFFERED``), before anything is sent. What the app does then is for the test to assert (an activity, say: ``foreground_activity()``). 

---

### <kbd>method</kbd> `Device.open_notifications`

```python
open_notifications() → None
```

Open the notification shade (the system's accessibility action). 

Only whether the system accepted it is reported: wait for what the test needs in the shade, and ``press_back`` closes it. 

---

### <kbd>method</kbd> `Device.open_quick_settings`

```python
open_quick_settings() → None
```

Open the quick settings panel; otherwise as ``open_notifications``. 

---

### <kbd>method</kbd> `Device.press_back`

```python
press_back() → None
```

Send ``KEYCODE_BACK``. 

---

### <kbd>method</kbd> `Device.press_home`

```python
press_home() → None
```

Send ``KEYCODE_HOME``. 

---

### <kbd>method</kbd> `Device.press_key`

```python
press_key(key_code: 'int') → None
```

Injects one Android key code (a mutation: never replayed on transport loss). 

---

### <kbd>method</kbd> `Device.pull_file`

```python
pull_file(
    device_path: 'str',
    to: 'str | os.PathLike[str] | None' = None
) → bytes | None
```

The regular file at ``device_path`` on the device (at most 512 MiB): its bytes, or, with ``to``, written to that local file (replaced only once the whole file arrived). 

---

### <kbd>method</kbd> `Device.push_file`

```python
push_file(
    device_path: 'str',
    source: 'bytes | bytearray | memoryview | str | os.PathLike[str]'
) → None
```

Copy a file to ``device_path`` on the device until ``detach()``. 

``source`` is the content (``bytes``) or a file on *this* machine (``str`` / ``PathLike``), streamed in 1 MiB chunks; at most 512 MiB. ``device_path`` is absolute and normalised: ``/data/local/tmp/…``, or shared storage (``/sdcard/Download/…``) for an app with storage access to read. Its directory must exist. A file already there is refused (``ServerError``, reason ``DEVICE_FILE``) unless this device handle pushed it, so a test never overwrites the device's own files; the size is read back. The file is deleted on ``detach()``. 

---

### <kbd>method</kbd> `Device.resolve_ref`

```python
resolve_ref(ref: 'str') → Selector
```

The selector behind a ``ScreenNode.ref`` (``e7`` or ``@e7``) of any snapshot of this attached device. Raises ``ServerError`` with reason ``UNKNOWN_REF`` for a ref the daemon never issued or whose node has gone, and ``REF_NOT_ADDRESSABLE`` for a node without a selector. 

Experimental: not covered by the compatibility promise; it may change in any release. 

---

### <kbd>method</kbd> `Device.screen_snapshot`

```python
screen_snapshot(
    timeout: 'float | None' = None,
    selector_candidates: 'bool' = False
) → ScreenSnapshot
```

The visible screen as ref-addressed ``ScreenNode``s, each with a selector the daemon found to match only that node — for exploring an app, not for tests (it is built from the diagnostic hierarchy dump). Refs stay stable across snapshots of this attached device, and each node says whether it was added since the previous snapshot; nodes that are gone are in ``removed``. Act on a node through its selector (``device.screen.element(node.selector)``) or ``resolve_ref``: the device still requires exactly one match at action time. With ``selector_candidates`` each node also lists every selector that matched only it (``ScreenNode.candidates``), for an inspector that lets a person choose. 

Experimental: not covered by the compatibility promise; it may change in any release. 

---

### <kbd>method</kbd> `Device.screenshot`

```python
screenshot(timeout: 'float | None' = None) → Screenshot
```

A PNG ``Screenshot`` of the screen, with its size. The server verifies the bytes against the driver's checksum and the client checks the returned sha256 again. Keep it with ``Screenshot.save(path)`` or use ``Screenshot.bytes`` directly. 

---

### <kbd>method</kbd> `Device.set_accessibility_display`

```python
set_accessibility_display(
    high_contrast_text: 'bool | None' = None,
    color_inversion: 'bool | None' = None,
    bold_text: 'bool | None' = None
) → None
```

Turn the accessibility display settings on or off until ``detach()``; a ``None`` setting is left as it is (``ValueError`` when all are ``None``). ``high_contrast_text`` draws text with a black or white outline, ``color_inversion`` inverts the display's colors (screenshots stay uninverted: the inversion happens in the display pipeline), and ``bold_text`` makes the system font bold (API 31+, ``FailureReason.UNSUPPORTED_API`` below, before anything changes). The values are what Settings › Accessibility writes, read back (``FailureReason.DEVICE_SETTING`` when the device did not take one). Read back with ``DeviceInfo.high_contrast_text``, ``color_inversion`` and ``bold_text``. 

---

### <kbd>method</kbd> `Device.set_animations`

```python
set_animations(enabled: 'bool') → None
```

Turn the window, transition and animator animations off (all three scales 0) or on (all 1) for this session. 

Like every device condition below, the value the device had before the session's first change is restored on ``detach()`` (or by the next attach when the server died first), and the change is read back: a value the device did not take raises ``ServerError`` (reason ``DEVICE_SETTING``). ``info().animations_enabled`` reports it. 

---

### <kbd>method</kbd> `Device.set_clipboard`

```python
set_clipboard(text: 'str', timeout: 'float | None' = None) → None
```

Put ``text`` on the device clipboard as plain text (at most 4096 characters). 

---

### <kbd>method</kbd> `Device.set_dark_mode`

```python
set_dark_mode(enabled: 'bool') → None
```

Dark theme on or off (``cmd uimode night``) until ``detach()``, see ``set_animations``. API 29+: below, ``ServerError`` (reason ``UNSUPPORTED_API``). Read back with ``info().dark_mode``. 

---

### <kbd>method</kbd> `Device.set_density`

```python
set_density(dpi: 'int | None') → None
```

Override the display density with ``dpi`` (100 to 1000), or with ``None`` go back to the display's physical density, until ``detach()``, see ``set_animations``. Read back with ``info().density_dpi``. 

---

### <kbd>method</kbd> `Device.set_display_rotation`

```python
set_display_rotation(rotation: 'DisplayRotation') → None
```

Rotate the display to the exact ``rotation`` and freeze it there; otherwise as ``set_orientation``. 

---

### <kbd>method</kbd> `Device.set_font_scale`

```python
set_font_scale(scale: 'float') → None
```

The system font scale, 0.5 to 2.0 (1.0 = default), until ``detach()``, see ``set_animations``. Read back with ``info().font_scale``. 

---

### <kbd>method</kbd> `Device.set_location`

```python
set_location(
    latitude: 'float',
    longitude: 'float',
    accuracy_m: 'float | None' = None,
    altitude_m: 'float | None' = None
) → None
```

Mock the device location until ``detach()``. 

The gps and network providers (and fused, API 31+) report this fix, sent again every second so an app that starts listening later still gets one. ``latitude`` -90..90, ``longitude`` -180..180, ``accuracy_m`` in meters (> 0; ``None`` = 5), ``altitude_m`` in meters (``None`` = none). The Tap driver app becomes the device's mock-location app and, when location is off, location is turned on; both are restored on ``detach()``, which ends the mock. Calling it again moves the fix. Apps that read location through Google Play services see it as Play services relays the platform providers. 

---

### <kbd>method</kbd> `Device.set_network`

```python
set_network(
    airplane_mode: 'bool | None' = None,
    wifi: 'bool | None' = None,
    mobile_data: 'bool | None' = None
) → None
```

Switch airplane mode, Wi-Fi and mobile data on or off until ``detach()``, see ``set_animations``; a switch left ``None`` stays as it is. 

These are the device's real switches (nothing is mocked): the app sees what it would see if a user flipped them, and turning Wi-Fi on says nothing about when it connects. Airplane mode is applied first, so ``set_network(airplane_mode=True, wifi=True)`` is airplane mode with Wi-Fi on. API 29+. A device reached over ADB on the network refuses Wi-Fi off or airplane mode on (``ServerError``, reason ``DEVICE_SETTING``): that would cut Tap off. Read back with ``info().airplane_mode``, ``wifi_enabled`` and ``mobile_data_enabled``. 

---

### <kbd>method</kbd> `Device.set_orientation`

```python
set_orientation(orientation: 'Orientation') → None
```

Rotate the display to ``orientation`` geometry, whatever the device's natural orientation, and freeze it there until ``unfreeze_rotation``. 

Fails only when Android refuses the request; the call then waits briefly for the display to turn. The foreground app may pin its own orientation and keep the display where it wants it: assert with ``info()`` (``orientation``, ``display_rotation``). The rotation settings the device had before the session's first rotation call are restored when the device is detached. 

---

### <kbd>method</kbd> `Device.set_stay_awake`

```python
set_stay_awake(enabled: 'bool') → None
```

Keep the screen on while the device is plugged in (USB, AC or wireless), or let it time out again, until ``detach()``: what Developer options › Stay awake sets. A device on ADB over USB is plugged in, so a long test does not find the screen off. Read back with ``DeviceInfo.stay_awake``. 

---

### <kbd>method</kbd> `Device.set_system_locales`

```python
set_system_locales(locales: 'Sequence[str]') → None
```

Set the device's languages (Settings › Languages) until ``detach()``, see ``set_animations``: BCP-47 tags in preference order, 1 to 16 of them (``"fr-FR"``, ``"en"``). 

Every app that follows the system sees the change as a configuration change; an app with its own languages (``App.set_locales``) keeps them. Android has no shell command for this: the Tap driver app applies it as Settings' language picker does, and the result is read back (``ServerError``, reason ``DEVICE_SETTING``, when the device reports another list). Read back with ``info().system_locales``. 

---

### <kbd>method</kbd> `Device.sleep`

```python
sleep() → None
```

Turn the screen off (``KEYCODE_SLEEP``; nothing happens when it is off). 

---

### <kbd>method</kbd> `Device.start_recording`

```python
start_recording(
    video: 'bool' = True,
    audio_source: 'str | None' = None,
    max_seconds: 'int | None' = None
) → None
```

Begin recording this device with scrcpy on the server host. 

The server runs ``tap start --scrcpy PATH`` (default ``scrcpy`` on its ``PATH``). Defaults to silent H.264 MP4 video. Set ``audio_source`` to ``output`` (Android 11+, mutes device playback), ``playback`` (Android 13+, retains local audio but apps may opt out) or ``mic``. With both tracks the result is Matroska; ``video=False`` records Opus audio only. Video is limited to 30 seconds/16 MiB, audio only to 60 seconds/3 MiB, and ``max_seconds`` ends the capture early. One recording per attached device; detach discards an unfinished one. Capture begins shortly after this returns, and a capture the device refuses is reported by ``stop_recording``. 

---

### <kbd>method</kbd> `Device.stop_recording`

```python
stop_recording() → Recording
```

Stop and return a checksummed, bounded MP4, Matroska or Opus artifact. 

---

### <kbd>method</kbd> `Device.type_text`

```python
type_text(value: 'str', timeout: 'float | None' = None) → None
```

Type ``value`` as real key events into whatever has input focus now. 

No target and no click (``Element.type_text`` taps first). Unsupported characters are rejected before any input with ``INVALID_REQUEST``/``UNSUPPORTED_CHARACTERS``; otherwise it reports whether every key event was accepted. Where the characters landed is for the test to assert. 

---

### <kbd>method</kbd> `Device.unfreeze_rotation`

```python
unfreeze_rotation() → None
```

Hand rotation back to the device's sensor (auto-rotate), without choosing a rotation. 

---

### <kbd>method</kbd> `Device.wake`

```python
wake() → None
```

Turn the screen on (``KEYCODE_WAKEUP``; nothing happens when it is on). The keyguard may still show: see ``dismiss_keyguard``. 

## <kbd>class</kbd> `Timeouts`
Per-device defaults in seconds. Every call also accepts an explicit timeout. 

### <kbd>method</kbd> `Timeouts.__init__`

```python
__init__(
    action: 'float' = 10.0,
    wait: 'float' = 10.0,
    lifecycle: 'float' = 30.0,
    poll_interval: 'float' = 0.1
) → None
```

