[PM-34487] llm: Add Android device interaction MCP server with ADB tooling (#6747)

This commit is contained in:
Patrick Honkonen
2026-05-07 11:18:42 -04:00
committed by GitHub
parent 7fd63f7a06
commit 340c585a99
26 changed files with 2670 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
# Dependencies
node_modules/
package-lock.json
# Build output
build/
dist/
*.js
*.js.map
*.d.ts
*.d.ts.map
# Keep source TypeScript files
!src/**/*.ts
# Environment variables
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Logs
*.log
# Testing
coverage/
# Temporary files
tmp/
temp/

View File

@@ -0,0 +1,34 @@
{
"name": "@bitwarden/android-device-mcp",
"version": "1.0.0",
"description": "MCP server for Android device interaction via ADB — UI hierarchy capture, element finding with obstruction detection, tap, and navigation",
"type": "module",
"main": "build/index.js",
"bin": {
"android-device-mcp": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod +x build/index.js",
"watch": "tsc --watch",
"dev": "tsc && node build/index.js",
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
"test": "vitest run",
"test:watch": "vitest"
},
"keywords": ["mcp", "android", "adb", "model-context-protocol", "ui-testing"],
"author": "Bitwarden",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "1.27.1",
"fast-xml-parser": "^4.5.0",
"zod": "3.24.2"
},
"devDependencies": {
"@types/node": "20.19.35",
"typescript": "5.8.3",
"vitest": "3.1.1"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@@ -0,0 +1,60 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('node:fs', () => ({
existsSync: vi.fn(() => false),
}));
vi.mock('node:child_process', () => ({
execFileSync: vi.fn(() => { throw new Error('not found'); }),
execFile: vi.fn(),
}));
import { existsSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { findAdb, _resetCache } from './adb.js';
const mockExistsSync = vi.mocked(existsSync);
const mockExecFileSync = vi.mocked(execFileSync);
describe('findAdb', () => {
beforeEach(() => {
vi.clearAllMocks();
_resetCache();
// Default: which fails, nothing on disk
mockExecFileSync.mockImplementation(() => { throw new Error('not found'); });
mockExistsSync.mockReturnValue(false);
});
it('finds adb in PATH via which', () => {
mockExecFileSync.mockReturnValue('/usr/local/bin/adb\n' as any);
expect(findAdb()).toBe('/usr/local/bin/adb');
});
it('finds adb in Android SDK location', () => {
mockExistsSync.mockImplementation((path) =>
String(path).includes('Library/Android/sdk'),
);
expect(findAdb()).toContain('Library/Android/sdk/platform-tools/adb');
});
it('finds adb in /usr/local/bin', () => {
mockExistsSync.mockImplementation((path) =>
String(path) === '/usr/local/bin/adb',
);
expect(findAdb()).toBe('/usr/local/bin/adb');
});
it('throws when adb not found anywhere', () => {
expect(() => findAdb()).toThrow('ADB not found');
});
it('caches the result after first discovery', () => {
mockExistsSync.mockImplementation((path) =>
String(path) === '/usr/local/bin/adb',
);
findAdb();
findAdb();
// existsSync only called during first discovery, cached after
expect(mockExistsSync).toHaveBeenCalledTimes(2); // SDK path + /usr/local/bin
});
});

View File

@@ -0,0 +1,141 @@
/**
* ADB client wrapper using child_process.execFile for safe command execution.
* Uses execFile (not exec) to prevent shell injection — arguments are passed
* as an array, never interpolated into a shell string.
*/
import { execFile as execFileCb, execFileSync } from 'node:child_process';
import { promisify } from 'node:util';
import { existsSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
const execFile = promisify(execFileCb);
let cachedAdbPath: string | null = null;
/** Clear the cached ADB path (for testing). */
export function _resetCache(): void {
cachedAdbPath = null;
}
/**
* Discover ADB binary location.
* Checks: PATH → ~/Library/Android/sdk/platform-tools/adb → /usr/local/bin/adb
*/
export function findAdb(): string {
if (cachedAdbPath) return cachedAdbPath;
// Check PATH via `which`
try {
const result = execFileSync('which', ['adb'], { encoding: 'utf-8' }).trim();
if (result) {
cachedAdbPath = result;
return result;
}
} catch {
// Not in PATH, try common locations
}
const candidates = [
join(homedir(), 'Library', 'Android', 'sdk', 'platform-tools', 'adb'),
'/usr/local/bin/adb',
];
for (const candidate of candidates) {
if (existsSync(candidate)) {
cachedAdbPath = candidate;
return candidate;
}
}
throw new Error(
'ADB not found. Install the Android SDK or add platform-tools to PATH.',
);
}
/**
* Execute an ADB command and return stdout.
*/
export async function exec(args: string[]): Promise<string> {
const adb = findAdb();
const { stdout } = await execFile(adb, args, {
maxBuffer: 10 * 1024 * 1024, // 10MB for large dumps
encoding: 'utf-8',
});
return stdout;
}
/**
* Execute an ADB shell command.
*/
export async function shell(command: string): Promise<string> {
return exec(['shell', command]);
}
/**
* Dump UI hierarchy to device, then pull to local path.
*/
export async function dumpHierarchy(outputPath: string): Promise<void> {
await shell('uiautomator dump /sdcard/view.xml');
await exec(['pull', '/sdcard/view.xml', outputPath]);
}
/**
* Capture screenshot to device, then pull to local path.
*/
export async function screenshot(outputPath: string): Promise<void> {
await shell('screencap -p /sdcard/screen.png');
await exec(['pull', '/sdcard/screen.png', outputPath]);
}
/**
* Tap at screen coordinates.
*/
export async function tap(x: number, y: number): Promise<void> {
await shell(`input tap ${Math.floor(x)} ${Math.floor(y)}`);
}
/**
* Send a key event.
*/
export async function keyevent(code: number): Promise<void> {
await shell(`input keyevent ${code}`);
}
/**
* Perform a swipe gesture.
*/
export async function swipe(
x1: number,
y1: number,
x2: number,
y2: number,
durationMs: number,
): Promise<void> {
await shell(`input swipe ${x1} ${y1} ${x2} ${y2} ${durationMs}`);
}
/**
* Get screen dimensions.
*/
export async function getScreenSize(): Promise<{ width: number; height: number }> {
const output = await shell('wm size');
const match = output.match(/(\d+)x(\d+)/);
if (!match) throw new Error(`Could not parse screen size from: ${output}`);
return { width: parseInt(match[1], 10), height: parseInt(match[2], 10) };
}
/**
* Get raw dumpsys window windows output.
*/
export async function dumpsysWindows(): Promise<string> {
return shell('dumpsys window windows');
}
/**
* Wait for a specified duration (seconds).
*/
export function sleep(seconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}

View File

@@ -0,0 +1,54 @@
/**
* Geometric primitives for UI element bounds and point operations.
*/
export interface Point {
x: number;
y: number;
}
export interface Rect {
left: number;
top: number;
right: number;
bottom: number;
}
export function center(r: Rect): Point {
return {
x: Math.floor((r.left + r.right) / 2),
y: Math.floor((r.top + r.bottom) / 2),
};
}
export function area(r: Rect): number {
const w = r.right - r.left;
const h = r.bottom - r.top;
return w > 0 && h > 0 ? w * h : 0;
}
export function containsPoint(r: Rect, p: Point): boolean {
return p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom;
}
export function overlaps(a: Rect, b: Rect): boolean {
return !(a.left >= b.right || a.right <= b.left || a.top >= b.bottom || a.bottom <= b.top);
}
export function boundsEqual(a: Rect, b: Rect): boolean {
return a.left === b.left && a.top === b.top && a.right === b.right && a.bottom === b.bottom;
}
/**
* Parse Android bounds string "[left,top][right,bottom]" into a Rect.
*/
export function parseBounds(bounds: string): Rect | null {
const match = bounds.match(/\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/);
if (!match) return null;
return {
left: parseInt(match[1], 10),
top: parseInt(match[2], 10),
right: parseInt(match[3], 10),
bottom: parseInt(match[4], 10),
};
}

View File

@@ -0,0 +1,334 @@
import { describe, it, expect } from 'vitest';
import {
center,
area,
containsPoint,
overlaps,
boundsEqual,
parseBounds,
type Rect,
} from './bounds.js';
import { largestVisibleStrip } from './visible-region.js';
import { detectObstruction } from './obstruction.js';
import type { UiNode } from '../parsers/xml.js';
import type { WindowInfo } from '../parsers/dumpsys.js';
describe('bounds', () => {
const rect: Rect = { left: 100, top: 200, right: 500, bottom: 600 };
describe('center', () => {
it('returns the center point of a rect', () => {
expect(center(rect)).toEqual({ x: 300, y: 400 });
});
it('floors fractional centers', () => {
expect(center({ left: 0, top: 0, right: 101, bottom: 101 })).toEqual({ x: 50, y: 50 });
});
});
describe('area', () => {
it('computes area of a valid rect', () => {
expect(area(rect)).toBe(400 * 400);
});
it('returns 0 for zero-width rect', () => {
expect(area({ left: 100, top: 200, right: 100, bottom: 600 })).toBe(0);
});
it('returns 0 for inverted rect', () => {
expect(area({ left: 500, top: 200, right: 100, bottom: 600 })).toBe(0);
});
});
describe('containsPoint', () => {
it('returns true for point inside rect', () => {
expect(containsPoint(rect, { x: 300, y: 400 })).toBe(true);
});
it('returns true for point on edge', () => {
expect(containsPoint(rect, { x: 100, y: 200 })).toBe(true);
expect(containsPoint(rect, { x: 500, y: 600 })).toBe(true);
});
it('returns false for point outside rect', () => {
expect(containsPoint(rect, { x: 50, y: 400 })).toBe(false);
expect(containsPoint(rect, { x: 300, y: 700 })).toBe(false);
});
});
describe('overlaps', () => {
it('returns true for overlapping rects', () => {
expect(overlaps(rect, { left: 400, top: 500, right: 700, bottom: 800 })).toBe(true);
});
it('returns false for non-overlapping rects', () => {
expect(overlaps(rect, { left: 600, top: 200, right: 800, bottom: 600 })).toBe(false);
});
it('returns false for adjacent rects (touching edges)', () => {
expect(overlaps(rect, { left: 500, top: 200, right: 700, bottom: 600 })).toBe(false);
});
});
describe('boundsEqual', () => {
it('returns true for identical rects', () => {
expect(boundsEqual(rect, { ...rect })).toBe(true);
});
it('returns false for different rects', () => {
expect(boundsEqual(rect, { ...rect, right: 501 })).toBe(false);
});
});
describe('parseBounds', () => {
it('parses Android bounds string', () => {
expect(parseBounds('[100,200][500,600]')).toEqual(rect);
});
it('parses zero-origin bounds', () => {
expect(parseBounds('[0,0][1080,2400]')).toEqual({
left: 0,
top: 0,
right: 1080,
bottom: 2400,
});
});
it('parses bounds with negative origin (partially off-screen element)', () => {
expect(parseBounds('[-40,-20][1040,100]')).toEqual({
left: -40,
top: -20,
right: 1040,
bottom: 100,
});
});
it('returns null for invalid format', () => {
expect(parseBounds('invalid')).toBeNull();
expect(parseBounds('[100,200]')).toBeNull();
});
});
});
describe('visible-region', () => {
// Target element: a list row spanning most of the screen width
const target: Rect = { left: 42, top: 1855, right: 1038, bottom: 2025 };
describe('largestVisibleStrip', () => {
it('returns null when fully obscured', () => {
const obstructor: Rect = { left: 0, top: 1800, right: 1080, bottom: 2100 };
expect(largestVisibleStrip(target, obstructor)).toBeNull();
});
it('finds bottom strip when obstructor covers top portion', () => {
const obstructor: Rect = { left: 0, top: 1800, right: 1080, bottom: 1940 };
const result = largestVisibleStrip(target, obstructor);
expect(result).not.toBeNull();
expect(result!.rect.top).toBe(1940);
expect(result!.rect.bottom).toBe(2025);
});
it('finds left strip when FAB covers right side', () => {
// FAB in bottom-right corner
const fab: Rect = { left: 891, top: 1875, right: 1038, bottom: 2022 };
const result = largestVisibleStrip(target, fab);
expect(result).not.toBeNull();
// Left strip should be largest (full height, left portion)
expect(result!.rect.left).toBe(42);
expect(result!.rect.right).toBe(891);
expect(result!.area).toBeGreaterThan(0);
});
it('picks the largest strip among candidates', () => {
// Small obstructor in the center — all 4 strips available
const small: Rect = { left: 400, top: 1900, right: 600, bottom: 1980 };
const result = largestVisibleStrip(target, small);
expect(result).not.toBeNull();
// Left strip: (400-42) * (2025-1855) = 358 * 170 = 60860
// Right strip: (1038-600) * 170 = 438 * 170 = 74460
// Right strip should win
expect(result!.rect.left).toBe(600);
expect(result!.rect.right).toBe(1038);
});
it('returns center point of the visible strip', () => {
const fab: Rect = { left: 891, top: 1875, right: 1038, bottom: 2022 };
const result = largestVisibleStrip(target, fab);
expect(result).not.toBeNull();
expect(result!.center.x).toBe(Math.floor((42 + 891) / 2));
expect(result!.center.y).toBe(Math.floor((1855 + 2025) / 2));
});
});
});
describe('obstruction detection', () => {
// Helper to create a minimal UiNode
function makeNode(overrides: Partial<UiNode> = {}): UiNode {
return {
text: '', contentDesc: '', resourceId: '', className: '',
packageName: '', bounds: null, clickable: false, focused: false,
enabled: true, selected: false, drawingOrder: 0, children: [],
...overrides,
};
}
const archiveRow = makeNode({
text: 'Archive',
bounds: { left: 42, top: 1855, right: 1038, bottom: 2025 },
clickable: true,
});
const fab = makeNode({
contentDesc: 'Add Item',
bounds: { left: 891, top: 1875, right: 1038, bottom: 2022 },
clickable: true,
});
// Hierarchy: root contains archiveRow and fab (fab is later = higher z-order)
const hierarchy = makeNode({
bounds: { left: 0, top: 0, right: 1080, bottom: 2400 },
children: [archiveRow, fab],
});
const noOverlayWindows: WindowInfo[] = [];
describe('clear path', () => {
it('returns not obstructed when target is the topmost clickable', () => {
// Tap center of archive row — only archiveRow contains this point, no FAB
const result = detectObstruction({
hierarchy,
windows: noOverlayWindows,
targetElement: archiveRow,
tapPoint: { x: 200, y: 1940 },
searchText: 'Archive',
});
expect(result.obstructed).toBe(false);
});
});
describe('FAB obstruction', () => {
it('detects FAB overlapping the target center', () => {
// Tap at a point where both archive row and FAB overlap — FAB is later in tree
const result = detectObstruction({
hierarchy,
windows: noOverlayWindows,
targetElement: archiveRow,
tapPoint: { x: 965, y: 1948 },
searchText: 'Archive',
});
expect(result.obstructed).toBe(true);
if (result.obstructed) {
expect(result.obstructor).toContain('Add Item');
expect(result.adjustedPoint).not.toBeNull();
expect(result.fullyObscured).toBe(false);
// Adjusted point should be in the left strip (away from FAB)
expect(result.adjustedPoint!.x).toBeLessThan(891);
}
});
});
describe('system overlay', () => {
it('detects TalkBack FloatingMenu overlay at tap point', () => {
const talkbackWindows: WindowInfo[] = [
{
name: 'FloatingMenu',
type: 'NAVIGATION_BAR_PANEL',
hasSurface: true,
touchableRegion: { left: 891, top: 1875, right: 1038, bottom: 2022 },
},
];
const result = detectObstruction({
hierarchy,
windows: talkbackWindows,
targetElement: archiveRow,
tapPoint: { x: 965, y: 1948 },
searchText: 'Archive',
});
expect(result.obstructed).toBe(true);
if (result.obstructed) {
expect(result.obstructor).toContain('FloatingMenu');
expect(result.adjustedPoint).not.toBeNull();
}
});
it('system overlay takes precedence over in-app elements', () => {
// Both a system overlay and FAB at the same point — system overlay detected first
const talkbackWindows: WindowInfo[] = [
{
name: 'FloatingMenu',
type: 'NAVIGATION_BAR_PANEL',
hasSurface: true,
touchableRegion: { left: 891, top: 1875, right: 1038, bottom: 2022 },
},
];
const result = detectObstruction({
hierarchy,
windows: talkbackWindows,
targetElement: archiveRow,
tapPoint: { x: 965, y: 1948 },
searchText: 'Archive',
});
expect(result.obstructed).toBe(true);
if (result.obstructed) {
expect(result.obstructor).toContain('system_overlay');
}
});
});
describe('fully obscured', () => {
it('reports fully obscured when obstructor covers entire target', () => {
const fullScreenOverlay: WindowInfo[] = [
{
name: 'SystemDialog',
type: 'SYSTEM_ALERT',
hasSurface: true,
touchableRegion: { left: 0, top: 0, right: 1080, bottom: 2400 },
},
];
const result = detectObstruction({
hierarchy,
windows: fullScreenOverlay,
targetElement: archiveRow,
tapPoint: { x: 540, y: 1940 },
searchText: 'Archive',
});
expect(result.obstructed).toBe(true);
if (result.obstructed) {
expect(result.fullyObscured).toBe(true);
expect(result.adjustedPoint).toBeNull();
}
});
});
describe('Compose parent wrapper', () => {
it('treats identical bounds as parent wrapper, not obstruction', () => {
// Compose pattern: clickable parent has same bounds as text child
const textChild = makeNode({
contentDesc: 'Download now',
bounds: { left: 84, top: 553, right: 996, bottom: 679 },
clickable: false,
});
const clickableParent = makeNode({
bounds: { left: 84, top: 553, right: 996, bottom: 679 },
clickable: true,
children: [textChild],
});
const tree = makeNode({
bounds: { left: 0, top: 0, right: 1080, bottom: 2400 },
children: [clickableParent],
});
const result = detectObstruction({
hierarchy: tree,
windows: noOverlayWindows,
targetElement: textChild,
tapPoint: { x: 540, y: 616 },
searchText: 'Download now',
});
expect(result.obstructed).toBe(false);
});
});
});

View File

@@ -0,0 +1,127 @@
/**
* Two-layer obstruction detection for UI elements.
*
* Layer 1: System overlay windows (TalkBack, PiP, accessibility services)
* detected via parsed `dumpsys window windows` output.
* Layer 2: In-app elements (FABs, dialogs, bottom sheets) detected via
* the UIAutomator XML hierarchy — topmost clickable at tap point.
*
* When obstruction is found, computes an alternative tap point using the
* largest visible strip of the target element not covered by the obstructor.
*/
import { type Point, type Rect, center, boundsEqual } from './bounds.js';
import { largestVisibleStrip, type VisibleStrip } from './visible-region.js';
import { type UiNode, findTopmostClickableAt } from '../parsers/xml.js';
import { type WindowInfo, findOverlayAtPoint } from '../parsers/dumpsys.js';
export type ObstructionResult =
| { obstructed: false }
| {
obstructed: true;
obstructor: string;
obstructorBounds: Rect;
adjustedPoint: Point | null;
visibleRegion: VisibleStrip | null;
fullyObscured: boolean;
};
export interface DetectObstructionParams {
hierarchy: UiNode;
windows: WindowInfo[];
targetElement: UiNode;
tapPoint: Point;
searchText: string;
}
/**
* Detect if the tap point is obstructed by a system overlay or in-app element.
*/
export function detectObstruction(params: DetectObstructionParams): ObstructionResult {
const { hierarchy, windows, targetElement, tapPoint, searchText } = params;
// Layer 1: System overlays (TalkBack, PiP, accessibility services)
const overlay = findOverlayAtPoint(windows, tapPoint);
if (overlay) {
return buildResult(
`system_overlay window=${overlay.name} type=${overlay.type}`,
overlay.touchableRegion!,
targetElement,
);
}
// Layer 2: In-app elements (FABs, dialogs, bottom sheets)
const topmost = findTopmostClickableAt(hierarchy, tapPoint);
if (topmost && topmost.bounds) {
// Check if topmost IS the target (no obstruction)
if (isTargetMatch(topmost, targetElement, searchText)) {
return { obstructed: false };
}
return buildResult(
formatElementId(topmost),
topmost.bounds,
targetElement,
);
}
return { obstructed: false };
}
/**
* Check if the topmost clickable element matches the target.
*
* Match criteria:
* - Search text appears in topmost's text or contentDesc
* - Bounds are identical (Compose parent wrapper pattern)
*/
function isTargetMatch(topmost: UiNode, target: UiNode, searchText: string): boolean {
const lower = searchText.toLowerCase();
// Text/content-desc match
if (topmost.text.toLowerCase().includes(lower)) return true;
if (topmost.contentDesc.toLowerCase().includes(lower)) return true;
// Bounds equality (Compose parent wrapper)
if (target.bounds && topmost.bounds && boundsEqual(target.bounds, topmost.bounds)) {
return true;
}
return false;
}
function buildResult(
obstructorId: string,
obstructorBounds: Rect,
target: UiNode,
): ObstructionResult {
if (!target.bounds) {
return {
obstructed: true,
obstructor: obstructorId,
obstructorBounds,
adjustedPoint: null,
visibleRegion: null,
fullyObscured: true,
};
}
const strip = largestVisibleStrip(target.bounds, obstructorBounds);
return {
obstructed: true,
obstructor: obstructorId,
obstructorBounds,
adjustedPoint: strip?.center ?? null,
visibleRegion: strip ?? null,
fullyObscured: strip === null,
};
}
function formatElementId(node: UiNode): string {
if (node.text) return `text="${node.text}"`;
if (node.contentDesc) return `desc="${node.contentDesc}"`;
if (node.resourceId) return `id="${node.resourceId}"`;
if (node.bounds) return `bounds=[${node.bounds.left},${node.bounds.top}][${node.bounds.right},${node.bounds.bottom}]`;
return 'unknown';
}

View File

@@ -0,0 +1,91 @@
/**
* Visible region computation for partially obstructed UI elements.
*
* When a target element is partially covered by an obstructor (FAB, PiP, dialog),
* this module finds the largest unobstructed rectangular strip and returns its
* center as an alternative tap point.
*/
import { type Rect, type Point, area, center } from './bounds.js';
export interface VisibleStrip {
rect: Rect;
center: Point;
area: number;
}
/**
* Find the largest visible rectangular strip of the target not covered by the obstructor.
*
* Evaluates 4 candidate strips:
* - Top: above the obstructor, full target width
* - Bottom: below the obstructor, full target width
* - Left: left of the obstructor, full target height
* - Right: right of the obstructor, full target height
*
* Returns the strip with the largest area, or null if fully obscured.
*/
export function largestVisibleStrip(target: Rect, obstructor: Rect): VisibleStrip | null {
const candidates: Rect[] = [];
// Top strip: above obstructor, full target width
if (obstructor.top > target.top) {
candidates.push({
left: target.left,
top: target.top,
right: target.right,
bottom: obstructor.top,
});
}
// Bottom strip: below obstructor, full target width
if (obstructor.bottom < target.bottom) {
candidates.push({
left: target.left,
top: obstructor.bottom,
right: target.right,
bottom: target.bottom,
});
}
// Left strip: left of obstructor, full target height
if (obstructor.left > target.left) {
candidates.push({
left: target.left,
top: target.top,
right: obstructor.left,
bottom: target.bottom,
});
}
// Right strip: right of obstructor, full target height
if (obstructor.right < target.right) {
candidates.push({
left: obstructor.right,
top: target.top,
right: target.right,
bottom: target.bottom,
});
}
if (candidates.length === 0) return null;
let best: Rect = candidates[0];
let bestArea = area(candidates[0]);
for (let i = 1; i < candidates.length; i++) {
const a = area(candidates[i]);
if (a > bestArea) {
best = candidates[i];
bestArea = a;
}
}
if (bestArea <= 0) return null;
return {
rect: best,
center: center(best),
area: bestArea,
};
}

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env node
/**
* Android Device MCP Server
* MCP server for Android device interaction via ADB — UI hierarchy capture,
* element finding with obstruction detection, tap, and navigation.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import type { ToolDefinition } from './utils/validation.js';
import capture from './tools/capture.js';
import findElement from './tools/find-element.js';
import tapAt from './tools/tap-at.js';
import tapElement from './tools/tap-element.js';
import navigate from './tools/navigate.js';
import inputText from './tools/input-text.js';
const tools: ToolDefinition[] = [capture, findElement, tapAt, tapElement, navigate, inputText];
async function main() {
const server = new Server(
{ name: 'android-device-mcp', version: '1.0.0' },
{ capabilities: { tools: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: tools.map(t => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema,
})),
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = tools.find(t => t.name === name);
if (!tool) {
throw new Error(`Unknown tool: ${name}`);
}
try {
const result = await tool.handler(args || {});
return { content: [{ type: 'text', text: result }] };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Tool error (${name}):`, message);
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

View File

@@ -0,0 +1,630 @@
WINDOW MANAGER WINDOWS (dumpsys window windows)
Window #0 Window{ba7e323 u0 ScreenDecorOverlayBottom}:
mDisplayId=0 mSession=Session{1e8a728 2463:u0a10210} mClient=android.os.BinderProxy@b7880dd
mOwnerUid=10210 showForAllUsers=true package=com.android.systemui appop=NONE
mAttrs={(0,0)(fillxwrap) gr=BOTTOM CENTER_VERTICAL sim={adjust=pan} layoutInDisplayCutoutMode=always ty=NAVIGATION_BAR_PANEL fmt=TRANSLUCENT
fl=NOT_FOCUSABLE NOT_TOUCHABLE NOT_TOUCH_MODAL LAYOUT_IN_SCREEN FLAG_SLIPPERY
pfl=SHOW_FOR_ALL_USERS NO_MOVE_ANIMATION IS_ROUNDED_CORNERS_OVERLAY COLOR_SPACE_AGNOSTIC FIT_INSETS_CONTROLLED TRUSTED_OVERLAY
vsysui=LAYOUT_STABLE
bhv=DEFAULT
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=1080 h=74 mLayoutSeq=18196
mBaseLayer=251000 mSubLayer=0 mToken=WindowToken{1806452 type=2024 android.os.BinderProxy@b7880dd}
mViewVisibility=0x0 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mTouchableInsets=3 mGivenInsetsPending=false
touchable region=SkRegion()
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mHasSurface=true isReadyForDisplay()=true mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,2326][1080,2400] last=[0,2326][1080,2400] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{10dcf18 ScreenDecorOverlayBottom}:
mSurface=Surface(name=ScreenDecorOverlayBottom#71)/@0xc8aad71
Surface: shown=true mDrawState=HAS_DRAWN mLastHidden=false
mEnterAnimationPending=false
isOnScreen=true
isVisible=true
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #1 Window{7f4a38f u0 ScreenDecorOverlay}:
mDisplayId=0 mSession=Session{1e8a728 2463:u0a10210} mClient=android.os.BinderProxy@ad30ea2
mOwnerUid=10210 showForAllUsers=true package=com.android.systemui appop=NONE
mAttrs={(0,0)(fillxwrap) gr=TOP CENTER_VERTICAL sim={adjust=pan} layoutInDisplayCutoutMode=always ty=NAVIGATION_BAR_PANEL fmt=TRANSLUCENT
fl=NOT_FOCUSABLE NOT_TOUCHABLE NOT_TOUCH_MODAL LAYOUT_IN_SCREEN FLAG_SLIPPERY
pfl=SHOW_FOR_ALL_USERS NO_MOVE_ANIMATION IS_ROUNDED_CORNERS_OVERLAY COLOR_SPACE_AGNOSTIC FIT_INSETS_CONTROLLED TRUSTED_OVERLAY
vsysui=LAYOUT_STABLE
bhv=DEFAULT
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=1080 h=128 mLayoutSeq=18196
mBaseLayer=251000 mSubLayer=0 mToken=WindowToken{2167869 type=2024 android.os.BinderProxy@ad30ea2}
mViewVisibility=0x0 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mTouchableInsets=3 mGivenInsetsPending=false
touchable region=SkRegion((492,0,610,128))
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mHasSurface=true isReadyForDisplay()=true mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,0][1080,128] last=[0,0][1080,128] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{e4a7756 ScreenDecorOverlay}:
mSurface=Surface(name=ScreenDecorOverlay#70)/@0x89533d7
Surface: shown=true mDrawState=HAS_DRAWN mLastHidden=false
mEnterAnimationPending=false
isOnScreen=true
isVisible=true
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #2 Window{cc49e92 u0 FloatingMenu}:
mDisplayId=0 mSession=Session{1e8a728 2463:u0a10210} mClient=android.os.BinderProxy@cbefcf4
mOwnerUid=10210 showForAllUsers=true package=com.android.systemui appop=NONE
mAttrs={(0,0)(fillxfill) sim={adjust=resize} layoutInDisplayCutoutMode=always ty=NAVIGATION_BAR_PANEL fmt=TRANSLUCENT wanim=0x1030003 receive insets ignoring z-order
fl=NOT_FOCUSABLE HARDWARE_ACCELERATED
pfl=SHOW_FOR_ALL_USERS UNRESTRICTED_GESTURE_EXCLUSION EXCLUDE_FROM_SCREEN_MAGNIFICATION FIT_INSETS_CONTROLLED
bhv=DEFAULT
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=1080 h=2400 mLayoutSeq=18196
mBaseLayer=251000 mSubLayer=0 mToken=WindowToken{71c3c1d type=2024 android.os.BinderProxy@cbefcf4}
mViewVisibility=0x0 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mTouchableInsets=3 mGivenInsetsPending=false
touchable region=SkRegion((953,297,1080,424))
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mHasSurface=true isReadyForDisplay()=true mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,0][1080,2400] last=[0,0][1080,2400] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{c9cc4c4 FloatingMenu}:
mAnimationIsEntrance=true mSurface=Surface(name=FloatingMenu#25467)/@0x64bafad
Surface: shown=true mDrawState=HAS_DRAWN mLastHidden=false
mEnterAnimationPending=false
isOnScreen=true
isVisible=true
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #3 Window{5cac0da u0 Taskbar}:
mDisplayId=0 mSession=Session{41a0116 2805:u0a10196} mClient=android.os.BinderProxy@b45f3fc
mOwnerUid=10196 showForAllUsers=true package=com.google.android.apps.nexuslauncher appop=NONE
mAttrs={(0,0)(fillx126) gr=BOTTOM CENTER_VERTICAL sim={adjust=nothing} layoutInDisplayCutoutMode=always ty=NAVIGATION_BAR fmt=TRANSLUCENT receive insets ignoring z-order
fl=NOT_FOCUSABLE NOT_TOUCH_MODAL WATCH_OUTSIDE_TOUCH FLAG_SLIPPERY
pfl=NO_MOVE_ANIMATION
bhv=DEFAULT
providedInsets:
InsetsFrameProvider: {id=#d3210001, index=0, type=navigationBars, source=FRAME, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=126}}
InsetsFrameProvider: {id=#d3210006, index=0, type=tappableElement, source=FRAME, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=126}, insetsSizeOverrides=[TypedInsetsSize: {windowType=VOICE_INTERACTION, insetsSize=Insets{left=0, top=0, right=0, bottom=126}}]}
InsetsFrameProvider: {id=#d3210005, index=0, type=mandatorySystemGestures, source=FRAME, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=126}}
InsetsFrameProvider: {id=#d3210004, index=0, type=systemGestures, source=DISPLAY, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=0}}
InsetsFrameProvider: {id=#d3210024, index=1, type=systemGestures, source=DISPLAY, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=0}}
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true
paramsForRotation:
ROTATION_0={(0,0)(fillx126) gr=BOTTOM CENTER_VERTICAL sim={adjust=nothing} layoutInDisplayCutoutMode=always ty=NAVIGATION_BAR fmt=TRANSLUCENT receive insets ignoring z-order
fl=NOT_FOCUSABLE NOT_TOUCH_MODAL WATCH_OUTSIDE_TOUCH FLAG_SLIPPERY
pfl=NO_MOVE_ANIMATION
bhv=DEFAULT
providedInsets:
InsetsFrameProvider: {id=#3fa90001, index=0, type=navigationBars, source=FRAME, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=126}}
InsetsFrameProvider: {id=#3fa90006, index=0, type=tappableElement, source=FRAME, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=126}, insetsSizeOverrides=[TypedInsetsSize: {windowType=VOICE_INTERACTION, insetsSize=Insets{left=0, top=0, right=0, bottom=126}}]}
InsetsFrameProvider: {id=#3fa90005, index=0, type=mandatorySystemGestures, source=FRAME, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=126}}
InsetsFrameProvider: {id=#3fa90004, index=0, type=systemGestures, source=DISPLAY, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=0}}
InsetsFrameProvider: {id=#3fa90024, index=1, type=systemGestures, source=DISPLAY, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=0}}
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
ROTATION_90={(0,0)(126xfill) gr=END CENTER_HORIZONTAL sim={adjust=nothing} layoutInDisplayCutoutMode=always ty=NAVIGATION_BAR fmt=TRANSLUCENT receive insets ignoring z-order
fl=NOT_FOCUSABLE NOT_TOUCH_MODAL WATCH_OUTSIDE_TOUCH FLAG_SLIPPERY
pfl=NO_MOVE_ANIMATION
bhv=DEFAULT
providedInsets:
InsetsFrameProvider: {id=#3fa90001, index=0, type=navigationBars, source=FRAME, flags=[], insetsSize=Insets{left=0, top=0, right=126, bottom=0}}
InsetsFrameProvider: {id=#3fa90006, index=0, type=tappableElement, source=FRAME, flags=[], insetsSize=Insets{left=0, top=0, right=126, bottom=0}, insetsSizeOverrides=[TypedInsetsSize: {windowType=VOICE_INTERACTION, insetsSize=Insets{left=0, top=0, right=126, bottom=0}}]}
InsetsFrameProvider: {id=#3fa90005, index=0, type=mandatorySystemGestures, source=FRAME, flags=[], insetsSize=Insets{left=0, top=0, right=126, bottom=0}}
InsetsFrameProvider: {id=#3fa90004, index=0, type=systemGestures, source=DISPLAY, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=0}}
InsetsFrameProvider: {id=#3fa90024, index=1, type=systemGestures, source=DISPLAY, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=0}}
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
ROTATION_180={(0,0)(fillx126) gr=BOTTOM CENTER_VERTICAL sim={adjust=nothing} layoutInDisplayCutoutMode=always ty=NAVIGATION_BAR fmt=TRANSLUCENT receive insets ignoring z-order
fl=NOT_FOCUSABLE NOT_TOUCH_MODAL WATCH_OUTSIDE_TOUCH FLAG_SLIPPERY
pfl=NO_MOVE_ANIMATION
bhv=DEFAULT
providedInsets:
InsetsFrameProvider: {id=#3fa90001, index=0, type=navigationBars, source=FRAME, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=128}}
InsetsFrameProvider: {id=#3fa90006, index=0, type=tappableElement, source=FRAME, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=126}, insetsSizeOverrides=[TypedInsetsSize: {windowType=VOICE_INTERACTION, insetsSize=Insets{left=0, top=0, right=0, bottom=126}}]}
InsetsFrameProvider: {id=#3fa90005, index=0, type=mandatorySystemGestures, source=FRAME, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=128}}
InsetsFrameProvider: {id=#3fa90004, index=0, type=systemGestures, source=DISPLAY, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=0}}
InsetsFrameProvider: {id=#3fa90024, index=1, type=systemGestures, source=DISPLAY, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=0}}
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
ROTATION_270={(0,0)(126xfill) gr=START CENTER_HORIZONTAL sim={adjust=nothing} layoutInDisplayCutoutMode=always ty=NAVIGATION_BAR fmt=TRANSLUCENT receive insets ignoring z-order
fl=NOT_FOCUSABLE NOT_TOUCH_MODAL WATCH_OUTSIDE_TOUCH FLAG_SLIPPERY
pfl=NO_MOVE_ANIMATION
bhv=DEFAULT
providedInsets:
InsetsFrameProvider: {id=#3fa90001, index=0, type=navigationBars, source=FRAME, flags=[], insetsSize=Insets{left=126, top=0, right=0, bottom=0}}
InsetsFrameProvider: {id=#3fa90006, index=0, type=tappableElement, source=FRAME, flags=[], insetsSize=Insets{left=126, top=0, right=0, bottom=0}, insetsSizeOverrides=[TypedInsetsSize: {windowType=VOICE_INTERACTION, insetsSize=Insets{left=126, top=0, right=0, bottom=0}}]}
InsetsFrameProvider: {id=#3fa90005, index=0, type=mandatorySystemGestures, source=FRAME, flags=[], insetsSize=Insets{left=126, top=0, right=0, bottom=0}}
InsetsFrameProvider: {id=#3fa90004, index=0, type=systemGestures, source=DISPLAY, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=0}}
InsetsFrameProvider: {id=#3fa90024, index=1, type=systemGestures, source=DISPLAY, flags=[], insetsSize=Insets{left=0, top=0, right=0, bottom=0}}
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}}
Requested w=1080 h=126 mLayoutSeq=18196
mBaseLayer=241000 mSubLayer=0 mToken=WindowToken{9fc53e8 type=2019 android.os.BinderProxy@a5e4338}
mViewVisibility=0x0 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mHasSurface=true isReadyForDisplay()=true mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,2274][1080,2400] last=[0,2274][1080,2400] insetsChanged=false
surface=[0,0][0,0]
ContainerAnimator:
mLeash=Surface(name=Surface(name=5cac0da Taskbar#73)/@0x536c694 - animation-leash of insets_animation#25499)/@0xa8eb2e2 mAnimationType=insets_animation
Animation: com.android.server.wm.InsetsSourceProvider$ControlAdapter@60f3673
ControlAdapter mCapturedLeash=Surface(name=Surface(name=5cac0da Taskbar#73)/@0x536c694 - animation-leash of insets_animation#25499)/@0xa8eb2e2
WindowStateAnimator{7036930 Taskbar}:
mSurface=Surface(name=Taskbar#75)/@0x1a099a9
Surface: shown=true mDrawState=HAS_DRAWN mLastHidden=false
mEnterAnimationPending=false
isOnScreen=true
isVisible=true
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #4 Window{b5c1512 u0 NotificationShade}:
mDisplayId=0 mSession=Session{1e8a728 2463:u0a10210} mClient=android.os.BinderProxy@11aef74
mOwnerUid=10210 showForAllUsers=true package=com.android.systemui appop=NONE
mAttrs={(0,0)(fillxfill) gr=TOP CENTER_VERTICAL sim={adjust=pan} layoutInDisplayCutoutMode=always ty=NOTIFICATION_SHADE fmt=TRANSLUCENT
fl=NOT_FOCUSABLE TOUCHABLE_WHEN_WAKING WATCH_OUTSIDE_TOUCH HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=OPTIMIZE_MEASURE COLOR_SPACE_AGNOSTIC FIT_INSETS_CONTROLLED
bhv=SHOW_TRANSIENT_BARS_BY_SWIPE
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=1080 h=2400 mLayoutSeq=18146
mBaseLayer=171000 mSubLayer=0 mToken=WindowToken{b50b90c type=2040 android.os.BinderProxy@8bb9b47}
mViewVisibility=0x4 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mTouchableInsets=3 mGivenInsetsPending=false
touchable region=SkRegion((0,0,1080,128)(492,128,610,160))
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mHasSurface=false isReadyForDisplay()=false mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,0][1080,2400] last=[0,0][1080,2400] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{1e91b2e NotificationShade}:
mDrawState=NO_SURFACE mLastHidden=false
mEnterAnimationPending=false
isOnScreen=false
isVisible=false
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #5 Window{92afd17 u0 StatusBar}:
mDisplayId=0 mSession=Session{1e8a728 2463:u0a10210} mClient=android.os.BinderProxy@a484b1
mOwnerUid=10210 showForAllUsers=true package=com.android.systemui appop=NONE
mAttrs={(0,0)(fillx128) gr=TOP CENTER_VERTICAL sim={adjust=pan} layoutInDisplayCutoutMode=always ty=STATUS_BAR fmt=TRANSLUCENT
fl=NOT_FOCUSABLE HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=COLOR_SPACE_AGNOSTIC FIT_INSETS_CONTROLLED
bhv=DEFAULT
providedInsets:
InsetsFrameProvider: {id=#25730000, index=0, type=statusBars, source=FRAME, flags=[], insetsSize=Insets{left=0, top=128, right=0, bottom=0}}
InsetsFrameProvider: {id=#25730006, index=0, type=tappableElement, source=FRAME, flags=[], insetsSize=Insets{left=0, top=128, right=0, bottom=0}}
InsetsFrameProvider: {id=#25730005, index=0, type=mandatorySystemGestures, source=FRAME, flags=[], mMinimalInsetsSizeInDisplayCutoutSafe=Insets{left=0, top=32, right=0, bottom=0}}
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true
paramsForRotation:
ROTATION_0={(0,0)(fillx128) gr=TOP CENTER_VERTICAL layoutInDisplayCutoutMode=always ty=STATUS_BAR fmt=TRANSLUCENT
fl=NOT_FOCUSABLE DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=COLOR_SPACE_AGNOSTIC FIT_INSETS_CONTROLLED
bhv=DEFAULT
providedInsets:
InsetsFrameProvider: {id=#25730000, index=0, type=statusBars, source=FRAME, flags=[], insetsSize=Insets{left=0, top=128, right=0, bottom=0}}
InsetsFrameProvider: {id=#25730006, index=0, type=tappableElement, source=FRAME, flags=[], insetsSize=Insets{left=0, top=128, right=0, bottom=0}}
InsetsFrameProvider: {id=#25730005, index=0, type=mandatorySystemGestures, source=FRAME, flags=[], insetsSize=Insets{left=0, top=128, right=0, bottom=0}, mMinimalInsetsSizeInDisplayCutoutSafe=Insets{left=0, top=32, right=0, bottom=0}}
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
ROTATION_90={(0,0)(fillx74) gr=TOP CENTER_VERTICAL layoutInDisplayCutoutMode=always ty=STATUS_BAR fmt=TRANSLUCENT
fl=NOT_FOCUSABLE DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=COLOR_SPACE_AGNOSTIC FIT_INSETS_CONTROLLED
bhv=DEFAULT
providedInsets:
InsetsFrameProvider: {id=#25730000, index=0, type=statusBars, source=FRAME, flags=[], insetsSize=Insets{left=0, top=74, right=0, bottom=0}}
InsetsFrameProvider: {id=#25730006, index=0, type=tappableElement, source=FRAME, flags=[], insetsSize=Insets{left=0, top=74, right=0, bottom=0}}
InsetsFrameProvider: {id=#25730005, index=0, type=mandatorySystemGestures, source=FRAME, flags=[], insetsSize=Insets{left=0, top=74, right=0, bottom=0}, mMinimalInsetsSizeInDisplayCutoutSafe=Insets{left=0, top=32, right=0, bottom=0}}
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
ROTATION_180={(0,0)(fillx74) gr=TOP CENTER_VERTICAL layoutInDisplayCutoutMode=always ty=STATUS_BAR fmt=TRANSLUCENT
fl=NOT_FOCUSABLE DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=COLOR_SPACE_AGNOSTIC FIT_INSETS_CONTROLLED
bhv=DEFAULT
providedInsets:
InsetsFrameProvider: {id=#25730000, index=0, type=statusBars, source=FRAME, flags=[], insetsSize=Insets{left=0, top=74, right=0, bottom=0}}
InsetsFrameProvider: {id=#25730006, index=0, type=tappableElement, source=FRAME, flags=[], insetsSize=Insets{left=0, top=74, right=0, bottom=0}}
InsetsFrameProvider: {id=#25730005, index=0, type=mandatorySystemGestures, source=FRAME, flags=[], insetsSize=Insets{left=0, top=74, right=0, bottom=0}, mMinimalInsetsSizeInDisplayCutoutSafe=Insets{left=0, top=32, right=0, bottom=0}}
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
ROTATION_270={(0,0)(fillx74) gr=TOP CENTER_VERTICAL layoutInDisplayCutoutMode=always ty=STATUS_BAR fmt=TRANSLUCENT
fl=NOT_FOCUSABLE DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=COLOR_SPACE_AGNOSTIC FIT_INSETS_CONTROLLED
bhv=DEFAULT
providedInsets:
InsetsFrameProvider: {id=#25730000, index=0, type=statusBars, source=FRAME, flags=[], insetsSize=Insets{left=0, top=74, right=0, bottom=0}}
InsetsFrameProvider: {id=#25730006, index=0, type=tappableElement, source=FRAME, flags=[], insetsSize=Insets{left=0, top=74, right=0, bottom=0}}
InsetsFrameProvider: {id=#25730005, index=0, type=mandatorySystemGestures, source=FRAME, flags=[], insetsSize=Insets{left=0, top=74, right=0, bottom=0}, mMinimalInsetsSizeInDisplayCutoutSafe=Insets{left=0, top=32, right=0, bottom=0}}
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}}
Requested w=1080 h=128 mLayoutSeq=18196
mBaseLayer=151000 mSubLayer=0 mToken=WindowToken{8609d96 type=2000 android.os.BinderProxy@5177b58}
mViewVisibility=0x0 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mTouchableInsets=3 mGivenInsetsPending=false
touchable region=SkRegion((0,0,1080,128))
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mHasSurface=true isReadyForDisplay()=true mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,0][1080,128] last=[0,0][1080,128] insetsChanged=false
surface=[0,0][0,0]
ContainerAnimator:
mLeash=Surface(name=Surface(name=92afd17 StatusBar#78)/@0x47a6386 - animation-leash of insets_animation#25498)/@0xa1cc6cf mAnimationType=insets_animation
Animation: com.android.server.wm.InsetsSourceProvider$ControlAdapter@5ede85c
ControlAdapter mCapturedLeash=Surface(name=Surface(name=92afd17 StatusBar#78)/@0x47a6386 - animation-leash of insets_animation#25498)/@0xa1cc6cf
WindowStateAnimator{6402765 StatusBar}:
mSurface=Surface(name=StatusBar#83)/@0x26fbc3a
Surface: shown=true mDrawState=HAS_DRAWN mLastHidden=false
mEnterAnimationPending=false
isOnScreen=true
isVisible=true
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #6 Window{175a4d2 u0 ShellDropTarget}:
mDisplayId=0 mSession=Session{1e8a728 2463:u0a10210} mClient=android.os.BinderProxy@32704a0
mOwnerUid=10210 showForAllUsers=true package=com.android.systemui appop=SYSTEM_ALERT_WINDOW
mAttrs={(0,0)(fillxfill) sim={adjust=pan} layoutInDisplayCutoutMode=always ty=APPLICATION_OVERLAY fmt=TRANSLUCENT
fl=NOT_FOCUSABLE HARDWARE_ACCELERATED
pfl=SHOW_FOR_ALL_USERS NO_MOVE_ANIMATION FIT_INSETS_CONTROLLED INTERCEPT_GLOBAL_DRAG_AND_DROP
bhv=DEFAULT
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=1080 h=2400 mLayoutSeq=5
mBaseLayer=111000 mSubLayer=0 mToken=WindowToken{ec5e859 type=2038 android.os.BinderProxy@620a66c}
mViewVisibility=0x4 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mLastReportedConfiguration={0.0 ?mcc0mnc ?localeList ?layoutDir ?swdp ?wdp ?hdp ?density ?lsize ?long ?round ?ldr ?wideColorGamut ?orien ?uimode ?night ?touch ?keyb/?/? ?nav/? winConfig={ mBounds=Rect(0, 0 - 0, 0) mAppBounds=null mMaxBounds=Rect(0, 0 - 0, 0) mDisplayRotation=undefined mWindowingMode=undefined mActivityType=undefined mAlwaysOnTop=undefined mRotation=undefined} ?fontWeightAdjustment}
mHasSurface=false isReadyForDisplay()=false mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,0][1080,2400] last=[0,0][1080,2400] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{3c900eb ShellDropTarget}:
mDrawState=NO_SURFACE mLastHidden=false
mEnterAnimationPending=false
mShownAlpha=0.0 mAlpha=1.0 mLastAlpha=0.0
isOnScreen=false
isVisible=false
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #7 Window{d056a34 u0 InputMethod}:
mDisplayId=0 mSession=Session{ed481a5 10035:u0a10168} mClient=android.os.BinderProxy@a88e346
mOwnerUid=10168 showForAllUsers=false package=com.google.android.inputmethod.latin appop=NONE
mAttrs={(0,0)(fillxfill) gr=BOTTOM CENTER_VERTICAL sim={adjust=pan} ty=INPUT_METHOD fmt=TRANSPARENT wanim=0x1030056 receive insets ignoring z-order
fl=NOT_FOCUSABLE LAYOUT_IN_SCREEN SPLIT_TOUCH HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=EDGE_TO_EDGE_ENFORCED FIT_INSETS_CONTROLLED
bhv=DEFAULT
fitTypes=statusBars navigationBars
fitSides=LEFT TOP RIGHT
dvrrWindowFrameRateHint=true}
Requested w=1080 h=2272 mLayoutSeq=18191
mIsImWindow=true mIsWallpaper=false mIsFloatingLayer=true
mBaseLayer=131000 mSubLayer=0 mToken=WindowToken{4aa0ed3 type=2011 android.os.Binder@fa630c2}
mViewVisibility=0x8 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,2272][0,0] mGivenVisibleInsets=[0,2272][0,0]
mTouchableInsets=3 mGivenInsetsPending=false
touchable region=SkRegion()
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mHasSurface=false isReadyForDisplay()=false mWindowRemovalAllowed=false
Frames: parent=[0,128][1080,2400] display=[0,128][1080,2400] frame=[0,128][1080,2400] last=[0,128][1080,2400] insetsChanged=false
surface=[0,0][0,0]
ContainerAnimator:
mLeash=Surface(name=Surface(name=d056a34 InputMethod#20167)/@0x9dfedd2 - animation-leash of insets_animation#25500)/@0xb9f2e48 mAnimationType=insets_animation
Animation: com.android.server.wm.InsetsSourceProvider$ControlAdapter@690d4e1
ControlAdapter mCapturedLeash=Surface(name=Surface(name=d056a34 InputMethod#20167)/@0x9dfedd2 - animation-leash of insets_animation#25500)/@0xb9f2e48
WindowStateAnimator{38d6206 InputMethod}:
mDrawState=NO_SURFACE mLastHidden=false
mEnterAnimationPending=false
isOnScreen=false
isVisible=false
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #8 Window{37bdea2 u0 com.x8bit.bitwarden.dev/com.x8bit.bitwarden.MainActivity}:
mDisplayId=0 taskId=1857 mSession=Session{e78c8d8 7233:u0a10379} mClient=android.os.BinderProxy@c7326d
mOwnerUid=10379 showForAllUsers=false package=com.x8bit.bitwarden.dev appop=NONE
mAttrs={(0,0)(fillxfill) sim={adjust=resize} layoutInDisplayCutoutMode=always ty=BASE_APPLICATION fmt=TRANSPARENT wanim=0x103030d
fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SPLIT_TOUCH DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=NO_MOVE_ANIMATION EDGE_TO_EDGE_ENFORCED FORCE_DRAW_STATUS_BAR_BACKGROUND FIT_INSETS_CONTROLLED
bhv=DEFAULT
fitSides=
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=1080 h=2400 mLayoutSeq=18196
mBaseLayer=21000 mSubLayer=0 mToken=ActivityRecord{49392223 u0 com.x8bit.bitwarden.dev/com.x8bit.bitwarden.MainActivity t1857}
mActivityRecord=ActivityRecord{49392223 u0 com.x8bit.bitwarden.dev/com.x8bit.bitwarden.MainActivity t1857}
drawnStateEvaluated=true mightAffectAllDrawn=true
mViewVisibility=0x0 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} s.2 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} s.2 fontWeightAdjustment=0}
mLastReportedActivityWindowInfo=ActivityWindowInfo{isEmbedded=false, taskBounds=Rect(0, 0 - 1080, 2400), taskFragmentBounds=Rect(0, 0 - 1080, 2400)}
mHasSurface=true isReadyForDisplay()=true mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,0][1080,2400] last=[0,0][1080,2400] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{db4c0c7 com.x8bit.bitwarden.dev/com.x8bit.bitwarden.MainActivity}:
mSurface=Surface(name=com.x8bit.bitwarden.dev/com.x8bit.bitwarden.MainActivity#25485)/@0x99ce6f4
Surface: shown=true mDrawState=HAS_DRAWN mLastHidden=false
mEnterAnimationPending=false
isOnScreen=true
isVisible=true
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #9 Window{cb57263 u0 com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity}:
mDisplayId=0 taskId=5 mSession=Session{41a0116 2805:u0a10196} mClient=android.os.BinderProxy@399a892
mOwnerUid=10196 showForAllUsers=false package=com.google.android.apps.nexuslauncher appop=NONE
mAttrs={(0,0)(fillxfill) sim={adjust=nothing} layoutInDisplayCutoutMode=always ty=BASE_APPLICATION fmt=TRANSPARENT wanim=0x1030301
fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SHOW_WALLPAPER SPLIT_TOUCH HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=NO_MOVE_ANIMATION OPTIMIZE_MEASURE EDGE_TO_EDGE_ENFORCED FORCE_DRAW_STATUS_BAR_BACKGROUND FIT_INSETS_CONTROLLED
vsysui=LAYOUT_STABLE LAYOUT_HIDE_NAVIGATION LAYOUT_FULLSCREEN
bhv=DEFAULT
fitSides=
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=1080 h=2400 mLayoutSeq=18155
mBaseLayer=21000 mSubLayer=0 mToken=ActivityRecord{62739071 u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t5}
mActivityRecord=ActivityRecord{62739071 u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t5}
drawnStateEvaluated=true mightAffectAllDrawn=true
mViewVisibility=0x8 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=home mAlwaysOnTop=undefined mRotation=ROTATION_0} s.24 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=home mAlwaysOnTop=undefined mRotation=ROTATION_0} s.24 fontWeightAdjustment=0}
mLastReportedActivityWindowInfo=ActivityWindowInfo{isEmbedded=false, taskBounds=Rect(0, 0 - 1080, 2400), taskFragmentBounds=Rect(0, 0 - 1080, 2400)}
mHasSurface=false isReadyForDisplay()=false mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,0][1080,2400] last=[0,0][1080,2400] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{ae4de1d com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity}:
mDrawState=NO_SURFACE mLastHidden=true
mEnterAnimationPending=false
mWallpaperX=0.0 mWallpaperY=0.5
mWallpaperXStep=0.33333334 mWallpaperYStep=1.0
mWallpaperZoomOut=0.32999983
isOnScreen=false
isVisible=false
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #10 Window{67f1aa4 u0 com.bitwarden.authenticator/com.bitwarden.authenticator.MainActivity}:
mDisplayId=0 taskId=1864 mSession=Session{47e596a 7150:u0a10327} mClient=android.os.BinderProxy@295b537
mOwnerUid=10327 showForAllUsers=false package=com.bitwarden.authenticator appop=NONE
mAttrs={(0,0)(fillxfill) sim={adjust=resize forwardNavigation} layoutInDisplayCutoutMode=always ty=BASE_APPLICATION fmt=TRANSPARENT wanim=0x103030d
fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SPLIT_TOUCH HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=NO_MOVE_ANIMATION EDGE_TO_EDGE_ENFORCED FORCE_DRAW_STATUS_BAR_BACKGROUND FIT_INSETS_CONTROLLED
bhv=DEFAULT
fitSides=
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=1080 h=2400 mLayoutSeq=18152
mBaseLayer=21000 mSubLayer=0 mToken=ActivityRecord{27882677 u0 com.bitwarden.authenticator/.MainActivity t1864}
mActivityRecord=ActivityRecord{27882677 u0 com.bitwarden.authenticator/.MainActivity t1864}
drawnStateEvaluated=true mightAffectAllDrawn=true
mViewVisibility=0x8 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} s.2 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} s.2 fontWeightAdjustment=0}
mLastReportedActivityWindowInfo=ActivityWindowInfo{isEmbedded=false, taskBounds=Rect(0, 0 - 1080, 2400), taskFragmentBounds=Rect(0, 0 - 1080, 2400)}
mHasSurface=false isReadyForDisplay()=false mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,0][1080,2400] last=[0,0][1080,2400] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{68d9892 com.bitwarden.authenticator/com.bitwarden.authenticator.MainActivity}:
mDrawState=NO_SURFACE mLastHidden=true
mEnterAnimationPending=false
isOnScreen=false
isVisible=false
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #11 Window{369ab3f u0 com.android.chrome/com.google.android.apps.chrome.Main}:
mDisplayId=0 taskId=1863 mSession=Session{732339d 12069:u0a10152} mClient=android.os.BinderProxy@7e20b5e
mOwnerUid=10152 showForAllUsers=false package=com.android.chrome appop=NONE
mAttrs={(0,0)(fillxfill) sim={state=always_hidden adjust=resize} layoutInDisplayCutoutMode=always ty=BASE_APPLICATION fmt=TRANSLUCENT wanim=0x103030d sysuil=true
fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SPLIT_TOUCH HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=NO_MOVE_ANIMATION EDGE_TO_EDGE_ENFORCED FORCE_DRAW_STATUS_BAR_BACKGROUND FIT_INSETS_CONTROLLED
bhv=DEFAULT
fitSides=
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=1080 h=2400 mLayoutSeq=18046
mBaseLayer=21000 mSubLayer=0 mToken=ActivityRecord{30085890 u0 com.android.chrome/com.google.android.apps.chrome.Main t1863}
mActivityRecord=ActivityRecord{30085890 u0 com.android.chrome/com.google.android.apps.chrome.Main t1863}
drawnStateEvaluated=true mightAffectAllDrawn=true
mViewVisibility=0x8 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} s.2 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} s.2 fontWeightAdjustment=0}
mLastReportedActivityWindowInfo=ActivityWindowInfo{isEmbedded=false, taskBounds=Rect(0, 0 - 1080, 2400), taskFragmentBounds=Rect(0, 0 - 1080, 2400)}
mHasSurface=false isReadyForDisplay()=false mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,0][1080,2400] last=[0,0][1080,2400] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{1b2a263 com.android.chrome/com.google.android.apps.chrome.Main}:
mDrawState=NO_SURFACE mLastHidden=true
mEnterAnimationPending=false
isOnScreen=false
isVisible=false
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #12 Window{94a5865 u0 com.google.android.apps.weather/com.google.android.apps.weather.home.HomeActivity}:
mDisplayId=0 taskId=1860 mSession=Session{5b870df 8279:u0a10259} mClient=android.os.BinderProxy@cee3d5c
mOwnerUid=10259 showForAllUsers=false package=com.google.android.apps.weather appop=NONE
mAttrs={(0,0)(fillxfill) sim={adjust=resize} layoutInDisplayCutoutMode=always ty=BASE_APPLICATION wanim=0x103030d
fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SPLIT_TOUCH HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=NO_MOVE_ANIMATION EDGE_TO_EDGE_ENFORCED FORCE_DRAW_STATUS_BAR_BACKGROUND FIT_INSETS_CONTROLLED
bhv=DEFAULT
fitSides=
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=1080 h=2400 mLayoutSeq=17742
mBaseLayer=21000 mSubLayer=0 mToken=ActivityRecord{191055369 u0 com.google.android.apps.weather/.home.HomeActivity t1860}
mActivityRecord=ActivityRecord{191055369 u0 com.google.android.apps.weather/.home.HomeActivity t1860}
drawnStateEvaluated=true mightAffectAllDrawn=true
mViewVisibility=0x8 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} s.2 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} s.2 fontWeightAdjustment=0}
mLastReportedActivityWindowInfo=ActivityWindowInfo{isEmbedded=false, taskBounds=Rect(0, 0 - 1080, 2400), taskFragmentBounds=Rect(0, 0 - 1080, 2400)}
mHasSurface=false isReadyForDisplay()=false mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,0][1080,2400] last=[0,0][1080,2400] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{21b7e60 com.google.android.apps.weather/com.google.android.apps.weather.home.HomeActivity}:
mDrawState=NO_SURFACE mLastHidden=true
mEnterAnimationPending=false
isOnScreen=false
isVisible=false
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #13 Window{2783d3c u0 com.android.settings/com.android.settings.homepage.SettingsHomepageActivity}:
mDisplayId=0 taskId=1858 mSession=Session{3c7c9ff 2864:1000} mClient=android.os.BinderProxy@e79e32f
mOwnerUid=1000 showForAllUsers=false package=com.android.settings appop=NONE
mAttrs={(0,0)(fillxfill) sim={adjust=pan} layoutInDisplayCutoutMode=always ty=BASE_APPLICATION wanim=0x1030301
fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SPLIT_TOUCH HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=NO_MOVE_ANIMATION EDGE_TO_EDGE_ENFORCED FORCE_DRAW_STATUS_BAR_BACKGROUND FIT_INSETS_CONTROLLED
bhv=DEFAULT
fitSides=
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=1080 h=2400 mLayoutSeq=17559
mBaseLayer=21000 mSubLayer=0 mToken=ActivityRecord{13824966 u0 com.android.settings/.homepage.SettingsHomepageActivity t1858}
mActivityRecord=ActivityRecord{13824966 u0 com.android.settings/.homepage.SettingsHomepageActivity t1858}
drawnStateEvaluated=true mightAffectAllDrawn=true
mViewVisibility=0x8 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} s.1 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} s.1 fontWeightAdjustment=0}
mLastReportedActivityWindowInfo=ActivityWindowInfo{isEmbedded=false, taskBounds=Rect(0, 0 - 1080, 2400), taskFragmentBounds=Rect(0, 0 - 1080, 2400)}
mHasSurface=false isReadyForDisplay()=false mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[0,0][1080,2400] frame=[0,0][1080,2400] last=[0,0][1080,2400] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{4763f19 com.android.settings/com.android.settings.homepage.SettingsHomepageActivity}:
mDrawState=NO_SURFACE mLastHidden=true
mEnterAnimationPending=false
isOnScreen=false
isVisible=false
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #14 Window{dae7553 u0 com.google.android.youtube/com.google.android.youtube.app.honeycomb.Shell$HomeActivity}:
mDisplayId=0 taskId=1861 mSession=Session{a444b7 32472:u0a10162} mClient=android.os.BinderProxy@afa2142
mOwnerUid=10162 showForAllUsers=false package=com.google.android.youtube appop=NONE
mAttrs={(0,0)(fillxfill) sim={adjust=resize} layoutInDisplayCutoutMode=always ty=BASE_APPLICATION fmt=TRANSLUCENT wanim=0x103030d sysuil=true
fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SPLIT_TOUCH HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=NO_MOVE_ANIMATION EDGE_TO_EDGE_ENFORCED FORCE_DRAW_STATUS_BAR_BACKGROUND FIT_INSETS_CONTROLLED
vsysui=LAYOUT_STABLE LAYOUT_HIDE_NAVIGATION LAYOUT_FULLSCREEN IMMERSIVE_STICKY
bhv=SHOW_TRANSIENT_BARS_BY_SWIPE
fitSides=
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=598 h=336 mLayoutSeq=18064
mBaseLayer=21000 mSubLayer=0 mToken=ActivityRecord{212995835 u0 com.google.android.youtube/.app.honeycomb.Shell$HomeActivity t1861}
mActivityRecord=ActivityRecord{212995835 u0 com.google.android.youtube/.app.honeycomb.Shell$HomeActivity t1861}
drawnStateEvaluated=true mightAffectAllDrawn=true
mViewVisibility=0x8 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} s.7 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw128dp w228dp h128dp 420dpi smll hdr widecg land night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(440, 202 - 1038, 538) mAppBounds=Rect(440, 202 - 1038, 538) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=pinned mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} s.6 fontWeightAdjustment=0}
mLastReportedActivityWindowInfo=ActivityWindowInfo{isEmbedded=false, taskBounds=Rect(440, 202 - 1038, 538), taskFragmentBounds=Rect(440, 202 - 1038, 538)}
mHasSurface=false isReadyForDisplay()=false mWindowRemovalAllowed=false
Frames: parent=[440,202][1038,538] display=[440,202][1038,538] frame=[440,202][1038,538] last=[440,202][1038,538] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{3e8abde com.google.android.youtube/com.google.android.youtube.app.honeycomb.Shell$HomeActivity}:
mDrawState=NO_SURFACE mLastHidden=true
mEnterAnimationPending=false
isOnScreen=false
isVisible=false
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
Window #15 Window{ee012ae u0 com.android.systemui.wallpapers.ImageWallpaper}:
mDisplayId=0 mSession=Session{1e8a728 2463:u0a10210} mClient=android.os.BinderProxy@fc2bf29
mOwnerUid=10210 showForAllUsers=true package=com.android.systemui appop=NONE
mAttrs={(0,0)(1080x2400) gr=TOP START CENTER layoutInDisplayCutoutMode=always ty=WALLPAPER fmt=RGBX_8888 wanim=0x103031d
fl=NOT_FOCUSABLE NOT_TOUCHABLE LAYOUT_IN_SCREEN LAYOUT_NO_LIMITS SCALED LAYOUT_INSET_DECOR
pfl=WANTS_OFFSET_NOTIFICATIONS SHOW_FOR_ALL_USERS
bhv=DEFAULT
frameRateBoostOnTouch=true
dvrrWindowFrameRateHint=true}
Requested w=1080 h=2400 mLayoutSeq=18162
mIsImWindow=false mIsWallpaper=true mIsFloatingLayer=true
mBaseLayer=11000 mSubLayer=0 mToken=WallpaperWindowToken{f41295f showWhenLocked=true}
mViewVisibility=0x0 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mFullConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mHasSurface=true isReadyForDisplay()=false mWindowRemovalAllowed=false
Frames: parent=[0,0][1080,2400] display=[-100000,-100000][100000,100000] frame=[0,0][1080,2400] last=[0,0][1080,2400] insetsChanged=false
surface=[0,0][0,0]
WindowStateAnimator{a2301bf com.android.systemui.wallpapers.ImageWallpaper}:
mSurface=Surface(name=com.android.systemui.wallpapers.ImageWallpaper#63)/@0x42a208c
Surface: shown=false mDrawState=HAS_DRAWN mLastHidden=true
mEnterAnimationPending=false
mWallpaperX=0.0 mWallpaperY=0.5
mWallpaperXStep=0.33333334 mWallpaperYStep=1.0
mWallpaperZoomOut=0.32999983
isOnScreen=false
isVisible=false
keepClearAreas: restricted=[], unrestricted=[]
mPrepareSyncSeqId=0
mBufferSeqId=0
mGlobalConfiguration={1.0 310mcc280mnc [en_US,ar_EG] ldltr sw411dp w411dp h914dp 420dpi nrml long compactNeeded hdr widecg port night finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1080, 2400) mAppBounds=Rect(0, 0 - 1080, 2400) mMaxBounds=Rect(0, 0 - 1080, 2400) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.3034 fontWeightAdjustment=0}
mHasPermanentDpad=false
mTopFocusedDisplayId=0
Minimum task size of display#0 220
Minimum task size of display#589 220
mBlurEnabled=true
mDisableSecureWindows=false
mHighResSnapshotScale=0.8
mSnapshotEnabled=true
SnapshotCache Task
Entry token=1864
topApp=ActivityRecord{27882677 u0 com.bitwarden.authenticator/.MainActivity t1864}
snapshot=TaskSnapshot{ mId=1775056698479 mCaptureTime=2306525709626021 mTopActivityComponent=com.bitwarden.authenticator/.MainActivity mSnapshot=android.hardware.HardwareBuffer@422b3d5 (864x1920) mColorSpace=sRGB IEC61966-2.1 (id=0, model=RGB) mOrientation=1 mRotation=0 mTaskSize=Point(1080, 2400) mContentInsets=[0,128][0,126] mLetterboxInsets=[0,0][0,0] mIsLowResolution=false mIsRealSnapshot=true mWindowingMode=1 mAppearance=0 mIsTranslucent=true mHasImeSurface=false mInternalReferences=2 mWriteToParcelCount=0 mUiMode=21 mDensityDpi=420
Entry token=1863
topApp=ActivityRecord{30085890 u0 com.android.chrome/com.google.android.apps.chrome.Main t1863}
snapshot=TaskSnapshot{ mId=1774987059375 mCaptureTime=2236884117126274 mTopActivityComponent=com.android.chrome/org.chromium.chrome.browser.ChromeTabbedActivity mSnapshot=android.hardware.HardwareBuffer@14fa7ea (864x1920) mColorSpace=sRGB IEC61966-2.1 (id=0, model=RGB) mOrientation=1 mRotation=0 mTaskSize=Point(1080, 2400) mContentInsets=[0,128][0,126] mLetterboxInsets=[0,0][0,0] mIsLowResolution=false mIsRealSnapshot=true mWindowingMode=1 mAppearance=0 mIsTranslucent=true mHasImeSurface=false mInternalReferences=2 mWriteToParcelCount=0 mUiMode=21 mDensityDpi=420
Entry token=1861
topApp=ActivityRecord{212995835 u0 com.google.android.youtube/.app.honeycomb.Shell$HomeActivity t1861}
snapshot=TaskSnapshot{ mId=1774986206569 mCaptureTime=2236031308978926 mTopActivityComponent=com.google.android.youtube/com.google.android.apps.youtube.app.watchwhile.MainActivity mSnapshot=android.hardware.HardwareBuffer@d0ffadb (864x1920) mColorSpace=sRGB IEC61966-2.1 (id=0, model=RGB) mOrientation=1 mRotation=0 mTaskSize=Point(1080, 2400) mContentInsets=[0,128][0,126] mLetterboxInsets=[0,0][0,0] mIsLowResolution=false mIsRealSnapshot=true mWindowingMode=1 mAppearance=0 mIsTranslucent=true mHasImeSurface=false mInternalReferences=2 mWriteToParcelCount=0 mUiMode=21 mDensityDpi=420
Entry token=1860
topApp=ActivityRecord{191055369 u0 com.google.android.apps.weather/.home.HomeActivity t1860}
snapshot=TaskSnapshot{ mId=1774985600020 mCaptureTime=2235424763889762 mTopActivityComponent=com.google.android.apps.weather/.home.HomeActivity mSnapshot=android.hardware.HardwareBuffer@e9eb978 (864x1920) mColorSpace=sRGB IEC61966-2.1 (id=0, model=RGB) mOrientation=1 mRotation=0 mTaskSize=Point(1080, 2400) mContentInsets=[0,128][0,126] mLetterboxInsets=[0,0][0,0] mIsLowResolution=false mIsRealSnapshot=true mWindowingMode=1 mAppearance=0 mIsTranslucent=false mHasImeSurface=false mInternalReferences=2 mWriteToParcelCount=0 mUiMode=21 mDensityDpi=420
Entry token=1858
topApp=ActivityRecord{13824966 u0 com.android.settings/.homepage.SettingsHomepageActivity t1858}
snapshot=TaskSnapshot{ mId=1774980521946 mCaptureTime=2230346684630203 mTopActivityComponent=com.android.settings/.homepage.SettingsHomepageActivity mSnapshot=android.hardware.HardwareBuffer@e7b851 (864x1920) mColorSpace=sRGB IEC61966-2.1 (id=0, model=RGB) mOrientation=1 mRotation=0 mTaskSize=Point(1080, 2400) mContentInsets=[0,128][0,126] mLetterboxInsets=[0,0][0,0] mIsLowResolution=false mIsRealSnapshot=true mWindowingMode=1 mAppearance=0 mIsTranslucent=false mHasImeSurface=false mInternalReferences=2 mWriteToParcelCount=0 mUiMode=21 mDensityDpi=420
mHighResSnapshotScale=0.6
mSnapshotEnabled=true
SnapshotCache Activity
UserSavedFile userId=0
mInputMethodWindow=Window{d056a34 u0 InputMethod}
mTraversalScheduled=false
mSystemBooted=true mDisplayEnabled=true
mTransactionSequence=47123
mRotation=0
mLastOrientation=-1
mWaitingForConfig=false
mWindowsInsetsChanged=0
mDisplayRotationWatchers: [ 2000->0 10196->0 10210->0]
Animation settings: disabled=false window=1.0 transition=1.0 animator=1.0

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,122 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseDumpsysWindows, findOverlayAtPoint } from './dumpsys.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const fixtureOutput = readFileSync(
join(__dirname, '__fixtures__', 'dumpsys-windows.txt'),
'utf-8',
);
describe('parseDumpsysWindows', () => {
it('parses all windows from real dumpsys output', () => {
const windows = parseDumpsysWindows(fixtureOutput);
expect(windows.length).toBeGreaterThan(5);
});
it('extracts window names', () => {
const windows = parseDumpsysWindows(fixtureOutput);
const names = windows.map(w => w.name);
expect(names).toContain('FloatingMenu');
expect(names).toContain('StatusBar');
});
it('extracts window types from mAttrs line', () => {
const windows = parseDumpsysWindows(fixtureOutput);
const floatingMenu = windows.find(w => w.name === 'FloatingMenu');
expect(floatingMenu).toBeDefined();
expect(floatingMenu!.type).toBe('NAVIGATION_BAR_PANEL');
});
it('does not match ty= in ROTATION_ lines or mViewVisibility', () => {
const windows = parseDumpsysWindows(fixtureOutput);
// Taskbar has ROTATION_ lines with ty= — should only capture the mAttrs ty=
const taskbar = windows.find(w => w.name === 'Taskbar');
expect(taskbar).toBeDefined();
expect(taskbar!.type).toBe('NAVIGATION_BAR');
});
it('extracts surface visibility', () => {
const windows = parseDumpsysWindows(fixtureOutput);
const floatingMenu = windows.find(w => w.name === 'FloatingMenu');
expect(floatingMenu!.hasSurface).toBe(true);
const notificationShade = windows.find(w => w.name === 'NotificationShade');
expect(notificationShade!.hasSurface).toBe(false);
});
it('extracts touchable region with coordinates', () => {
const windows = parseDumpsysWindows(fixtureOutput);
const floatingMenu = windows.find(w => w.name === 'FloatingMenu');
expect(floatingMenu!.touchableRegion).not.toBeNull();
expect(floatingMenu!.touchableRegion!.left).toBeGreaterThanOrEqual(0);
expect(floatingMenu!.touchableRegion!.right).toBeLessThanOrEqual(1080);
});
it('handles empty SkRegion() as null touchable region', () => {
const windows = parseDumpsysWindows(fixtureOutput);
const screenDecorBottom = windows.find(w => w.name === 'ScreenDecorOverlayBottom');
expect(screenDecorBottom).toBeDefined();
expect(screenDecorBottom!.touchableRegion).toBeNull();
});
it('parses app window as BASE_APPLICATION type', () => {
const windows = parseDumpsysWindows(fixtureOutput);
const appWindow = windows.find(w => w.name.includes('bitwarden'));
expect(appWindow).toBeDefined();
expect(appWindow!.type).toBe('BASE_APPLICATION');
});
});
describe('findOverlayAtPoint', () => {
it('finds FloatingMenu overlay at its touchable region', () => {
const windows = parseDumpsysWindows(fixtureOutput);
const floatingMenu = windows.find(w => w.name === 'FloatingMenu');
expect(floatingMenu?.touchableRegion).not.toBeNull();
const region = floatingMenu!.touchableRegion!;
const center = {
x: Math.floor((region.left + region.right) / 2),
y: Math.floor((region.top + region.bottom) / 2),
};
const overlay = findOverlayAtPoint(windows, center);
// Should find some overlay at this point (FloatingMenu or ScreenDecorOverlay)
expect(overlay).not.toBeNull();
expect(overlay!.type).not.toBe('BASE_APPLICATION');
});
it('returns null for point with no overlays', () => {
const windows = parseDumpsysWindows(fixtureOutput);
// Point in the middle of the screen — unlikely to have overlay touchable regions
const overlay = findOverlayAtPoint(windows, { x: 540, y: 1000 });
expect(overlay).toBeNull();
});
it('excludes BASE_APPLICATION windows', () => {
const windows = parseDumpsysWindows(fixtureOutput);
// The app window covers the whole screen but should never be returned
const overlay = findOverlayAtPoint(windows, { x: 540, y: 1200 });
if (overlay) {
expect(overlay.type).not.toBe('BASE_APPLICATION');
}
});
it('excludes windows without visible surface', () => {
const windows = parseDumpsysWindows(fixtureOutput);
// NotificationShade has touchable region but mHasSurface=false
const shadeRegion = windows.find(w => w.name === 'NotificationShade')?.touchableRegion;
if (shadeRegion) {
const overlay = findOverlayAtPoint(windows, {
x: Math.floor((shadeRegion.left + shadeRegion.right) / 2),
y: Math.floor((shadeRegion.top + shadeRegion.bottom) / 2),
});
// Should not return NotificationShade since its surface is not visible
if (overlay) {
expect(overlay.name).not.toBe('NotificationShade');
}
}
});
});

View File

@@ -0,0 +1,105 @@
/**
* Structured parser for `adb shell dumpsys window windows` output.
*
* Extracts window name, type, surface visibility, and touchable region
* from the multi-line per-window blocks. Replaces the fragile awk
* state machine from the shell scripts.
*/
import { type Rect, type Point, containsPoint } from '../geometry/bounds.js';
export interface WindowInfo {
name: string;
type: string;
hasSurface: boolean;
touchableRegion: Rect | null;
}
/**
* Parse `dumpsys window windows` output into structured window objects.
*/
export function parseDumpsysWindows(output: string): WindowInfo[] {
const windows: WindowInfo[] = [];
let current: Partial<WindowInfo> | null = null;
for (const line of output.split('\n')) {
// New window block: " Window #N Window{hash u0 NAME}:"
const windowMatch = line.match(/Window #\d+ Window\{[0-9a-f]+ \S+ (.+)\}:/);
if (windowMatch) {
if (current?.name) {
windows.push(finalizeWindow(current));
}
current = { name: windowMatch[1], type: '', hasSurface: false, touchableRegion: null };
continue;
}
if (!current) continue;
// Window type: " ty=TYPE " (leading space to avoid matching mViewVisibility=0x0)
// Only match on the mAttrs line, not ROTATION_ lines
if (!current.type && line.includes('mAttrs=') && line.includes(' ty=')) {
const tyMatch = line.match(/ ty=(\S+)/);
if (tyMatch) {
current.type = tyMatch[1];
}
}
// Surface visibility
if (line.includes('mHasSurface=true')) {
current.hasSurface = true;
}
// Touchable region: SkRegion((l,t,r,b)) or SkRegion((l,t,r,b)(l2,t2,r2,b2))
// We take the first rect if multiple. Empty SkRegion() means no touchable area.
if (line.includes('touchable region=SkRegion(')) {
const regionMatch = line.match(/SkRegion\(\((\d+),(\d+),(\d+),(\d+)\)/);
if (regionMatch) {
current.touchableRegion = {
left: parseInt(regionMatch[1], 10),
top: parseInt(regionMatch[2], 10),
right: parseInt(regionMatch[3], 10),
bottom: parseInt(regionMatch[4], 10),
};
}
// SkRegion() with no coords = no touchable area, leave as null
}
}
// Don't forget the last window
if (current?.name) {
windows.push(finalizeWindow(current));
}
return windows;
}
function finalizeWindow(partial: Partial<WindowInfo>): WindowInfo {
return {
name: partial.name ?? '',
type: partial.type ?? '',
hasSurface: partial.hasSurface ?? false,
touchableRegion: partial.touchableRegion ?? null,
};
}
/**
* Find the first overlay window whose touchable region contains the given point.
*
* Filters out BASE_APPLICATION windows (the app itself) and windows without
* a visible surface or touchable region. Only windows that actually intercept
* taps are considered.
*/
export function findOverlayAtPoint(windows: WindowInfo[], point: Point): WindowInfo | null {
for (const win of windows) {
if (
win.hasSurface &&
win.type !== 'BASE_APPLICATION' &&
win.type !== '' &&
win.touchableRegion &&
containsPoint(win.touchableRegion, point)
) {
return win;
}
}
return null;
}

View File

@@ -0,0 +1,120 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseHierarchy, findElementByText, findTopmostClickableAt } from './xml.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const fixtureXml = readFileSync(join(__dirname, '__fixtures__', 'view.xml'), 'utf-8');
describe('parseHierarchy', () => {
it('parses real UIAutomator XML into a node tree', () => {
const root = parseHierarchy(fixtureXml);
expect(root.className).toBe('android.widget.FrameLayout');
expect(root.packageName).toBe('com.x8bit.bitwarden.dev');
expect(root.bounds).toEqual({ left: 0, top: 0, right: 1080, bottom: 2400 });
});
it('preserves the full tree depth with children', () => {
const root = parseHierarchy(fixtureXml);
expect(root.children.length).toBeGreaterThan(0);
// Should have deeply nested children
let depth = 0;
let node = root;
while (node.children.length > 0) {
node = node.children[0];
depth++;
}
expect(depth).toBeGreaterThan(5);
});
it('parses boolean attributes correctly', () => {
const root = parseHierarchy(fixtureXml);
// Root FrameLayout is not clickable
expect(root.clickable).toBe(false);
expect(root.enabled).toBe(true);
});
it('throws on invalid XML', () => {
expect(() => parseHierarchy('<invalid>')).toThrow();
});
it('throws on XML without hierarchy root', () => {
expect(() => parseHierarchy('<?xml version="1.0"?><other/>')).toThrow('missing <hierarchy>');
});
});
describe('findElementByText', () => {
it('finds element by text attribute', () => {
const root = parseHierarchy(fixtureXml);
const el = findElementByText(root, 'Login');
expect(el).not.toBeNull();
expect(el!.text).toBe('Login');
});
it('finds element by content-desc', () => {
const root = parseHierarchy(fixtureXml);
const el = findElementByText(root, 'Add Item');
expect(el).not.toBeNull();
expect(el!.contentDesc).toBe('Add Item');
});
it('is case-insensitive', () => {
const root = parseHierarchy(fixtureXml);
const el = findElementByText(root, 'login');
expect(el).not.toBeNull();
expect(el!.text).toBe('Login');
});
it('returns null for non-existent text', () => {
const root = parseHierarchy(fixtureXml);
expect(findElementByText(root, 'NONEXISTENT_TEXT_12345')).toBeNull();
});
it('returns element with parsed bounds', () => {
const root = parseHierarchy(fixtureXml);
const el = findElementByText(root, 'Settings');
expect(el).not.toBeNull();
expect(el!.bounds).not.toBeNull();
expect(el!.bounds!.left).toBeGreaterThanOrEqual(0);
expect(el!.bounds!.right).toBeLessThanOrEqual(1080);
});
});
describe('findTopmostClickableAt', () => {
it('finds the topmost clickable element at a point', () => {
const root = parseHierarchy(fixtureXml);
// Point in the center of the screen — should find something clickable
const el = findTopmostClickableAt(root, { x: 540, y: 1200 });
// May or may not find something depending on layout, but shouldn't crash
if (el) {
expect(el.clickable).toBe(true);
expect(el.bounds).not.toBeNull();
}
});
it('returns null for a point with no clickable elements', () => {
const root = parseHierarchy(fixtureXml);
// Point in the status bar area — unlikely to have clickable app elements
const el = findTopmostClickableAt(root, { x: 540, y: 50 });
// Could be null or a system element — just verify no crash
expect(el === null || el.clickable === true).toBe(true);
});
it('returns the LAST clickable in document order (highest z-order)', () => {
const root = parseHierarchy(fixtureXml);
// Find the "Add Item" FAB element to get its center
const fab = findElementByText(root, 'Add Item');
if (fab?.bounds) {
const fabCenter = {
x: Math.floor((fab.bounds.left + fab.bounds.right) / 2),
y: Math.floor((fab.bounds.top + fab.bounds.bottom) / 2),
};
const topmost = findTopmostClickableAt(root, fabCenter);
expect(topmost).not.toBeNull();
// The topmost clickable at the FAB's center should be the FAB itself
// or its clickable parent (bounds should overlap)
expect(topmost!.bounds).not.toBeNull();
}
});
});

View File

@@ -0,0 +1,121 @@
/**
* UIAutomator XML hierarchy parser.
*
* Converts Android's single-line UIAutomator XML dump into a typed, traversable
* node tree. Replaces the fragile grep/awk approach from the shell scripts.
*/
import { XMLParser } from 'fast-xml-parser';
import { type Rect, type Point, parseBounds, containsPoint } from '../geometry/bounds.js';
export interface UiNode {
text: string;
contentDesc: string;
resourceId: string;
className: string;
packageName: string;
bounds: Rect | null;
clickable: boolean;
focused: boolean;
enabled: boolean;
selected: boolean;
drawingOrder: number;
children: UiNode[];
}
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
// Ensure 'node' is always an array even when there's only one child
isArray: (name) => name === 'node',
});
/**
* Parse a UIAutomator XML dump into a typed node tree.
*/
export function parseHierarchy(xml: string): UiNode {
const parsed = parser.parse(xml);
const hierarchy = parsed?.hierarchy;
if (!hierarchy) {
throw new Error('Invalid UIAutomator XML: missing <hierarchy> root');
}
const rootNodes = hierarchy.node;
if (!rootNodes || !Array.isArray(rootNodes) || rootNodes.length === 0) {
throw new Error('Invalid UIAutomator XML: no nodes found');
}
return convertNode(rootNodes[0]);
}
function convertNode(raw: any): UiNode {
const children: UiNode[] = [];
if (raw.node) {
const childNodes = Array.isArray(raw.node) ? raw.node : [raw.node];
for (const child of childNodes) {
children.push(convertNode(child));
}
}
return {
text: raw.text ?? '',
contentDesc: raw['content-desc'] ?? '',
resourceId: raw['resource-id'] ?? '',
className: raw.class ?? '',
packageName: raw.package ?? '',
bounds: parseBounds(raw.bounds ?? ''),
clickable: raw.clickable === 'true',
focused: raw.focused === 'true',
enabled: raw.enabled === 'true',
selected: raw.selected === 'true',
drawingOrder: parseInt(raw['drawing-order'] ?? '0', 10),
children,
};
}
/**
* Find the first element matching search text in text or content-desc.
* Searches depth-first.
*/
export function findElementByText(root: UiNode, searchText: string): UiNode | null {
const lower = searchText.toLowerCase();
function search(node: UiNode): UiNode | null {
if (
node.text.toLowerCase().includes(lower) ||
node.contentDesc.toLowerCase().includes(lower)
) {
return node;
}
for (const child of node.children) {
const found = search(child);
if (found) return found;
}
return null;
}
return search(root);
}
/**
* Find the topmost clickable element at a given point.
*
* In UIAutomator's depth-first XML, the LAST clickable element whose bounds
* contain the point is the one that receives the tap (highest z-order at that
* point). This traverses the full tree and returns the last match.
*/
export function findTopmostClickableAt(root: UiNode, point: Point): UiNode | null {
let result: UiNode | null = null;
function traverse(node: UiNode): void {
if (node.clickable && node.bounds && containsPoint(node.bounds, point)) {
result = node;
}
for (const child of node.children) {
traverse(child);
}
}
traverse(root);
return result;
}

View File

@@ -0,0 +1,52 @@
/**
* Capture tool — dump UI hierarchy XML and/or screenshot from the connected device.
*/
import { z } from 'zod';
import type { ToolDefinition } from '../utils/validation.js';
import { validateInput } from '../utils/validation.js';
import * as adb from '../adb/adb.js';
import { resolve } from 'node:path';
const CaptureSchema = z.object({
xml: z.boolean().optional().default(true),
screenshot: z.boolean().optional().default(true),
});
const capture: ToolDefinition = {
name: 'capture',
description:
'Capture current Android device state. Dumps UI hierarchy XML and/or takes a screenshot. ' +
'Files are saved to the current working directory as view.xml and screen.png.',
inputSchema: {
type: 'object',
properties: {
xml: { type: 'boolean', description: 'Capture UI hierarchy XML (default: true)' },
screenshot: { type: 'boolean', description: 'Capture screenshot (default: true)' },
},
},
async handler(input: unknown): Promise<string> {
const { xml, screenshot } = validateInput(CaptureSchema, input);
const results: string[] = [];
if (xml) {
const xmlPath = resolve('view.xml');
await adb.dumpHierarchy(xmlPath);
results.push(`UI hierarchy saved to: ${xmlPath}`);
}
if (screenshot) {
const pngPath = resolve('screen.png');
await adb.screenshot(pngPath);
results.push(`Screenshot saved to: ${pngPath}`);
}
if (results.length === 0) {
return 'Nothing captured. Set xml and/or screenshot to true.';
}
return results.join('\n');
},
};
export default capture;

View File

@@ -0,0 +1,65 @@
/**
* Shared pipeline for finding a UI element with obstruction detection.
* Used by both find_element and tap_element tools.
*/
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import * as adb from '../adb/adb.js';
import { type Point, center } from '../geometry/bounds.js';
import { detectObstruction, type ObstructionResult } from '../geometry/obstruction.js';
import { parseHierarchy, findElementByText, type UiNode } from '../parsers/xml.js';
import { parseDumpsysWindows } from '../parsers/dumpsys.js';
export interface FindElementResult {
target: UiNode;
tapPoint: Point;
effectivePoint: Point;
obstruction: ObstructionResult;
}
/**
* Dump hierarchy, find element by text, run obstruction detection.
* Returns null with an error message if element not found.
*/
export async function findElementWithObstruction(
text: string,
): Promise<{ result: FindElementResult } | { error: string }> {
const xmlPath = resolve('view.xml');
await adb.dumpHierarchy(xmlPath);
const xml = readFileSync(xmlPath, 'utf-8');
const hierarchy = parseHierarchy(xml);
const target = findElementByText(hierarchy, text);
if (!target) {
return { error: `Element not found: "${text}"\n\nNo element with matching text or content-desc was found in the UI hierarchy.` };
}
if (!target.bounds) {
return { error: `Element found but has no bounds: "${text}"` };
}
const tapPoint = center(target.bounds);
let dumpsysOutput: string;
try {
dumpsysOutput = await adb.dumpsysWindows();
} catch {
dumpsysOutput = '';
}
const windows = parseDumpsysWindows(dumpsysOutput);
const obstruction = detectObstruction({
hierarchy,
windows,
targetElement: target,
tapPoint,
searchText: text,
});
const effectivePoint = obstruction.obstructed && obstruction.adjustedPoint
? obstruction.adjustedPoint
: tapPoint;
return { result: { target, tapPoint, effectivePoint, obstruction } };
}

View File

@@ -0,0 +1,77 @@
/**
* Find element tool — locate a UI element by text/content-desc with obstruction detection.
*/
import { z } from 'zod';
import type { ToolDefinition } from '../utils/validation.js';
import { validateInput } from '../utils/validation.js';
import { findElementWithObstruction } from './find-element-pipeline.js';
const FindElementSchema = z.object({
text: z.string().min(1),
});
const findElement: ToolDefinition = {
name: 'find_element',
description:
'Find a UI element by text or content-desc and return tap coordinates. ' +
'Includes two-layer obstruction detection: system overlays (TalkBack, PiP) via dumpsys, ' +
'and in-app elements (FABs, dialogs) via the UI hierarchy. When obstructed, returns ' +
'adjusted coordinates targeting the largest visible region of the element.',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: 'Text or content-desc to search for' },
},
required: ['text'],
},
async handler(input: unknown): Promise<string> {
const { text } = validateInput(FindElementSchema, input);
const outcome = await findElementWithObstruction(text);
if ('error' in outcome) return outcome.error;
const { target, tapPoint, effectivePoint, obstruction } = outcome.result;
const lines: string[] = [];
if (!obstruction.obstructed) {
lines.push(`Element found: "${target.text || target.contentDesc}"`);
lines.push(`Coordinates: (${effectivePoint.x}, ${effectivePoint.y})`);
lines.push('Status: CLEAR');
} else {
lines.push(`Element found: "${target.text || target.contentDesc}"`);
lines.push(`Status: OBSTRUCTED by ${obstruction.obstructor}`);
if (obstruction.fullyObscured) {
lines.push(`Coordinates: (${effectivePoint.x}, ${effectivePoint.y}) — FULLY OBSCURED, original center used`);
} else {
lines.push(`Adjusted coordinates: (${effectivePoint.x}, ${effectivePoint.y}) — center of largest visible strip`);
}
}
const result = {
found: true,
text: target.text,
contentDesc: target.contentDesc,
resourceId: target.resourceId,
bounds: target.bounds,
center: tapPoint,
effectivePoint,
obstructed: obstruction.obstructed,
...(obstruction.obstructed ? {
obstructor: obstruction.obstructor,
obstructorBounds: obstruction.obstructorBounds,
fullyObscured: obstruction.fullyObscured,
visibleRegion: obstruction.visibleRegion?.rect ?? null,
} : {}),
};
lines.push('');
lines.push('```json');
lines.push(JSON.stringify(result, null, 2));
lines.push('```');
return lines.join('\n');
},
};
export default findElement;

