UNPKG

appium-adb

Version:

Android Debug Bridge interface

337 lines (315 loc) 10.7 kB
import {log} from '../logger'; import _ from 'lodash'; import {fs, util} from '@appium/support'; import {SubProcess, exec, type ExecError} from 'teen_process'; import type {ADB} from '../adb'; import type {ScreenrecordOptions, StringRecord} from './types'; /** * Get the path to adb executable amd assign it * to this.executable.path and this.binaries.adb properties. * * @return ADB instance. */ export async function getAdbWithCorrectAdbPath(this: ADB): Promise<ADB> { this.executable.path = await this.getSdkBinaryPath('adb'); return this; } /** * Get the full path to aapt tool and assign it to * this.binaries.aapt property */ export async function initAapt(this: ADB): Promise<void> { await this.getSdkBinaryPath('aapt'); } /** * Get the full path to aapt2 tool and assign it to * this.binaries.aapt2 property */ export async function initAapt2(this: ADB): Promise<void> { await this.getSdkBinaryPath('aapt2'); } /** * Get the full path to zipalign tool and assign it to * this.binaries.zipalign property */ export async function initZipAlign(this: ADB): Promise<void> { await this.getSdkBinaryPath('zipalign'); } /** * Get the full path to bundletool binary and assign it to * this.binaries.bundletool property */ export async function initBundletool(this: ADB): Promise<void> { try { const binaries = this.binaries as StringRecord; binaries.bundletool = await fs.which('bundletool.jar'); } catch { throw new Error( 'bundletool.jar binary is expected to be present in PATH. ' + 'Visit https://github.com/google/bundletool for more details.', ); } } /** * Retrieve the API level of the device under test. * * @return The API level as integer number, for example 21 for * Android Lollipop. The result of this method is cached, so all the further * calls return the same value as the first one. */ export async function getApiLevel(this: ADB): Promise<number> { if (!_.isInteger(this._apiLevel)) { try { const strOutput = await this.getDeviceProperty('ro.build.version.sdk'); let apiLevel = parseInt(strOutput.trim(), 10); // Workaround for preview/beta platform API level const charCodeQ = 'q'.charCodeAt(0); // 28 is the first API Level, where Android SDK started returning letters in response to getPlatformVersion const apiLevelDiff = apiLevel - 28; const codename = String.fromCharCode(charCodeQ + apiLevelDiff); if (apiLevelDiff >= 0 && (await this.getPlatformVersion()).toLowerCase() === codename) { log.debug( `Release version is ${codename.toUpperCase()} but found API Level ${apiLevel}. Setting API Level to ${apiLevel + 1}`, ); apiLevel++; } this._apiLevel = apiLevel; log.debug(`Device API level: ${this._apiLevel}`); if (isNaN(this._apiLevel)) { throw new Error(`The actual output '${strOutput}' cannot be converted to an integer`); } } catch (e) { throw new Error(`Error getting device API level. Original error: ${(e as Error).message}`); } } return this._apiLevel as number; } /** * Verify whether a device is connected. * * @return True if at least one device is visible to adb. */ export async function isDeviceConnected(this: ADB): Promise<boolean> { const devices = await this.getConnectedDevices(); return devices.length > 0; } /** * Clear the active text field on the device under test by sending * special keyevents to it. * * @param length - The maximum length of the text in the field to be cleared. */ export async function clearTextField(this: ADB, length: number = 100): Promise<void> { // assumes that the EditText field already has focus log.debug(`Clearing up to ${length} characters`); if (length === 0) { return; } const args = ['input', 'keyevent']; for (let i = 0; i < length; i++) { // we cannot know where the cursor is in the text field, so delete both before // and after so that we get rid of everything // https://developer.android.com/reference/android/view/KeyEvent.html#KEYCODE_DEL // https://developer.android.com/reference/android/view/KeyEvent.html#KEYCODE_FORWARD_DEL args.push('67', '112'); } await this.shell(args); } /** * Send the special keycode to the device under test in order to emulate * Back button tap. */ export async function back(this: ADB): Promise<void> { log.debug('Pressing the BACK button'); await this.keyevent(4); } /** * Send the special keycode to the device under test in order to emulate * Home button tap. */ export async function goToHome(this: ADB): Promise<void> { log.debug('Pressing the HOME button'); await this.keyevent(3); } /** * @return the actual path to adb executable. */ export function getAdbPath(this: ADB): string { return this.executable.path; } /** * Restart the device under test using adb commands. * * @throws {Error} If start fails. */ export async function restart(this: ADB): Promise<void> { try { await this.stopLogcat(); await this.restartAdb(); await this.waitForDevice(60); await this.startLogcat(this._logcatStartupParams); } catch (e) { const err = e as Error; throw new Error(`Restart failed. Original error: ${err.message}`); } } /** * Retrieve the `adb bugreport` command output. This * operation may take up to several minutes. * * @param timeout - Command timeout in milliseconds * @returns Command stdout */ export async function bugreport(this: ADB, timeout: number = 120000): Promise<string> { return await this.adbExec(['bugreport'], {timeout}); } /** * Initiate screenrecord utility on the device * * @param destination - Full path to the writable media file destination * on the device file system. * @param options - Screenrecord options * @returns screenrecord process, which can be then controlled by the client code */ export function screenrecord( this: ADB, destination: string, options: ScreenrecordOptions = {}, ): SubProcess { const cmd: string[] = ['screenrecord']; const {videoSize, bitRate, timeLimit, bugReport} = options; if (util.hasValue(videoSize)) { cmd.push('--size', videoSize); } if (util.hasValue(timeLimit)) { cmd.push('--time-limit', `${timeLimit}`); } if (util.hasValue(bitRate)) { cmd.push('--bit-rate', `${bitRate}`); } if (bugReport) { cmd.push('--bugreport'); } cmd.push(destination); const fullCmd = [...this.executable.defaultArgs, 'shell', ...cmd]; log.debug(`Building screenrecord process with the command line: adb ${util.quote(fullCmd)}`); return new SubProcess(this.executable.path, fullCmd); } /** * Retrieves the list of features supported by the device under test * * @returns the list of supported feature names or an empty list. * An example adb command output: * ``` * cmd * ls_v2 * fixed_push_mkdir * shell_v2 * abb * stat_v2 * apex * abb_exec * remount_shell * fixed_push_symlink_timestamp * ``` * @throws {Error} if there was an error while retrieving the list */ export async function listFeatures(this: ADB): Promise<string[]> { this._memoizedFeatures = this._memoizedFeatures || _.memoize( async () => await this.adbExec(['features']), () => this.curDeviceId, ); try { const memoizedFeatures = this._memoizedFeatures; if (!memoizedFeatures) { throw new Error('Memoized features function is not initialized'); } return (await memoizedFeatures()) .split(/\s+/) .map((x) => x.trim()) .filter(Boolean); } catch (e) { const err = e as ExecError; if (_.includes(err.stderr, 'unknown command')) { return []; } throw err; } } /** * Checks the state of streamed install feature. * This feature allows to speed up apk installation * since it does not require the original apk to be pushed to * the device under test first, which also saves space. * Although, it is required that both the device under test * and the adb server have the mentioned functionality. * See https://github.com/aosp-mirror/platform_system_core/blob/master/adb/client/adb_install.cpp * for more details * * @returns `true` if the feature is supported by both adb and the * device under test */ export async function isStreamedInstallSupported(this: ADB): Promise<boolean> { const proto = Object.getPrototypeOf(this); proto._helpOutput = proto._helpOutput || (await this.adbExec(['help'])); return proto._helpOutput.includes('--streaming') && (await this.listFeatures()).includes('cmd'); } /** * Checks whether incremental install feature is supported by ADB. * Read https://developer.android.com/preview/features#incremental * for more details on it. * * @returns `true` if the feature is supported by both adb and the * device under test */ export async function isIncrementalInstallSupported(this: ADB): Promise<boolean> { const {binary} = await this.getVersion(); if (!binary) { return false; } return ( util.compareVersions(`${binary.version}`, '>=', '30.0.1') && (await this.listFeatures()).includes('abb_exec') ); } /** * Takes a screenshot of the given display or the default display. * * @param displayId A valid display identifier. If * no identifier is provided then the screenshot of the default display is returned. * Note that only recent Android APIs provide multi-screen support. * @returns PNG screenshot payload */ export async function takeScreenshot(this: ADB, displayId?: number | string): Promise<Buffer> { const args = [...this.executable.defaultArgs, 'exec-out', 'screencap', '-p']; // @ts-ignore This validation works as expected const displayIdStr = isNaN(Number(displayId)) ? null : `${displayId}`; if (displayIdStr) { args.push('-d', displayIdStr); } const displayDescr = displayIdStr ? `display #${displayIdStr}` : 'default display'; let stdout: Buffer; try { ({stdout} = await exec(this.executable.path, args, {encoding: 'binary', isBuffer: true})); } catch (e) { const err = e as ExecError; let outputStr = ''; if (err.stderr) { const stderr = err.stderr as string | Buffer; outputStr = typeof stderr === 'string' ? stderr : stderr.toString('utf-8'); } else if (err.stdout) { const stdout = err.stdout as string | Buffer; outputStr = typeof stdout === 'string' ? stdout : stdout.toString('utf-8'); } throw new Error( `Screenshot of the ${displayDescr} failed. ` + `Code: '${err.code ?? 'unknown'}', output: '${outputStr}'`, ); } if (stdout.length === 0) { throw new Error(`Screenshot of the ${displayDescr} returned no data`); } return stdout; }