appium-adb
Version:
Android Debug Bridge interface
470 lines • 18.3 kB
JavaScript
;
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