View File

@@ -0,0 +1,70 @@
/**
* Input text tool — type text into the focused field, with optional clearing.
*/
import { z } from 'zod';
import type { ToolDefinition } from '../utils/validation.js';
import { validateInput } from '../utils/validation.js';
import * as adb from '../adb/adb.js';
const KEYCODE_MOVE_END = 123;
const KEYCODE_DEL = 67;
const InputTextSchema = z.object({
text: z.string().min(1),
clear: z.boolean().default(false),
});
/**
* Clear the currently focused text field by moving to the end and
* sending enough delete key events to remove all characters.
* Uses a generous count to ensure complete clearing.
*/
async function clearField(): Promise<void> {
await adb.keyevent(KEYCODE_MOVE_END);
// Send 50 deletes — more than enough for any reasonable field length.
// ADB processes them almost instantly and extras on an empty field are no-ops.
const deletes = Array(50).fill(String(KEYCODE_DEL)).join(' ');
await adb.shell(`input keyevent ${deletes}`);
}
const inputText: ToolDefinition = {
name: 'input_text',
description:
'Type text into the currently focused input field. Optionally clear existing content first. ' +
'The field must already be focused (tap it first if needed).',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: 'Text to type into the focused field' },
clear: {
type: 'boolean',
description: 'Clear existing field content before typing (default: false)',
},
},
required: ['text'],
},
async handler(input: unknown): Promise<string> {
const { text, clear } = validateInput(InputTextSchema, input);
if (clear) {
await clearField();
}
// Escape characters that the Android shell interprets inside double quotes:
// " $ ` \ are all special in sh double-quoted strings.
const escaped = text
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\$/g, '\\$')
.replace(/`/g, '\\`');
await adb.shell(`input text "${escaped}"`);
const lines: string[] = [];
if (clear) lines.push('Cleared existing content');
lines.push(`Typed: "${text}"`);
return lines.join('\n');
},
};
export default inputText;

