diff --git a/README.md b/README.md index cf2bf802..6a41f199 100644 --- a/README.md +++ b/README.md @@ -1510,6 +1510,25 @@ Example output for different data types: ] ``` +### mobile: screenshots + +Retrieves a screenshot of each display available to Android. +This functionality is only supported since Android 10. + +#### Arguments + +Name | Type | Required | Description | Example +--- | --- | --- | --- | --- +displayId | number or string | no | Display identifier to take a screenshot for. If not provided then all display screenshots are going to be returned. If no matches were found then an error is thrown. Actual display identifiers could be retrived from the `adb shell dumpsys SurfaceFlinger --display-id` command output. | 1 + +#### Returns + +A dictionary where each key is the diplay identifier and the value has the following keys: +- `id`: The same display identifier +- `name`: Display name +- `isDefault`: Whether this display is the default one +- `payload`: The actual PNG screenshot data encoded to base64 string + ### mobile: statusBar Performs commands on the system status bar. A thin wrapper over `adb shell cmd statusbar` CLI. Works on Android 8 (Oreo) and newer. Available since driver version 2.23 diff --git a/lib/commands/execute.js b/lib/commands/execute.js index 1e23b56f..4b200c14 100644 --- a/lib/commands/execute.js +++ b/lib/commands/execute.js @@ -112,6 +112,8 @@ extensions.executeMobile = async function executeMobile (mobileCommand, opts = { getPerformanceDataTypes: 'getPerformanceDataTypes', statusBar: 'mobilePerformStatusBarCommand', + + screenshots: 'mobileScreenshots', }; if (!_.has(mobileCommandsMapping, mobileCommand)) { diff --git a/lib/commands/index.js b/lib/commands/index.js index 0eb5f676..6d8ca581 100644 --- a/lib/commands/index.js +++ b/lib/commands/index.js @@ -1,6 +1,7 @@ import executeCmds from './execute'; import generalCmds from './general'; import servicesCmds from './services'; +import screenshotCmds from './screenshot'; import idlingResourcesCmds from './idling-resources'; const commands = {}; @@ -10,6 +11,7 @@ Object.assign( executeCmds, servicesCmds, idlingResourcesCmds, + screenshotCmds, // add other command types here ); diff --git a/lib/commands/screenshot.js b/lib/commands/screenshot.js new file mode 100644 index 00000000..36e169c5 --- /dev/null +++ b/lib/commands/screenshot.js @@ -0,0 +1,79 @@ +import _ from 'lodash'; +import B from 'bluebird'; + +const commands = {}; + +// Display 4619827259835644672 (HWC display 0): port=0 pnpId=GGL displayName="EMU_display_0" +const DISPLAY_PATTERN = /^Display\s+(\d+)\s+\(.+display\s+(\d+)\).+displayName="([^"]*)/gm; + +/** + * @typedef {Object} ScreenshotsInfo + * + * A dictionary where each key contains a unique display identifier + * and values are dictionaries with following items: + * - id: Display identifier + * - name: Display name, could be empty + * - isDefault: Whether this display is the default one + * - payload: The actual PNG screenshot data encoded to base64 string + */ + +/** + * @typedef {Object} ScreenshotsOpts + * @property {number|string?} displayId Android display identifier to take a screenshot for. + * If not provided then screenshots of all displays are going to be returned. + * If no matches were found then an error is thrown. + */ + +/** + * Retrieves screenshots of each display available to Android. + * This functionality is only supported since Android 10. + * + * @param {ScreenshotsOpts} opts + * @returns {Promise} + */ +commands.mobileScreenshots = async function mobileScreenshots (opts = {}) { + const displaysInfo = await this.adb.shell(['dumpsys', 'SurfaceFlinger', '--display-id']); + const infos = {}; + let match; + while ((match = DISPLAY_PATTERN.exec(displaysInfo))) { + infos[match[1]] = { + id: match[1], + isDefault: match[2] === '0', + name: match[3], + }; + } + if (_.isEmpty(infos)) { + this.log.debug(displaysInfo); + throw new Error('Cannot determine the information about connected Android displays'); + } + this.log.info(`Parsed Android display infos: ${JSON.stringify(infos)}`); + + const toB64Screenshot = async (dispId) => (await this.adb.takeScreenshot(dispId)) + .toString('base64'); + + const {displayId} = opts; + const displayIdStr = isNaN(displayId) ? null : `${displayId}`; + if (displayIdStr) { + if (!infos[displayIdStr]) { + throw new Error( + `The provided display identifier '${displayId}' is not known. ` + + `Only the following displays have been detected: ${JSON.stringify(infos)}` + ); + } + return { + [displayIdStr]: { + ...infos[displayIdStr], + payload: await toB64Screenshot(displayIdStr), + } + }; + } + + const allInfos = _.values(infos); + const screenshots = await B.all(allInfos.map(({id}) => toB64Screenshot(id))); + for (const [info, payload] of _.zip(allInfos, screenshots)) { + info.payload = payload; + } + return infos; +}; + +export default commands; diff --git a/package.json b/package.json index dcf13abc..0f643565 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ ], "dependencies": { "@babel/runtime": "^7.4.3", - "appium-adb": "^9.10.2", + "appium-adb": "^9.13.2", "appium-android-driver": "^5.14.0", "asyncbox": "^2.3.1", "bluebird": "^3.5.0",