Phone Harness
ShawnPana/phone-harness
Control the user's phone — an iPhone through the Mac's iPhone Mirroring window, an Android over adb, or a rented cloud Android: open apps, tap, type, swipe, read the screen.
A skill your agent uses to drive cross-app and system-UI flows from instrumentation tests using UiAutomator 2.3.0 — UiDevice, BySelector / UiObject2 (modern), UiSelector / UiObject (legacy), Until…
$ npx skills add skydoves/android-testing-skills --skill cross-app-tests-with-uiautomator -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install skydoves/android-testing-skills cross-app-tests-with-uiautomator --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/skydoves/android-testing-skills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/instrumentation/uiautomator/cross-app-tests-with-uiautomator .claude/skills/cross-app-tests-with-uiautomator && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "cross-app-tests-with-uiautomator" agent skill from https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/uiautomator/cross-app-tests-with-uiautomator into .claude/skills/cross-app-tests-with-uiautomator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "cross-app-tests-with-uiautomator", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/uiautomator/cross-app-tests-with-uiautomatorType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add skydoves/android-testing-skills --skill cross-app-tests-with-uiautomator -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install skydoves/android-testing-skills cross-app-tests-with-uiautomator --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/skydoves/android-testing-skills.git skills-src && mkdir -p .agents/skills && cp -r skills-src/instrumentation/uiautomator/cross-app-tests-with-uiautomator .agents/skills/cross-app-tests-with-uiautomator && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "cross-app-tests-with-uiautomator" agent skill from https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/uiautomator/cross-app-tests-with-uiautomator into .agents/skills/cross-app-tests-with-uiautomator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "cross-app-tests-with-uiautomator", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add skydoves/android-testing-skills --skill cross-app-tests-with-uiautomator -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install skydoves/android-testing-skills cross-app-tests-with-uiautomator --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/skydoves/android-testing-skills.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/instrumentation/uiautomator/cross-app-tests-with-uiautomator .cursor/skills/cross-app-tests-with-uiautomator && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "cross-app-tests-with-uiautomator" agent skill from https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/uiautomator/cross-app-tests-with-uiautomator into .cursor/skills/cross-app-tests-with-uiautomator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "cross-app-tests-with-uiautomator", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/skydoves/android-testing-skills.git --path instrumentation/uiautomator/cross-app-tests-with-uiautomator--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add skydoves/android-testing-skills --skill cross-app-tests-with-uiautomator -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install skydoves/android-testing-skills cross-app-tests-with-uiautomator --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/skydoves/android-testing-skills.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/instrumentation/uiautomator/cross-app-tests-with-uiautomator .gemini/skills/cross-app-tests-with-uiautomator && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "cross-app-tests-with-uiautomator" agent skill from https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/uiautomator/cross-app-tests-with-uiautomator into .gemini/skills/cross-app-tests-with-uiautomator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "cross-app-tests-with-uiautomator", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install skydoves/android-testing-skills cross-app-tests-with-uiautomatorInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add skydoves/android-testing-skills --skill cross-app-tests-with-uiautomator -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/skydoves/android-testing-skills.git skills-src && mkdir -p .github/skills && cp -r skills-src/instrumentation/uiautomator/cross-app-tests-with-uiautomator .github/skills/cross-app-tests-with-uiautomator && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "cross-app-tests-with-uiautomator" agent skill from https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/uiautomator/cross-app-tests-with-uiautomator into .github/skills/cross-app-tests-with-uiautomator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "cross-app-tests-with-uiautomator", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add skydoves/android-testing-skills --skill cross-app-tests-with-uiautomator -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install skydoves/android-testing-skills cross-app-tests-with-uiautomator --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/skydoves/android-testing-skills.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/instrumentation/uiautomator/cross-app-tests-with-uiautomator .opencode/skills/cross-app-tests-with-uiautomator && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "cross-app-tests-with-uiautomator" agent skill from https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/uiautomator/cross-app-tests-with-uiautomator into .opencode/skills/cross-app-tests-with-uiautomator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "cross-app-tests-with-uiautomator", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
cross-app-tests-with-uiautomatorA skill your agent uses to drive cross-app and system-UI flows from instrumentation tests using UiAutomator 2.3.0 — UiDevice, BySelector / UiObject2 (modern), UiSelector / UiObject (legacy), Until…
Cross App Tests With Uiautomator is an agent skill from skydoves/android-testing-skills. Use this skill to drive cross-app and system-UI flows from instrumentation tests using UiAutomator 2.3.0 — UiDevice, BySelector / UiObject2 (modern), UiSelector / UiObject (legacy), Until conditions, and Configurator global timeouts. Covers the singleton acquisition (UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())), pressBack/pressHome/pressRecentApps, device.findObject(By...) cached-node refresh and StaleObjectException recovery, the executeShellCommand @Discouraged path vs…
Its SKILL.md is about 4.6k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Mobile, covering Mobile testing and debugging. It works with Android. The repository describes itself as: ⚡️ A set of skills for Android testing: Compose UI, AndroidX Test, JVM unit tests, and ADB. The licence is Apache-2.0.
Read from SKILL.md and the folder at commit 8665ed5. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are kotlin and xml).
From the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
schemas.android.comAlso links to:
developer.android.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Cross App Tests With Uiautomator loads about 4.6k tokens when it runs. Until then it costs about 221 tokens; SKILL.md has 1,133 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from skydoves/android-testing-skills at commit 8665ed5, republished under its Apache-2.0 licence (© skydoves). 1,133 words, ~4,623 tokens.
.claude/skills/cross-app-tests-with-uiautomator/SKILL.md (or your agent's skills folder).UiAutomator is the only AndroidX test framework that can drive UI across process boundaries — Settings, the dialer, system notifications, another app's activity. Espresso refuses (InjectEventSecurityException) when an event would cross to a foreign window. UiAutomator works against the platform AccessibilityNodeInfo tree, so it sees every window. This skill encodes the modern BySelector / UiObject2 API, the singleton acquisition, the StaleObjectException recovery pattern, and the cross-app launch path.
InjectEventSecurityException because the test reached a foreign window.StaleObjectException and is debugging cached-node lifetime.BySelector and UiSelector in the same test and is confused which is current.../../espresso/writing-espresso-tests/SKILL.md for Views.../../runner/running-instrumented-tests-with-androidjunit4/SKILL.md.../../../adb/control/injecting-input-and-state/SKILL.md.../../../adb/observability/extracting-logs-with-logcat/SKILL.md.../../runner/running-instrumented-tests-with-androidjunit4/SKILL.md.androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0").accessibility_service available (the default).Context.startActivity to a foreign package on API 30+, a <queries> entry in the test APK manifest naming the target package or intent action.UiDevice once per test, via the singleton with explicit instrumentation. The no-arg getInstance() is @Deprecated (R4 lines 56-58) — it throws IllegalStateException if not previously initialized:import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
private lateinit var device: UiDevice
@Before fun setUp() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}UiDevice methods. Source: R4 lines 88-104.device.pressBack()
device.pressHome()
device.pressMenu()
device.pressRecentApps() // GLOBAL_ACTION_RECENTS
device.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN)
device.openNotification() // GLOBAL_ACTION_NOTIFICATIONS
device.openQuickSettings() // GLOBAL_ACTION_QUICK_SETTINGS
device.wakeUp() // power-on if off
device.sleep() // power-off if onBySelector (modern) via the By factory. Selectors are immutable-ish builders; the package-private constructor enforces use of the static factories. Criteria are joined as implicit AND. Source: R4 lines 185-258.import androidx.test.uiautomator.By
By.text("Submit") // exact, case-sensitive
By.textContains("ubmi") // case-sensitive substring
By.textMatches("Sub.*") // Pattern.compile regex
By.desc("Open menu") // content description
By.res("com.example.app", "submit_btn") // package + res id
By.res("com.example.app:id/submit_btn") // qualified res id
By.clazz("android.widget.Button") // fully-qualified, or ".widget.Button" auto-prefixed with "android"
By.clazz(android.widget.Button::class.java)
By.pkg("com.android.settings")
By.depth(2) // depth from window root
By.checkable(true)
By.hasChild(By.text("Inner")) // sugar for hasDescendant(child, 1)
By.hasDescendant(By.text("anywhere"))
By.hasParent(By.clazz("...LinearLayout"))
By.hasAncestor(By.res("...container"))UiObject2 via device.findObject(...) and act. findObject returns null when no match (NEVER throws). For interactions, prefer device.wait(Until.findObject(By...), timeoutMillis) so the lookup is robust to slow renders:import androidx.test.uiautomator.Until
val submit = device.wait(Until.findObject(By.res("com.example", "submit")), 5_000)
?: error("Submit not found within 5s")
submit.click()
submit.longClick()
submit.setText("hello")
submit.clear()
submit.swipe(Direction.LEFT, 0.8f) // percent ∈ [0,1]
submit.scroll(Direction.DOWN, 1.0f)
submit.scrollUntil(Direction.DOWN, Until.findObject(By.text("Bottom")))
submit.pinchOpen(0.5f)
submit.pinchClose(0.5f)UiObject2 caches an AccessibilityNodeInfo. Each action calls getAccessibilityNodeInfo() which calls device.waitForIdle() then mCachedNode.refresh(); on failure it runs registered UiWatchers, retries once, and on a second failure throws StaleObjectException (R4 lines 281-286, 622-637).
Until conditions. Three condition flavors per R4 lines 495-540:// SearchCondition (consume the device tree)
device.wait(Until.hasObject(By.text("Loaded")), 5_000)
device.wait(Until.gone(By.text("Loading")), 5_000)
val obj: UiObject2? = device.wait(Until.findObject(By.text("OK")), 5_000)
val list: List<UiObject2>? = device.wait(Until.findObjects(By.clazz("...Button")), 3_000)
// UiObject2Condition (consume a UiObject2 — for state predicates)
val checked = button.wait(Until.checked(true), 5_000)
// EventCondition (consume AccessibilityEvents — for window transitions)
val newWindowFired = device.performActionAndWait(
{ button.click() }, Until.newWindow(), 10_000,
)StaleObjectException by re-finding. StaleObjectException extends RuntimeException — unchecked, must be handled when holding a long-lived UiObject2:fun clickRetry(selector: BySelector, timeoutMillis: Long = 5_000) {
val obj = device.wait(Until.findObject(selector), timeoutMillis)
?: error("Not found: $selector")
try {
obj.click()
} catch (e: StaleObjectException) {
device.wait(Until.findObject(selector), timeoutMillis)?.click()
?: throw e
}
}Better: re-look-up before each interaction instead of holding a long-lived reference. Common causes: activity transition, Compose recomposition replacing semantic nodes, RecyclerView recycle, configuration change (R4 lines 639-645).
Context.startActivity over executeShellCommand. executeShellCommand is @Discouraged per R4 lines 147-151 (UiDevice.java:1432); the Javadoc recommends UiAutomation.executeShellCommandRwe for stdin/stderr. For routine app launches, use the in-process intent route plus a <queries> manifest entry on API 30+:<!-- src/androidTest/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<package android:name="com.android.settings" />
<intent>
<action android:name="android.settings.SETTINGS" />
</intent>
</queries>
<!-- ... -->
</manifest>val ctx = ApplicationProvider.getApplicationContext<Context>()
ctx.startActivity(
Intent(android.provider.Settings.ACTION_WIFI_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
)
device.wait(Until.hasObject(By.pkg("com.android.settings").depth(0)), 10_000)If the user really needs the shell route (e.g. a deep-link no Intent resolves):
device.executeShellCommand("am start -n com.android.settings/.Settings")
device.wait(Until.hasObject(By.pkg("com.android.settings")), 5_000)Configurator global timeouts when needed; restore on teardown. Settings are process-wide. Defaults (R4 lines 550-558): wait-for-idle 10s, wait-for-selector 10s (legacy UiObject only), action-acknowledgment 3s, scroll-event-wait 1s.import androidx.test.uiautomator.Configurator
@Before fun setUp() {
val c = Configurator.getInstance()
originalIdle = c.waitForIdleTimeout
c.waitForIdleTimeout = 5_000 // tighter for fast tests
}
@After fun tearDown() {
Configurator.getInstance().waitForIdleTimeout = originalIdle
}setKeyInjectionDelay is @Deprecated ("This parameter is no longer used (text is set directly rather than by key)." — R4 lines 575-577). Do not call it.
9. Recognize when to fall back to legacy UiSelector / UiObject. UiSelector re-resolves the node on every action via Configurator.getWaitForSelectorTimeout(), so it cannot go stale, but every call is more expensive and uses the checked UiObjectNotFoundException. The modern BySelector / UiObject2 is the recommended default. Use legacy only for interop with old test code or when re-resolution semantics are mandatory. R4 lines 594-619.
10. Never call findObject(By.text("...")) immediately after launch and assume it sees the target window. By.text matches whatever is on screen now, which may be a transient splash or the previous app. Always combine with By.pkg(...) or wait explicitly:
device.pressHome()
ctx.startActivity(Intent(Settings.ACTION_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
device.wait(Until.hasObject(By.pkg("com.android.settings")), 5_000) // gate on package
device.wait(Until.findObject(By.text("Network & internet")), 5_000)?.click()// WRONG
device.findObject(By.text("Settings")).click()
// WRONG because: at the moment of the call, By.text("Settings") matches whatever text reads
// "Settings" anywhere on the current screen — that might be a launcher tile, a notification,
// or a previous-app menu item. The test silently navigates to the wrong place.// RIGHT
device.pressHome()
device.executeShellCommand("am start -a android.settings.SETTINGS")
device.wait(Until.hasObject(By.pkg("com.android.settings")), 5_000)
device.wait(Until.findObject(By.text("Network & internet")), 5_000)?.click()UiDevice.getInstance() no-arg// WRONG
val device = UiDevice.getInstance()
// WRONG because: UiDevice.java:344 — the no-arg getInstance() is @Deprecated and throws
// IllegalStateException if the singleton has not been previously initialized with the
// instrumentation argument. Setting it implicitly via androidTest is unreliable across
// runner versions.// RIGHT
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())UiObject2 across a transition// WRONG
val tile = device.wait(Until.findObject(By.descContains("Wi-Fi")), 5_000)
device.openQuickSettings()
tile.click() // throws StaleObjectException — quick settings replaced the layout// RIGHT — re-acquire after the transition
device.openQuickSettings()
val tile = device.wait(Until.findObject(By.descContains("Wi-Fi").clickable(true)), 5_000)
tile?.click()
device.pressBack()device.openNotification()
val shade = device.wait(
Until.findObject(By.res("com.android.systemui:id/notification_stack_scroller")),
5_000,
) ?: error("Notification shade not visible")
for (n in shade.findObjects(By.res("com.android.systemui:id/notification_layout"))) {
n.swipe(Direction.RIGHT, 1.0f)
}
device.pressBack()@Before fun launchAppFromHome() {
device.pressHome()
val ctx = ApplicationProvider.getApplicationContext<Context>()
val launch = ctx.packageManager.getLaunchIntentForPackage("com.example.app")!!
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
ctx.startActivity(launch)
device.wait(Until.hasObject(By.pkg("com.example.app").depth(0)), 5_000)
}UiDevice via UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()). The no-arg getInstance() is @Deprecated and unreliable.BySelector / UiObject2 over UiSelector / UiObject for new tests. Legacy is for interop only.device.wait(Until.hasObject(By.pkg("...")), timeout) before resolving inner elements after a cross-window change. Bare findObject runs against the current window without gating.StaleObjectException (re-find via device.wait(Until.findObject(By...), timeout)) when keeping a UiObject2 reference across recompose, transition, or recyclerview recycle.Configurator settings in @After if a test mutated them. Settings are process-wide and leak across tests.Configurator.setKeyInjectionDelay — @Deprecated and a no-op.executeShellCommand for routine app launches — it is @Discouraged. Use Context.startActivity plus a <queries> manifest entry on API 30+.device.findObject(By.text("...")) without gating on package — text matches the current window contents, which may be a transient splash.device.wait(Until.findObject(...), timeout)?.click() rather than calling findObject then click two lines apart. The two-step form races with rendering.<queries> for any foreign package the test launches — silent failures on API 30+ otherwise.UiDevice.getInstance() with no arguments.Configurator.setKeyInjectionDelay.UiObject2 reference has a try { ... } catch (StaleObjectException) around its action — or the test re-finds before each interaction.<queries> entry for that package or its intent action in src/androidTest/AndroidManifest.xml../gradlew :<module>:connectedDebugAndroidTest runs without IllegalStateException from UiDevice.getInstance() and without unhandled StaleObjectException propagating to the test result.findObject(By.text(...)) is called immediately after a cross-window transition without a preceding device.wait(Until.hasObject(By.pkg(...)), ...).Configurator was mutated in @Before, an @After restores the original values.androidx/test/uiautomator/UiDevice.java — the singleton entry point. Lines 344 (@Deprecated no-arg getInstance), 357 (current getInstance(Instrumentation)), 1432 (@Discouraged executeShellCommand).androidx/test/uiautomator/By.java — static factory for BySelector. Source for text/desc/res/clazz/pkg/depth/checkable/hasChild/hasDescendant/hasParent/hasAncestor.androidx/test/uiautomator/BySelector.java (838 lines) — full criteria catalog. Lines 128 (auto-prefix .widget.Button with android), 198/211/224 (regex / contains / starts-with conventions), 605-650 (maxDepth), 660 (displayId).androidx/test/uiautomator/UiObject2.java (1152 lines) — click, setText, swipe, scroll, scrollUntil, pinchOpen/Close, fling. Line 1031 — getAccessibilityNodeInfo() throws StaleObjectException after refresh failure + watcher retry.androidx/test/uiautomator/Until.java lines 43-456 — hasObject, gone, findObject, findObjects, newWindow, scrollFinished, plus UiObject2Condition predicates.androidx/test/uiautomator/Configurator.java lines 36 (warning to restore), 108 (waitForSelector legacy-only), 163 (actionAcknowledgment legacy-only), 189 (setKeyInjectionDelay @Deprecated), 295 (setDefaultDisplayId).androidx/test/uiautomator/StaleObjectException.java — extends RuntimeException.tasks/research/R4-uiautomator.md — full UiAutomator 2.3.0 deep-dive. Lines 56-58 (deprecated singleton), 185-258 (BySelector catalog), 268-369 (UiObject2 actions), 495-540 (Until conditions), 594-619 (modern vs legacy table), 622-662 (StaleObjectException recovery).docs/CORPUS.md Section H.6 — UiAutomator essentials and the StaleObjectException recovery summary.© skydoves, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in instrumentation/uiautomator/cross-app-tests-with-uiautomator of skydoves/android-testing-skills.
Open the folder on GitHubat commit 8665ed5
Cross App Tests With Uiautomator next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Cross App Tests With Uiautomator this skillskydoves/android-testing-skills | 334 | — | ~4.6k | Automated safety check: Pass | Apache-2.0 | |
| Phone HarnessShawnPana/phone-harness | 3.2k | — | ~4.3k | Automated safety check: Pass | MIT | |
| Mobile QAtloncorp/tlon-apps | 107 | — | ~2.4k | Automated safety check: Pass | MIT | |
| Androidyang1ming/android-harness | 176 | — | ~259 | Automated safety check: Pass | MIT | |
| Debug Receiverstimusus/Shuttle2 | 229 | — | ~1.9k | Automated safety check: Pass | Apache-2.0 | |
| Debug Bridgegetknit/knit | 133 | — | ~2.3k | Automated safety check: Pass | GPL-3.0 |
ShawnPana/phone-harness
Control the user's phone — an iPhone through the Mac's iPhone Mirroring window, an Android over adb, or a rented cloud Android: open apps, tap, type, swipe, read the screen.
tloncorp/tlon-apps
Run a mobile QA checklist on a physical Android device over adb for tlon-apps, then triage what fails into fixes.
yang1ming/android-harness
Direct Android device control through ADB. An agent skill from yang1ming/android-harness.
timusus/Shuttle2
Drive the S2 debug build's playback and queue over ADB broadcasts — play the whole library, play/pause, skip, seek, remove a queue item, toggle shuffle/repeat, dump playback state as JSON, reimport…
getknit/knit
Drive and verify Knit on a device or emulator through the headless debug bridge (am broadcast to app.getknit.knit.debug.<ACTION, replies as JSON) — send a message on one phone and confirm it landed…
Moustachauve/WLED-Android
Production-ready scripts for Android app testing, building, and automation.
skydoves/android-testing-skills
A skill your agent uses to verify Compose layout measurements from a UI test using assertWidthIsEqualTo, assertHeightIsEqualTo, assertWidthIsAtLeast, assertHeightIsAtLeast…
skydoves/android-testing-skills
A skill your agent uses to verify a Compose semantics node's properties from a UI test using assertExists, assertDoesNotExist, assertIsDisplayed, assertIsNotDisplayed, assertIsDeactivated…
skydoves/android-testing-skills
A skill your agent uses to render every Jetpack Compose @Preview as a screenshot on a real Android device or emulator and publish a browsable HTML catalog from CI.
skydoves/android-testing-skills
A skill your agent uses to capture visual artefacts from a device for test failures, golden image generation, QA repro, and demo videos.
skydoves/android-testing-skills
A skill your agent uses to pick the correct Compose UI test entry point.
skydoves/android-testing-skills
A skill your agent uses to pick which behaviors to cover in an Android test suite using Google's five-category state vocabulary plus the explicit "what NOT to test" list from…
Works with
Categories
A skill your agent uses to drive cross-app and system-UI flows from instrumentation tests using UiAutomator 2.3.0 — UiDevice, BySelector / UiObject2 (modern), UiSelector / UiObject (legacy), Until…. Cross App Tests With Uiautomator is an agent skill from skydoves/android-testing-skills.0 — UiDevice, BySelector / UiObject2 (modern), UiSelector / UiObject (legacy), Until conditions, and Configurator global timeouts.
Cross App Tests With Uiautomator fits situations like: drive cross-app and system-UI flows from instrumentation tests using UiAutomator 2.3.0 — UiDevice; bySelector / UiObject2 (modern); uiSelector / UiObject (legacy); until conditions.
Run `npx skills add skydoves/android-testing-skills --skill cross-app-tests-with-uiautomator -a claude-code`. Or copy the skill folder (instrumentation/uiautomator/cross-app-tests-with-uiautomator in skydoves/android-testing-skills) into .claude/skills/cross-app-tests-with-uiautomator in your project. Claude Code loads it when a task matches its description.
Run `npx skills add skydoves/android-testing-skills --skill cross-app-tests-with-uiautomator -a codex`. Or copy the skill folder (instrumentation/uiautomator/cross-app-tests-with-uiautomator in skydoves/android-testing-skills) into .agents/skills/cross-app-tests-with-uiautomator in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add skydoves/android-testing-skills --skill cross-app-tests-with-uiautomator -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/cross-app-tests-with-uiautomator, .gemini/skills/cross-app-tests-with-uiautomator, .github/skills/cross-app-tests-with-uiautomator and .opencode/skills/cross-app-tests-with-uiautomator in your project.
SKILL.md names no scripts, command-line tools or credentials: Cross App Tests With Uiautomator is instructions for the agent only.
SKILL.md names 2 domains. In commands or code: schemas.android.com; the agent is likely to contact it when it follows the instructions. As links in the text: developer.android.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Cross App Tests With Uiautomator is published under the Apache-2.0 licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.
About 4.6k tokens (SKILL.md is roughly 18k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Cross App Tests With Uiautomator: Phone Harness (ShawnPana/phone-harness, 3.2k stars), Mobile QA (tloncorp/tlon-apps, 107 stars), Android (yang1ming/android-harness, 176 stars) and Debug Receivers (timusus/Shuttle2, 229 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
skydoves (a GitHub user) maintains it in skydoves/android-testing-skills, which has 334 GitHub stars. The repository holds 50 skills in this directory. The repository was last updated on May 25, 2026.
Source: skydoves/android-testing-skills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.