View File

@@ -0,0 +1,62 @@
/**
* Navigate tool — perform common navigation actions on the device.
*/
import { z } from 'zod';
import { resolve } from 'node:path';
import type { ToolDefinition } from '../utils/validation.js';
import { validateInput } from '../utils/validation.js';
import * as adb from '../adb/adb.js';
const NavigateSchema = z.object({
action: z.enum(['home', 'back', 'app-drawer']),
waitSeconds: z.number().min(0).default(1),
});
const navigate: ToolDefinition = {
name: 'navigate',
description:
'Perform a navigation action on the Android device: go home, press back, or open the app drawer. ' +
'Captures a screenshot after the action completes.',
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['home', 'back', 'app-drawer'],
description: 'Navigation action to perform',
},
waitSeconds: { type: 'number', description: 'Seconds to wait after action before capture (default: 1)' },
},
required: ['action'],
},
async handler(input: unknown): Promise<string> {
const { action, waitSeconds } = validateInput(NavigateSchema, input);
switch (action) {
case 'home':
await adb.keyevent(3);
break;
case 'back':
await adb.keyevent(4);
break;
case 'app-drawer': {
const screen = await adb.getScreenSize();
const cx = Math.floor(screen.width / 2);
const fromY = Math.floor(screen.height * 0.93);
const toY = Math.floor(screen.height * 0.17);
await adb.swipe(cx, fromY, cx, toY, 1000);
break;
}
}
await adb.sleep(waitSeconds ?? 1);
const pngPath = resolve('screen.png');
await adb.screenshot(pngPath);
return `Navigated: ${action}\nScreenshot saved to: ${pngPath}`;
},
};
export default navigate;

