Skip to content

Commit

Permalink
feat: Add 'mobile: screenshots' extension (#890)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach committed Jun 26, 2023
1 parent 6f52e62 commit 1989c14
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 1 deletion.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/commands/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ extensions.executeMobile = async function executeMobile (mobileCommand, opts = {
getPerformanceDataTypes: 'getPerformanceDataTypes',

statusBar: 'mobilePerformStatusBarCommand',

screenshots: 'mobileScreenshots',
};

if (!_.has(mobileCommandsMapping, mobileCommand)) {
Expand Down
2 changes: 2 additions & 0 deletions lib/commands/index.js
Original file line number Diff line number Diff line change
@@ -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 = {};
Expand All @@ -10,6 +11,7 @@ Object.assign(
executeCmds,
servicesCmds,
idlingResourcesCmds,
screenshotCmds,
// add other command types here
);

Expand Down
79 changes: 79 additions & 0 deletions lib/commands/screenshot.js
Original file line number Diff line number Diff line change
@@ -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<ScreenshotsInfo>}
*/
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;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 1989c14

Please sign in to comment.