appium-android-driver
Version:
Android UiAutomator and Chrome support for Appium
310 lines (288 loc) • 11.1 kB
text/typescript
import * as semver from 'semver';
import _ from 'lodash';
import B from 'bluebird';
import path from 'node:path';
import {setMockLocationApp} from '../geolocation';
import {SETTINGS_HELPER_ID} from 'io.appium.settings';
import {hideKeyboardCompletely, initUnicodeKeyboard} from '../keyboard';
import {
createBaseADB,
prepareEmulator,
pushSettingsApp,
} from './utils';
import {adjustTimeZone} from '../time';
import {retryInterval} from 'asyncbox';
import {
GET_SERVER_LOGS_FEATURE,
nativeLogEntryToSeleniumEntry
} from '../../utils';
import type {AndroidDriver} from '../../driver';
import type {ADBDeviceInfo, ADBLaunchInfo} from '../types';
import type {ADB} from 'appium-adb';
/**
* Gets device information from capabilities, including UDID and emulator port.
*
* @returns Device information with UDID and emulator port
*/
export async function getDeviceInfoFromCaps(this: AndroidDriver): Promise<ADBDeviceInfo> {
// we can create a throwaway ADB instance here, so there is no dependency
// on instantiating on earlier (at this point, we have no udid)
// we can only use this ADB object for commands that would not be confused
// if multiple devices are connected
const adb = await createBaseADB.bind(this)();
let udid = this.opts.udid;
let emPort: number | false | undefined;
// a specific avd name was given. try to initialize with that
if (this.opts?.avd) {
await prepareEmulator.bind(this)(adb);
udid = adb.curDeviceId;
emPort = adb.emulatorPort;
} else {
// no avd given. lets try whatever's plugged in devices/emulators
this.log.info('Retrieving device list');
const devices = await adb.getDevicesWithRetry();
// udid was given, lets try to init with that device
if (udid) {
if (!_.includes(_.map(devices, 'udid'), udid)) {
throw this.log.errorWithException(`Device ${udid} was not in the list of connected devices`);
}
emPort = adb.getPortFromEmulatorString(udid);
} else if (this.opts.platformVersion) {
this.opts.platformVersion = `${this.opts.platformVersion}`.trim();
// a platform version was given. lets try to find a device with the same os
const platformVersion = semver.coerce(this.opts.platformVersion) || this.opts.platformVersion;
this.log.info(`Looking for a device with Android '${platformVersion}'`);
// in case we fail to find something, give the user a useful log that has
// the device udids and os versions so they know what's available
const availDevices: string[] = [];
let partialMatchCandidate: Record<string, string | semver.SemVer> | undefined;
// first try started devices/emulators
for (const device of devices) {
// direct adb calls to the specific device
adb.setDeviceId(device.udid);
const rawDeviceOS = await adb.getPlatformVersion();
// The device OS could either be a number, like `6.0`
// or an abbreviation, like `R`
availDevices.push(`${device.udid} (${rawDeviceOS})`);
const deviceOS = semver.coerce(rawDeviceOS) || rawDeviceOS;
if (!deviceOS) {
continue;
}
const semverPV = platformVersion;
const semverDO = deviceOS;
const bothVersionsCanBeCoerced = semver.valid(deviceOS) && semver.valid(platformVersion);
const bothVersionsAreStrings = _.isString(deviceOS) && _.isString(platformVersion);
if (
(bothVersionsCanBeCoerced &&
(semverDO as semver.SemVer).version ===
(semverPV as semver.SemVer).version) ||
(bothVersionsAreStrings && _.toLower(deviceOS) === _.toLower(platformVersion))
) {
// Got an exact match - proceed immediately
udid = device.udid;
break;
} else if (!bothVersionsCanBeCoerced) {
// There is no point to check for partial match if either of version numbers is not coercible
continue;
}
const pvMajor = (semverPV as semver.SemVer).major;
const pvMinor = (semverPV as semver.SemVer).minor;
const dvMajor = (semverDO as semver.SemVer).major;
const dvMinor = (semverDO as semver.SemVer).minor;
if (
((!_.includes(this.opts.platformVersion, '.') && pvMajor === dvMajor) ||
(pvMajor === dvMajor && pvMinor === dvMinor)) &&
// Got a partial match - make sure we consider the most recent
// device version available on the host system
((partialMatchCandidate && semver.gt(deviceOS, _.values(partialMatchCandidate)[0])) ||
!partialMatchCandidate)
) {
partialMatchCandidate = {[device.udid]: deviceOS};
}
}
if (!udid && partialMatchCandidate) {
udid = _.keys(partialMatchCandidate)[0];
adb.setDeviceId(udid);
}
if (!udid) {
// we couldn't find anything! quit
throw this.log.errorWithException(
`Unable to find an active device or emulator ` +
`with OS ${this.opts.platformVersion}. The following are available: ` +
availDevices.join(', '),
);
}
emPort = adb.getPortFromEmulatorString(udid);
} else {
// a udid was not given, grab the first device we see
udid = devices[0].udid;
emPort = adb.getPortFromEmulatorString(udid);
}
}
this.log.info(`Using device: ${udid}`);
return {udid: String(udid), emPort: emPort ?? false};
}
/**
* Creates an ADB instance configured with the device UDID and emulator port from options.
*
* @returns A configured ADB instance
*/
export async function createADB(this: AndroidDriver): Promise<ADB> {
const {udid, emPort} = this.opts as any;
const adb = await createBaseADB.bind(this)();
adb.setDeviceId(udid ?? '');
if (emPort) {
adb.setEmulatorPort(emPort);
}
return adb;
}
/**
* Gets launch information (package and activity) from the app manifest or options.
*
* @returns Launch information with package and activity names, or undefined if not needed
*/
export async function getLaunchInfo(this: AndroidDriver): Promise<ADBLaunchInfo | undefined> {
const {
app: appOpt,
appPackage: appPackageOpt,
appActivity: appActivityOpt,
appWaitPackage: appWaitPackageOpt,
appWaitActivity: appWaitActivityOpt
} = this.opts;
let appPackage = appPackageOpt;
let appActivity = appActivityOpt;
let appWaitPackage = appWaitPackageOpt;
let appWaitActivity = appWaitActivityOpt;
if ((appPackage && appActivity) || (!appOpt && !appPackage)) {
return;
}
let apkPackage: string | undefined;
let apkActivity: string | undefined;
if (appOpt) {
this.log.debug(`Parsing package and activity from the '${path.basename(appOpt)}' file manifest`);
({apkPackage, apkActivity} = await this.adb.packageAndLaunchActivityFromManifest(appOpt));
} else if (appPackage) {
this.log.debug(`Parsing activity from the installed '${appPackage}' package manifest`);
apkActivity = await this.adb.resolveLaunchableActivity(appPackage);
}
if (apkPackage && !appPackage) {
appPackage = apkPackage;
}
if (apkActivity && !appActivity) {
appActivity = apkActivity;
}
this.log.debug(`Resolved launch package -> activity: ${appPackage} -> ${appActivity}`);
if (!appWaitPackage) {
appWaitPackage = appPackage;
}
if (!appWaitActivity) {
appWaitActivity = appActivity;
}
this.log.debug(`Resolved wait package -> activity: ${appWaitPackage} -> ${appWaitActivity}`);
return {appPackage, appWaitPackage, appActivity, appWaitActivity};
}
/**
* Initializes the device with various settings like locale, keyboard, logcat, etc.
*/
export async function initDevice(this: AndroidDriver): Promise<void> {
const {
skipDeviceInitialization,
locale,
language,
localeScript,
unicodeKeyboard,
hideKeyboard,
disableWindowAnimation,
skipUnlock,
mockLocationApp,
skipLogcatCapture,
logcatFormat,
logcatFilterSpecs,
timeZone,
} = this.opts;
if (skipDeviceInitialization) {
this.log.info(`'skipDeviceInitialization' is set. Skipping device initialization.`);
} else {
if (this.isEmulator()) {
// Check if the device wake up only for an emulator.
// It takes 1 second or so even when the device is already awake in a real device.
await this.adb.waitForDevice();
}
// pushSettingsApp required before calling ensureDeviceLocale for API Level 24+
// Some feature such as location/wifi are not necessary for all users,
// but they require the settings app. So, try to configure it while Appium
// does not throw error even if they fail.
const shouldThrowError = Boolean(
language ||
locale ||
localeScript ||
unicodeKeyboard ||
hideKeyboard ||
disableWindowAnimation ||
!skipUnlock,
);
await pushSettingsApp.bind(this)(shouldThrowError);
}
const setupPromises: Promise<void>[] = [];
if (!this.isEmulator()) {
setupPromises.push((async () => {
if (mockLocationApp || _.isUndefined(mockLocationApp)) {
await setMockLocationApp.bind(this)(mockLocationApp || SETTINGS_HELPER_ID);
} else {
await this.mobileResetGeolocation();
}
})());
}
if (language && locale) {
setupPromises.push(this.ensureDeviceLocale(language, locale, localeScript));
}
if (skipLogcatCapture) {
this.log.info(`'skipLogcatCapture' is set. Skipping starting logcat capture.`);
} else {
const logcatStartupPromise = async (): Promise<void> => {
await this.adb.startLogcat({
format: logcatFormat,
filterSpecs: logcatFilterSpecs,
});
this.eventEmitter.emit('syslogStarted', this.adb.logcat);
if (this.adb.logcat) {
this.assignBiDiLogListener(this.adb.logcat, {
type: 'syslog',
});
}
};
setupPromises.push(logcatStartupPromise());
}
setupPromises.push((async () => {
if (hideKeyboard) {
// Sometimes we have a race condition when Android
// does not register input services soon enough
// after Settings app is installed
await retryInterval(3, 500, async () => await hideKeyboardCompletely.bind(this)());
} else if (hideKeyboard === false) {
await this.adb.shell(['ime', 'reset']);
}
})());
if (unicodeKeyboard) {
setupPromises.push((async () => {
this.log.warn(
`The 'unicodeKeyboard' capability has been deprecated and will be removed. ` +
`Set the 'hideKeyboard' capability to 'true' in order to make the on-screen keyboard invisible.`,
);
await initUnicodeKeyboard.bind(this)();
})());
}
if (timeZone) {
setupPromises.push(adjustTimeZone.bind(this)(timeZone));
}
if (this.isFeatureEnabled(GET_SERVER_LOGS_FEATURE)) {
[, this._bidiServerLogListener] = this.assignBiDiLogListener(
this.log.unwrap(), {
type: 'server',
srcEventName: 'log',
entryTransformer: nativeLogEntryToSeleniumEntry,
}
);
}
await B.all(setupPromises);
}