diff --git a/lib/tools/adb-commands.js b/lib/tools/adb-commands.js index e23eebb7..640a0ef8 100644 --- a/lib/tools/adb-commands.js +++ b/lib/tools/adb-commands.js @@ -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'; @@ -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. * diff --git a/lib/tools/index.js b/lib/tools/index.js index ceabd990..626bcb92 100644 --- a/lib/tools/index.js +++ b/lib/tools/index.js @@ -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, @@ -18,6 +19,7 @@ Object.assign( apksUtilsMethods, settingsClientCommands, lockManagementCommands, + keyboardCommands, ); export default methods; diff --git a/lib/tools/keyboard-commands.js b/lib/tools/keyboard-commands.js new file mode 100644 index 00000000..2be21fb6 --- /dev/null +++ b/lib/tools/keyboard-commands.js @@ -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; diff --git a/lib/tools/lockmgmt.js b/lib/tools/lockmgmt.js index de75f753..05971e7c 100644 --- a/lib/tools/lockmgmt.js +++ b/lib/tools/lockmgmt.js @@ -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]; @@ -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 @@ -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}`); } }; @@ -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}`); } }; @@ -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; diff --git a/lib/tools/system-calls.js b/lib/tools/system-calls.js index ba23997c..c4464b11 100644 --- a/lib/tools/system-calls.js +++ b/lib/tools/system-calls.js @@ -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; @@ -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))) { diff --git a/test/adb-specs.js b/test/adb-specs.js deleted file mode 100644 index 2d7ba9a5..00000000 --- a/test/adb-specs.js +++ /dev/null @@ -1,91 +0,0 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; - - -chai.use(chaiAsPromised); - -describe.skip('ADB To be implemented methods', function () { - - // it('packageAndLaunchActivityFromManifest', async () => { }); - // it('processExists', async () => { }); - // it('compileManifest', async () => { }); - // it('insertManifest', async () => { }); - // it('signWithDefaultCert', async () => { }); - // it('signWithCustomCert', async () => { }); - // it('sign', async () => { }); - // it('zipAlignApk', async () => { }); - // it('checkApkCert', async () => { }); - // it('getKeystoreHash', async () => { }); - // it('getDevicesWithRetry', async () => { }); - // it('getApiLevel', async () => { }); - // it('getEmulatorPort', async () => { }); - // it('rimraf', async () => { }); - // it('push', async () => { }); - // it('pull', async () => { }); - // it('getPortFromEmulatorString', async () => { }); - // it('getRunningAVD', async () => { }); - // it('getRunningAVDWithRetry', async () => { }); - // it('killAllEmulators', async () => { }); - // it('launchAVD', async () => { }); - // it('waitForEmulatorReady', async () => { }); - // it('getConnectedDevices', async () => { }); - // it('getConnectedEmulators', async () => { }); - // it('forwardPort', async () => { }); - // it('forwardAbstractPort', async () => { }); - // it('isDeviceConnected', async () => { }); - // it('ping', async () => { }); - // it('setDeviceId', async () => { }); - // it('setEmulatorPort', async () => { }); - // it('waitForDevice', async () => { }); - // it('restartAdb', async () => { }); - it('restart', async function () { }); - it('stopLogcatstartLogcat', async function () { }); - it('getLogcatLogs', async function () { }); - it('getPIDsByName', async function () { }); - it('killProcessesByName', async function () { }); - it('killProcessByPID', async function () { }); - // it('startApp', async () => { }); - // it('isValidClass', async () => { }); - it('broadcastProcessEnd', async function () { }); - it('broadcast', async function () { }); - it('endAndroidCoverage', async function () { }); - it('androidCoverage', async function () { }); - // it('getFocusedPackageAndActivity', async () => { }); - // it('waitForActivityOrNot', async () => { }); - // it('waitForActivity', async () => { }); - // it('waitForNotActivity', async () => { }); - // it('uninstallApk', async () => { }); - // it('installRemote', async () => { }); - // it('install', async () => { }); - // it('mkdir', async () => { }); - it('instrument', async function () { }); - // TODO should deprecate not used in appium - // it('checkAndSignApk', async () => { }); - // it('forceStop', async () => { }); - // it('clear', async () => { }); - // it('stopAndClear', async () => { }); - // it('isAppInstalled', async () => { }); - // it('lock', async () => { }); - // it('back', async () => { }); - // it('goToHome', async () => { }); - // it('keyevent', async () => { }); - // it('isScreenLocked', async () => { }); - // it('isSoftKeyboardPresent', async () => { }); - // it('isEmulator', async () => { }); - // it('isAirplaneModeOn', async () => { }); - // it('setAirplaneMode', async () => { }); - // it('broadcastAirplaneMode', async () => { }); - // it('isWifiOn', async () => { }); - // it('setWifi', async () => { }); - // it('isDataOn', async () => { }); - // it('setData', async () => { }); - // it('setWifiAndData', async () => { }); - // it('availableIMEs', async () => { }); - // it('defaultIME', async () => { }); - // it('enableIME', async () => { }); - // it('disableIME', async () => { }); - // it('setIME', async () => { }); - // it('hasInternetPermissionFromManifest', async () => { }); - it('reboot', async function () { }); - // it('getAdbServerPort', async () => { }); -}); diff --git a/test/functional/lock-mgmt-e2e-specs.js b/test/functional/lock-mgmt-e2e-specs.js index 665ac9db..6693abd3 100644 --- a/test/functional/lock-mgmt-e2e-specs.js +++ b/test/functional/lock-mgmt-e2e-specs.js @@ -18,4 +18,30 @@ describe('Lock Management', function () { await adb.verifyLockCredential().should.eventually.be.true; await adb.isLockEnabled().should.eventually.be.false; }); + + describe('Lock and unlock life cycle', function () { + const password = '1234'; + + before(function () { + if (process.env.CI) { + // We don't want to lock the device for all other tests if this test fails + return this.skip(); + } + }); + afterEach(async function () { + await adb.clearLockCredential(password); + }); + + it('device lock and unlock scenario should work', async function () { + await adb.setLockCredential('password', password); + await adb.keyevent(26); + await adb.isLockEnabled().should.eventually.be.true; + await adb.isScreenLocked().should.eventually.be.true; + await adb.clearLockCredential(password); + await adb.cycleWakeUp(); + await adb.dismissKeyguard(); + await adb.isLockEnabled().should.eventually.be.false; + await adb.isScreenLocked().should.eventually.be.false; + }); + }); });