UNPKG

appium-android-driver

Version:

Android UiAutomator and Chrome support for Appium

380 lines (379 loc) 14.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.INPUT_KEYS_WAIT_TIME = exports.UNLOCK_WAIT_TIME = exports.KEYCODE_NUMPAD_ENTER = exports.FINGERPRINT_UNLOCK = exports.PATTERN_UNLOCK = exports.PASSWORD_UNLOCK = exports.PIN_UNLOCK_KEY_EVENT = exports.PIN_UNLOCK = void 0; exports.toCredentialType = toCredentialType; exports.validateUnlockCapabilities = validateUnlockCapabilities; exports.fastUnlock = fastUnlock; exports.encodePassword = encodePassword; exports.stringKeyToArr = stringKeyToArr; exports.fingerprintUnlock = fingerprintUnlock; exports.pinUnlock = pinUnlock; exports.pinUnlockWithKeyEvent = pinUnlockWithKeyEvent; exports.passwordUnlock = passwordUnlock; exports.getPatternKeyPosition = getPatternKeyPosition; exports.getPatternActions = getPatternActions; exports.verifyUnlock = verifyUnlock; exports.patternUnlock = patternUnlock; const support_1 = require("@appium/support"); const asyncbox_1 = require("asyncbox"); const lodash_1 = __importDefault(require("lodash")); exports.PIN_UNLOCK = 'pin'; exports.PIN_UNLOCK_KEY_EVENT = 'pinWithKeyEvent'; exports.PASSWORD_UNLOCK = 'password'; exports.PATTERN_UNLOCK = 'pattern'; exports.FINGERPRINT_UNLOCK = 'fingerprint'; const UNLOCK_TYPES = [ exports.PIN_UNLOCK, exports.PIN_UNLOCK_KEY_EVENT, exports.PASSWORD_UNLOCK, exports.PATTERN_UNLOCK, exports.FINGERPRINT_UNLOCK, ]; exports.KEYCODE_NUMPAD_ENTER = 66; exports.UNLOCK_WAIT_TIME = 100; exports.INPUT_KEYS_WAIT_TIME = 100; const NUMBER_ZERO_KEYCODE = 7; const TOUCH_DELAY_MS = 1000; /** * Converts an unlock type to a credential type string. * * @param unlockType - The unlock type * @returns The credential type string * @throws {Error} If the unlock type is not known */ function toCredentialType(unlockType) { const result = { [exports.PIN_UNLOCK]: 'pin', [exports.PIN_UNLOCK_KEY_EVENT]: 'pin', [exports.PASSWORD_UNLOCK]: 'password', [exports.PATTERN_UNLOCK]: 'pattern', }[unlockType]; if (result) { return result; } throw new Error(`Unlock type '${unlockType}' is not known`); } /** * Validates unlock capabilities and returns them if valid. * * @param caps - The capabilities to validate * @returns The validated capabilities * @throws {Error} If the capabilities are invalid */ function validateUnlockCapabilities(caps) { const { unlockKey, unlockType } = caps ?? {}; if (!isNonEmptyString(unlockType)) { throw new Error('A non-empty unlock key value must be provided'); } if ([exports.PIN_UNLOCK, exports.PIN_UNLOCK_KEY_EVENT, exports.FINGERPRINT_UNLOCK].includes(unlockType)) { if (!/^[0-9]+$/.test(lodash_1.default.trim(unlockKey))) { throw new Error(`Unlock key value '${unlockKey}' must only consist of digits`); } } else if (unlockType === exports.PATTERN_UNLOCK) { if (!/^[1-9]{2,9}$/.test(lodash_1.default.trim(unlockKey))) { throw new Error(`Unlock key value '${unlockKey}' must only include from two to nine digits in range 1..9`); } if (/([1-9]).*?\1/.test(lodash_1.default.trim(unlockKey))) { throw new Error(`Unlock key value '${unlockKey}' must define a valid pattern where repeats are not allowed`); } } else if (unlockType === exports.PASSWORD_UNLOCK) { // Dont trim password key, you can use blank spaces in your android password // ¯\_(ツ)_/¯ if (!/.{4,}/g.test(String(unlockKey))) { throw new Error(`The minimum allowed length of unlock key value '${unlockKey}' is 4 characters`); } } else { throw new Error(`Invalid unlock type '${unlockType}'. ` + `Only the following unlock types are supported: ${UNLOCK_TYPES}`); } return caps; } /** * Performs a fast unlock using ADB commands. * * @param opts - Fast unlock options with credential and credential type */ async function fastUnlock(opts) { const { credential, credentialType } = opts; this.log.info(`Unlocking the device via ADB using ${credentialType} credential '${credential}'`); const wasLockEnabled = await this.adb.isLockEnabled(); if (wasLockEnabled) { await this.adb.clearLockCredential(credential); // not sure why, but the device's screen still remains locked // if a preliminary wake up cycle has not been performed await this.adb.cycleWakeUp(); } else { this.log.info('No active lock has been detected. Proceeding to the keyguard dismissal'); } try { await this.adb.dismissKeyguard(); } finally { if (wasLockEnabled) { await this.adb.setLockCredential(credentialType, credential); } } } /** * Encodes a password by replacing spaces with %s. * * @param key - The password key * @returns The encoded password */ function encodePassword(key) { return `${key}`.replace(/\s/gi, '%s'); } /** * Converts a string key to an array of characters. * * @param key - The key string * @returns An array of characters */ function stringKeyToArr(key) { return `${key}`.trim().replace(/\s+/g, '').split(/\s*/); } /** * Unlocks the device using fingerprint. * * @param capabilities - Driver capabilities containing unlockKey */ async function fingerprintUnlock(capabilities) { await this.adb.fingerprint(String(capabilities.unlockKey)); await (0, asyncbox_1.sleep)(exports.UNLOCK_WAIT_TIME); } /** * Unlocks the device using PIN by clicking on-screen buttons. * * @param capabilities - Driver capabilities containing unlockKey */ async function pinUnlock(capabilities) { this.log.info(`Trying to unlock device using pin ${capabilities.unlockKey}`); await this.adb.dismissKeyguard(); const keys = stringKeyToArr(String(capabilities.unlockKey)); const els = await this.findElOrEls('id', 'com.android.systemui:id/digit_text', true); if (lodash_1.default.isEmpty(els)) { // fallback to pin with key event return await pinUnlockWithKeyEvent.bind(this)(capabilities); } const pins = {}; for (const el of els) { const text = await this.getAttribute('text', support_1.util.unwrapElement(el)); if (text) { pins[text] = el; } } for (const pin of keys) { const el = pins[pin]; await this.click(support_1.util.unwrapElement(el)); } await waitForUnlock(this.adb); } /** * Unlocks the device using PIN by sending key events. * * @param capabilities - Driver capabilities containing unlockKey */ async function pinUnlockWithKeyEvent(capabilities) { this.log.info(`Trying to unlock device using pin with keycode ${capabilities.unlockKey}`); await this.adb.dismissKeyguard(); const keys = stringKeyToArr(String(capabilities.unlockKey)); // Some device does not have system key ids like 'com.android.keyguard:id/key' // Then, sending keyevents are more reliable to unlock the screen. for (const pin of keys) { // 'pin' is number (0-9) in string. // Number '0' is keycode '7'. number '9' is keycode '16'. await this.adb.shell(['input', 'keyevent', String(parseInt(pin, 10) + NUMBER_ZERO_KEYCODE)]); } await waitForUnlock(this.adb); } /** * Unlocks the device using password. * * @param capabilities - Driver capabilities containing unlockKey */ async function passwordUnlock(capabilities) { const { unlockKey } = capabilities; this.log.info(`Trying to unlock device using password ${unlockKey}`); await this.adb.dismissKeyguard(); // Replace blank spaces with %s const key = encodePassword(String(unlockKey)); // Why adb ? It was less flaky await this.adb.shell(['input', 'text', key]); // Why sleeps ? Avoid some flakyness waiting for the input to receive the keys await (0, asyncbox_1.sleep)(exports.INPUT_KEYS_WAIT_TIME); await this.adb.shell(['input', 'keyevent', String(exports.KEYCODE_NUMPAD_ENTER)]); // Waits a bit for the device to be unlocked await waitForUnlock(this.adb); } /** * Calculates the position of a pattern key based on the initial position and piece size. * * @param key - The pattern key number (1-9) * @param initPos - The initial position of the pattern view * @param piece - The size of each pattern piece * @returns The calculated position for the key */ function getPatternKeyPosition(key, initPos, piece) { /* How the math works: We have 9 buttons divided in 3 columns and 3 rows inside the lockPatternView, every button has a position on the screen corresponding to the lockPatternView since it is the parent view right at the middle of each column or row. */ const cols = 3; const pins = 9; const xPos = (key, x, piece) => Math.round(x + (key % cols || cols) * piece - piece / 2); const yPos = (key, y, piece) => Math.round(y + (Math.ceil((key % pins || pins) / cols) * piece - piece / 2)); return { x: xPos(key, initPos.x, piece), y: yPos(key, initPos.y, piece), }; } /** * Generates pointer actions for pattern unlock gesture. * * @param keys - Array of pattern keys (string or number) * @param initPos - The initial position of the pattern view * @param piece - The size of each pattern piece * @returns An array of W3C action objects for pattern unlock */ function getPatternActions(keys, initPos, piece) { // https://www.w3.org/TR/webdriver2/#actions const pointerActions = []; const intKeys = keys.map((key) => (lodash_1.default.isString(key) ? lodash_1.default.parseInt(key) : key)); let lastPos; for (const key of intKeys) { const keyPos = getPatternKeyPosition(key, initPos, piece); if (!lastPos) { pointerActions.push({ type: 'pointerMove', duration: TOUCH_DELAY_MS, x: keyPos.x, y: keyPos.y }, { type: 'pointerDown', button: 0 }); lastPos = keyPos; continue; } const moveTo = { x: 0, y: 0 }; const diffX = keyPos.x - lastPos.x; if (diffX > 0) { moveTo.x = piece; if (Math.abs(diffX) > piece) { moveTo.x += piece; } } else if (diffX < 0) { moveTo.x = -1 * piece; if (Math.abs(diffX) > piece) { moveTo.x -= piece; } } const diffY = keyPos.y - lastPos.y; if (diffY > 0) { moveTo.y = piece; if (Math.abs(diffY) > piece) { moveTo.y += piece; } } else if (diffY < 0) { moveTo.y = -1 * piece; if (Math.abs(diffY) > piece) { moveTo.y -= piece; } } pointerActions.push({ type: 'pointerMove', duration: TOUCH_DELAY_MS, x: moveTo.x + lastPos.x, y: moveTo.y + lastPos.y, }); lastPos = keyPos; } pointerActions.push({ type: 'pointerUp', button: 0 }); return [ { type: 'pointer', id: 'patternUnlock', parameters: { pointerType: 'touch', }, actions: pointerActions, }, ]; } /** * Verifies that the device has been unlocked. * * @param timeoutMs - Optional timeout in milliseconds (default: 2000) * @throws {Error} If the device fails to unlock within the timeout */ async function verifyUnlock(timeoutMs = null) { try { await (0, asyncbox_1.waitForCondition)(async () => !(await this.adb.isScreenLocked()), { waitMs: timeoutMs ?? 2000, intervalMs: 500, }); } catch { throw new Error('The device has failed to be unlocked'); } this.log.info('The device has been successfully unlocked'); } /** * Unlocks the device using pattern gesture. * * @param capabilities - Driver capabilities containing unlockKey */ async function patternUnlock(capabilities) { const { unlockKey } = capabilities; this.log.info(`Trying to unlock device using pattern ${unlockKey}`); await this.adb.dismissKeyguard(); const keys = stringKeyToArr(String(unlockKey)); /* We set the device pattern buttons as number of a regular phone * | • • • | | 1 2 3 | * | • • • | --> | 4 5 6 | * | • • • | | 7 8 9 | The pattern view buttons are not seeing by the uiautomator since they are included inside a FrameLayout, so we are going to try clicking on the buttons using the parent view bounds and math. */ const apiLevel = await this.adb.getApiLevel(); const el = await this.findElOrEls('id', `com.android.${apiLevel >= 21 ? 'systemui' : 'keyguard'}:id/lockPatternView`, false); const initPos = await this.getLocation(support_1.util.unwrapElement(el)); const size = await this.getSize(support_1.util.unwrapElement(el)); // Get actions to perform const actions = getPatternActions(keys, initPos, size.width / 3); // Perform gesture await this.performActions(actions); // Waits a bit for the device to be unlocked await (0, asyncbox_1.sleep)(exports.UNLOCK_WAIT_TIME); } // #region Private Functions /** * Type guard to check if a value is a non-empty string. */ function isNonEmptyString(value) { return typeof value === 'string' && value !== ''; } /** * Waits for the display to be unlocked. * Some devices automatically accept typed 'pin' and 'password' code * without pressing the Enter key. But some devices need it. * This method waits a few seconds first for such automatic acceptance case. * If the device is still locked, then this method will try to send * the enter key code. * * @param adb - The instance of ADB */ async function waitForUnlock(adb) { await (0, asyncbox_1.sleep)(exports.UNLOCK_WAIT_TIME); if (!(await adb.isScreenLocked())) { return; } await adb.keyevent(exports.KEYCODE_NUMPAD_ENTER); await (0, asyncbox_1.sleep)(exports.UNLOCK_WAIT_TIME); } // #endregion //# sourceMappingURL=helpers.js.map