Screen States
Detect and respond to different UI states
Screen state detection is the foundation of robust automation. By identifying what's currently on screen, your automation can respond appropriately to each situation.
The ScreenState Pattern
Define all possible screen states as an enum:
export enum ScreenState { // System states Unknown = "Unknown", Crash = "Crash", PhoneDialog = "PhoneDialog", NoInternet = "NoInternet", NotificationShade = "NotificationShade",
// App states SplashScreen = "SplashScreen", LoginScreen = "LoginScreen", LoginEnterPassword = "LoginEnterPassword", HomeScreen = "HomeScreen", ProfileScreen = "ProfileScreen", SettingsScreen = "SettingsScreen",
// Dialog states PermissionDialog = "PermissionDialog", ConfirmDialog = "ConfirmDialog", ErrorDialog = "ErrorDialog",
// Loading states Loading = "Loading",
// Success/failure states Success = "Success", RateLimited = "RateLimited",}Detection Strategies
1. Text-Based Detection
The simplest approach - look for specific text on screen:
function detectScreenState(screen: AndroidNode): ScreenState { const allNodes = getAllNodes(screen);
// Check for specific text if (allNodes.find(n => n.text === "Sign in")) { return ScreenState.LoginScreen; }
if (allNodes.find(n => n.text === "Enter your password")) { return ScreenState.LoginEnterPassword; }
if (allNodes.find(n => n.text?.toLowerCase() === "home")) { return ScreenState.HomeScreen; }
return ScreenState.Unknown;}2. ID-Based Detection
More reliable - use resource IDs which don't change with language:
function detectScreenState(screen: AndroidNode): ScreenState { const allNodes = getAllNodes(screen);
// Check for specific view IDs if (findNodesById(screen, "com.example:id/login_form").length) { return ScreenState.LoginScreen; }
if (findNodesById(screen, "com.example:id/home_feed").length) { return ScreenState.HomeScreen; }
if (findNodesById(screen, "android:id/aerr_close").find(n => n.clickable)) { return ScreenState.Crash; }
return ScreenState.Unknown;}3. Composite Detection
Combine multiple conditions for accuracy:
function detectScreenState(screen: AndroidNode): ScreenState { const allNodes = getAllNodes(screen);
// Login screen: has email field AND sign-in button const hasEmailField = allNodes.find(n => n.className === "android.widget.EditText" && (n.hintText?.toLowerCase()?.includes("email") || n.viewId?.includes("email")) ); const hasSignInButton = allNodes.find(n => n.clickable && n.text?.toLowerCase() === "sign in" );
if (hasEmailField && hasSignInButton) { return ScreenState.LoginScreen; }
// Password screen: has password field (isPassword = true) const hasPasswordField = allNodes.find(n => n.className === "android.widget.EditText" && n.isPassword );
if (hasPasswordField && !hasEmailField) { return ScreenState.LoginEnterPassword; }
return ScreenState.Unknown;}4. Package Name Filtering
Filter by app package to avoid false positives from system UI:
const APP_PACKAGE = "com.example.myapp";
function detectScreenState(screen: AndroidNode): ScreenState { const allNodes = getAllNodes(screen);
// Only consider nodes from our target app const appNodes = allNodes.filter(n => n.packageName === APP_PACKAGE);
// Check for phone dialog (different package) if (allNodes.every(n => n.packageName === "com.android.phone")) { return ScreenState.PhoneDialog; }
// Check for system crash dialog if (allNodes.find(n => n.viewId === "android:id/aerr_close" && n.packageName === "android" )) { return ScreenState.Crash; }
// Now check app-specific screens if (appNodes.find(n => n.viewId?.includes("login"))) { return ScreenState.LoginScreen; }
return ScreenState.Unknown;}Complete Detection Function
import { ScreenState } from "./screenStates.js";
const APP_PACKAGE = "com.mysocial.app";
export function detectScreenState(screen: AndroidNode): ScreenState { const allNodes = getAllNodes(screen);
// === SYSTEM STATES (check first) ===
// Crash dialog if (findNodesById(screen, "android:id/aerr_close").find(n => n.clickable)) { return ScreenState.Crash; }
// Phone dialog if (allNodes.every(n => n.packageName === "com.android.phone")) { return ScreenState.PhoneDialog; }
// No internet if (allNodes.find(n => n.text?.toLowerCase()?.includes("no internet") || n.text?.toLowerCase()?.includes("you're offline") )) { return ScreenState.NoInternet; }
// Loading (only loading indicator visible) if (allNodes.length <= 3 && allNodes.find(n => n.className?.includes("ProgressBar") )) { return ScreenState.Loading; }
// === APP-SPECIFIC STATES ===
// Login screen if (allNodes.find(n => n.className === "android.widget.EditText" && n.hintText?.toLowerCase()?.includes("email") && n.packageName === APP_PACKAGE )) { return ScreenState.LoginScreen; }
// Password entry if (allNodes.find(n => n.isPassword && n.packageName === APP_PACKAGE )) { return ScreenState.LoginEnterPassword; }
// Home screen (has bottom navigation with Home selected) const homeTab = allNodes.find(n => n.description === "Home" && n.isSelected && n.packageName === APP_PACKAGE ); if (homeTab) { return ScreenState.HomeScreen; }
// Profile screen if (allNodes.find(n => n.description === "Profile" && n.isSelected && n.packageName === APP_PACKAGE )) { return ScreenState.ProfileScreen; }
// Permission dialog if (allNodes.find(n => n.packageName === "com.android.permissioncontroller" || (n.text?.toLowerCase()?.includes("allow") && n.clickable) )) { return ScreenState.PermissionDialog; }
return ScreenState.Unknown;}Handling Unknown States
Unknown screens will happen. Handle them gracefully:
let unknownScreenCount = 0;const MAX_UNKNOWN_SCREENS = 3;
async function getCurrentScreenState(): Promise<{ state: ScreenState; screen: AndroidNode;}> { let screen = await agent.actions.screenContent(); let state = detectScreenState(screen);
// Retry a few times if unknown if (state === ScreenState.Unknown) { for (let i = 0; i < 3; i++) { await sleep(2000); screen = await agent.actions.screenContent(); state = detectScreenState(screen);
if (state !== ScreenState.Unknown) { unknownScreenCount = 0; break; } } }
// Track consecutive unknown screens if (state === ScreenState.Unknown) { unknownScreenCount++; console.warn(`Unknown screen #${unknownScreenCount}`);
// Store for debugging await agent.utils.outOfSteps.storeScreen( screen, "unknown", "Unknown", MAX_UNKNOWN_SCREENS - unknownScreenCount, ScreenshotRecord.HIGH_QUALITY );
if (unknownScreenCount >= MAX_UNKNOWN_SCREENS) { throw new Error("Too many unknown screens"); } } else { unknownScreenCount = 0; }
return { state, screen };}Stage-Specific Detection
Different stages may need different detection logic:
import { Stage } from "./stages.js";import { ScreenState } from "./screenStates.js";
export function detectScreenState( screen: AndroidNode, currentStage: Stage): ScreenState { // Always check system states first const systemState = detectSystemStates(screen); if (systemState !== ScreenState.Unknown) { return systemState; }
// Stage-specific detection switch (currentStage) { case Stage.Login: return detectLoginStates(screen);
case Stage.NavigateToMessages: case Stage.ProcessMessages: return detectMessageStates(screen);
case Stage.NavigateToFeed: case Stage.LikePosts: return detectFeedStates(screen);
default: return detectCommonStates(screen); }}
function detectSystemStates(screen: AndroidNode): ScreenState { // ... crash, phone dialog, no internet}
function detectLoginStates(screen: AndroidNode): ScreenState { // ... login-specific screens}
function detectMessageStates(screen: AndroidNode): ScreenState { // ... message-specific screens}Best Practices
Check System States First
Always check for crashes, dialogs, and system UI before app-specific states. These can appear at any time and need immediate handling.
Use Multiple Conditions
Don't rely on a single text match. Combine multiple checks (text + ID + class + package) for reliable detection.
Order Matters
Check more specific states before general ones. For example, check "LoginEnterPassword" before "LoginScreen".
Filter by Package Name
Use package name filtering to avoid false positives from system UI, notifications, or other apps.
Store Unknown Screens
Use agent.utils.outOfSteps.storeScreen() for unknown states. This helps debug what screens you missed.