/* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { DefaultCreateSessionResult, DriverData, ExternalDriver, InitialOpts, Orientation, RouteMatcher, SingularSessionData, StringRecord, SessionCapabilities, } from '@appium/types'; import {DEFAULT_ADB_PORT} from 'appium-adb'; import {AndroidDriver, utils} from 'appium-android-driver'; import {SETTINGS_HELPER_ID} from 'io.appium.settings'; import {BaseDriver, DeviceSettings} from 'appium/driver'; import {fs, mjpeg, util} from 'appium/support'; import {retryInterval} from 'asyncbox'; import B from 'bluebird'; import _ from 'lodash'; import os from 'node:os'; import path from 'node:path'; import {checkPortStatus, findAPortNotInUse} from 'portscanner'; import type {ExecError} from 'teen_process'; import UIAUTOMATOR2_CONSTRAINTS, {type Uiautomator2Constraints} from './constraints'; import {APKS_EXTENSION, APK_EXTENSION} from './extensions'; import {newMethodMap} from './method-map'; import {signApp} from './helpers'; import type { Uiautomator2Settings, Uiautomator2DeviceDetails, Uiautomator2DriverCaps, Uiautomator2DriverOpts, Uiautomator2SessionCaps, Uiautomator2SessionInfo, Uiautomator2StartSessionOpts, W3CUiautomator2DriverCaps, EmptyObject, } from './types'; import {SERVER_PACKAGE_ID, SERVER_TEST_PACKAGE_ID, UiAutomator2Server} from './uiautomator2'; import type {UiAutomator2ServerOptions} from './uiautomator2'; import { mobileGetActionHistory, mobileScheduleAction, mobileUnscheduleAction, performActions, releaseActions, } from './commands/actions'; import { getAlertText, mobileAcceptAlert, mobileDismissAlert, postAcceptAlert, postDismissAlert, } from './commands/alert'; import {mobileInstallMultipleApks} from './commands/app-management'; import {mobileGetBatteryInfo} from './commands/battery'; import {getClipboard, setClipboard} from './commands/clipboard'; import { active, getAttribute, elementEnabled, elementDisplayed, elementSelected, getName, getLocation, getSize, getElementRect, getElementScreenshot, getText, setValueImmediate, doSetElementValue, click, clear, mobileReplaceElementValue, } from './commands/element'; import {doFindElementOrEls} from './commands/find'; import { mobileClickGesture, mobileDoubleClickGesture, mobileDragGesture, mobileFlingGesture, mobileLongClickGesture, mobilePinchCloseGesture, mobilePinchOpenGesture, mobileScroll, mobileScrollBackTo, mobileScrollGesture, mobileSwipeGesture, } from './commands/gestures'; import { pressKeyCode, longPressKeyCode, mobilePressKey, mobileType, doSendKeys, keyevent, } from './commands/keyboard'; import { getPageSource, getOrientation, setOrientation, openNotifications, suspendChromedriverProxy, mobileGetDeviceInfo, mobileResetAccessibilityCache, } from './commands/misc'; import {mobileListWindows, mobileListDisplays} from './commands/windows'; import {setUrl, mobileDeepLink, back} from './commands/navigation'; import { mobileScreenshots, mobileViewportScreenshot, getScreenshot, getViewportScreenshot, } from './commands/screenshot'; import { getStatusBarHeight, getDevicePixelRatio, getDisplayDensity, getViewPortRect, getWindowRect, getWindowSize, mobileViewPortRect, } from './commands/viewport'; import {executeMethodMap} from './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 = util.getLockFileGuard( path.resolve(os.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'; // 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: RouteMatcher[] = [ ['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/capabilities')], ['GET', new RegExp('^/session/[^/]+/appium/commands')], ['GET', new RegExp('^/session/[^/]+/appium/device/[^/]+')], ['GET', new RegExp('^/session/[^/]+/appium/extensions')], ['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: RouteMatcher[] = [ ['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'] as const; class AndroidUiautomator2Driver extends AndroidDriver implements ExternalDriver { static newMethodMap = newMethodMap; static executeMethodMap = executeMethodMap; uiautomator2: UiAutomator2Server; systemPort: number | undefined; _originalIme: string | null; mjpegStream?: mjpeg.MJpegStream; override caps: Uiautomator2DriverCaps; override opts: Uiautomator2DriverOpts; override desiredCapConstraints: Uiautomator2Constraints; constructor(opts: InitialOpts = {} as InitialOpts, shouldValidateCaps = true) { // `shell` overwrites adb.shell, so remove // @ts-expect-error FIXME: what is this? delete opts.shell; super(opts, shouldValidateCaps); this.locatorStrategies = [ 'xpath', 'id', 'class name', 'accessibility id', 'css selector', '-android uiautomator', ]; this.desiredCapConstraints = _.cloneDeep(UIAUTOMATOR2_CONSTRAINTS); this.jwpProxyActive = false; this.jwpProxyAvoid = NO_PROXY; this._originalIme = null; this.settings = new DeviceSettings( {ignoreUnimportantViews: false, allowInvisibleElements: false}, this.onSettingsUpdate.bind(this), ); // handle webview mechanics from AndroidDriver this.sessionChromedrivers = {}; this.caps = {} as Uiautomator2DriverCaps; this.opts = opts as Uiautomator2DriverOpts; // memoize functions here, so that they are done on a per-instance basis for (const fn of MEMOIZED_FUNCTIONS) { this[fn] = _.memoize(this[fn]) as any; } } override validateDesiredCaps(caps: any): caps is Uiautomator2DriverCaps { return super.validateDesiredCaps(caps); } async createSession( w3cCaps1: W3CUiautomator2DriverCaps, w3cCaps2?: W3CUiautomator2DriverCaps, w3cCaps3?: W3CUiautomator2DriverCaps, driverData?: DriverData[], ): Promise { try { // TODO handle otherSessionData for multiple sessions const [sessionId, caps] = (await BaseDriver.prototype.createSession.call( this, w3cCaps1, w3cCaps2, w3cCaps3, driverData, )) as DefaultCreateSessionResult; const startSessionOpts: Uiautomator2StartSessionOpts = { ...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: DEFAULT_ADB_PORT, androidInstallTimeout: 90000, }; _.defaults(this.opts, defaultOpts); this.opts.adbPort = this.opts.adbPort || 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} = utils.getChromePkg(this.opts.browserName!); let activity: string = defaultActivity; 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, [ APK_EXTENSION, 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 mjpeg.MJpegStream(this.opts.mjpegScreenshotUrl); await this.mjpegStream.start(); } return [sessionId, result]; } catch (e) { await this.deleteSession(); throw e; } } async getDeviceDetails(): Promise { const [ pixelRatio, statBarHeight, viewportRect, {apiVersion, platformVersion, manufacturer, model, realDisplaySize, displayDensity}, ] = await B.all([ this.getDevicePixelRatio(), this.getStatusBarHeight(), this.getViewPortRect(), this.mobileGetDeviceInfo(), ]); return { pixelRatio, statBarHeight, viewportRect, deviceApiLevel: _.parseInt(apiVersion), platformVersion, deviceManufacturer: manufacturer, deviceModel: model, deviceScreenSize: realDisplaySize, deviceScreenDensity: displayDensity, }; } override get driverData() { // TODO fill out resource info here return {}; } override async getSession(): Promise> { const sessionData = await BaseDriver.prototype.getSession.call(this); this.log.debug('Getting session details from server to mix in'); const uia2Data = (await this.uiautomator2!.jwproxy.command('/', 'GET', {})) as any; return {...sessionData, ...uia2Data}; } async allocateSystemPort() { const forwardPort = async (localPort: number) => { this.log.debug( `Forwarding UiAutomator2 Server port ${DEVICE_PORT} to local port ${localPort}`, ); if ((await 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 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(): Promise { 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: Promise[] = []; 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 (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 B.all(preflightPromises); this.opts = {...this.opts, ...(appInfo ?? {})}; return appInfo; } async performSessionExecution(capsWithSessionInfo: StringRecord): Promise { await B.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 B.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(): Promise { // 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 (util.hasValue(this.opts.orientation)) { this.log.debug(`Setting initial orientation to '${this.opts.orientation}'`); await this.setOrientation(this.opts.orientation as 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 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: Uiautomator2StartSessionOpts, ): Promise { const appInfo = await this.performSessionPreExecSetup(); // set actual device name, udid, platform version, screen size, screen density, model and manufacturer details const sessionInfo: Uiautomator2SessionInfo = { 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: Promise = (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() { const uiautomator2Opts: UiAutomator2ServerOptions = { host: this.opts.remoteAdbHost || LOCALHOST_IP4, systemPort: this.systemPort as number, 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 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( SETTINGS_HELPER_ID, SERVER_PACKAGE_ID, SERVER_TEST_PACKAGE_ID, ); } catch (e) { const err = e as ExecError; 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(utils.parseArray(this.opts.uninstallOtherPackages), [ SETTINGS_HELPER_ID, SERVER_PACKAGE_ID, SERVER_TEST_PACKAGE_ID, ]); } // Install any "otherApps" that were specified in caps if (this.opts.otherApps) { let otherApps; try { otherApps = utils.parseArray(this.opts.otherApps); } catch (e) { throw this.log.errorWithException( `Could not parse "otherApps" capability: ${(e as Error).message}`, ); } otherApps = await B.all( otherApps.map((app) => this.helpers.configureApp(app, [APK_EXTENSION, 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 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 (!_.isEmpty(this._screenRecordingProperties)) { await this.stopRecordingScreen(); } }, async () => { if (await this.mobileIsMediaProjectionRecordingRunning()) { await this.mobileStopMediaProjectionRecording(); } }, async () => { if (!_.isEmpty(this._screenStreamingProps)) { await this.mobileStopScreenStreaming(); } }, ]; try { await this.stopChromedriverProxies(); } catch (err) { this.log.warn(`Unable to stop ChromeDriver proxies: ${(err as Error).message}`); } if (this.jwpProxyActive) { try { await this.uiautomator2.deleteSession(); } catch (err) { this.log.warn(`Unable to proxy deleteSession to UiAutomator2: ${(err as Error).message}`); } this.jwpProxyActive = false; } if (this.adb) { await B.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 as Error).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 as Error).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 as 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 as 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 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: string): boolean { // we always have an active proxy to the UiAutomator2 server return true; } // eslint-disable-next-line @typescript-eslint/no-unused-vars canProxy(sessionId: string): boolean { // we can always proxy to the uiautomator2 server return true; } getProxyAvoidList(): RouteMatcher[] { // 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 (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: Uiautomator2Settings) { 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', )) as Partial; return {...driverSettings, ...serverSettings} as any; } // needed to make the typechecker happy async getAppiumSessionCapabilities(): Promise> { return (await super.getAppiumSessionCapabilities()) as SessionCapabilities; } mobileGetActionHistory = mobileGetActionHistory; mobileScheduleAction = mobileScheduleAction; mobileUnscheduleAction = mobileUnscheduleAction; performActions = performActions; releaseActions = releaseActions; getAlertText = getAlertText; mobileAcceptAlert = mobileAcceptAlert; mobileDismissAlert = mobileDismissAlert; postAcceptAlert = postAcceptAlert; postDismissAlert = postDismissAlert; mobileInstallMultipleApks = mobileInstallMultipleApks; mobileGetBatteryInfo = mobileGetBatteryInfo; active = active; getAttribute = getAttribute; elementEnabled = elementEnabled; elementDisplayed = elementDisplayed; elementSelected = elementSelected; getName = getName; getLocation = getLocation; getSize = getSize; getElementRect = getElementRect; getElementScreenshot = getElementScreenshot; getText = getText; setValueImmediate = setValueImmediate; doSetElementValue = doSetElementValue; click = click; clear = clear; mobileReplaceElementValue = mobileReplaceElementValue; doFindElementOrEls = doFindElementOrEls; mobileClickGesture = mobileClickGesture; mobileDoubleClickGesture = mobileDoubleClickGesture; mobileDragGesture = mobileDragGesture; mobileFlingGesture = mobileFlingGesture; mobileLongClickGesture = mobileLongClickGesture; mobilePinchCloseGesture = mobilePinchCloseGesture; mobilePinchOpenGesture = mobilePinchOpenGesture; mobileScroll = mobileScroll; mobileScrollBackTo = mobileScrollBackTo; mobileScrollGesture = mobileScrollGesture; mobileSwipeGesture = mobileSwipeGesture; pressKeyCode = pressKeyCode; longPressKeyCode = longPressKeyCode; mobilePressKey = mobilePressKey; mobileType = mobileType; doSendKeys = doSendKeys; keyevent = keyevent; getPageSource = getPageSource; getOrientation = getOrientation; setOrientation = setOrientation; openNotifications = openNotifications; suspendChromedriverProxy = suspendChromedriverProxy as any; mobileGetDeviceInfo = mobileGetDeviceInfo; mobileResetAccessibilityCache = mobileResetAccessibilityCache; mobileListWindows = mobileListWindows; mobileListDisplays = mobileListDisplays; getClipboard = getClipboard; setClipboard = setClipboard; setUrl = setUrl; mobileDeepLink = mobileDeepLink; back = back; mobileScreenshots = mobileScreenshots; mobileViewportScreenshot = mobileViewportScreenshot; getScreenshot = getScreenshot; getViewportScreenshot = getViewportScreenshot; getStatusBarHeight = getStatusBarHeight; getDevicePixelRatio = getDevicePixelRatio; getDisplayDensity = getDisplayDensity; getViewPortRect = getViewPortRect; getWindowRect = getWindowRect; getWindowSize = getWindowSize; mobileViewPortRect = mobileViewPortRect; } export {AndroidUiautomator2Driver};