View File

@@ -0,0 +1,44 @@
/**
* Tap at coordinates tool — tap a specific screen location, wait, and capture screenshot.
*/
import { z } from 'zod';
import { resolve } from 'node:path';
import type { ToolDefinition } from '../utils/validation.js';
import { validateInput } from '../utils/validation.js';
import * as adb from '../adb/adb.js';
const TapAtSchema = z.object({
x: z.number().int().nonnegative(),
y: z.number().int().nonnegative(),
waitSeconds: z.number().min(0).default(2),
});
const tapAt: ToolDefinition = {
name: 'tap_at',
description:
'Tap at specific screen coordinates, wait for the UI to settle, and capture a screenshot. ' +
'Returns the path to the captured screenshot.',
inputSchema: {
type: 'object',
properties: {
x: { type: 'number', description: 'X coordinate to tap' },
y: { type: 'number', description: 'Y coordinate to tap' },
waitSeconds: { type: 'number', description: 'Seconds to wait after tap before capture (default: 2)' },
},
required: ['x', 'y'],
},
async handler(input: unknown): Promise<string> {
const { x, y, waitSeconds } = validateInput(TapAtSchema, input);
await adb.tap(x, y);
await adb.sleep(waitSeconds ?? 2);
const pngPath = resolve('screen.png');
await adb.screenshot(pngPath);
return `Tapped at (${x}, ${y}), waited ${waitSeconds}s\nScreenshot saved to: ${pngPath}`;
},
};
export default tapAt;

