mirror of
https://github.com/bitwarden/android.git
synced 2026-05-08 04:16:40 -05:00
[PM-34487] llm: Add Android device interaction MCP server with ADB tooling (#6747)
This commit is contained in:
37
.claude/mcp/android-device-server/.gitignore
vendored
Normal file
37
.claude/mcp/android-device-server/.gitignore
vendored
Normal 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/
|
||||
34
.claude/mcp/android-device-server/package.json
Normal file
34
.claude/mcp/android-device-server/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
60
.claude/mcp/android-device-server/src/adb/adb.spec.ts
Normal file
60
.claude/mcp/android-device-server/src/adb/adb.spec.ts
Normal 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
|
||||
});
|
||||
});
|
||||
141
.claude/mcp/android-device-server/src/adb/adb.ts
Normal file
141
.claude/mcp/android-device-server/src/adb/adb.ts
Normal 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));
|
||||
}
|
||||
54
.claude/mcp/android-device-server/src/geometry/bounds.ts
Normal file
54
.claude/mcp/android-device-server/src/geometry/bounds.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
334
.claude/mcp/android-device-server/src/geometry/geometry.spec.ts
Normal file
334
.claude/mcp/android-device-server/src/geometry/geometry.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
127
.claude/mcp/android-device-server/src/geometry/obstruction.ts
Normal file
127
.claude/mcp/android-device-server/src/geometry/obstruction.ts
Normal 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';
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
64
.claude/mcp/android-device-server/src/index.ts
Normal file
64
.claude/mcp/android-device-server/src/index.ts
Normal 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);
|
||||
});
|
||||
@@ -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
122
.claude/mcp/android-device-server/src/parsers/dumpsys.spec.ts
Normal file
122
.claude/mcp/android-device-server/src/parsers/dumpsys.spec.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
105
.claude/mcp/android-device-server/src/parsers/dumpsys.ts
Normal file
105
.claude/mcp/android-device-server/src/parsers/dumpsys.ts
Normal 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;
|
||||
}
|
||||
120
.claude/mcp/android-device-server/src/parsers/xml.spec.ts
Normal file
120
.claude/mcp/android-device-server/src/parsers/xml.spec.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
121
.claude/mcp/android-device-server/src/parsers/xml.ts
Normal file
121
.claude/mcp/android-device-server/src/parsers/xml.ts
Normal 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;
|
||||
}
|
||||
52
.claude/mcp/android-device-server/src/tools/capture.ts
Normal file
52
.claude/mcp/android-device-server/src/tools/capture.ts
Normal 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;
|
||||
@@ -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 } };
|
||||
}
|
||||
77
.claude/mcp/android-device-server/src/tools/find-element.ts
Normal file
77
.claude/mcp/android-device-server/src/tools/find-element.ts
Normal 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;
|
||||
70
.claude/mcp/android-device-server/src/tools/input-text.ts
Normal file
70
.claude/mcp/android-device-server/src/tools/input-text.ts
Normal 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;
|
||||
62
.claude/mcp/android-device-server/src/tools/navigate.ts
Normal file
62
.claude/mcp/android-device-server/src/tools/navigate.ts
Normal 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;
|
||||
44
.claude/mcp/android-device-server/src/tools/tap-at.ts
Normal file
44
.claude/mcp/android-device-server/src/tools/tap-at.ts
Normal 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;
|
||||
65
.claude/mcp/android-device-server/src/tools/tap-element.ts
Normal file
65
.claude/mcp/android-device-server/src/tools/tap-element.ts
Normal 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;
|
||||
32
.claude/mcp/android-device-server/src/utils/validation.ts
Normal file
32
.claude/mcp/android-device-server/src/utils/validation.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
19
.claude/mcp/android-device-server/tsconfig.json
Normal file
19
.claude/mcp/android-device-server/tsconfig.json
Normal 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"]
|
||||
}
|
||||
134
.claude/skills/interacting-with-android-device/SKILL.md
Normal file
134
.claude/skills/interacting-with-android-device/SKILL.md
Normal 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`
|
||||
Reference in New Issue
Block a user