Skip to content

Fingerprinting

Screen Fingerprinting

Screen fingerprinting generates stable identifiers for UI screens that remain consistent despite dynamic content changes, scrolling, keyboard appearance, and user interactions.

This strategy is critical for reliably identifying screens and building accurate navigation graphs across diverse scenarios.

Research Foundation

The implementation is based on extensive research testing multiple strategies across real-world scenarios:

  • 4 screen types tested (discover-tap, discover-swipe, discover-chat, discover-text)
  • 11 observations captured with varying states
  • 6 strategies evaluated
  • 100% success rate for non-keyboard scenarios achieved with shallow scrollable markers

Research findings documented in: - scratch/UPDATED_FINDINGS.md - Final strategy and results - scratch/FINDINGS.md - Initial research and strategy comparison - scratch/SCROLLABLE_TABS_FINDINGS.md - Critical scrollable tabs discovery


Tiered Fingerprinting Strategy

AutoMobile uses a tiered fallback approach with confidence levels:

Tier 1: Navigation Resource-ID (95% confidence)

When: SDK-instrumented apps with navigation.* resource-ids

How: Extract and hash the navigation resource-id

Example:

// Hierarchy contains:
{ "resource-id": "navigation.HomeDestination" }

// Fingerprint:
{
  hash: "abc123...",
  method: "navigation-id",
  confidence: 95,
  navigationId: "navigation.HomeDestination"
}

Advantages: - Perfect identifier for SDK apps - Immune to content changes - Very stable

Limitations: - Only works with AutoMobile SDK - Disappears when keyboard occludes app


Tier 2: Cached Navigation ID (85% confidence)

When: Keyboard detected + navigation ID was recently cached

How: Use cached navigation ID from previous observation (within TTL)

Example:

// Before keyboard: navigation.TextScreen visible
// Keyboard appears: only keyboard elements visible
// Use cached navigation.TextScreen (within 10 second TTL)

compute(hierarchyWithKeyboard, {
  cachedNavigationId: "navigation.TextScreen",
  cachedNavigationIdTimestamp: previousTimestamp
})

Advantages: - Handles keyboard occlusion gracefully - Maintains high confidence - Prevents false screen changes

Limitations: - Requires temporal tracking - Cache expires after TTL (default: 10 seconds)


Tier 3: Shallow Scrollable (75% confidence)

When: No navigation ID available, no keyboard detected

How: Enhanced hierarchy filtering with shallow scrollable markers

Strategy: 1. Shallow Scrollable Markers: Keep container metadata, drop all children 2. Selected State Preservation: Extract and preserve selected="true" items 3. Dynamic Content Filtering: Remove time, numbers, system UI 4. Static Text Inclusion: Keep labels and titles for differentiation

Example:

// Before filtering:
{
  "scrollable": "true",
  "resource-id": "tab_row",
  "node": [
    { "selected": "true", "node": { "text": "Home" } },
    { "selected": "false", "node": { "text": "Profile" } },
    { "selected": "false", "node": { "text": "Settings" } }
  ]
}

// After filtering (shallow marker + selected):
{
  "_scrollable": true,
  "resource-id": "tab_row",
  "_selected": [
    { "selected": "true", "text": "Home" }
  ]
}

Advantages: - Handles scrolling perfectly - Prevents tab collision (different screens with same structure) - Works for non-SDK apps - Reduces noise from dynamic content

Limitations: - Lower confidence than navigation ID - May struggle with very similar screens


Tier 4: Shallow Scrollable + Keyboard (60% confidence)

When: Keyboard detected, no cached navigation ID, no current navigation ID

How: Same as Tier 3 but with keyboard element filtering

Additional Filtering: - Remove nodes with keyboard indicators (Delete, Enter, emoji) - Filter keyboard and inputmethod resource-ids

Advantages: - Best effort for keyboard scenarios without cache - Still provides reasonable differentiation

Limitations: - Lowest confidence - May miss subtle screen differences


Key Features

1. Shallow Scrollable Markers

Problem: Scrolling changes visible content completely

Solution: Keep container, drop children

// Same screen, different scroll positions produce SAME fingerprint
Before scroll: button_regular, button_elevated, press_duration_tracker
After scroll:  filter_chip_1, icon_button_delete, slider_control

Both fingerprint to: hash(scrollable container metadata)

Impact: 100% success for scrolling scenarios


2. Selected State Preservation

Problem: Different tabs/screens have same structure but different selected state

Critical Fix: Preserve selected items even in scrollable containers

Example of Collision Prevention:

// Without selected state preservation - COLLISION
Home Screen:     scrollable tab_row  hash(container)
Settings Screen: scrollable tab_row  hash(container)
// Both get SAME fingerprint! ❌

// With selected state preservation - NO COLLISION
Home Screen:     scrollable + _selected: ["Home"]   hash1
Settings Screen: scrollable + _selected: ["Settings"]  hash2
// Different fingerprints! ✅

Impact: Prevents false positives in tab-based navigation


3. Keyboard Detection & Filtering

Indicators: - content-desc containing: Delete, Enter, keyboard, emoji, Shift - resource-id containing: keyboard, inputmethod

Actions:

flowchart LR
    A["Keyboard detected"] --> B["Set keyboardDetected = true"];
    B --> C["Filter keyboard elements
from hierarchy"]; C --> D["Attempt cached
navigation ID"]; D --> E["Degrade confidence level"]; classDef decision fill:#FF3300,stroke-width:0px,color:white; classDef logic fill:#525FE1,stroke-width:0px,color:white; classDef result stroke-width:0px; class A,E result; class B,C,D logic;

Impact: Graceful handling of keyboard occlusion


4. Editable Text Filtering

Detection: - className contains EditText - text-entry-mode="true" - editable="true" - resource-id contains: edit, input, text_field, search

Action: Omit text content from editable fields

Rationale: User input is dynamic and shouldn’t affect screen identity

Impact: Same screen despite different user input


5. Dynamic Content Filtering

Time Patterns: 8:55, 8:55 AM, 9:00 PM Number Patterns: 42, 100, 0 Percentage Patterns: 45%, 90%

System UI: - com.android.systemui:id/* resource-ids - android:id/* resource-ids - Battery/signal content-descriptions

Impact: Stable fingerprints despite constantly changing data


Computing a Fingerprint

import { ScreenFingerprint } from './features/navigation/ScreenFingerprint';

const result = ScreenFingerprint.compute(hierarchy, {
  cachedNavigationId: previousResult?.navigationId,
  cachedNavigationIdTimestamp: previousResult?.timestamp,
  cacheTTL: 10000 // optional, defaults to 10s
});

console.log(result.hash);        // SHA-256 fingerprint
console.log(result.confidence);  // 95, 85, 75, or 60
console.log(result.method);      // navigation-id, cached-navigation-id, etc.
console.log(result.keyboardDetected);

Stateful Tracking Pattern

class NavigationTracker {
  private lastFingerprint: FingerprintResult | null = null;

  async onHierarchyChange(hierarchy: AccessibilityHierarchy) {
    // Compute with cache
    const fingerprint = ScreenFingerprint.compute(hierarchy, {
      cachedNavigationId: this.lastFingerprint?.navigationId,
      cachedNavigationIdTimestamp: this.lastFingerprint?.timestamp,
    });

    // Check if screen changed
    if (!this.lastFingerprint || fingerprint.hash !== this.lastFingerprint.hash) {
      console.log('Screen changed!');
      this.onScreenChange(fingerprint);
    }

    // Cache for next observation
    if (fingerprint.navigationId) {
      this.lastFingerprint = fingerprint;
    }
  }
}