appium-android-driver
Version:
Android UiAutomator and Chrome support for Appium
196 lines (186 loc) • 7.34 kB
text/typescript
import {errors} from 'appium/driver';
import type {StringRecord} from '@appium/types';
import type {AndroidDriver} from '../driver';
import type {StatusBarCommand, WindowProperties} from './types';
import {util} from '@appium/support';
const WINDOW_TITLE_PATTERN = /^\s+Window\s#\d+\sWindow\{[0-9a-f]+\s\w+\s([\w-]+)\}:$/;
const FRAME_PATTERN = /\bm?[Ff]rame=\[([0-9.-]+),([0-9.-]+)\]\[([0-9.-]+),([0-9.-]+)\]/;
// https://github.com/appium/appium-uiautomator2-driver/issues/904
const STATUS_BAR_TYPE_PATTERN = /\bty=STATUS_BAR\b/;
const NAVIGATION_BAR_TYPE_PATTERN = /\bty=NAVIGATION_BAR\b/;
const VIEW_VISIBILITY_PATTERN = /\bmViewVisibility=(0x[0-9a-fA-F]+)/;
// https://developer.android.com/reference/android/view/View#VISIBLE
const VIEW_VISIBLE = 0x0;
const STATUS_BAR_WINDOW_NAME_PREFIX = 'StatusBar';
const NAVIGATION_BAR_WINDOW_NAME_PREFIX = 'NavigationBar';
const DEFAULT_WINDOW_PROPERTIES: WindowProperties = {
visible: false,
x: 0,
y: 0,
width: 0,
height: 0,
};
interface SystemBarsResult {
statusBar?: WindowProperties;
navigationBar?: WindowProperties;
}
/**
* Gets the system bars (status bar and navigation bar) properties.
*
* @returns Promise that resolves to an object containing statusBar and navigationBar properties.
* @throws {Error} If system bars details cannot be retrieved or parsed.
*/
export async function getSystemBars(this: AndroidDriver): Promise<StringRecord> {
let stdout: string;
try {
stdout = await this.adb.shell(['dumpsys', 'window', 'windows']);
} catch (e) {
throw new Error(
`Cannot retrieve system bars details. Original error: ${(e as Error).message}`,
{
cause: e,
},
);
}
return parseWindows.bind(this)(stdout);
}
/**
* Performs a status bar command.
*
* @param command The status bar command to perform.
* @param component The name of the tile component.
* It is only required for `(add|remove|click)Tile` commands.
* Example value: `com.package.name/.service.QuickSettingsTileComponent`
* @returns Promise that resolves to the command output string.
* @throws {errors.InvalidArgumentError} If the command is unknown.
*/
export async function mobilePerformStatusBarCommand(
this: AndroidDriver,
command: StatusBarCommand,
component?: string,
): Promise<string> {
const toStatusBarCommandCallable =
(cmd: string, argsCallable?: () => string[] | string) => async (): Promise<string> => {
let argsCallableArray = argsCallable ? argsCallable() : [];
if (!Array.isArray(argsCallableArray)) {
argsCallableArray = [argsCallableArray];
}
return await this.adb.shell(['cmd', 'statusbar', cmd, ...argsCallableArray]);
};
const tileCommandArgsCallable = () => component as string;
const statusBarCommands = Object.fromEntries(
(
[
['expandNotifications', ['expand-notifications']],
['expandSettings', ['expand-settings']],
['collapse', ['collapse']],
['addTile', ['add-tile', tileCommandArgsCallable]],
['removeTile', ['remove-tile', tileCommandArgsCallable]],
['clickTile', ['click-tile', tileCommandArgsCallable]],
['getStatusIcons', ['get-status-icons']],
] as const
).map(([name, args]) => [name, toStatusBarCommandCallable(args[0], args[1])]),
) as Record<StatusBarCommand, () => Promise<string>>;
const action = statusBarCommands[command];
if (!action) {
throw new errors.InvalidArgumentError(
`The '${command}' status bar command is unknown. Only the following commands ` +
`are supported: ${Object.keys(statusBarCommands)}`,
);
}
return await action();
}
/**
* Parses window properties from adb dumpsys output
*
* @param name The name of the window whose properties are being parsed
* @param props The list of particular window property lines.
* Check the corresponding unit tests for more details on the input format.
* @returns Parsed properties object
* @throws {Error} If there was an issue while parsing the properties string
*/
export function parseWindowProperties(
this: AndroidDriver,
name: string,
props: string[],
): WindowProperties {
const result = structuredClone(DEFAULT_WINDOW_PROPERTIES);
const propLines = props.join('\n');
const frameMatch = FRAME_PATTERN.exec(propLines);
if (!frameMatch) {
this.log.debug(propLines);
throw new Error(`Cannot parse the frame size from '${name}' window properties`);
}
result.x = parseFloat(frameMatch[1]);
result.y = parseFloat(frameMatch[2]);
result.width = parseFloat(frameMatch[3]) - result.x;
result.height = parseFloat(frameMatch[4]) - result.y;
const visibilityMatch = VIEW_VISIBILITY_PATTERN.exec(propLines);
if (!visibilityMatch) {
this.log.debug(propLines);
throw new Error(`Cannot parse the visibility value from '${name}' window properties`);
}
result.visible = parseInt(visibilityMatch[1], 16) === VIEW_VISIBLE;
return result;
}
/**
* Extracts status and navigation bar information from the window manager output.
*
* @param lines Output from dumpsys command.
* Check the corresponding unit tests for more details on the input format.
* @return An object containing two items where keys are statusBar and navigationBar,
* and values are corresponding WindowProperties objects
* @throws {Error} If no window properties could be parsed
*/
export function parseWindows(this: AndroidDriver, lines: string): SystemBarsResult {
const windows: StringRecord<string[]> = {};
let currentWindowName: string | null = null;
for (const line of lines.split('\n').map((line) => String(line)?.trimEnd())) {
const match = WINDOW_TITLE_PATTERN.exec(line as any);
if (match) {
currentWindowName = match[1];
windows[currentWindowName] = [];
continue;
}
if (String(line)?.trim()?.length === 0) {
currentWindowName = null;
continue;
}
if (currentWindowName && Array.isArray(windows[currentWindowName])) {
windows[currentWindowName].push(line);
}
}
if (util.isEmpty(windows)) {
this.log.debug(lines);
throw new Error('Cannot parse any window information from the dumpsys output');
}
const result: SystemBarsResult = {};
for (const [name, props] of Object.entries(windows)) {
if (
name.startsWith(STATUS_BAR_WINDOW_NAME_PREFIX) ||
props.some((line: string) => STATUS_BAR_TYPE_PATTERN.test(line))
) {
result.statusBar = parseWindowProperties.bind(this)(name, props);
} else if (
name.startsWith(NAVIGATION_BAR_WINDOW_NAME_PREFIX) ||
props.some((line: string) => NAVIGATION_BAR_TYPE_PATTERN.test(line))
) {
result.navigationBar = parseWindowProperties.bind(this)(name, props);
}
}
const unmatchedWindows = (
[
['statusBar', STATUS_BAR_WINDOW_NAME_PREFIX],
['navigationBar', NAVIGATION_BAR_WINDOW_NAME_PREFIX],
] as const
).filter(([name]) => result[name as keyof SystemBarsResult] == null);
for (const [window, namePrefix] of unmatchedWindows) {
this.log.info(
`No windows have been found whose title matches to ` +
`'${namePrefix}'. Assuming it is invisible. ` +
`Only the following windows are available: ${Object.keys(windows)}`,
);
result[window as keyof SystemBarsResult] = structuredClone(DEFAULT_WINDOW_PROPERTIES);
}
return result;
}