appium-adb 9.4.0 → 9.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/lib/helpers.js +92 -3
- package/build/lib/tools/adb-commands.js +52 -2
- package/build/lib/tools/apk-utils.js +11 -3
- package/lib/helpers.js +100 -4
- package/lib/tools/adb-commands.js +50 -2
- package/lib/tools/apk-utils.js +16 -2
- package/package.json +1 -1
package/lib/helpers.js
CHANGED
|
@@ -12,6 +12,8 @@ const APK_EXTENSION = '.apk';
|
|
|
12
12
|
const APK_INSTALL_TIMEOUT = 60000;
|
|
13
13
|
const APKS_INSTALL_TIMEOUT = APK_INSTALL_TIMEOUT * 2;
|
|
14
14
|
const DEFAULT_ADB_EXEC_TIMEOUT = 20000; // in milliseconds
|
|
15
|
+
const MAIN_ACTION = 'android.intent.action.MAIN';
|
|
16
|
+
const LAUNCHER_CATEGORY = 'android.intent.category.LAUNCHER';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Retrieves the actual path to SDK root folder from the system environment
|
|
@@ -570,10 +572,8 @@ function parseManifest (manifest) {
|
|
|
570
572
|
continue;
|
|
571
573
|
}
|
|
572
574
|
|
|
573
|
-
const isMainAction = actions
|
|
574
|
-
|
|
575
|
-
const isLauncherCategory = categories
|
|
576
|
-
.some(({name}) => name === 'android.intent.category.LAUNCHER');
|
|
575
|
+
const isMainAction = actions.some(({name}) => name === MAIN_ACTION);
|
|
576
|
+
const isLauncherCategory = categories.some(({name}) => name === LAUNCHER_CATEGORY);
|
|
577
577
|
if (isMainAction && isLauncherCategory) {
|
|
578
578
|
result.activity = activity.name;
|
|
579
579
|
return result;
|
|
@@ -930,6 +930,101 @@ function escapeShellArg (arg) {
|
|
|
930
930
|
return arg.replace(/&/g, '\\&');
|
|
931
931
|
}
|
|
932
932
|
|
|
933
|
+
/**
|
|
934
|
+
* Parses the name of launchable package activity
|
|
935
|
+
* from dumpsys output.
|
|
936
|
+
*
|
|
937
|
+
* @param {string} dumpsys the actual dumpsys output
|
|
938
|
+
* @returns {string[]} Either the fully qualified
|
|
939
|
+
* activity name as a single list item or an empty list if nothing could be parsed.
|
|
940
|
+
* In Android 6 and older there is no reliable way to determine
|
|
941
|
+
* the category name for the given activity, so this API just
|
|
942
|
+
* returns all activity names belonging to 'android.intent.action.MAIN'
|
|
943
|
+
* with the expectation that the app manifest could be parsed next
|
|
944
|
+
* in order to determine category names for these.
|
|
945
|
+
*/
|
|
946
|
+
function parseLaunchableActivityNames (dumpsys) {
|
|
947
|
+
const mainActivityNameRe = new RegExp(`^\\s*${_.escapeRegExp(MAIN_ACTION)}:$`);
|
|
948
|
+
const categoryNameRe = /^\s*Category:\s+"([a-zA-Z0-9._/-]+)"$/;
|
|
949
|
+
const blocks = [];
|
|
950
|
+
let blockStartIndent;
|
|
951
|
+
let block = [];
|
|
952
|
+
for (const line of dumpsys.split('\n').map(_.trimEnd)) {
|
|
953
|
+
const currentIndent = line.length - _.trimStart(line).length;
|
|
954
|
+
if (mainActivityNameRe.test(line)) {
|
|
955
|
+
blockStartIndent = currentIndent;
|
|
956
|
+
if (!_.isEmpty(block)) {
|
|
957
|
+
blocks.push(block);
|
|
958
|
+
block = [];
|
|
959
|
+
}
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
if (_.isNil(blockStartIndent)) {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (currentIndent > blockStartIndent) {
|
|
967
|
+
block.push(line);
|
|
968
|
+
} else {
|
|
969
|
+
if (!_.isEmpty(block)) {
|
|
970
|
+
blocks.push(block);
|
|
971
|
+
block = [];
|
|
972
|
+
}
|
|
973
|
+
blockStartIndent = null;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (!_.isEmpty(block)) {
|
|
977
|
+
blocks.push(block);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const result = [];
|
|
981
|
+
for (const item of blocks) {
|
|
982
|
+
let hasCategory = false;
|
|
983
|
+
let isLauncherCategory = false;
|
|
984
|
+
for (const line of item) {
|
|
985
|
+
const match = categoryNameRe.exec(line);
|
|
986
|
+
if (!match) {
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
hasCategory = true;
|
|
991
|
+
isLauncherCategory = match[1] === LAUNCHER_CATEGORY;
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
// On older Android versions the category name
|
|
995
|
+
// might not be listed, so we just try to fetch
|
|
996
|
+
// all matches instead
|
|
997
|
+
if (hasCategory && !isLauncherCategory) {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
for (const activityNameStr of item.map(_.trim).filter(Boolean)) {
|
|
1002
|
+
const fqActivityName = activityNameStr.split(/\s+/)[1];
|
|
1003
|
+
if (!matchComponentName(fqActivityName)) {
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (isLauncherCategory) {
|
|
1008
|
+
return [fqActivityName];
|
|
1009
|
+
}
|
|
1010
|
+
result.push(fqActivityName);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return result;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Check if the given string is a valid component name
|
|
1018
|
+
*
|
|
1019
|
+
* @param {string} classString The string to verify
|
|
1020
|
+
* @return {?Array<Match>} The result of Regexp.exec operation
|
|
1021
|
+
* or _null_ if no matches are found
|
|
1022
|
+
*/
|
|
1023
|
+
function matchComponentName (classString) {
|
|
1024
|
+
// some.package/some.package.Activity
|
|
1025
|
+
return /^[a-z0-9./_]+$/i.exec(classString);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
933
1028
|
export {
|
|
934
1029
|
getAndroidPlatformAndPath, unzipFile,
|
|
935
1030
|
getIMEListFromOutput, getJavaForOs, isShowingLockscreen, isCurrentFocusOnKeyguard,
|
|
@@ -940,4 +1035,5 @@ export {
|
|
|
940
1035
|
DEFAULT_ADB_EXEC_TIMEOUT, parseManifest, parseAaptStrings, parseAapt2Strings,
|
|
941
1036
|
formatConfigMarker, parseJsonData, unsignApk, toAvdLocaleArgs, requireSdkRoot,
|
|
942
1037
|
getSdkRootFromEnv, getAndroidPrefsRoot, dirExists, escapeShellArg,
|
|
1038
|
+
parseLaunchableActivityNames, matchComponentName,
|
|
943
1039
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import log from '../logger.js';
|
|
2
2
|
import {
|
|
3
3
|
getIMEListFromOutput, getSurfaceOrientation, extractMatchingPermissions,
|
|
4
|
+
parseLaunchableActivityNames, matchComponentName,
|
|
4
5
|
} from '../helpers.js';
|
|
5
6
|
import path from 'path';
|
|
6
7
|
import _ from 'lodash';
|
|
7
|
-
import { fs, util } from '@appium/support';
|
|
8
|
+
import { fs, util, tempDir } from '@appium/support';
|
|
8
9
|
import { EOL } from 'os';
|
|
9
10
|
import Logcat from '../logcat';
|
|
10
11
|
import { sleep, waitForCondition } from 'asyncbox';
|
|
@@ -207,7 +208,54 @@ methods.mkdir = async function mkdir (remotePath) {
|
|
|
207
208
|
*/
|
|
208
209
|
methods.isValidClass = function isValidClass (classString) {
|
|
209
210
|
// some.package/some.package.Activity
|
|
210
|
-
return
|
|
211
|
+
return matchComponentName(classString);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Fetches the fully qualified name of the launchable activity for the
|
|
216
|
+
* given package. It is expected the package is already installed on
|
|
217
|
+
* the device under test.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} pkg - The target package identifier
|
|
220
|
+
* @return {string} Fully qualified name of the launchable activity
|
|
221
|
+
* @throws {Error} If there was an error while resolving the activity name
|
|
222
|
+
*/
|
|
223
|
+
methods.resolveLaunchableActivity = async function resolveLaunchableActivity (pkg) {
|
|
224
|
+
if (await this.getApiLevel() < 24) {
|
|
225
|
+
const stdout = await this.shell(['dumpsys', 'package', pkg]);
|
|
226
|
+
const names = parseLaunchableActivityNames(stdout);
|
|
227
|
+
if (_.isEmpty(names)) {
|
|
228
|
+
log.debug(stdout);
|
|
229
|
+
throw new Error(`Unable to resolve the launchable activity of '${pkg}'. Is it installed on the device?`);
|
|
230
|
+
}
|
|
231
|
+
if (names.length === 1) {
|
|
232
|
+
return names[0];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const tmpRoot = await tempDir.openDir();
|
|
236
|
+
try {
|
|
237
|
+
const tmpApp = await this.pullApk(pkg, tmpRoot);
|
|
238
|
+
const {apkActivity} = await this.packageAndLaunchActivityFromManifest(tmpApp);
|
|
239
|
+
return apkActivity;
|
|
240
|
+
} catch (e) {
|
|
241
|
+
log.debug(e.stack);
|
|
242
|
+
log.warn(`Unable to resolve the launchable activity of '${pkg}'. ` +
|
|
243
|
+
`The very first match of the dumpsys output is going to be used. ` +
|
|
244
|
+
`Original error: ${e.message}`);
|
|
245
|
+
return names[0];
|
|
246
|
+
} finally {
|
|
247
|
+
await fs.rimraf(tmpRoot);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const {stdout, stderr} = await this.shell(['cmd', 'package', 'resolve-activity', '--brief', pkg], {
|
|
251
|
+
outputFormat: this.EXEC_OUTPUT_FORMAT.FULL
|
|
252
|
+
});
|
|
253
|
+
for (const line of (stdout || '').split('\n').map(_.trim)) {
|
|
254
|
+
if (this.isValidClass(line)) {
|
|
255
|
+
return line;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
throw new Error(`Unable to resolve the launchable activity of '${pkg}'. Original error: ${stderr || stdout}`);
|
|
211
259
|
};
|
|
212
260
|
|
|
213
261
|
/**
|
package/lib/tools/apk-utils.js
CHANGED
|
@@ -1102,10 +1102,24 @@ apkUtilsMethods.getPackageInfo = async function getPackageInfo (pkg) {
|
|
|
1102
1102
|
return result;
|
|
1103
1103
|
};
|
|
1104
1104
|
|
|
1105
|
+
/**
|
|
1106
|
+
* Fetches base.apk of the given package to the local file system
|
|
1107
|
+
*
|
|
1108
|
+
* @param {string} pkg The package identifier (must be already installed on the device)
|
|
1109
|
+
* @param {string} tmpDir The destination folder path
|
|
1110
|
+
* @returns {string} Full path to the downloaded file
|
|
1111
|
+
* @throws {Error} If there was an error while fetching the .apk
|
|
1112
|
+
*/
|
|
1105
1113
|
apkUtilsMethods.pullApk = async function pullApk (pkg, tmpDir) {
|
|
1106
|
-
const
|
|
1114
|
+
const stdout = _.trim(await this.shell(['pm', 'path', pkg]));
|
|
1115
|
+
const packageMarker = 'package:';
|
|
1116
|
+
if (!_.startsWith(stdout, packageMarker)) {
|
|
1117
|
+
throw new Error(`Cannot pull the .apk package for '${pkg}'. Original error: ${stdout}`);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const remotePath = stdout.replace(packageMarker, '');
|
|
1107
1121
|
const tmpApp = path.resolve(tmpDir, `${pkg}.apk`);
|
|
1108
|
-
await this.pull(
|
|
1122
|
+
await this.pull(remotePath, tmpApp);
|
|
1109
1123
|
log.debug(`Pulled app for package '${pkg}' to '${tmpApp}'`);
|
|
1110
1124
|
return tmpApp;
|
|
1111
1125
|
};
|