appium-uiautomator2-driver
Version:
UiAutomator2 integration for Appium
303 lines • 14.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.allocateSystemPort = allocateSystemPort;
exports.releaseSystemPort = releaseSystemPort;
exports.allocateMjpegServerPort = allocateMjpegServerPort;
exports.releaseMjpegServerPort = releaseMjpegServerPort;
exports.performPreExecSetup = performPreExecSetup;
exports.performExecution = performExecution;
exports.performPostExecSetup = performPostExecSetup;
exports.startSession = startSession;
exports.initServer = initServer;
exports.requireServer = requireServer;
const io_appium_settings_1 = require("io.appium.settings");
const support_1 = require("appium/support");
const asyncbox_1 = require("asyncbox");
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const portscanner_1 = require("portscanner");
const core_1 = require("./core");
const packages_1 = require("./packages");
// The range of ports we can use on the system for communicating to the
// UiAutomator2 HTTP server on the device
const DEVICE_PORT_RANGE = [8200, 8299];
// The guard is needed to avoid dynamic system port allocation conflicts for
// parallel driver sessions
const DEVICE_PORT_ALLOCATION_GUARD = support_1.util.getLockFileGuard(node_path_1.default.resolve(node_os_1.default.tmpdir(), 'uia2_device_port_guard'), { timeout: 25, tryRecovery: true });
// This is the port that UiAutomator2 listens to on the device. We will forward
// one of the ports above on the system to this port on the device.
const DEVICE_PORT = 6790;
// This is the port that the UiAutomator2 MJPEG server listens to on the device.
// We will forward one of the ports above on the system to this port on the
// device.
const MJPEG_SERVER_DEVICE_PORT = 7810;
const MIN_SUPPORTED_API_LEVEL = 26;
const LOCALHOST_IP4 = '127.0.0.1';
/** Forwards a local port to the UiAutomator2 server port on the device. */
async function allocateSystemPort() {
const adb = this.requireAdb();
const forwardPort = async (localPort) => {
this.log.debug(`Forwarding UiAutomator2 Server port ${DEVICE_PORT} to local port ${localPort}`);
if ((await (0, portscanner_1.checkPortStatus)(localPort, LOCALHOST_IP4)) === 'open') {
throw this.log.errorWithException(`UiAutomator2 Server cannot start because the local port #${localPort} is busy. ` +
`Make sure the port you provide via 'systemPort' capability is not occupied. ` +
`This situation might often be a result of an inaccurate sessions management, e.g. ` +
`old automation sessions on the same device must always be closed before starting new ones.`);
}
await adb.forwardPort(localPort, DEVICE_PORT);
};
if (this.opts.systemPort) {
this.systemPort = this.opts.systemPort;
return await forwardPort(this.systemPort);
}
await DEVICE_PORT_ALLOCATION_GUARD(async () => {
const [startPort, endPort] = DEVICE_PORT_RANGE;
try {
this.systemPort = await (0, portscanner_1.findAPortNotInUse)(startPort, endPort);
}
catch {
throw this.log.errorWithException(`Cannot find any free port in range ${startPort}..${endPort}}. ` +
`Please set the available port number by providing the systemPort capability or ` +
`double check the processes that are locking ports within this range and terminate ` +
`these which are not needed anymore`);
}
await forwardPort(this.systemPort);
});
}
/** Removes the UiAutomator2 server port forward. */
async function releaseSystemPort() {
const adb = this.adb;
const systemPort = this.systemPort;
if (!systemPort || !adb) {
return;
}
if (this.opts.systemPort) {
// We assume if the systemPort is provided manually then it must be unique,
// so there is no need for the explicit synchronization
await adb.removePortForward(systemPort);
}
else {
await DEVICE_PORT_ALLOCATION_GUARD(async () => await adb.removePortForward(systemPort));
}
}
/** Forwards a local port to the UiAutomator2 MJPEG server port on the device. */
async function allocateMjpegServerPort() {
if (this.opts.mjpegServerPort) {
const adb = this.requireAdb();
this.log.debug(`MJPEG broadcasting requested, forwarding MJPEG server port ${MJPEG_SERVER_DEVICE_PORT} ` +
`to local port ${this.opts.mjpegServerPort}`);
await adb.forwardPort(this.opts.mjpegServerPort, MJPEG_SERVER_DEVICE_PORT);
}
}
/** Removes the UiAutomator2 MJPEG server port forward. */
async function releaseMjpegServerPort() {
if (this.opts.mjpegServerPort && this.adb) {
await this.adb.removePortForward(this.opts.mjpegServerPort);
}
}
/** Runs device preparation steps before the UiAutomator2 server session starts. */
async function performPreExecSetup() {
const apiLevel = await this.adb.getApiLevel();
if (apiLevel < MIN_SUPPORTED_API_LEVEL) {
throw this.log.errorWithException('UIAutomator2 only supports Android 8.0 (Oreo) and above');
}
const preflightPromises = [];
if (apiLevel >= 28) {
// Android P
preflightPromises.push((async () => {
this.log.info('Relaxing hidden api policy');
try {
await this.adb.setHiddenApiPolicy('1', !!this.opts.ignoreHiddenApiPolicyError);
}
catch (err) {
throw this.log.errorWithException('Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. ' +
'This might be happening because the device under test is not configured properly. ' +
'Please check https://github.com/appium/appium/issues/13802 for more details. ' +
'You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ' +
'ignore this error, which might later lead to unexpected crashes or behavior of ' +
`the automation server. Original error: ${err.message}`);
}
})());
}
if (support_1.util.hasValue(this.opts.gpsEnabled)) {
preflightPromises.push((async () => {
this.log.info(`Trying to ${this.opts.gpsEnabled ? 'enable' : 'disable'} gps location provider`);
await this.adb.toggleGPSLocationProvider(Boolean(this.opts.gpsEnabled));
})());
}
if (this.opts.hideKeyboard) {
preflightPromises.push((async () => {
this._originalIme = await this.adb.defaultIME();
})());
}
let appInfo;
preflightPromises.push((async () => {
// get appPackage et al from manifest if necessary
appInfo = await this.getLaunchInfo();
})());
// start settings app, set the language/locale, start logcat etc...
preflightPromises.push(this.initDevice());
await Promise.all(preflightPromises);
this.opts = { ...this.opts, ...(appInfo ?? {}) };
return appInfo;
}
/** Installs and starts the UiAutomator2 server for the current session. */
async function performExecution(capsWithSessionInfo) {
await Promise.all([
// Prepare the device by forwarding the UiAutomator2 port
// This call mutates this.systemPort if it is not set explicitly
this.allocateSystemPort(),
// Prepare the device by forwarding the UiAutomator2 MJPEG server port (if
// applicable)
this.allocateMjpegServerPort(),
]);
const [uiautomator2] = await Promise.all([
// set up the modified UiAutomator2 server etc
this.initUiAutomator2Server(),
(async () => {
// Should be after installing io.appium.settings
if (this.opts.disableWindowAnimation && (await this.adb.getApiLevel()) < 26) {
// API level 26 is Android 8.0.
// Granting android.permission.SET_ANIMATION_SCALE is necessary to handle animations under API level 26
// Read https://github.com/appium/appium/pull/11640#issuecomment-438260477
// `--no-window-animation` works over Android 8 to disable all of animations
if (await this.adb.isAnimationOn()) {
this.log.info('Disabling animation via io.appium.settings');
await this.settingsApp.setAnimationState(false);
this._wasWindowAnimationDisabled = true;
}
else {
this.log.info('Window animation is already disabled');
}
}
})(),
// set up app under test
// prepare our actual AUT, get it on the device, etc...
this.initAUT(),
]);
// launch UiAutomator2 and wait till its online and we have a session
await uiautomator2.startSession(capsWithSessionInfo);
// now that everything has started successfully, turn on proxying so all
// subsequent session requests go straight to/from uiautomator2
this.jwpProxyActive = true;
}
/** Runs post-start steps after the UiAutomator2 server session is online. */
async function performPostExecSetup() {
// Unlock the device after the session is started.
if (!this.opts.skipUnlock) {
// unlock the device to prepare it for testing
await this.unlock();
}
else {
this.log.debug(`'skipUnlock' capability set, so skipping device unlock`);
}
if (this.isChromeSession) {
// start a chromedriver session
await this.startChromeSession();
}
else if (this.opts.autoLaunch && this.opts.appPackage) {
await this.ensureAppStarts();
}
// if the initial orientation is requested, set it
if (support_1.util.hasValue(this.opts.orientation)) {
this.log.debug(`Setting initial orientation to '${this.opts.orientation}'`);
await this.setOrientation(this.opts.orientation);
}
// if we want to immediately get into a webview, set our context
// appropriately
if (this.opts.autoWebview) {
const viewName = this.defaultWebviewName();
const timeout = this.opts.autoWebviewTimeout || 2000;
this.log.info(`Setting auto webview to context '${viewName}' with timeout ${timeout}ms`);
await (0, asyncbox_1.retryInterval)(timeout / 500, 500, this.setContext.bind(this), viewName);
}
// We would like to notify about the initial context setting
if ((await this.getCurrentContext()) === this.defaultContextName()) {
await this.notifyBiDiContextChange();
}
}
/** Orchestrates UiAutomator2 server session startup and returns session capabilities. */
async function startSession(caps) {
const appInfo = await this.performSessionPreExecSetup();
// set actual device name, udid, platform version, screen size, screen density, model and manufacturer details
const deviceName = this.adb?.curDeviceId;
const deviceUDID = this.opts.udid;
if (!deviceName) {
throw this.log.errorWithException('Could not determine device name (ADB curDeviceId is empty)');
}
if (!deviceUDID) {
throw this.log.errorWithException('Device UDID is not set in session options');
}
const sessionInfo = {
deviceName,
deviceUDID,
};
const capsWithSessionInfo = {
...caps,
...sessionInfo,
};
// Adding AUT info in the capabilities if it does not exist in caps
if (appInfo) {
for (const capName of ['appPackage', 'appActivity']) {
if (!capsWithSessionInfo[capName] && appInfo[capName]) {
capsWithSessionInfo[capName] = appInfo[capName];
}
}
}
await this.performSessionExecution(capsWithSessionInfo);
const deviceInfoPromise = (async () => {
try {
return await this.getDeviceDetails();
}
catch (e) {
this.log.warn(`Cannot fetch device details. Original error: ${e.message}`);
return {};
}
})();
await this.performSessionPostExecSetup();
return { ...capsWithSessionInfo, ...(await deviceInfoPromise) };
}
/** Creates the UiAutomator2 server client and installs server APKs when needed. */
async function initServer() {
const uiautomator2Opts = {
host: this.opts.remoteAdbHost || LOCALHOST_IP4,
systemPort: this.systemPort,
adb: this.adb,
disableWindowAnimation: !!this.opts.disableWindowAnimation,
disableSuppressAccessibilityService: this.opts.disableSuppressAccessibilityService,
readTimeout: this.opts.uiautomator2ServerReadTimeout,
basePath: this.basePath,
};
// now that we have package and activity, we can create an instance of
// uiautomator2 with the appropriate options
this.uiautomator2 = new core_1.UiAutomator2Server(this.log, uiautomator2Opts);
this.proxyReqRes = this.uiautomator2.proxyReqRes.bind(this.uiautomator2);
this.proxyCommand = this.uiautomator2.proxyCommand.bind(this.uiautomator2);
if (this.opts.skipServerInstallation) {
this.log.info(`'skipServerInstallation' is set. Skipping UIAutomator2 server installation.`);
}
else {
await this.uiautomator2.installServerApk(this.opts.uiautomator2ServerInstallTimeout);
try {
await this.requireAdb().addToDeviceIdleWhitelist(io_appium_settings_1.SETTINGS_HELPER_ID, packages_1.SERVER_PACKAGE_ID, packages_1.SERVER_TEST_PACKAGE_ID);
}
catch (e) {
const err = e;
this.log.warn(`Cannot add server packages to the Doze whitelist. Original error: ` +
(err.stderr || err.message));
}
}
return this.uiautomator2;
}
/** Returns the initialized UiAutomator2 server client or throws if it is missing. */
function requireServer() {
const server = this.uiautomator2;
if (!server) {
throw this.log.errorWithException('UiAutomator2 server is not initialized');
}
return server;
}
//# sourceMappingURL=session.js.map