Device
One attached device backed by one live driver session, proxied by the host server. All calls are suspend. The client does not serialize calls on one device: issue them sequentially (the normal test style), and use coroutineScope + async for concurrency across devices (sibling failure cancels the other's in-flight RPC). Nothing here caches UI state: App.element and 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.
Every suspending call requires an owning scope (tapScope in scripts, tapTest in JUnit); construction (attachDevice) does too. Detaching is suspend (no AutoCloseable): callers use try/finally inside the scope. Per-call withDeadlineAfter is still applied server-side; caller cancellation promptly cancels the gRPC call client-side.
Command admission is linearized with detach: each operation is admitted under a short lock (closed/invalidated gates checked at admission, an in-flight counter incremented), runs its RPC without holding the lock, then releases the counter in a cancellation-safe finally. Admission is reentrant by counting, so compound helpers (awaitUntil, ElementWait, App calls that fan back into execute) never deadlock. Once detachAndReport starts, new operations are rejected locally with TapUsageException and the connection-invalid gate (ServerException UNAVAILABLE) applies at the same admission point; admitted operations finish before the single Detach RPC (120 s gRPC deadline, mapped). Concurrent and duplicate closes share one RPC and one result, including the quarantine detail.
Properties
True once detachAndReport started (new operations are rejected locally).
Functions
Adds a photo or video to the device's gallery and returns its device path: bytes are written as fileName (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) to /sdcard/Pictures/Tap/ or /sdcard/Movies/Tap/, and indexed by the media scanner so gallery apps and photo pickers list it (read back). A name already there is refused (FailureReason.DEVICE_FILE) unless this device handle added it. Deleted, and dropped from the index, on detach; nothing else in the gallery is touched.
Waits until a matching notification is active (one already posted counts) and returns the newest. title and text match with mode (any when null); packageName narrows it to one app's notifications (App.awaitNotification passes its own). Notifications are read as data from a notification listener in the Tap driver app, not from the shade, so nothing is opened on screen; the server gives the driver notification access for the session (taken back on detach). Group summaries are left out. Throws WaitTimeoutException (NO_NOTIFICATION) when none matches within timeout.
Waits until a runtime-permission dialog shows and returns the choices it offers. Buttons are recognised by the permission controller's resource ids, never by label or position. Throws WaitTimeoutException (NO_PERMISSION_PROMPT) when none appears within timeout.
Waits for a toast and returns it: one shown in the last 3.5 s (the longest a toast stays up) or arriving within timeout. text matches with mode (any text when null); a toast from any package counts unless packageName names one (App.awaitToast passes its own). Not consuming: the same toast can satisfy two calls in a row. Throws WaitTimeoutException (NO_TOAST) when none matches.
Host-side polling for conditions the driver cannot evaluate in one command (cross-device, backend state). Prefer App.await / Screen.await for UI conditions: they poll on the device in one RPC. Uses delay, so test-root cancellation and sibling failure cancel the poll promptly. Admitted like any command, so detachAndReport waits for an in-flight poll and rejects new polls once close starts; the condition runs inside the admission (reentrant), so device helpers called from it do not deadlock.
Takes a screenshot, the hierarchy, the device info and the driver log at once, each within timeout, and returns them as one Capture. Device failures never escape: a part that fails or runs out of time is null in the result, with its cause in Capture.failures. Only the caller's own cancellation propagates. Use it wherever a test wants evidence (after a step, in a catch); the JUnit extension and the pytest plugin call it for every failed test.
Taps 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, Android 12+ 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.
detachAndReport that fails when the device was quarantined.
Detaches the device. Returns the quarantine detail when the device could not be left clean (the pool keeps it out of circulation), else null. Single-flight: concurrent and duplicate callers share one Detach RPC (120 s gRPC deadline, mapped) and one shared completion, including the quarantine detail. Admitted operations finish before the RPC under an explicit total drain bound (DeviceBounds.drainMs); operations starting after detach began are rejected locally. A detach invoked from the same admitted operation (for example an awaitUntil condition calling detach) fails immediately with TapUsageException instead of waiting for itself. Runs under a bounded non-cancellable context so teardown completes even when the caller is cancelled.
Dismisses a keyguard that has no PIN, pattern or password (wm dismiss-keyguard); with no keyguard showing it sends nothing. A secure keyguard fails with ACTION_REJECTED / KEYGUARD_SECURE before any input: Tap never unlocks one. info reports the state.
Dismisses the one active notification that matches (as for awaitNotification) as a swipe does. No match or several matches throw CommandException (NOT_FOUND / AMBIGUOUS), and an ongoing notification ACTION_REJECTED (NOT_CLEARABLE), before anything changes.
The diagnostic accessibility Hierarchy. Never used by selectors; keep it out of assertions.
The activity on top of the screen (the resumed one, the focused one in multi-window), or null 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.
Hides 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 keyboardShown when it matters.
API level, model, display size and the package owning the focused window.
Whether a soft keyboard (any input method's window) is on screen; info reports the same.
The active notifications, newest first (group summaries left out), as awaitNotification reads them.
Opens the one active notification that matches (as for awaitNotification) as a tap on it in the shade does: sends its content intent and, when it auto-cancels, removes it. With action, presses its action button with exactly that title instead, and the notification stays. No match or several matches throw CommandException (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: foregroundActivity).
Opens the notification shade (the system's accessibility action, as a swipe down from the status bar would). Only whether the system accepted it is reported: wait for what the test needs in the shade, and pressBack closes it.
Opens the quick settings panel; otherwise as openNotifications.
Copies bytes to devicePath on the device (absolute and normalised, at most 512 MiB): /data/local/tmp/…, or shared storage (/sdcard/Download/…) for an app with storage access to read. The directory must exist. A file already there is refused (FailureReason.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.
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, so a corrupted transfer fails instead of producing a broken file. Keep it with Screenshot.save or use Screenshot.bytes directly.
Turns the accessibility display settings on or off until detach, see setAnimations; a null setting is left as it is. highContrastText draws text with an outline in black or white, colorInversion inverts the display's colors (screenshots stay uninverted: the inversion happens in the display pipeline), and boldText 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.highContrastText, DeviceInfo.colorInversion and DeviceInfo.boldText.
Turns the window, transition and animator animations off (all three scales 0) or on (all
Puts text on the device clipboard as plain text (at most 4096 characters).
Dark theme on or off (cmd uimode night) until detach, see setAnimations. API 29+: below, ServerException (FailureReason.UNSUPPORTED_API). Read back with DeviceInfo.darkMode.
Overrides the display density with dpi (100 to 1000), or with null goes back to the display's physical density, until detach, see setAnimations. Read back with DeviceInfo.densityDpi.
Rotates the display to the exact rotation and freezes it there; otherwise as setOrientation.
The system font scale, 0.5 to 2.0 (1.0 = default), until detach, see setAnimations. Read back with DeviceInfo.fontScale.
Mocks 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, accuracyM in meters (> 0; null = 5), altitudeM in meters (null = 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.
Switches airplane mode, Wi-Fi and mobile data on or off until detach, see setAnimations; a null switch is left 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 setNetwork(airplaneMode = 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 (FailureReason.DEVICE_SETTING): that would cut Tap off. Read back with DeviceInfo.airplaneMode, DeviceInfo.wifiEnabled and DeviceInfo.mobileDataEnabled.
Rotates the display to orientation geometry, whatever the device's natural orientation, and freezes it there until unfreezeRotation. 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, displayRotation). The rotation settings the device had before the session's first rotation call are restored when the device is detached.
Keeps the screen on while the device is plugged in (USB, AC or wireless), or lets it time out again, until detach, see setAnimations: 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.stayAwake.
Sets the device's languages (Settings › Languages) until detach, see setAnimations: 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.setLocales) 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 (FailureReason.DEVICE_SETTING when the device reports another list). Read back with DeviceInfo.systemLocales.
setSystemLocales with the tags as arguments.
Begin recording this device with scrcpy on the server host (tap start --scrcpy, default scrcpy on PATH). video defaults to silent H.264 MP4; audioSource may be output (Android 11+, redirects local playback), playback (Android 13+, keeps local sound but apps can opt out), or mic. Audio only (video = false) is Opus; both tracks are Matroska. Video is bounded to 30 seconds/16 MiB, audio only to 60 seconds/3 MiB, and maxSeconds 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 stopRecording.
Stop and return a checksummed Recording, formatted as MP4, Matroska or Opus.
Types value as real key events into whatever has input focus now: no target and no click (see Element.typeText for tap-then-type). 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.
Hands rotation back to the device's sensor (auto-rotate), without choosing a rotation.
Turns the screen on (KEYCODE_WAKEUP; nothing happens when it is on). The keyguard may still show: see dismissKeyguard.