View File

@@ -0,0 +1,65 @@
/**
* Tap element tool — find an element by text, tap it, and capture screenshot.
* Uses the shared find-element pipeline for obstruction detection.
*/
import { z } from 'zod';
import { resolve } from 'node:path';
import type { ToolDefinition } from '../utils/validation.js';
import { validateInput } from '../utils/validation.js';
import * as adb from '../adb/adb.js';
import { findElementWithObstruction } from './find-element-pipeline.js';
const TapElementSchema = z.object({
text: z.string().min(1),
waitSeconds: z.number().min(0).default(2),
});
const tapElement: ToolDefinition = {
name: 'tap_element',
description:
'Find a UI element by text or content-desc, tap it, and capture a screenshot. ' +
'Automatically detects obstructions and adjusts tap coordinates to the largest visible region. ' +
'Returns element info, tap coordinates, obstruction status, and screenshot path.',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: 'Text or content-desc of the element to tap' },
waitSeconds: { type: 'number', description: 'Seconds to wait after tap before capture (default: 2)' },
},
required: ['text'],
},
async handler(input: unknown): Promise<string> {
const { text, waitSeconds } = validateInput(TapElementSchema, input);
const outcome = await findElementWithObstruction(text);
if ('error' in outcome) return `Error: ${outcome.error}`;
const { target, effectivePoint, obstruction } = outcome.result;
const lines: string[] = [];
lines.push(`Element found: "${target.text || target.contentDesc}"`);
if (obstruction.obstructed) {
lines.push(`WARNING: Obstructed by ${obstruction.obstructor}`);
if (obstruction.fullyObscured) {
lines.push('FULLY OBSCURED — tapping original center as best effort');
} else {
lines.push(`Using adjusted coordinates: (${effectivePoint.x}, ${effectivePoint.y})`);
}
}
await adb.tap(effectivePoint.x, effectivePoint.y);
await adb.sleep(waitSeconds ?? 2);
const pngPath = resolve('screen.png');
await adb.screenshot(pngPath);
lines.push(`Tapped at (${effectivePoint.x}, ${effectivePoint.y})`);
lines.push(`Screenshot saved to: ${pngPath}`);
return lines.join('\n');
},
};
export default tapElement;

