UNPKG

appium-adb

Version:

Android Debug Bridge interface

470 lines 18.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.isEmulatorConnected = isEmulatorConnected; exports.verifyEmulatorConnected = verifyEmulatorConnected; exports.fingerprint = fingerprint; exports.rotate = rotate; exports.powerAC = powerAC; exports.sensorSet = sensorSet; exports.powerCapacity = powerCapacity; exports.powerOFF = powerOFF; exports.sendSMS = sendSMS; exports.gsmCall = gsmCall; exports.gsmSignal = gsmSignal; exports.gsmVoice = gsmVoice; exports.networkSpeed = networkSpeed; exports.execEmuConsoleCommand = execEmuConsoleCommand; exports.getEmuVersionInfo = getEmuVersionInfo; exports.getEmuImageProperties = getEmuImageProperties; exports.checkAvdExist = checkAvdExist; exports.sendTelnetCommand = sendTelnetCommand; const logger_1 = require("../logger"); const lodash_1 = __importDefault(require("lodash")); const node_net_1 = __importDefault(require("node:net")); const support_1 = require("@appium/support"); const bluebird_1 = __importDefault(require("bluebird")); const node_path_1 = __importDefault(require("node:path")); const ini = __importStar(require("ini")); /** * Retrieves the list of available Android emulators * * @returns */ async function listEmulators() { let avdsRoot = process.env.ANDROID_AVD_HOME; if (await dirExists(avdsRoot ?? '')) { return await getAvdConfigPaths(avdsRoot); } if (avdsRoot) { logger_1.log.warn(`The value of the ANDROID_AVD_HOME environment variable '${avdsRoot}' is not an existing directory`); } const prefsRoot = await getAndroidPrefsRoot(); if (!prefsRoot) { return []; } avdsRoot = node_path_1.default.resolve(prefsRoot, 'avd'); if (!(await dirExists(avdsRoot))) { logger_1.log.debug(`Virtual devices config root '${avdsRoot}' is not an existing directory`); return []; } return await getAvdConfigPaths(avdsRoot); } /** * Get configuration paths of all virtual devices * * @param avdsRoot Path to the directory that contains the AVD .ini files * @returns */ async function getAvdConfigPaths(avdsRoot) { const configs = await support_1.fs.glob('*.ini', { cwd: avdsRoot, absolute: true, }); return configs .map((confPath) => { const avdName = node_path_1.default.basename(confPath).split('.').slice(0, -1).join('.'); return { name: avdName, config: confPath }; }) .filter(({ name }) => lodash_1.default.trim(name)); } /** * Check the emulator state. * * @returns True if Emulator is visible to adb. */ async function isEmulatorConnected() { const emulators = await this.getConnectedEmulators(); return !!lodash_1.default.find(emulators, (x) => x && x.udid === this.curDeviceId); } /** * Verify the emulator is connected. * * @throws If Emulator is not visible to adb. */ async function verifyEmulatorConnected() { if (!(await this.isEmulatorConnected())) { throw new Error(`The emulator "${this.curDeviceId}" was unexpectedly disconnected`); } } /** * Emulate fingerprint touch event on the connected emulator. * * @param fingerprintId - The ID of the fingerprint. */ async function fingerprint(fingerprintId) { if (!fingerprintId) { throw new Error('Fingerprint id parameter must be defined'); } // the method used only works for API level 23 and above const level = await this.getApiLevel(); if (level < 23) { throw new Error(`Device API Level must be >= 23. Current Api level '${level}'`); } await this.adbExecEmu(['finger', 'touch', fingerprintId]); } /** * Change the display orientation on the connected emulator. * The orientation is changed (PI/2 is added) every time * this method is called. */ async function rotate() { await this.adbExecEmu(['rotate']); } /** * Emulate power state change on the connected emulator. * * @param state - Either 'on' or 'off'. */ async function powerAC(state = 'on') { if (lodash_1.default.values(this.POWER_AC_STATES).indexOf(state) === -1) { throw new TypeError(`Wrong power AC state sent '${state}'. ` + `Supported values: ${lodash_1.default.values(this.POWER_AC_STATES)}]`); } await this.adbExecEmu(['power', 'ac', state]); } /** * Emulate sensors values on the connected emulator. * * @param sensor - Sensor type declared in SENSORS items. * @param value - Number to set as the sensor value. * @throws - If sensor type or sensor value is not defined */ async function sensorSet(sensor, value) { if (!lodash_1.default.includes(this.SENSORS, sensor)) { throw new TypeError(`Unsupported sensor sent '${sensor}'. ` + `Supported values: ${lodash_1.default.values(this.SENSORS)}]`); } if (lodash_1.default.isNil(value)) { throw new TypeError(`Missing/invalid sensor value argument. ` + `You need to provide a valid value to set to the sensor in ` + `format <value-a>[:<value-b>[:<value-c>[...]]].`); } await this.adbExecEmu(['sensor', 'set', sensor, `${value}`]); } /** * Emulate power capacity change on the connected emulator. * * @param percent - Percentage value in range [0, 100]. */ async function powerCapacity(percent = 100) { const percentInt = parseInt(`${percent}`, 10); if (isNaN(percentInt) || percentInt < 0 || percentInt > 100) { throw new TypeError(`The percentage value should be valid integer between 0 and 100`); } await this.adbExecEmu(['power', 'capacity', `${percentInt}`]); } /** * Emulate power off event on the connected emulator. */ async function powerOFF() { await this.powerAC(this.POWER_AC_STATES.POWER_AC_OFF); await this.powerCapacity(0); } /** * Emulate send SMS event on the connected emulator. * * @param phoneNumber - The phone number of message sender. * @param message - The message content. * @throws If phone number has invalid format. */ async function sendSMS(phoneNumber, message = '') { if (lodash_1.default.isEmpty(message)) { throw new TypeError('SMS message must not be empty'); } if (!lodash_1.default.isInteger(phoneNumber) && lodash_1.default.isEmpty(phoneNumber)) { throw new TypeError('Phone number most not be empty'); } await this.adbExecEmu(['sms', 'send', `${phoneNumber}`, message]); } /** * Emulate GSM call event on the connected emulator. * * @param phoneNumber - The phone number of the caller. * @param action - One of available GSM call actions. * @throws If phone number has invalid format. * @throws If _action_ value is invalid. */ async function gsmCall(phoneNumber, action) { if (!lodash_1.default.values(this.GSM_CALL_ACTIONS).includes(action)) { throw new TypeError(`Invalid gsm action param ${action}. Supported values: ${lodash_1.default.values(this.GSM_CALL_ACTIONS)}`); } if (!lodash_1.default.isInteger(phoneNumber) && lodash_1.default.isEmpty(phoneNumber)) { throw new TypeError('Phone number most not be empty'); } await this.adbExecEmu(['gsm', action, `${phoneNumber}`]); } /** * Emulate GSM signal strength change event on the connected emulator. * * @param strength - A number in range [0, 4]; * @throws If _strength_ value is invalid. */ async function gsmSignal(strength = 4) { const strengthInt = parseInt(`${strength}`, 10); if (!lodash_1.default.includes(this.GSM_SIGNAL_STRENGTHS, strengthInt)) { throw new TypeError(`Invalid signal strength param ${strength}. Supported values: ${lodash_1.default.values(this.GSM_SIGNAL_STRENGTHS)}`); } logger_1.log.info('gsm signal-profile <strength> changes the reported strength on next (15s) update.'); await this.adbExecEmu(['gsm', 'signal-profile', `${strength}`]); } /** * Emulate GSM voice event on the connected emulator. * * @param state - Either 'on' or 'off'. * @throws If _state_ value is invalid. */ async function gsmVoice(state = 'on') { // gsm voice <state> allows you to change the state of your GPRS connection if (!lodash_1.default.values(this.GSM_VOICE_STATES).includes(state)) { throw new TypeError(`Invalid gsm voice state param ${state}. Supported values: ${lodash_1.default.values(this.GSM_VOICE_STATES)}`); } await this.adbExecEmu(['gsm', 'voice', state]); } /** * Emulate network speed change event on the connected emulator. * * @param speed * One of possible NETWORK_SPEED values. * @throws If _speed_ value is invalid. */ async function networkSpeed(speed = 'full') { // network speed <speed> allows you to set the network speed emulation. if (!lodash_1.default.values(this.NETWORK_SPEED).includes(speed)) { throw new Error(`Invalid network speed param ${speed}. Supported values: ${lodash_1.default.values(this.NETWORK_SPEED)}`); } await this.adbExecEmu(['network', 'speed', speed]); } /** * Executes a command through emulator telnet console interface and returns its output * * @param cmd - The actual command to execute. See * https://developer.android.com/studio/run/emulator-console for more details * on available commands * @param opts * @returns The command output * @throws If there was an error while connecting to the Telnet console * or if the given command returned non-OK response */ async function execEmuConsoleCommand(cmd, opts = {}) { let port = parseInt(`${opts.port}`, 10); if (!port) { const portMatch = /emulator-(\d+)/i.exec(this.curDeviceId); if (!portMatch) { throw new Error(`Cannot parse the console port number from the device identifier '${this.curDeviceId}'. ` + `Is it an emulator?`); } port = parseInt(portMatch[1], 10); } const host = '127.0.0.1'; const { execTimeout = 60000, connTimeout = 5000, initTimeout = 5000 } = opts; await this.resetTelnetAuthToken(); const okFlag = /^OK$/m; const nokFlag = /^KO\b/m; const eol = '\r\n'; const client = node_net_1.default.connect({ host, port, }); return await new bluebird_1.default((resolve, reject) => { const connTimeoutObj = setTimeout(() => reject(new Error(`Cannot connect to the Emulator console at ${host}:${port} ` + `after ${connTimeout}ms`)), connTimeout); let execTimeoutObj; let initTimeoutObj; let isCommandSent = false; let serverResponse = []; client.once('error', (e) => { clearTimeout(connTimeoutObj); reject(new Error(`Cannot connect to the Emulator console at ${host}:${port}. ` + `Original error: ${e.message}`)); }); client.once('connect', () => { clearTimeout(connTimeoutObj); initTimeoutObj = setTimeout(() => reject(new Error(`Did not get the initial response from the Emulator console at ${host}:${port} ` + `after ${initTimeout}ms`)), initTimeout); }); client.on('data', (chunk) => { const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk; serverResponse.push(buf); const output = Buffer.concat(serverResponse).toString('utf8').trim(); if (okFlag.test(output)) { // The initial incoming data chunk confirms the interface is ready for input if (!isCommandSent) { clearTimeout(initTimeoutObj); serverResponse = []; const cmdStr = lodash_1.default.isArray(cmd) ? support_1.util.quote(cmd) : `${cmd}`; logger_1.log.debug(`Executing Emulator console command: ${cmdStr}`); client.write(cmdStr); client.write(eol); isCommandSent = true; execTimeoutObj = setTimeout(() => reject(new Error(`Did not get any response from the Emulator console at ${host}:${port} ` + `to '${cmd}' command after ${execTimeout}ms`)), execTimeout); return; } clearTimeout(execTimeoutObj); client.end(); const outputArr = output.split(eol); // remove the redundant OK flag from the resulting command output return resolve(outputArr .slice(0, outputArr.length - 1) .join('\n') .trim()); } else if (nokFlag.test(output)) { clearTimeout(initTimeoutObj); clearTimeout(execTimeoutObj); client.end(); const outputArr = output.split(eol); return reject(lodash_1.default.trim(lodash_1.default.last(outputArr) || '')); } }); }); } /** * Retrieves emulator version from the file system * * @returns If no version info could be parsed then an empty * object is returned */ async function getEmuVersionInfo() { const propsPath = node_path_1.default.join(this.sdkRoot, 'emulator', 'source.properties'); if (!(await support_1.fs.exists(propsPath))) { return {}; } const content = await support_1.fs.readFile(propsPath, 'utf8'); const revisionMatch = /^Pkg\.Revision=([\d.]+)$/m.exec(content); const result = {}; if (revisionMatch) { result.revision = revisionMatch[1]; } const buildIdMatch = /^Pkg\.BuildId=(\d+)$/m.exec(content); if (buildIdMatch) { result.buildId = parseInt(buildIdMatch[1], 10); } return result; } /** * Retrieves emulator image properties from the local file system * * @param avdName Emulator name. Should NOT start with '@' character * @throws if there was a failure while extracting the properties * @returns The content of emulator image properties file. * Usually this configuration .ini file has the following content: * avd.ini.encoding=UTF-8 * path=/Users/username/.android/avd/Pixel_XL_API_30.avd * path.rel=avd/Pixel_XL_API_30.avd * target=android-30 */ async function getEmuImageProperties(avdName) { const avds = await listEmulators(); const avd = avds.find(({ name }) => name === avdName); if (!avd) { let msg = `Cannot find '${avdName}' emulator. `; if (lodash_1.default.isEmpty(avds)) { msg += `No emulators have been detected on your system`; } else { msg += `Available avd names are: ${avds.map(({ name }) => name)}`; } throw new Error(msg); } return ini.parse(await support_1.fs.readFile(avd.config, 'utf8')); } /** * Check if given emulator exists in the list of available avds. * * @param avdName - The name of emulator to verify for existence. * Should NOT start with '@' character * @throws If the emulator with given name does not exist. */ async function checkAvdExist(avdName) { const avds = await listEmulators(); if (!avds.some(({ name }) => name === avdName)) { let msg = `Avd '${avdName}' is not available. `; if (lodash_1.default.isEmpty(avds)) { msg += `No emulators have been detected on your system`; } else { msg += `Please select your avd name from one of these: '${avds.map(({ name }) => name)}'`; } throw new Error(msg); } return true; } /** * Send an arbitrary Telnet command to the device under test. * * @param command - The command to be sent. * @returns The actual output of the given command. */ async function sendTelnetCommand(command) { return await this.execEmuConsoleCommand(command, { port: await this.getEmulatorPort() }); } // #region Private functions /** * Retrieves the full path to the Android preferences root * * @returns The full path to the folder or `null` if the folder cannot be found */ async function getAndroidPrefsRoot() { let location = process.env.ANDROID_EMULATOR_HOME; if (await dirExists(location ?? '')) { return location ?? null; } if (location) { logger_1.log.warn(`The value of the ANDROID_EMULATOR_HOME environment variable '${location}' is not an existing directory`); } const home = process.env.HOME || process.env.USERPROFILE; if (home) { location = node_path_1.default.resolve(home, '.android'); } if (!(await dirExists(location ?? ''))) { logger_1.log.debug(`Android config root '${location}' is not an existing directory`); return null; } return location ?? null; } /** * Check if a path exists on the filesystem and is a directory * * @param location The full path to the directory * @returns */ async function dirExists(location) { return (await support_1.fs.exists(location)) && (await support_1.fs.stat(location)).isDirectory(); } // #endregion //# sourceMappingURL=emulator-commands.js.map