appium-uiautomator2-driver
Version:
UiAutomator2 integration for Appium
817 lines • 40.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AndroidUiautomator2Driver = void 0;
const appium_adb_1 = require("appium-adb");
const appium_android_driver_1 = require("appium-android-driver");
const io_appium_settings_1 = require("io.appium.settings");
const driver_1 = require("appium/driver");
const support_1 = require("appium/support");
const asyncbox_1 = require("asyncbox");
const bluebird_1 = __importDefault(require("bluebird"));
const lodash_1 = __importDefault(require("lodash"));
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const portscanner_1 = require("portscanner");
const constraints_1 = __importDefault(require("./constraints"));
const extensions_1 = require("./extensions");
const method_map_1 = require("./method-map");
const helpers_1 = require("./helpers");
const uiautomator2_1 = require("./uiautomator2");
const actions_1 = require("./commands/actions");
const alert_1 = require("./commands/alert");
const app_management_1 = require("./commands/app-management");
const battery_1 = require("./commands/battery");
const clipboard_1 = require("./commands/clipboard");
const element_1 = require("./commands/element");
const find_1 = require("./commands/find");
const gestures_1 = require("./commands/gestures");
const keyboard_1 = require("./commands/keyboard");
const misc_1 = require("./commands/misc");
const navigation_1 = require("./commands/navigation");
const screenshot_1 = require("./commands/screenshot");
const viewport_1 = require("./commands/viewport");
const execute_method_map_1 = require("./execute-method-map");
// 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 LOCALHOST_IP4 = '127.0.0.1';
// NO_PROXY contains the paths that we never want to proxy to UiAutomator2 server.
// TODO: Add the list of paths that we never want to proxy to UiAutomator2 server.
// TODO: Need to segregate the paths better way using regular expressions wherever applicable.
// (Not segregating right away because more paths to be added in the NO_PROXY list)
const NO_PROXY = [
['DELETE', new RegExp('^/session/[^/]+/actions')],
['GET', new RegExp('^/session/(?!.*/)')],
['GET', new RegExp('^/session/[^/]+/alert_[^/]+')],
['GET', new RegExp('^/session/[^/]+/alert/[^/]+')],
['GET', new RegExp('^/session/[^/]+/appium/[^/]+/current_activity')],
['GET', new RegExp('^/session/[^/]+/appium/[^/]+/current_package')],
['GET', new RegExp('^/session/[^/]+/appium/app/[^/]+')],
['GET', new RegExp('^/session/[^/]+/appium/device/[^/]+')],
['GET', new RegExp('^/session/[^/]+/appium/settings')],
['GET', new RegExp('^/session/[^/]+/context')],
['GET', new RegExp('^/session/[^/]+/contexts')],
['GET', new RegExp('^/session/[^/]+/element/[^/]+/attribute')],
['GET', new RegExp('^/session/[^/]+/element/[^/]+/displayed')],
['GET', new RegExp('^/session/[^/]+/element/[^/]+/enabled')],
['GET', new RegExp('^/session/[^/]+/element/[^/]+/location_in_view')],
['GET', new RegExp('^/session/[^/]+/element/[^/]+/name')],
['GET', new RegExp('^/session/[^/]+/element/[^/]+/screenshot')],
['GET', new RegExp('^/session/[^/]+/element/[^/]+/selected')],
['GET', new RegExp('^/session/[^/]+/ime/[^/]+')],
['GET', new RegExp('^/session/[^/]+/location')],
['GET', new RegExp('^/session/[^/]+/network_connection')],
['GET', new RegExp('^/session/[^/]+/screenshot')],
['GET', new RegExp('^/session/[^/]+/timeouts')],
['GET', new RegExp('^/session/[^/]+/url')],
['POST', new RegExp('^/session/[^/]+/[^/]+_alert$')],
['POST', new RegExp('^/session/[^/]+/actions')],
['POST', new RegExp('^/session/[^/]+/alert/[^/]+')],
['POST', new RegExp('^/session/[^/]+/app/[^/]')],
['POST', new RegExp('^/session/[^/]+/appium/[^/]+/start_activity')],
['POST', new RegExp('^/session/[^/]+/appium/app/[^/]+')],
['POST', new RegExp('^/session/[^/]+/appium/compare_images')],
['POST', new RegExp('^/session/[^/]+/appium/device/(?!set_clipboard)[^/]+')],
['POST', new RegExp('^/session/[^/]+/appium/element/[^/]+/replace_value')],
['POST', new RegExp('^/session/[^/]+/appium/element/[^/]+/value')],
['POST', new RegExp('^/session/[^/]+/appium/getPerformanceData')],
['POST', new RegExp('^/session/[^/]+/appium/performanceData/types')],
['POST', new RegExp('^/session/[^/]+/appium/settings')],
['POST', new RegExp('^/session/[^/]+/appium/execute_driver')],
['POST', new RegExp('^/session/[^/]+/appium/start_recording_screen')],
['POST', new RegExp('^/session/[^/]+/appium/stop_recording_screen')],
['POST', new RegExp('^/session/[^/]+/appium/.*event')],
['POST', new RegExp('^/session/[^/]+/context')],
['POST', new RegExp('^/session/[^/]+/element')],
['POST', new RegExp('^/session/[^/]+/ime/[^/]+')],
['POST', new RegExp('^/session/[^/]+/keys')],
['POST', new RegExp('^/session/[^/]+/location')],
['POST', new RegExp('^/session/[^/]+/network_connection')],
['POST', new RegExp('^/session/[^/]+/timeouts')],
['POST', new RegExp('^/session/[^/]+/url')],
// MJSONWP commands
['GET', new RegExp('^/session/[^/]+/log/types')],
['POST', new RegExp('^/session/[^/]+/execute')],
['POST', new RegExp('^/session/[^/]+/execute_async')],
['POST', new RegExp('^/session/[^/]+/log')],
// W3C commands
// For Selenium v4 (W3C does not have this route)
['GET', new RegExp('^/session/[^/]+/se/log/types')],
['GET', new RegExp('^/session/[^/]+/window/rect')],
['POST', new RegExp('^/session/[^/]+/execute/async')],
['POST', new RegExp('^/session/[^/]+/execute/sync')],
// For Selenium v4 (W3C does not have this route)
['POST', new RegExp('^/session/[^/]+/se/log')],
];
// This is a set of methods and paths that we never want to proxy to Chromedriver.
const CHROME_NO_PROXY = [
['GET', new RegExp('^/session/[^/]+/appium')],
['GET', new RegExp('^/session/[^/]+/context')],
['GET', new RegExp('^/session/[^/]+/element/[^/]+/rect')],
['GET', new RegExp('^/session/[^/]+/orientation')],
['POST', new RegExp('^/session/[^/]+/appium')],
['POST', new RegExp('^/session/[^/]+/context')],
['POST', new RegExp('^/session/[^/]+/orientation')],
// this is needed to make the mobile: commands working in web context
['POST', new RegExp('^/session/[^/]+/execute$')],
['POST', new RegExp('^/session/[^/]+/execute/sync')],
// MJSONWP commands
['GET', new RegExp('^/session/[^/]+/log/types$')],
['POST', new RegExp('^/session/[^/]+/log$')],
// W3C commands
// For Selenium v4 (W3C does not have this route)
['GET', new RegExp('^/session/[^/]+/se/log/types$')],
// For Selenium v4 (W3C does not have this route)
['POST', new RegExp('^/session/[^/]+/se/log$')],
];
const MEMOIZED_FUNCTIONS = ['getStatusBarHeight', 'getDevicePixelRatio'];
class AndroidUiautomator2Driver extends appium_android_driver_1.AndroidDriver {
constructor(opts = {}, shouldValidateCaps = true) {
// `shell` overwrites adb.shell, so remove
// @ts-expect-error FIXME: what is this?
delete opts.shell;
super(opts, shouldValidateCaps);
this.mobileGetActionHistory = actions_1.mobileGetActionHistory;
this.mobileScheduleAction = actions_1.mobileScheduleAction;
this.mobileUnscheduleAction = actions_1.mobileUnscheduleAction;
this.performActions = actions_1.performActions;
this.releaseActions = actions_1.releaseActions;
this.getAlertText = alert_1.getAlertText;
this.mobileAcceptAlert = alert_1.mobileAcceptAlert;
this.mobileDismissAlert = alert_1.mobileDismissAlert;
this.postAcceptAlert = alert_1.postAcceptAlert;
this.postDismissAlert = alert_1.postDismissAlert;
this.mobileInstallMultipleApks = app_management_1.mobileInstallMultipleApks;
this.mobileGetBatteryInfo = battery_1.mobileGetBatteryInfo;
this.active = element_1.active;
this.getAttribute = element_1.getAttribute;
this.elementEnabled = element_1.elementEnabled;
this.elementDisplayed = element_1.elementDisplayed;
this.elementSelected = element_1.elementSelected;
this.getName = element_1.getName;
this.getLocation = element_1.getLocation;
this.getSize = element_1.getSize;
this.getElementRect = element_1.getElementRect;
this.getElementScreenshot = element_1.getElementScreenshot;
this.getText = element_1.getText;
this.setValueImmediate = element_1.setValueImmediate;
this.doSetElementValue = element_1.doSetElementValue;
this.click = element_1.click;
this.clear = element_1.clear;
this.mobileReplaceElementValue = element_1.mobileReplaceElementValue;
this.doFindElementOrEls = find_1.doFindElementOrEls;
this.mobileClickGesture = gestures_1.mobileClickGesture;
this.mobileDoubleClickGesture = gestures_1.mobileDoubleClickGesture;
this.mobileDragGesture = gestures_1.mobileDragGesture;
this.mobileFlingGesture = gestures_1.mobileFlingGesture;
this.mobileLongClickGesture = gestures_1.mobileLongClickGesture;
this.mobilePinchCloseGesture = gestures_1.mobilePinchCloseGesture;
this.mobilePinchOpenGesture = gestures_1.mobilePinchOpenGesture;
this.mobileScroll = gestures_1.mobileScroll;
this.mobileScrollBackTo = gestures_1.mobileScrollBackTo;
this.mobileScrollGesture = gestures_1.mobileScrollGesture;
this.mobileSwipeGesture = gestures_1.mobileSwipeGesture;
this.pressKeyCode = keyboard_1.pressKeyCode;
this.longPressKeyCode = keyboard_1.longPressKeyCode;
this.mobilePressKey = keyboard_1.mobilePressKey;
this.mobileType = keyboard_1.mobileType;
this.doSendKeys = keyboard_1.doSendKeys;
this.keyevent = keyboard_1.keyevent;
this.getPageSource = misc_1.getPageSource;
this.getOrientation = misc_1.getOrientation;
this.setOrientation = misc_1.setOrientation;
this.openNotifications = misc_1.openNotifications;
this.suspendChromedriverProxy = misc_1.suspendChromedriverProxy;
this.mobileGetDeviceInfo = misc_1.mobileGetDeviceInfo;
this.getClipboard = clipboard_1.getClipboard;
this.setClipboard = clipboard_1.setClipboard;
this.setUrl = navigation_1.setUrl;
this.mobileDeepLink = navigation_1.mobileDeepLink;
this.back = navigation_1.back;
this.mobileScreenshots = screenshot_1.mobileScreenshots;
this.mobileViewportScreenshot = screenshot_1.mobileViewportScreenshot;
this.getScreenshot = screenshot_1.getScreenshot;
this.getViewportScreenshot = screenshot_1.getViewportScreenshot;
this.getStatusBarHeight = viewport_1.getStatusBarHeight;
this.getDevicePixelRatio = viewport_1.getDevicePixelRatio;
this.getDisplayDensity = viewport_1.getDisplayDensity;
this.getViewPortRect = viewport_1.getViewPortRect;
this.getWindowRect = viewport_1.getWindowRect;
this.getWindowSize = viewport_1.getWindowSize;
this.mobileViewPortRect = viewport_1.mobileViewPortRect;
this.locatorStrategies = [
'xpath',
'id',
'class name',
'accessibility id',
'css selector',
'-android uiautomator',
];
this.desiredCapConstraints = lodash_1.default.cloneDeep(constraints_1.default);
this.jwpProxyActive = false;
this.jwpProxyAvoid = NO_PROXY;
this._originalIme = null;
this.settings = new driver_1.DeviceSettings({ ignoreUnimportantViews: false, allowInvisibleElements: false }, this.onSettingsUpdate.bind(this));
// handle webview mechanics from AndroidDriver
this.sessionChromedrivers = {};
this.caps = {};
this.opts = opts;
// memoize functions here, so that they are done on a per-instance basis
for (const fn of MEMOIZED_FUNCTIONS) {
this[fn] = lodash_1.default.memoize(this[fn]);
}
}
validateDesiredCaps(caps) {
return super.validateDesiredCaps(caps);
}
async createSession(w3cCaps1, w3cCaps2, w3cCaps3, driverData) {
try {
// TODO handle otherSessionData for multiple sessions
const [sessionId, caps] = (await driver_1.BaseDriver.prototype.createSession.call(this, w3cCaps1, w3cCaps2, w3cCaps3, driverData));
const startSessionOpts = {
...caps,
platform: 'LINUX',
webStorageEnabled: false,
takesScreenshot: true,
javascriptEnabled: true,
databaseEnabled: false,
networkConnectionEnabled: true,
locationContextEnabled: false,
warnings: {},
desired: caps,
};
const defaultOpts = {
fullReset: false,
autoLaunch: true,
adbPort: appium_adb_1.DEFAULT_ADB_PORT,
androidInstallTimeout: 90000,
};
lodash_1.default.defaults(this.opts, defaultOpts);
this.opts.adbPort = this.opts.adbPort || appium_adb_1.DEFAULT_ADB_PORT;
// get device udid for this session
const { udid, emPort } = await this.getDeviceInfoFromCaps();
this.opts.udid = udid;
// @ts-expect-error do not put random stuff on opts
this.opts.emPort = emPort;
// now that we know our java version and device info, we can create our
// ADB instance
this.adb = await this.createADB();
if (this.isChromeSession) {
this.log.info(`We're going to run a Chrome-based session`);
const { pkg, activity: defaultActivity } = appium_android_driver_1.utils.getChromePkg(this.opts.browserName);
let activity = defaultActivity;
if (await this.adb.getApiLevel() >= 24) {
try {
activity = await this.adb.resolveLaunchableActivity(pkg);
}
catch (e) {
this.log.warn(`Using the default ${pkg} activity ${activity}. Original error: ${e.message}`);
}
}
this.opts.appPackage = this.caps.appPackage = pkg;
this.opts.appActivity = this.caps.appActivity = activity;
this.log.info(`Chrome-type package and activity are ${pkg} and ${activity}`);
}
if (this.opts.app) {
// find and copy, or download and unzip an app url or path
this.opts.app = await this.helpers.configureApp(this.opts.app, [
extensions_1.APK_EXTENSION,
extensions_1.APKS_EXTENSION,
]);
await this.checkAppPresent();
}
else if (this.opts.appPackage) {
// the app isn't an actual app file but rather something we want to
// assume is on the device and just launch via the appPackage
this.log.info(`Starting '${this.opts.appPackage}' directly on the device`);
}
else {
this.log.info(`Neither 'app' nor 'appPackage' was set. Starting UiAutomator2 ` +
'without the target application');
}
const result = await this.startUiAutomator2Session(startSessionOpts);
if (this.opts.mjpegScreenshotUrl) {
this.log.info(`Starting MJPEG stream reading URL: '${this.opts.mjpegScreenshotUrl}'`);
this.mjpegStream = new support_1.mjpeg.MJpegStream(this.opts.mjpegScreenshotUrl);
await this.mjpegStream.start();
}
return [sessionId, result];
}
catch (e) {
await this.deleteSession();
throw e;
}
}
async getDeviceDetails() {
const [pixelRatio, statBarHeight, viewportRect, { apiVersion, platformVersion, manufacturer, model, realDisplaySize, displayDensity },] = await bluebird_1.default.all([
this.getDevicePixelRatio(),
this.getStatusBarHeight(),
this.getViewPortRect(),
this.mobileGetDeviceInfo(),
]);
return {
pixelRatio,
statBarHeight,
viewportRect,
deviceApiLevel: lodash_1.default.parseInt(apiVersion),
platformVersion,
deviceManufacturer: manufacturer,
deviceModel: model,
deviceScreenSize: realDisplaySize,
deviceScreenDensity: displayDensity,
};
}
get driverData() {
// TODO fill out resource info here
return {};
}
async getSession() {
const sessionData = await driver_1.BaseDriver.prototype.getSession.call(this);
this.log.debug('Getting session details from server to mix in');
const uia2Data = (await this.uiautomator2.jwproxy.command('/', 'GET', {}));
return { ...sessionData, ...uia2Data };
}
async allocateSystemPort() {
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 this.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);
});
}
async releaseSystemPort() {
if (!this.systemPort || !this.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 this.adb.removePortForward(this.systemPort);
}
else {
await DEVICE_PORT_ALLOCATION_GUARD(async () => await this.adb.removePortForward(this.systemPort));
}
}
async allocateMjpegServerPort() {
if (this.opts.mjpegServerPort) {
this.log.debug(`MJPEG broadcasting requested, forwarding MJPEG server port ${MJPEG_SERVER_DEVICE_PORT} ` +
`to local port ${this.opts.mjpegServerPort}`);
await this.adb.forwardPort(this.opts.mjpegServerPort, MJPEG_SERVER_DEVICE_PORT);
}
}
async releaseMjpegServerPort() {
if (this.opts.mjpegServerPort) {
await this.adb.removePortForward(this.opts.mjpegServerPort);
}
}
async performSessionPreExecSetup() {
const apiLevel = await this.adb.getApiLevel();
if (apiLevel < 21) {
throw this.log.errorWithException('UIAutomator2 is only supported since Android 5.0 (Lollipop). ' +
'You could still use other supported backends in order to automate older Android versions.');
}
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 bluebird_1.default.all(preflightPromises);
this.opts = { ...this.opts, ...(appInfo ?? {}) };
return appInfo;
}
async performSessionExecution(capsWithSessionInfo) {
await bluebird_1.default.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 bluebird_1.default.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;
}
async performSessionPostExecSetup() {
// 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();
}
}
async startUiAutomator2Session(caps) {
const appInfo = await this.performSessionPreExecSetup();
// set actual device name, udid, platform version, screen size, screen density, model and manufacturer details
const sessionInfo = {
deviceName: this.adb.curDeviceId,
deviceUDID: this.opts.udid,
};
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) };
}
async initUiAutomator2Server() {
// broken out for readability
const uiautomator2Opts = {
// @ts-expect-error FIXME: maybe `address` instead of `host`?
host: this.opts.remoteAdbHost || this.opts.host || LOCALHOST_IP4,
systemPort: this.systemPort,
devicePort: DEVICE_PORT,
adb: this.adb,
tmpDir: this.opts.tmpDir,
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 uiautomator2_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.adb.addToDeviceIdleWhitelist(io_appium_settings_1.SETTINGS_HELPER_ID, uiautomator2_1.SERVER_PACKAGE_ID, uiautomator2_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;
}
async initAUT() {
// Uninstall any uninstallOtherPackages which were specified in caps
if (this.opts.uninstallOtherPackages) {
await this.uninstallOtherPackages(appium_android_driver_1.utils.parseArray(this.opts.uninstallOtherPackages), [io_appium_settings_1.SETTINGS_HELPER_ID, uiautomator2_1.SERVER_PACKAGE_ID, uiautomator2_1.SERVER_TEST_PACKAGE_ID]);
}
// Install any "otherApps" that were specified in caps
if (this.opts.otherApps) {
let otherApps;
try {
otherApps = appium_android_driver_1.utils.parseArray(this.opts.otherApps);
}
catch (e) {
throw this.log.errorWithException(`Could not parse "otherApps" capability: ${e.message}`);
}
otherApps = await bluebird_1.default.all(otherApps.map((app) => this.helpers.configureApp(app, [extensions_1.APK_EXTENSION, extensions_1.APKS_EXTENSION])));
await this.installOtherApks(otherApps);
}
if (this.opts.app) {
if ((this.opts.noReset && !(await this.adb.isAppInstalled(this.opts.appPackage))) ||
!this.opts.noReset) {
if (!this.opts.noSign &&
!(await this.adb.checkApkCert(this.opts.app, this.opts.appPackage, {
requireDefaultCert: false,
}))) {
await (0, helpers_1.signApp)(this.adb, this.opts.app);
}
if (!this.opts.skipUninstall) {
await this.adb.uninstallApk(this.opts.appPackage);
}
await this.installAUT();
}
else {
this.log.debug('noReset has been requested and the app is already installed. Doing nothing');
}
}
else {
if (this.opts.fullReset) {
throw this.log.errorWithException('Full reset requires an app capability, use fastReset if app is not provided');
}
this.log.debug('No app capability. Assuming it is already on the device');
if (this.opts.fastReset && this.opts.appPackage) {
await this.resetAUT();
}
}
}
async ensureAppStarts() {
// make sure we have an activity and package to wait for
const appWaitPackage = this.opts.appWaitPackage || this.opts.appPackage;
const appWaitActivity = this.opts.appWaitActivity || this.opts.appActivity;
this.log.info(`Starting '${this.opts.appPackage}/${this.opts.appActivity}' ` +
`and waiting for '${appWaitPackage}/${appWaitActivity}'`);
if (this.opts.noReset &&
!this.opts.forceAppLaunch &&
(await this.adb.processExists(this.opts.appPackage))) {
this.log.info(`'${this.opts.appPackage}' is already running and noReset is enabled. ` +
`Set forceAppLaunch capability to true if the app must be forcefully restarted on session startup.`);
return;
}
await this.adb.startApp({
pkg: this.opts.appPackage,
activity: this.opts.appActivity,
action: this.opts.intentAction || 'android.intent.action.MAIN',
category: this.opts.intentCategory || 'android.intent.category.LAUNCHER',
flags: this.opts.intentFlags || '0x10200000', // FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
waitPkg: this.opts.appWaitPackage,
waitActivity: this.opts.appWaitActivity,
waitForLaunch: this.opts.appWaitForLaunch,
waitDuration: this.opts.appWaitDuration,
optionalIntentArguments: this.opts.optionalIntentArguments,
stopApp: this.opts.forceAppLaunch || !this.opts.dontStopAppOnReset,
retry: true,
user: this.opts.userProfile,
});
}
async deleteSession() {
this.log.debug('Deleting UiAutomator2 session');
const screenRecordingStopTasks = [
async () => {
if (!lodash_1.default.isEmpty(this._screenRecordingProperties)) {
await this.stopRecordingScreen();
}
},
async () => {
if (await this.mobileIsMediaProjectionRecordingRunning()) {
await this.mobileStopMediaProjectionRecording();
}
},
async () => {
if (!lodash_1.default.isEmpty(this._screenStreamingProps)) {
await this.mobileStopScreenStreaming();
}
},
];
try {
await this.stopChromedriverProxies();
}
catch (err) {
this.log.warn(`Unable to stop ChromeDriver proxies: ${err.message}`);
}
if (this.jwpProxyActive) {
try {
await this.uiautomator2.deleteSession();
}
catch (err) {
this.log.warn(`Unable to proxy deleteSession to UiAutomator2: ${err.message}`);
}
this.jwpProxyActive = false;
}
if (this.adb) {
await bluebird_1.default.all(screenRecordingStopTasks.map((task) => {
(async () => {
try {
await task();
}
catch { }
})();
}));
if (this.opts.appPackage) {
if (!this.isChromeSession &&
((!this.opts.dontStopAppOnReset && !this.opts.noReset) ||
(this.opts.noReset && this.opts.shouldTerminateApp))) {
try {
await this.adb.forceStop(this.opts.appPackage);
}
catch (err) {
this.log.warn(`Unable to force stop app: ${err.message}`);
}
}
if (this.opts.fullReset && !this.opts.skipUninstall) {
this.log.debug(`Capability 'fullReset' set to 'true', Uninstalling '${this.opts.appPackage}'`);
try {
await this.adb.uninstallApk(this.opts.appPackage);
}
catch (err) {
this.log.warn(`Unable to uninstall app: ${err.message}`);
}
}
}
// This value can be true if test target device is <= 26
if (this._wasWindowAnimationDisabled) {
this.log.info('Restoring window animation state');
await this.settingsApp.setAnimationState(true);
}
if (this._originalIme) {
try {
await this.adb.setIME(this._originalIme);
}
catch (e) {
this.log.warn(`Cannot restore the original IME: ${e.message}`);
}
}
try {
await this.releaseSystemPort();
}
catch (error) {
this.log.warn(`Unable to remove system port forward: ${error.message}`);
// Ignore, this block will also be called when we fall in catch block
// and before even port forward.
}
try {
await this.releaseMjpegServerPort();
}
catch (error) {
this.log.warn(`Unable to remove MJPEG server port forward: ${error.message}`);
// Ignore, this block will also be called when we fall in catch block
// and before even port forward.
}
if ((await this.adb.getApiLevel()) >= 28) {
// Android P
this.log.info('Restoring hidden api policy to the device default configuration');
await this.adb.setDefaultHiddenApiPolicy(!!this.opts.ignoreHiddenApiPolicyError);
}
}
if (this.mjpegStream) {
this.log.info('Closing MJPEG stream');
this.mjpegStream.stop();
}
await super.deleteSession();
}
async checkAppPresent() {
this.log.debug('Checking whether app is actually present');
if (!this.opts.app || !(await support_1.fs.exists(this.opts.app))) {
throw this.log.errorWithException(`Could not find app apk at '${this.opts.app}'`);
}
}
async onSettingsUpdate() {
// intentionally do nothing here, since commands.updateSettings proxies
// settings to the uiauto2 server already
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
proxyActive(sessionId) {
// we always have an active proxy to the UiAutomator2 server
return true;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
canProxy(sessionId) {
// we can always proxy to the uiautomator2 server
return true;
}
getProxyAvoidList() {
// we are maintaining two sets of NO_PROXY lists, one for chromedriver(CHROME_NO_PROXY)
// and one for uiautomator2(NO_PROXY), based on current context will return related NO_PROXY list
if (support_1.util.hasValue(this.chromedriver)) {
// if the current context is webview(chromedriver), then return CHROME_NO_PROXY list
this.jwpProxyAvoid = CHROME_NO_PROXY;
}
else {
this.jwpProxyAvoid = NO_PROXY;
}
if (this.opts.nativeWebScreenshot) {
this.jwpProxyAvoid = [
...this.jwpProxyAvoid,
['GET', new RegExp('^/session/[^/]+/screenshot')],
];
}
return this.jwpProxyAvoid;
}
async updateSettings(settings) {
await this.settings.update(settings);
await this.uiautomator2.jwproxy.command('/appium/settings', 'POST', { settings });
}
async getSettings() {
const driverSettings = this.settings.getSettings();
const serverSettings = (await this.uiautomator2.jwproxy.command('/appium/settings', 'GET'));
return { ...driverSettings, ...serverSettings };
}
}
exports.AndroidUiautomator2Driver = AndroidUiautomator2Driver;
AndroidUiautomator2Driver.newMethodMap = method_map_1.newMethodMap;
AndroidUiautomator2Driver.executeMethodMap = execute_method_map_1.executeMethodMap;
//# sourceMappingURL=driver.js.map