Skip to content

Commit

Permalink
feat: Add a method to set device lock credential (#579)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach committed Sep 26, 2021
1 parent 8d9387a commit 7c398c9
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 152 deletions.
46 changes: 1 addition & 45 deletions lib/tools/adb-commands.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import log from '../logger.js';
import {
getIMEListFromOutput, isShowingLockscreen, isCurrentFocusOnKeyguard,
getSurfaceOrientation, isScreenOnFully, extractMatchingPermissions,
getIMEListFromOutput, getSurfaceOrientation, extractMatchingPermissions,
} from '../helpers.js';
import path from 'path';
import _ from 'lodash';
Expand Down Expand Up @@ -666,49 +665,6 @@ methods.getScreenOrientation = async function getScreenOrientation () {
return getSurfaceOrientation(stdout);
};

/**
* Retrieve the screen lock state of the device under test.
*
* @return {boolean} True if the device is locked.
*/
methods.isScreenLocked = async function isScreenLocked () {
let stdout = await this.shell(['dumpsys', 'window']);
if (process.env.APPIUM_LOG_DUMPSYS) {
// optional debugging
// if the method is not working, turn it on and send us the output
let dumpsysFile = path.resolve(process.cwd(), 'dumpsys.log');
log.debug(`Writing dumpsys output to ${dumpsysFile}`);
await fs.writeFile(dumpsysFile, stdout);
}
return (isShowingLockscreen(stdout) || isCurrentFocusOnKeyguard(stdout) ||
!isScreenOnFully(stdout));
};

/**
* @typedef {Object} KeyboardState
* @property {boolean} isKeyboardShown - Whether soft keyboard is currently visible.
* @property {boolean} canCloseKeyboard - Whether the keyboard can be closed.
*/

/**
* Retrieve the state of the software keyboard on the device under test.
*
* @return {KeyboardState} The keyboard state.
*/
methods.isSoftKeyboardPresent = async function isSoftKeyboardPresent () {
try {
const stdout = await this.shell(['dumpsys', 'input_method']);
const inputShownMatch = /mInputShown=(\w+)/.exec(stdout);
const inputViewShownMatch = /mIsInputViewShown=(\w+)/.exec(stdout);
return {
isKeyboardShown: !!(inputShownMatch && inputShownMatch[1] === 'true'),
canCloseKeyboard: !!(inputViewShownMatch && inputViewShownMatch[1] === 'true'),
};
} catch (e) {
throw new Error(`Error finding softkeyboard. Original error: ${e.message}`);
}
};

/**
* Send an arbitrary Telnet command to the device under test.
*
Expand Down
2 changes: 2 additions & 0 deletions lib/tools/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import apksUtilsMethods from './apks-utils.js';
import emuMethods from './adb-emu-commands.js';
import settingsClientCommands from './settings-client-commands';
import lockManagementCommands from './lockmgmt';
import keyboardCommands from './keyboard-commands';

Object.assign(
methods,
Expand All @@ -18,6 +19,7 @@ Object.assign(
apksUtilsMethods,
settingsClientCommands,
lockManagementCommands,
keyboardCommands,
);

export default methods;
Expand Down
64 changes: 64 additions & 0 deletions lib/tools/keyboard-commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import log from '../logger.js';
import { waitForCondition } from 'asyncbox';

const KEYCODE_ESC = 111;
const KEYCODE_BACK = 4;

const keyboardCommands = {};

/**
* Hides software keyboard if it is visible.
* Noop if the keyboard is already hidden.
*
* @param {number} timeoutMs [1000] For how long to wait (in milliseconds)
* until the keyboard is actually hidden.
* @returns {boolean} `false` if the keyboard was already hidden
* @throws {Error} If the keyboard cannot be hidden.
*/
keyboardCommands.hideKeyboard = async function hideKeyboard (timeoutMs = 1000) {
let {isKeyboardShown, canCloseKeyboard} = await this.isSoftKeyboardPresent();
if (!isKeyboardShown) {
log.info('Keyboard has no UI; no closing necessary');
return false;
}
// Try ESC then BACK if the first one fails
for (const keyCode of [KEYCODE_ESC, KEYCODE_BACK]) {
if (canCloseKeyboard) {
await this.keyevent(keyCode);
}
try {
return await waitForCondition(async () => {
({isKeyboardShown} = await this.isSoftKeyboardPresent());
return !isKeyboardShown;
}, {waitMs: timeoutMs, intervalMs: 500});
} catch (ign) {}
}
throw new Error(`The software keyboard cannot be hidden`);
};

/**
* @typedef {Object} KeyboardState
* @property {boolean} isKeyboardShown - Whether soft keyboard is currently visible.
* @property {boolean} canCloseKeyboard - Whether the keyboard can be closed.
*/

/**
* Retrieve the state of the software keyboard on the device under test.
*
* @return {KeyboardState} The keyboard state.
*/
keyboardCommands.isSoftKeyboardPresent = async function isSoftKeyboardPresent () {
try {
const stdout = await this.shell(['dumpsys', 'input_method']);
const inputShownMatch = /mInputShown=(\w+)/.exec(stdout);
const inputViewShownMatch = /mIsInputViewShown=(\w+)/.exec(stdout);
return {
isKeyboardShown: !!(inputShownMatch && inputShownMatch[1] === 'true'),
canCloseKeyboard: !!(inputViewShownMatch && inputViewShownMatch[1] === 'true'),
};
} catch (e) {
throw new Error(`Error finding softkeyboard. Original error: ${e.message}`);
}
};

export default keyboardCommands;
169 changes: 155 additions & 14 deletions lib/tools/lockmgmt.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import log from '../logger.js';
import path from 'path';
import _ from 'lodash';
import { fs } from 'appium-support';
import {
isShowingLockscreen, isCurrentFocusOnKeyguard, isScreenOnFully,
} from '../helpers.js';
import B from 'bluebird';

const lockManagementMethods = {};

const CREDENTIAL_CANNOT_BE_NULL_OR_EMPTY_ERROR = `Credential can't be null or empty`;
const CREDENTIAL_DID_NOT_MATCH_ERROR = `didn't match`;
const SUPPORTED_LOCK_CREDENTIAL_TYPES = ['password', 'pin', 'pattern'];
const KEYCODE_POWER = 26;
const KEYCODE_WAKEUP = 224; // works over API Level 20
const HIDE_KEYBOARD_WAIT_TIME = 100;

function buildCommand (verb, oldCredential = null, ...args) {
const cmd = ['locksettings', verb];
Expand All @@ -17,6 +27,24 @@ function buildCommand (verb, oldCredential = null, ...args) {
return cmd;
}

async function swipeUp (adb) {
const output = await adb.shell(['dumpsys', 'window']);
const dimensionsMatch = /init=(\d+)x(\d+)/.exec(output);
if (!dimensionsMatch) {
throw new Error('Cannot retrieve the display size');
}
const displayWidth = parseInt(dimensionsMatch[1], 10);
const displayHeight = parseInt(dimensionsMatch[2], 10);
const x0 = displayWidth / 2;
const y0 = displayHeight / 5 * 4;
const x1 = x0;
const y1 = displayHeight / 5;
await adb.shell([
'input', 'touchscreen', 'swipe',
...([x0, y0, x1, y1].map((c) => Math.trunc(c)))
]);
}

/**
* Check whether the device supports lock settings management with `locksettings`
* command line tool. This tool has been added to Android toolset since API 27 Oreo
Expand Down Expand Up @@ -51,13 +79,20 @@ lockManagementMethods.isLockManagementSupported = async function isLockManagemen
*/
lockManagementMethods.verifyLockCredential = async function verifyLockCredential (credential = null) {
try {
const output = await this.shell(buildCommand('verify', credential));
return _.includes(output, 'verified successfully');
} catch (e) {
if (_.includes(e.stderr || e.stdout, CREDENTIAL_CANNOT_BE_NULL_OR_EMPTY_ERROR)) {
const {stdout, stderr} = await this.shell(buildCommand('verify', credential), {
outputFormat: this.EXEC_OUTPUT_FORMAT.FULL
});
if (_.includes(stdout, 'verified successfully')) {
return true;
}
if ([`didn't match`, CREDENTIAL_CANNOT_BE_NULL_OR_EMPTY_ERROR]
.some((x) => _.includes(stderr || stdout, x))) {
return false;
}
throw new Error(`Device lock credential verification failed. Original error: ${e.message}`);
throw new Error(stderr || stdout);
} catch (e) {
throw new Error(`Device lock credential verification failed. ` +
`Original error: ${e.stderr || e.stdout || e.message}`);
}
};

Expand All @@ -75,12 +110,16 @@ lockManagementMethods.verifyLockCredential = async function verifyLockCredential
*/
lockManagementMethods.clearLockCredential = async function clearLockCredential (credential = null) {
try {
const output = await this.shell(buildCommand('clear', credential));
if (!['user has no password', 'Lock credential cleared'].some((x) => _.includes(output, x))) {
throw new Error(output);
const {stdout, stderr} = await this.shell(buildCommand('clear', credential), {
outputFormat: this.EXEC_OUTPUT_FORMAT.FULL
});
if (!['user has no password', 'Lock credential cleared']
.some((x) => _.includes(stderr || stdout, x))) {
throw new Error(stderr || stdout);
}
} catch (e) {
throw new Error(`Cannot clear device lock credential. Original error: ${e.message}`);
throw new Error(`Cannot clear device lock credential. ` +
`Original error: ${e.stderr || e.stdout || e.message}`);
}
};

Expand All @@ -93,15 +132,117 @@ lockManagementMethods.clearLockCredential = async function clearLockCredential (
*/
lockManagementMethods.isLockEnabled = async function isLockEnabled () {
try {
const output = await this.shell(buildCommand('get-disabled'));
return /\bfalse\b/.test(output);
} catch (e) {
if ([CREDENTIAL_DID_NOT_MATCH_ERROR, CREDENTIAL_CANNOT_BE_NULL_OR_EMPTY_ERROR]
.some((x) => _.includes(e.stderr || e.stdout, x))) {
const {stdout, stderr} = await this.shell(buildCommand('get-disabled'), {
outputFormat: this.EXEC_OUTPUT_FORMAT.FULL
});
if (/\bfalse\b/.test(stdout)
|| [CREDENTIAL_DID_NOT_MATCH_ERROR, CREDENTIAL_CANNOT_BE_NULL_OR_EMPTY_ERROR].some(
(x) => _.includes(stderr || stdout, x))) {
return true;
}
if (/\btrue\b/.test(stdout)) {
return false;
}
throw new Error(stderr || stdout);
} catch (e) {
throw new Error(`Cannot check if device lock is enabled. Original error: ${e.message}`);
}
};

/**
* Sets the device lock.
*
* @param {!string} credentialType One of: password, pin, pattern.
* @param {!string} credential A non-empty credential value to be set.
* Make sure your new credential matches to the actual system security requirements,
* e.g. a minimum password length. A pattern is specified by a non-separated list
* of numbers that index the cell on the pattern in a 1-based manner in left
* to right and top to bottom order, i.e. the top-left cell is indexed with 1,
* whereas the bottom-right cell is indexed with 9. Example: 1234.
* @param {?string} oldCredential [null] An old credential string.
* It is only required to be set in case you need to change the current
* credential rather than to set a new one. Setting it to a wrong value will
* make this method to fail and throw an exception.
* @throws {Error} If there was a failure while verifying input arguments or setting
* the credential
*/
lockManagementMethods.setLockCredential = async function setLockCredential (
credentialType, credential, oldCredential = null) {
if (!SUPPORTED_LOCK_CREDENTIAL_TYPES.includes(credentialType)) {
throw new Error(`Device lock credential type '${credentialType}' is unknown. ` +
`Only the following credential types are supported: ${SUPPORTED_LOCK_CREDENTIAL_TYPES}`);
}
if (_.isEmpty(credential) && !_.isInteger(credential)) {
throw new Error('Device lock credential cannot be empty');
}
const cmd = buildCommand(`set-${credentialType}`, oldCredential, credential);
try {
const {stdout, stderr} = await this.shell(cmd, {
outputFormat: this.EXEC_OUTPUT_FORMAT.FULL
});
if (!_.includes(stdout, 'set to')) {
throw new Error(stderr || stdout);
}
} catch (e) {
throw new Error(`Setting of device lock ${credentialType} credential failed. ` +
`Original error: ${e.stderr || e.stdout || e.message}`);
}
};

/**
* Retrieve the screen lock state of the device under test.
*
* @return {boolean} True if the device is locked.
*/
lockManagementMethods.isScreenLocked = async function isScreenLocked () {
const stdout = await this.shell(['dumpsys', 'window']);
if (process.env.APPIUM_LOG_DUMPSYS) {
// optional debugging
// if the method is not working, turn it on and send us the output
const dumpsysFile = path.resolve(process.cwd(), 'dumpsys.log');
log.debug(`Writing dumpsys output to ${dumpsysFile}`);
await fs.writeFile(dumpsysFile, stdout);
}
return isShowingLockscreen(stdout) || isCurrentFocusOnKeyguard(stdout) || !isScreenOnFully(stdout);
};

/**
* Dismisses keyguard overlay.
*/
lockManagementMethods.dismissKeyguard = async function dismissKeyguard () {
log.info('Waking up the device to dismiss the keyguard');
// Screen off once to force pre-inputted text field clean after wake-up
// Just screen on if the screen defaults off
await this.cycleWakeUp();

if (await this.getApiLevel() > 21) {
await this.shell(['wm', 'dismiss-keyguard']);
return;
}

const stdout = await this.shell(['dumpsys', 'window', 'windows']);
if (!isCurrentFocusOnKeyguard(stdout)) {
log.debug('The keyguard seems to be inactive');
return;
}

log.debug('Swiping up to dismiss the keyguard');
if (await this.hideKeyboard()) {
await B.delay(HIDE_KEYBOARD_WAIT_TIME);
}
log.debug('Dismissing notifications from the unlock view');
await this.shell(['service', 'call', 'notification', '1']);
await this.back();
await swipeUp(this);
};

/**
* Presses the corresponding key combination to make sure the device's screen
* is not turned off and is locked if the latter is enabled.
*/
lockManagementMethods.cycleWakeUp = async function cycleWakeUp () {
await this.keyevent(KEYCODE_POWER);
await this.keyevent(KEYCODE_WAKEUP);
};

export default lockManagementMethods;
4 changes: 2 additions & 2 deletions lib/tools/system-calls.js
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ systemCallMethods.adbExec = async function adbExec (cmd, opts = {}) {
opts.timeout = opts.timeout || this.adbExecTimeout || DEFAULT_ADB_EXEC_TIMEOUT;
opts.timeoutCapName = opts.timeoutCapName || 'adbExecTimeout'; // For error message

const {outputFormat = systemCallMethods.EXEC_OUTPUT_FORMAT.STDOUT} = opts;
const {outputFormat = this.EXEC_OUTPUT_FORMAT.STDOUT} = opts;

cmd = _.isArray(cmd) ? cmd : [cmd];
let adbRetried = false;
Expand All @@ -426,7 +426,7 @@ systemCallMethods.adbExec = async function adbExec (cmd, opts = {}) {
// sometimes ADB prints out weird stdout warnings that we don't want
// to include in any of the response data, so let's strip it out
stdout = stdout.replace(LINKER_WARNING_REGEXP, '').trim();
return outputFormat === systemCallMethods.EXEC_OUTPUT_FORMAT.FULL ? {stdout, stderr} : stdout;
return outputFormat === this.EXEC_OUTPUT_FORMAT.FULL ? {stdout, stderr} : stdout;
} catch (e) {
const errText = `${e.message}, ${e.stdout}, ${e.stderr}`;
if (ADB_RETRY_ERROR_PATTERNS.some((p) => p.test(errText))) {
Expand Down
Loading

0 comments on commit 7c398c9

Please sign in to comment.