View File

@@ -0,0 +1,32 @@
/**
* Input validation and tool definition types.
*/
import { z } from 'zod';
/**
* Shape of a tool module's default export.
* Each tool file exports a ToolDefinition with metadata and a handler function.
*/
export interface ToolDefinition {
name: string;
description: string;
inputSchema: any;
handler: (input: any) => Promise<any>;
}
/**
* Validate input against a Zod schema.
* @throws {Error} with formatted validation messages on failure
*/
export function validateInput<T>(schema: z.ZodSchema<T>, input: unknown): T {
try {
return schema.parse(input);
} catch (error) {
if (error instanceof z.ZodError) {
const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
throw new Error(`Validation failed: ${messages.join(', ')}`);
}
throw error;
}
}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build", "src/**/*.spec.ts"]
}

View File

@@ -0,0 +1,134 @@
---
name: interacting-with-android-device
description: Instructions for capturing UI state, comparing with mocks, and interacting with an Android device using MCP tools backed by ADB.
allowed-tools: mcp__android-device__capture, mcp__android-device__find_element, mcp__android-device__tap_at, mcp__android-device__tap_element, mcp__android-device__navigate, mcp__android-device__input_text, Bash(adb:*), Bash(sleep:*), Bash(./gradlew install*:*), Read, Glob
---
# Interacting with Android Device
## Quick Start: MCP Tools
The `android-device` MCP server provides 6 tools for device interaction. These replace the previous shell scripts with proper XML parsing, structured dumpsys parsing, and native obstruction detection.
**Available tools:**
- `capture` — Capture UI hierarchy XML and/or screenshot. Params: `{ xml?: boolean, screenshot?: boolean }`. Default: both.
- `find_element` — Find element by `text` or `content-desc`, return coordinates with **obstruction detection**. Params: `{ text: string }`. Returns JSON with coordinates, bounds, and obstruction status.
- `tap_at` — Tap at specific coordinates, wait, capture screenshot. Params: `{ x, y, waitSeconds? }`.
- `tap_element` — Find, tap, and capture in one call (recommended). Params: `{ text, waitSeconds? }`. Auto-adjusts coordinates when obstructed.
- `navigate` — Navigation actions: home, back, app-drawer. Params: `{ action, waitSeconds? }`. Captures screenshot after action.
- `input_text` — Type text into the focused field. Params: `{ text, clear? }`. Set `clear: true` to erase existing content first.
**Use these MCP tools instead of raw ADB commands** to save tokens, get structured results, and benefit from automatic obstruction detection.
## 1. Capturing Current State
To understand what is currently on the device, use the `capture` tool:
* It saves `view.xml` (UI hierarchy) and `screen.png` (screenshot) to the working directory
* Read `view.xml` to find coordinates (`bounds`) and properties (like `text` or `resource-id`) of UI elements
* Use `screen.png` for visual verification against design mocks
## 2. Interacting with the Device
### Using MCP Tools (Recommended)
* **Find and tap an element by text** — use `tap_element`:
This finds the element, detects obstructions, taps (with adjusted coordinates if needed), and captures a screenshot — all in one call.
* **Tap at specific coordinates** — use `tap_at`:
When you already have coordinates from `find_element` or manual inspection.
* **Navigate (home, back, app-drawer)** — use `navigate`:
Performs the action and captures a screenshot.
* **Find element without tapping** — use `find_element`:
Returns coordinates and full element info. Useful when you need to inspect before acting.
* **Type text into a field** — use `input_text`:
Types text into the currently focused field. Set `clear: true` to erase existing content first. Tap the field before calling this if it isn't already focused.
### Raw ADB Commands (When MCP Tools Aren't Sufficient)
* **Key Events**:
* Back: `adb shell input keyevent 4`
* Home: `adb shell input keyevent 3`
* Enter: `adb shell input keyevent 66`
* **Scrolling/Swiping**: Use `adb shell input swipe <x1> <y1> <x2> <y2> <duration_ms>` where:
* `(x1, y1)` = starting point
* `(x2, y2)` = ending point
* `duration_ms` = duration in milliseconds (1000ms is typical; adjust for speed/distance)
* **Note**: For expanding containers/drawers, use large distances (e.g., 2400->300 for a 2992px tall screen)
## 3. Obstruction Detection
The `find_element` and `tap_element` tools automatically detect when another element would intercept the tap. This catches:
* **System overlays** (Layer 1): TalkBack floating menu, PiP windows, accessibility services — detected via `dumpsys window windows` touchable regions
* **In-app elements** (Layer 2): FABs, dialogs, bottom sheets, snackbars — detected by finding the topmost clickable element at the tap point in the UI hierarchy
When obstruction is detected:
* Coordinates are **auto-adjusted** to the center of the largest unobstructed strip (top/bottom/left/right of the obstructor)
* The response includes the obstructor identity, bounds, and visible region info
* If fully obscured (no visible region), the original center is returned as best-effort
* **Compose parent wrapper** pattern (identical bounds) is recognized as non-obstruction
## 4. Verification Workflow
Follow these steps for a complete UI test:
1. **Build and Install**: Ensure the latest version of the app is running: `./gradlew installDebug`.
2. **Inspect**: Use `capture` to dump the UI hierarchy and take a screenshot.
3. **Compare**: Check the current UI against any mock image files in the project.
4. **Interact**: Use `tap_element` to tap a UI element by text. The tool handles coordinate calculation and obstruction detection automatically.
5. **Verify**: Use `capture` again to confirm the UI has updated as expected (e.g., a new screen is shown, or a success message appeared).
## 5. Examples
### Example: Navigate to Settings and Check for Updates
```
# Go to home screen
navigate({ action: "home" })
# Open app drawer
navigate({ action: "app-drawer" })
# Find and tap through settings
tap_element({ text: "Settings", waitSeconds: 2 })
tap_element({ text: "System", waitSeconds: 2 })
tap_element({ text: "Software updates", waitSeconds: 2 })
tap_element({ text: "Check for update", waitSeconds: 5 })
```
### Example: Swiping
For swipe gestures not covered by the navigate tool, use raw ADB:
```bash
adb shell input swipe 672 2800 672 500 1000 && sleep 1 && adb shell screencap -p /sdcard/screen.png && adb pull /sdcard/screen.png .
```
## 6. Best Practices
### Coordinate Calculation
* Prefer `find_element` or `tap_element` over manual coordinate calculation — they handle bounds parsing, center computation, and obstruction detection automatically
* When multiple instances of an element exist (e.g., in prediction row and full list), check the `find_element` response to verify you're targeting the correct one
### Navigation and State Evaluation
* **Verify after each interaction**: Don't assume an action succeeded — use `capture` after interactions to confirm the UI changed as expected
* **Check both visual and structural state**: Use screenshot for visual verification, XML dump for structural confirmation (element presence, text content, state changes)
* **Identify navigation failures early**: If a tap opened the wrong screen, use `navigate({ action: "back" })` to recover immediately
### Interaction Patterns
* **Scrolling before interaction**: When looking for an element, check if it's visible on screen first. If not, scroll using swipe gestures to reveal it
* **Use consistent scroll direction**: For vertical scrolling in lists/settings, use downward swipes (higher Y -> lower Y) to scroll down
* **Handle app crashes gracefully**: Don't retry the same action — use back button and try an alternative approach
* **Check Accessibility**: Use the `content-desc` and `text` properties in the UI hierarchy to ensure the UI is accessible for screen readers
## 7. Troubleshooting
### Device Not Connected
If tools report ADB errors:
* Check USB connection or emulator status
* Enable USB debugging on the device (Settings > Developer Options > USB Debugging)
* Accept the RSA key prompt on the device if asked
* Restart the device or disconnect/reconnect the USB cable
* Run `adb devices` to verify the device is visible
### MCP Server Not Available
If tools are not listed in `/mcp`:
* Ensure Node.js 18+ is installed
* The server auto-builds on first use via `.mcp.json` at the project root
* Check `.claude/mcp/android-device-server/` exists with `package.json`
* Try manual build: `cd .claude/mcp/android-device-server && npm install && npm run build`

9
.mcp.json Normal file
View File

@@ -0,0 +1,9 @@
{
"mcpServers": {
"android-device": {
"type": "stdio",
"command": "bash",
"args": ["-c", "cd .claude/mcp/android-device-server && npm install --silent >/dev/null 2>&1 && npm run build >/dev/null 2>&1 && exec node build/index.js"]
}
}
}