appium-android-driver
Version:
Android UiAutomator and Chrome support for Appium
380 lines (379 loc) • 14.2 kB
JavaScript
;
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