with one click
uiautomator-cross-app
// Cross-app UI testing with UI Automator — launching external apps, system UI interaction, and multi-app flow orchestration on Android.
// Cross-app UI testing with UI Automator — launching external apps, system UI interaction, and multi-app flow orchestration on Android.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | uiautomator-cross-app |
| description | Cross-app UI testing with UI Automator — launching external apps, system UI interaction, and multi-app flow orchestration on Android. |
| tech_stack | ["android"] |
| language | ["kotlin","java"] |
| capability | ["integration-testing","e2e-testing"] |
| version | androidx.test.uiautomator:uiautomator 2.4.0-alpha05 |
| collected_at | "2026-04-22T00:00:00.000Z" |
Source: https://developer.android.com/training/testing/other-components/ui-automator-legacy, https://developer.android.com/training/testing/other-components/ui-automator, https://developer.android.com/reference/androidx/test/uiautomator/package-summary
UI Automator is uniquely suited for cross-app testing because it operates at the system level through Android's accessibility framework — it can interact with visible elements on screen regardless of which Activity or app is in focus. Unlike Espresso (confined to the target app's process), UI Automator enables tests that launch external apps, interact with system UI, navigate between apps, and verify behavior across app boundaries.
1. Get UiDevice instance
2. Start from Home screen (known state)
3. Navigate to first app / system UI
4. Wait for app transition (Until.newWindow / Until.hasObject)
5. Interact across app boundaries
6. Verify state in target app
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 18)
public class CrossAppTest {
private UiDevice device;
@Before
public void setUp() {
device = UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation());
device.pressHome();
String launcherPkg = device.getLauncherPackageName();
device.wait(Until.hasObject(By.pkg(launcherPkg).depth(0)), 5000);
}
}
@Test
fun crossAppFlow() = uiAutomator {
startApp("com.example.sourceapp")
waitForAppToBeVisible("com.example.sourceapp")
// ... interact across apps ...
}
Intent-based launch — most flexible, works with any package:
Context ctx = InstrumentationRegistry.getInstrumentation().getTargetContext();
Intent intent = ctx.getPackageManager()
.getLaunchIntentForPackage("com.example.targetapp");
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); // critical!
ctx.startActivity(intent);
device.wait(Until.hasObject(By.pkg("com.example.targetapp").depth(0)), 5000);
Shell command — best for system components (Settings, dialer):
device.executeShellCommand("am start -a android.settings.SETTINGS");
device.executeShellCommand("am start -a android.settings.WIFI_SETTINGS");
device.executeShellCommand("am start -a android.settings.APPLICATION_SETTINGS");
Modern DSL (2.4.0+, preferred for new code) — must be inside uiAutomator { }:
startApp("com.example.targetapp") // by package name
startActivity(SettingsActivity::class.java) // by Activity class
startIntent(myIntent) // arbitrary Intent
clearAppData("com.example.targetapp") // reset app to fresh state
Scoping with By.pkg() — locate elements in ANY app on screen:
// Find launcher icon regardless of what app is foreground
UiObject2 icon = device.findObject(
By.pkg(device.getLauncherPackageName()).text("Settings"));
// Read system UI elements (SystemUI package)
UiObject2 clock = device.findObject(
By.res("com.android.systemui:id/clock"));
// Find element in a specific target app
UiObject2 button = device.findObject(
By.pkg("com.google.android.gm").text("Compose"));
Until.newWindow() — the primary mechanism for detecting app transitions:
// Launch an app and wait for its window to appear
UiObject2 gmail = device.findObject(By.text("Gmail"));
boolean opened = gmail.clickAndWait(Until.newWindow(), 3000);
// opened == false means timeout — the transition didn't happen
Until.hasObject(By.pkg(...)) — wait for a package's UI to exist:
device.wait(Until.hasObject(By.pkg("com.example.app").depth(0)), 5000);
// Open notification shade / Quick Settings
device.openNotification();
device.openQuickSettings();
// Open system Settings
device.executeShellCommand("am start -a android.settings.SETTINGS");
device.wait(Until.hasObject(By.pkg("com.android.settings").depth(0)), 5000);
// Interact with Quick Settings tiles (resource IDs vary by OEM)
device.openQuickSettings();
UiObject2 dndTile = device.findObject(
By.res("com.android.systemui:id/dnd_tile"));
if (dndTile != null) dndTile.click();
device.pressHome();
String launcherPkg = device.getLauncherPackageName(); // never hardcode!
device.wait(Until.hasObject(By.pkg(launcherPkg).depth(0)), 5000);
// Find app icon by text on launcher
UiObject2 appIcon = device.findObject(
By.pkg(launcherPkg).text("Calculator"));
if (appIcon != null) {
appIcon.clickAndWait(Until.newWindow(), 5000);
}
@Test
public void testShareToGmail() {
// 1. Launch source app
Context ctx = InstrumentationRegistry.getInstrumentation().getTargetContext();
Intent intent = ctx.getPackageManager()
.getLaunchIntentForPackage("com.example.sourceapp");
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
ctx.startActivity(intent);
device.wait(Until.hasObject(By.pkg("com.example.sourceapp").depth(0)), 5000);
// 2. Tap share
device.findObject(By.res("com.example.sourceapp:id/share_button")).click();
// 3. Select Gmail from share sheet
device.wait(Until.hasObject(By.text("Gmail")), 3000);
device.findObject(By.text("Gmail")).clickAndWait(Until.newWindow(), 5000);
// 4. Verify we landed in Gmail
UiObject2 composeField = device.findObject(
By.pkg("com.google.android.gm").textStartsWith("Subject"));
assertNotNull(composeField);
}
FLAG_ACTIVITY_CLEAR_TASK when launching via Intent — stale Activity instances from prior test runs will break Until.hasObject() expectations.Until.newWindow() is critical for app switches: A plain click() followed by wait() is unreliable — the new window may never appear or may appear after the wait expires. Always use clickAndWait(Until.newWindow(), timeout).device.getLauncherPackageName() — launcher packages differ across OEMs (e.g., com.google.android.apps.nexuslauncher vs com.miui.home).com.android.systemui:id/clock may change across Android versions and OEM skins. Fall back to text or contentDescription selectors when IDs fail.executeShellCommand() returns stdout only; it cannot detect command failure, handle input, or manage pipes/quotes reliably. Prefer Intent-based launch or modern DSL.waitForAppToBeVisible requires accessibility events: Some apps or custom views may not post accessibility events reliably, causing timeouts. Fall back to Until.hasObject() if needed.watchFor(PermissionDialog) (modern) or registerWatcher() (legacy) to handle them.@SdkSuppress(minSdkVersion = 18).device.pressHome(), use openNotification()/openQuickSettings(), and rely on executeShellCommand() for Settings — all from the UiDevice skill.By.pkg() to scope searches. For complex nested hierarchies, use hasChild()/hasDescendant() with BySelector.@Test methods that each test one app transition. Use clearAppData() between tests to reset target app state.