appium-adb 14.0.4 → 14.0.5
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/CHANGELOG.md +6 -0
- package/build/lib/tools/app-commands.d.ts +125 -150
- package/build/lib/tools/app-commands.d.ts.map +1 -1
- package/build/lib/tools/app-commands.js +194 -234
- package/build/lib/tools/app-commands.js.map +1 -1
- package/lib/tools/{app-commands.js → app-commands.ts} +279 -294
- package/package.json +1 -1
|
@@ -3,9 +3,21 @@ import { fs, tempDir, util, system } from '@appium/support';
|
|
|
3
3
|
import { log } from '../logger.js';
|
|
4
4
|
import { waitForCondition } from 'asyncbox';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import type { ADB } from '../adb.js';
|
|
7
|
+
import type { ExecError } from 'teen_process';
|
|
8
|
+
import type {
|
|
9
|
+
StringRecord,
|
|
10
|
+
InstallState,
|
|
11
|
+
ResolveActivityOptions,
|
|
12
|
+
IsAppInstalledOptions,
|
|
13
|
+
StartUriOptions,
|
|
14
|
+
StartAppOptions,
|
|
15
|
+
AppInfo,
|
|
16
|
+
PackageActivityInfo,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
|
|
19
|
+
// Constants
|
|
20
|
+
export const APP_INSTALL_STATE: StringRecord<InstallState> = {
|
|
9
21
|
UNKNOWN: 'unknown',
|
|
10
22
|
NOT_INSTALLED: 'notInstalled',
|
|
11
23
|
NEWER_VERSION_INSTALLED: 'newerVersionInstalled',
|
|
@@ -22,17 +34,17 @@ const RESOLVER_ACTIVITY_NAME = 'android/com.android.internal.app.ResolverActivit
|
|
|
22
34
|
const MAIN_ACTION = 'android.intent.action.MAIN';
|
|
23
35
|
const LAUNCHER_CATEGORY = 'android.intent.category.LAUNCHER';
|
|
24
36
|
|
|
37
|
+
// Public methods
|
|
25
38
|
|
|
26
39
|
/**
|
|
27
40
|
* Verify whether the given argument is a
|
|
28
41
|
* valid class name.
|
|
29
42
|
*
|
|
30
|
-
* @
|
|
31
|
-
* @
|
|
32
|
-
* @return {boolean} The result of Regexp.exec operation
|
|
43
|
+
* @param classString - The actual class name to be verified.
|
|
44
|
+
* @returns The result of Regexp.exec operation
|
|
33
45
|
* or _null_ if no matches are found.
|
|
34
46
|
*/
|
|
35
|
-
export function isValidClass (classString) {
|
|
47
|
+
export function isValidClass (this: ADB, classString: string): boolean {
|
|
36
48
|
// some.package/some.package.Activity
|
|
37
49
|
return !!matchComponentName(classString);
|
|
38
50
|
}
|
|
@@ -42,13 +54,12 @@ export function isValidClass (classString) {
|
|
|
42
54
|
* given package. It is expected the package is already installed on
|
|
43
55
|
* the device under test.
|
|
44
56
|
*
|
|
45
|
-
* @
|
|
46
|
-
* @param
|
|
47
|
-
* @
|
|
48
|
-
* @return {Promise<string>} Fully qualified name of the launchable activity
|
|
57
|
+
* @param pkg - The target package identifier
|
|
58
|
+
* @param opts - Options for resolving the activity
|
|
59
|
+
* @returns Fully qualified name of the launchable activity
|
|
49
60
|
* @throws {Error} If there was an error while resolving the activity name
|
|
50
61
|
*/
|
|
51
|
-
export async function resolveLaunchableActivity (pkg, opts = {}) {
|
|
62
|
+
export async function resolveLaunchableActivity (this: ADB, pkg: string, opts: ResolveActivityOptions = {}): Promise<string> {
|
|
52
63
|
const { preferCmd = true } = opts;
|
|
53
64
|
if (!preferCmd || await this.getApiLevel() < 24) {
|
|
54
65
|
const stdout = await this.shell(['dumpsys', 'package', pkg]);
|
|
@@ -65,9 +76,9 @@ export async function resolveLaunchableActivity (pkg, opts = {}) {
|
|
|
65
76
|
try {
|
|
66
77
|
const tmpApp = await this.pullApk(pkg, tmpRoot);
|
|
67
78
|
const {apkActivity} = await this.packageAndLaunchActivityFromManifest(tmpApp);
|
|
68
|
-
return
|
|
79
|
+
return apkActivity as string;
|
|
69
80
|
} catch (e) {
|
|
70
|
-
const err =
|
|
81
|
+
const err = e as Error;
|
|
71
82
|
log.debug(err.stack);
|
|
72
83
|
log.warn(`Unable to resolve the launchable activity of '${pkg}'. ` +
|
|
73
84
|
`The very first match of the dumpsys output is going to be used. ` +
|
|
@@ -92,33 +103,31 @@ export async function resolveLaunchableActivity (pkg, opts = {}) {
|
|
|
92
103
|
|
|
93
104
|
/**
|
|
94
105
|
* Forcefully stops the app and puts it in the "stopped" state.
|
|
95
|
-
* Android treats a
|
|
106
|
+
* Android treats a "stopped" app as if it was never launched since boot:
|
|
96
107
|
* - It cannot receive broadcast intents (except for explicit ones).
|
|
97
108
|
* - Scheduled jobs, alarms, and services are cancelled.
|
|
98
|
-
* - The app won
|
|
99
|
-
* It
|
|
109
|
+
* - The app won't restart until the user explicitly launches it again.
|
|
110
|
+
* It's the same as when a user swipes an app away from Settings → Apps → Force Stop.
|
|
100
111
|
*
|
|
101
|
-
* @
|
|
102
|
-
* @
|
|
103
|
-
* @return {Promise<string>} The output of the corresponding adb command.
|
|
112
|
+
* @param pkg - The package name to be stopped.
|
|
113
|
+
* @returns The output of the corresponding adb command.
|
|
104
114
|
*/
|
|
105
|
-
export async function forceStop (pkg) {
|
|
115
|
+
export async function forceStop (this: ADB, pkg: string): Promise<string> {
|
|
106
116
|
return await this.shell(['am', 'force-stop', pkg]);
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
/**
|
|
110
|
-
* Gracefully kills the app
|
|
120
|
+
* Gracefully kills the app's process, similar to how Android would do it
|
|
111
121
|
* automatically when low on memory.
|
|
112
|
-
* It only kills the process, without changing the app
|
|
122
|
+
* It only kills the process, without changing the app's "stopped" state.
|
|
113
123
|
* Background services or broadcast receivers may restart soon after,
|
|
114
124
|
* if they are still scheduled or registered.
|
|
115
125
|
* No data or state (like alarms, jobs, etc.) are cleared.
|
|
116
126
|
*
|
|
117
|
-
* @
|
|
118
|
-
* @
|
|
119
|
-
* @return {Promise<string>} The output of the corresponding adb command.
|
|
127
|
+
* @param pkg - The package name to be stopped.
|
|
128
|
+
* @returns The output of the corresponding adb command.
|
|
120
129
|
*/
|
|
121
|
-
export async function killPackage (pkg) {
|
|
130
|
+
export async function killPackage (this: ADB, pkg: string): Promise<string> {
|
|
122
131
|
return await this.shell(['am', 'kill', pkg]);
|
|
123
132
|
}
|
|
124
133
|
|
|
@@ -126,11 +135,10 @@ export async function killPackage (pkg) {
|
|
|
126
135
|
* Clear the user data of the particular application on the device
|
|
127
136
|
* under test.
|
|
128
137
|
*
|
|
129
|
-
* @
|
|
130
|
-
* @
|
|
131
|
-
* @return {Promise<string>} The output of the corresponding adb command.
|
|
138
|
+
* @param pkg - The package name to be cleared.
|
|
139
|
+
* @returns The output of the corresponding adb command.
|
|
132
140
|
*/
|
|
133
|
-
export async function clear (pkg) {
|
|
141
|
+
export async function clear (this: ADB, pkg: string): Promise<string> {
|
|
134
142
|
return await this.shell(['pm', 'clear', pkg]);
|
|
135
143
|
}
|
|
136
144
|
|
|
@@ -139,15 +147,14 @@ export async function clear (pkg) {
|
|
|
139
147
|
* This method is only useful on Android 6.0+ and for applications
|
|
140
148
|
* that support components-based permissions setting.
|
|
141
149
|
*
|
|
142
|
-
* @
|
|
143
|
-
* @param
|
|
144
|
-
* @param {string} [apk] - The path to the actual apk file.
|
|
150
|
+
* @param pkg - The package name to be processed.
|
|
151
|
+
* @param apk - The path to the actual apk file.
|
|
145
152
|
* @throws {Error} If there was an error while granting permissions
|
|
146
153
|
*/
|
|
147
|
-
export async function grantAllPermissions (pkg, apk) {
|
|
154
|
+
export async function grantAllPermissions (this: ADB, pkg: string, apk?: string): Promise<void> {
|
|
148
155
|
const apiLevel = await this.getApiLevel();
|
|
149
156
|
let targetSdk = 0;
|
|
150
|
-
let dumpsysOutput = null;
|
|
157
|
+
let dumpsysOutput: string | null = null;
|
|
151
158
|
try {
|
|
152
159
|
if (!apk) {
|
|
153
160
|
/**
|
|
@@ -193,12 +200,11 @@ export async function grantAllPermissions (pkg, apk) {
|
|
|
193
200
|
* This call is more performant than `grantPermission` one, since it combines
|
|
194
201
|
* multiple `adb shell` calls into a single command.
|
|
195
202
|
*
|
|
196
|
-
* @
|
|
197
|
-
* @param
|
|
198
|
-
* @param {Array<string>} permissions - The list of permissions to be granted.
|
|
203
|
+
* @param pkg - The package name to be processed.
|
|
204
|
+
* @param permissions - The list of permissions to be granted.
|
|
199
205
|
* @throws {Error} If there was an error while changing permissions.
|
|
200
206
|
*/
|
|
201
|
-
export async function grantPermissions (pkg, permissions) {
|
|
207
|
+
export async function grantPermissions (this: ADB, pkg: string, permissions: string[]): Promise<void> {
|
|
202
208
|
// As it consumes more time for granting each permission,
|
|
203
209
|
// trying to grant all permission by forming equivalent command.
|
|
204
210
|
// Also, it is necessary to split long commands into chunks, since the maximum length of
|
|
@@ -207,7 +213,7 @@ export async function grantPermissions (pkg, permissions) {
|
|
|
207
213
|
try {
|
|
208
214
|
await this.shellChunks((perm) => ['pm', 'grant', pkg, perm], permissions);
|
|
209
215
|
} catch (e) {
|
|
210
|
-
const err =
|
|
216
|
+
const err = e as ExecError;
|
|
211
217
|
if (!IGNORED_PERM_ERRORS.some((pattern) => pattern.test(err.stderr || err.message))) {
|
|
212
218
|
throw err;
|
|
213
219
|
}
|
|
@@ -217,16 +223,15 @@ export async function grantPermissions (pkg, permissions) {
|
|
|
217
223
|
/**
|
|
218
224
|
* Grant single permission for the particular package.
|
|
219
225
|
*
|
|
220
|
-
* @
|
|
221
|
-
* @param
|
|
222
|
-
* @param {string} permission - The full name of the permission to be granted.
|
|
226
|
+
* @param pkg - The package name to be processed.
|
|
227
|
+
* @param permission - The full name of the permission to be granted.
|
|
223
228
|
* @throws {Error} If there was an error while changing permissions.
|
|
224
229
|
*/
|
|
225
|
-
export async function grantPermission (pkg, permission) {
|
|
230
|
+
export async function grantPermission (this: ADB, pkg: string, permission: string): Promise<void> {
|
|
226
231
|
try {
|
|
227
232
|
await this.shell(['pm', 'grant', pkg, permission]);
|
|
228
233
|
} catch (e) {
|
|
229
|
-
const err =
|
|
234
|
+
const err = e as ExecError;
|
|
230
235
|
if (!NOT_CHANGEABLE_PERM_ERROR.test(err.stderr || err.message)) {
|
|
231
236
|
throw err;
|
|
232
237
|
}
|
|
@@ -236,16 +241,15 @@ export async function grantPermission (pkg, permission) {
|
|
|
236
241
|
/**
|
|
237
242
|
* Revoke single permission from the particular package.
|
|
238
243
|
*
|
|
239
|
-
* @
|
|
240
|
-
* @param
|
|
241
|
-
* @param {string} permission - The full name of the permission to be revoked.
|
|
244
|
+
* @param pkg - The package name to be processed.
|
|
245
|
+
* @param permission - The full name of the permission to be revoked.
|
|
242
246
|
* @throws {Error} If there was an error while changing permissions.
|
|
243
247
|
*/
|
|
244
|
-
export async function revokePermission (pkg, permission) {
|
|
248
|
+
export async function revokePermission (this: ADB, pkg: string, permission: string): Promise<void> {
|
|
245
249
|
try {
|
|
246
250
|
await this.shell(['pm', 'revoke', pkg, permission]);
|
|
247
251
|
} catch (e) {
|
|
248
|
-
const err =
|
|
252
|
+
const err = e as ExecError;
|
|
249
253
|
if (!NOT_CHANGEABLE_PERM_ERROR.test(err.stderr || err.message)) {
|
|
250
254
|
throw err;
|
|
251
255
|
}
|
|
@@ -255,14 +259,13 @@ export async function revokePermission (pkg, permission) {
|
|
|
255
259
|
/**
|
|
256
260
|
* Retrieve the list of granted permissions for the particular package.
|
|
257
261
|
*
|
|
258
|
-
* @
|
|
259
|
-
* @param
|
|
260
|
-
* @param {string?} [cmdOutput=null] - Optional parameter containing command output of
|
|
262
|
+
* @param pkg - The package name to be processed.
|
|
263
|
+
* @param cmdOutput - Optional parameter containing command output of
|
|
261
264
|
* _dumpsys package_ command. It may speed up the method execution.
|
|
262
|
-
* @
|
|
265
|
+
* @returns The list of granted permissions or an empty list.
|
|
263
266
|
* @throws {Error} If there was an error while changing permissions.
|
|
264
267
|
*/
|
|
265
|
-
export async function getGrantedPermissions (pkg, cmdOutput = null) {
|
|
268
|
+
export async function getGrantedPermissions (this: ADB, pkg: string, cmdOutput: string | null = null): Promise<string[]> {
|
|
266
269
|
log.debug('Retrieving granted permissions');
|
|
267
270
|
const stdout = cmdOutput || await this.shell(['dumpsys', 'package', pkg]);
|
|
268
271
|
return extractMatchingPermissions(stdout, ['install', 'runtime'], true);
|
|
@@ -271,13 +274,12 @@ export async function getGrantedPermissions (pkg, cmdOutput = null) {
|
|
|
271
274
|
/**
|
|
272
275
|
* Retrieve the list of denied permissions for the particular package.
|
|
273
276
|
*
|
|
274
|
-
* @
|
|
275
|
-
* @param
|
|
276
|
-
* @param {string?} [cmdOutput=null] - Optional parameter containing command output of
|
|
277
|
+
* @param pkg - The package name to be processed.
|
|
278
|
+
* @param cmdOutput - Optional parameter containing command output of
|
|
277
279
|
* _dumpsys package_ command. It may speed up the method execution.
|
|
278
|
-
* @
|
|
280
|
+
* @returns The list of denied permissions or an empty list.
|
|
279
281
|
*/
|
|
280
|
-
export async function getDeniedPermissions (pkg, cmdOutput = null) {
|
|
282
|
+
export async function getDeniedPermissions (this: ADB, pkg: string, cmdOutput: string | null = null): Promise<string[]> {
|
|
281
283
|
log.debug('Retrieving denied permissions');
|
|
282
284
|
const stdout = cmdOutput || await this.shell(['dumpsys', 'package', pkg]);
|
|
283
285
|
return extractMatchingPermissions(stdout, ['install', 'runtime'], false);
|
|
@@ -286,13 +288,12 @@ export async function getDeniedPermissions (pkg, cmdOutput = null) {
|
|
|
286
288
|
/**
|
|
287
289
|
* Retrieve the list of requested permissions for the particular package.
|
|
288
290
|
*
|
|
289
|
-
* @
|
|
290
|
-
* @param
|
|
291
|
-
* @param {string?} [cmdOutput=null] - Optional parameter containing command output of
|
|
291
|
+
* @param pkg - The package name to be processed.
|
|
292
|
+
* @param cmdOutput - Optional parameter containing command output of
|
|
292
293
|
* _dumpsys package_ command. It may speed up the method execution.
|
|
293
|
-
* @
|
|
294
|
+
* @returns The list of requested permissions or an empty list.
|
|
294
295
|
*/
|
|
295
|
-
export async function getReqPermissions (pkg, cmdOutput = null) {
|
|
296
|
+
export async function getReqPermissions (this: ADB, pkg: string, cmdOutput: string | null = null): Promise<string[]> {
|
|
296
297
|
log.debug('Retrieving requested permissions');
|
|
297
298
|
const stdout = cmdOutput || await this.shell(['dumpsys', 'package', pkg]);
|
|
298
299
|
return extractMatchingPermissions(stdout, ['requested']);
|
|
@@ -301,15 +302,14 @@ export async function getReqPermissions (pkg, cmdOutput = null) {
|
|
|
301
302
|
/**
|
|
302
303
|
* Stop the particular package if it is running and clears its application data.
|
|
303
304
|
*
|
|
304
|
-
* @
|
|
305
|
-
* @param {string} pkg - The package name to be processed.
|
|
305
|
+
* @param pkg - The package name to be processed.
|
|
306
306
|
*/
|
|
307
|
-
export async function stopAndClear (pkg) {
|
|
307
|
+
export async function stopAndClear (this: ADB, pkg: string): Promise<void> {
|
|
308
308
|
try {
|
|
309
309
|
await this.forceStop(pkg);
|
|
310
310
|
await this.clear(pkg);
|
|
311
311
|
} catch (e) {
|
|
312
|
-
const err =
|
|
312
|
+
const err = e as Error;
|
|
313
313
|
throw new Error(`Cannot stop and clear ${pkg}. Original error: ${err.message}`);
|
|
314
314
|
}
|
|
315
315
|
}
|
|
@@ -317,19 +317,19 @@ export async function stopAndClear (pkg) {
|
|
|
317
317
|
/**
|
|
318
318
|
* Get the package info from the installed application.
|
|
319
319
|
*
|
|
320
|
-
* @
|
|
321
|
-
* @
|
|
322
|
-
* @return {Promise<import('./types').AppInfo>} The parsed application information.
|
|
320
|
+
* @param pkg - The name of the installed package.
|
|
321
|
+
* @returns The parsed application information.
|
|
323
322
|
*/
|
|
324
|
-
export async function getPackageInfo (pkg) {
|
|
323
|
+
export async function getPackageInfo (this: ADB, pkg: string): Promise<AppInfo> {
|
|
325
324
|
log.debug(`Getting package info for '${pkg}'`);
|
|
326
|
-
const result = {name: pkg};
|
|
327
|
-
let stdout;
|
|
325
|
+
const result: AppInfo = {name: pkg};
|
|
326
|
+
let stdout: string;
|
|
328
327
|
try {
|
|
329
328
|
stdout = await this.shell(['dumpsys', 'package', pkg]);
|
|
330
329
|
} catch (err) {
|
|
331
|
-
|
|
332
|
-
log.
|
|
330
|
+
const error = err as Error;
|
|
331
|
+
log.debug(error.stack);
|
|
332
|
+
log.warn(`Got an unexpected error while dumping package info: ${error.message}`);
|
|
333
333
|
return result;
|
|
334
334
|
}
|
|
335
335
|
|
|
@@ -353,13 +353,12 @@ export async function getPackageInfo (pkg) {
|
|
|
353
353
|
/**
|
|
354
354
|
* Fetches base.apk of the given package to the local file system
|
|
355
355
|
*
|
|
356
|
-
* @
|
|
357
|
-
* @param
|
|
358
|
-
* @
|
|
359
|
-
* @returns {Promise<string>} Full path to the downloaded file
|
|
356
|
+
* @param pkg - The package identifier (must be already installed on the device)
|
|
357
|
+
* @param tmpDir - The destination folder path
|
|
358
|
+
* @returns Full path to the downloaded file
|
|
360
359
|
* @throws {Error} If there was an error while fetching the .apk
|
|
361
360
|
*/
|
|
362
|
-
export async function pullApk (pkg, tmpDir) {
|
|
361
|
+
export async function pullApk (this: ADB, pkg: string, tmpDir: string): Promise<string> {
|
|
363
362
|
const stdout = _.trim(await this.shell(['pm', 'path', pkg]));
|
|
364
363
|
const packageMarker = 'package:';
|
|
365
364
|
if (!_.startsWith(stdout, packageMarker)) {
|
|
@@ -378,11 +377,10 @@ export async function pullApk (pkg, tmpDir) {
|
|
|
378
377
|
* The action literally simulates
|
|
379
378
|
* clicking the corresponding application icon on the dashboard.
|
|
380
379
|
*
|
|
381
|
-
* @
|
|
382
|
-
* @param {string} appId - Application package identifier
|
|
380
|
+
* @param appId - Application package identifier
|
|
383
381
|
* @throws {Error} If the app cannot be activated
|
|
384
382
|
*/
|
|
385
|
-
export async function activateApp (appId) {
|
|
383
|
+
export async function activateApp (this: ADB, appId: string): Promise<void> {
|
|
386
384
|
log.debug(`Activating '${appId}'`);
|
|
387
385
|
const apiLevel = await this.getApiLevel();
|
|
388
386
|
// Fallback to Monkey in older APIs
|
|
@@ -398,7 +396,8 @@ export async function activateApp (appId) {
|
|
|
398
396
|
output = await this.shell(cmd);
|
|
399
397
|
log.debug(`Command stdout: ${output}`);
|
|
400
398
|
} catch (e) {
|
|
401
|
-
|
|
399
|
+
const error = e as Error;
|
|
400
|
+
throw log.errorWithException(`Cannot activate '${appId}'. Original error: ${error.message}`);
|
|
402
401
|
}
|
|
403
402
|
if (output.includes('monkey aborted')) {
|
|
404
403
|
throw log.errorWithException(`Cannot activate '${appId}'. Are you sure it is installed?`);
|
|
@@ -435,19 +434,17 @@ export async function activateApp (appId) {
|
|
|
435
434
|
/**
|
|
436
435
|
* Check whether the particular package is present on the device under test.
|
|
437
436
|
*
|
|
438
|
-
* @
|
|
439
|
-
* @param
|
|
440
|
-
* @
|
|
441
|
-
* @return {Promise<boolean>} True if the package is installed.
|
|
437
|
+
* @param pkg - The name of the package to check.
|
|
438
|
+
* @param opts - Options for checking installation
|
|
439
|
+
* @returns True if the package is installed.
|
|
442
440
|
*/
|
|
443
|
-
export async function isAppInstalled (pkg, opts = {}) {
|
|
441
|
+
export async function isAppInstalled (this: ADB, pkg: string, opts: IsAppInstalledOptions = {}): Promise<boolean> {
|
|
444
442
|
const {
|
|
445
443
|
user,
|
|
446
444
|
} = opts;
|
|
447
445
|
|
|
448
446
|
log.debug(`Getting install status for ${pkg}`);
|
|
449
|
-
|
|
450
|
-
let isInstalled;
|
|
447
|
+
let isInstalled: boolean;
|
|
451
448
|
if (await this.getApiLevel() < 26) {
|
|
452
449
|
try {
|
|
453
450
|
const cmd = ['pm', 'path'];
|
|
@@ -465,13 +462,13 @@ export async function isAppInstalled (pkg, opts = {}) {
|
|
|
465
462
|
if (util.hasValue(user)) {
|
|
466
463
|
cmd.push('--user', user);
|
|
467
464
|
}
|
|
468
|
-
|
|
469
|
-
let stdout;
|
|
465
|
+
let stdout: string;
|
|
470
466
|
try {
|
|
471
467
|
stdout = await this.shell(cmd);
|
|
472
468
|
} catch (e) {
|
|
469
|
+
const error = e as ExecError;
|
|
473
470
|
// https://github.com/appium/appium-uiautomator2-driver/issues/810
|
|
474
|
-
if (_.includes(
|
|
471
|
+
if (_.includes(error.stderr || error.stdout || error.message, 'access user') && _.isEmpty(user)) {
|
|
475
472
|
stdout = await this.shell([...cmd, '--user', '0']);
|
|
476
473
|
} else {
|
|
477
474
|
throw e;
|
|
@@ -486,12 +483,11 @@ export async function isAppInstalled (pkg, opts = {}) {
|
|
|
486
483
|
/**
|
|
487
484
|
* Start the particular URI on the device under test.
|
|
488
485
|
*
|
|
489
|
-
* @
|
|
490
|
-
* @param
|
|
491
|
-
* @param
|
|
492
|
-
* @param {import('./types').StartUriOptions} [opts={}]
|
|
486
|
+
* @param uri - The name of URI to start.
|
|
487
|
+
* @param pkg - The name of the package to start the URI with.
|
|
488
|
+
* @param opts - Options for starting the URI
|
|
493
489
|
*/
|
|
494
|
-
export async function startUri (uri, pkg = null, opts = {}) {
|
|
490
|
+
export async function startUri (this: ADB, uri: string, pkg: string | null = null, opts: StartUriOptions = {}): Promise<void> {
|
|
495
491
|
const {
|
|
496
492
|
waitForLaunch = true,
|
|
497
493
|
} = opts;
|
|
@@ -523,53 +519,51 @@ export async function startUri (uri, pkg = null, opts = {}) {
|
|
|
523
519
|
/**
|
|
524
520
|
* Start the particular package/activity on the device under test.
|
|
525
521
|
*
|
|
526
|
-
* @
|
|
527
|
-
* @
|
|
528
|
-
* @return {Promise<string>} The output of the corresponding adb command.
|
|
522
|
+
* @param startAppOptions - Startup options mapping.
|
|
523
|
+
* @returns The output of the corresponding adb command.
|
|
529
524
|
* @throws {Error} If there is an error while executing the activity
|
|
530
525
|
*/
|
|
531
|
-
export async function startApp (startAppOptions) {
|
|
526
|
+
export async function startApp (this: ADB, startAppOptions: StartAppOptions): Promise<string> {
|
|
532
527
|
if (!startAppOptions.pkg || !(startAppOptions.activity || startAppOptions.action)) {
|
|
533
528
|
throw new Error('pkg, and activity or intent action, are required to start an application');
|
|
534
529
|
}
|
|
535
530
|
|
|
536
|
-
|
|
537
|
-
if (
|
|
538
|
-
|
|
531
|
+
const options = _.clone(startAppOptions);
|
|
532
|
+
if (options.activity) {
|
|
533
|
+
options.activity = options.activity.replace('$', '\\$');
|
|
539
534
|
}
|
|
540
535
|
// initializing defaults
|
|
541
|
-
_.defaults(
|
|
542
|
-
waitPkg:
|
|
536
|
+
_.defaults(options, {
|
|
537
|
+
waitPkg: options.pkg,
|
|
543
538
|
waitForLaunch: true,
|
|
544
539
|
waitActivity: false,
|
|
545
540
|
retry: true,
|
|
546
541
|
stopApp: true
|
|
547
542
|
});
|
|
548
543
|
// preventing null waitpkg
|
|
549
|
-
|
|
544
|
+
options.waitPkg = options.waitPkg || options.pkg;
|
|
550
545
|
|
|
551
546
|
const apiLevel = await this.getApiLevel();
|
|
552
|
-
const cmd = buildStartCmd(
|
|
553
|
-
const intentName = `${
|
|
554
|
-
? ' ' +
|
|
547
|
+
const cmd = buildStartCmd(options, apiLevel);
|
|
548
|
+
const intentName = `${options.action}${options.optionalIntentArguments
|
|
549
|
+
? ' ' + options.optionalIntentArguments
|
|
555
550
|
: ''}`;
|
|
556
551
|
try {
|
|
557
|
-
const shellOpts = {};
|
|
558
|
-
if (_.isInteger(
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
shellOpts.timeout = startAppOptions.waitDuration;
|
|
552
|
+
const shellOpts: { timeout?: number } = {};
|
|
553
|
+
if (options.waitDuration !== undefined && _.isInteger(options.waitDuration)
|
|
554
|
+
&& options.waitDuration >= 0) {
|
|
555
|
+
shellOpts.timeout = options.waitDuration;
|
|
562
556
|
}
|
|
563
557
|
const stdout = await this.shell(cmd, shellOpts);
|
|
564
558
|
if (stdout.includes('Error: Activity class') && stdout.includes('does not exist')) {
|
|
565
|
-
if (
|
|
559
|
+
if (options.retry && options.activity && !options.activity.startsWith('.')) {
|
|
566
560
|
log.debug(`We tried to start an activity that doesn't exist, ` +
|
|
567
|
-
`retrying with '.${
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
return await this.startApp(
|
|
561
|
+
`retrying with '.${options.activity}' activity name`);
|
|
562
|
+
options.activity = `.${options.activity}`;
|
|
563
|
+
options.retry = false;
|
|
564
|
+
return await this.startApp(options);
|
|
571
565
|
}
|
|
572
|
-
throw new Error(`Activity name '${
|
|
566
|
+
throw new Error(`Activity name '${options.activity}' used to start the app doesn't ` +
|
|
573
567
|
`exist or cannot be launched! Make sure it exists and is a launchable activity`);
|
|
574
568
|
} else if (stdout.includes('Error: Intent does not match any activities')
|
|
575
569
|
|| stdout.includes('Error: Activity not started, unable to resolve Intent')) {
|
|
@@ -577,27 +571,28 @@ export async function startApp (startAppOptions) {
|
|
|
577
571
|
`exist or cannot be launched! Make sure it exists and is a launchable activity`);
|
|
578
572
|
} else if (stdout.includes('java.lang.SecurityException')) {
|
|
579
573
|
// if the app is disabled on a real device it will throw a security exception
|
|
580
|
-
throw new Error(`The permission to start '${
|
|
574
|
+
throw new Error(`The permission to start '${options.activity}' activity has been denied.` +
|
|
581
575
|
`Make sure the activity/package names are correct.`);
|
|
582
576
|
}
|
|
583
|
-
if (
|
|
584
|
-
await this.waitForActivity(
|
|
577
|
+
if (options.waitActivity) {
|
|
578
|
+
await this.waitForActivity(options.waitPkg, options.waitActivity, options.waitDuration);
|
|
585
579
|
}
|
|
586
580
|
return stdout;
|
|
587
581
|
} catch (e) {
|
|
588
|
-
const
|
|
582
|
+
const error = e as Error;
|
|
583
|
+
const appDescriptor = options.pkg || intentName;
|
|
589
584
|
throw new Error(`Cannot start the '${appDescriptor}' application. ` +
|
|
590
585
|
`Consider checking the driver's troubleshooting documentation. ` +
|
|
591
|
-
`Original error: ${
|
|
586
|
+
`Original error: ${error.message}`);
|
|
592
587
|
}
|
|
593
588
|
}
|
|
594
589
|
|
|
595
590
|
/**
|
|
596
591
|
* Helper method to call `adb dumpsys window windows/displays`
|
|
597
|
-
*
|
|
598
|
-
* @returns
|
|
592
|
+
*
|
|
593
|
+
* @returns The output of the dumpsys command
|
|
599
594
|
*/
|
|
600
|
-
export async function dumpWindows () {
|
|
595
|
+
export async function dumpWindows (this: ADB): Promise<string> {
|
|
601
596
|
const apiLevel = await this.getApiLevel();
|
|
602
597
|
|
|
603
598
|
// With version 29, Android changed the dumpsys syntax
|
|
@@ -610,18 +605,18 @@ export async function dumpWindows () {
|
|
|
610
605
|
/**
|
|
611
606
|
* Get the name of currently focused package and activity.
|
|
612
607
|
*
|
|
613
|
-
* @
|
|
614
|
-
* @return {Promise<import('./types').PackageActivityInfo>}
|
|
608
|
+
* @returns The focused package and activity information
|
|
615
609
|
* @throws {Error} If there is an error while parsing the data.
|
|
616
610
|
*/
|
|
617
|
-
export async function getFocusedPackageAndActivity () {
|
|
611
|
+
export async function getFocusedPackageAndActivity (this: ADB): Promise<PackageActivityInfo> {
|
|
618
612
|
log.debug('Getting focused package and activity');
|
|
619
|
-
let stdout;
|
|
613
|
+
let stdout: string;
|
|
620
614
|
try {
|
|
621
615
|
stdout = await this.dumpWindows();
|
|
622
616
|
} catch (e) {
|
|
617
|
+
const error = e as Error;
|
|
623
618
|
throw new Error(
|
|
624
|
-
`Could not retrieve the currently focused package and activity. Original error: ${
|
|
619
|
+
`Could not retrieve the currently focused package and activity. Original error: ${error.message}`
|
|
625
620
|
);
|
|
626
621
|
}
|
|
627
622
|
|
|
@@ -634,17 +629,14 @@ export async function getFocusedPackageAndActivity () {
|
|
|
634
629
|
const nullCurrentFocusRe = /^\s*mCurrentFocus=null/m;
|
|
635
630
|
const currentFocusAppRe = new RegExp('^\\s*mCurrentFocus.+\\{.+\\s([^\\s\\/]+)\\/([^\\s]+)\\b', 'mg');
|
|
636
631
|
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
const currentFocusAppCandidates = [];
|
|
641
|
-
/** @type {[import('./types').PackageActivityInfo[], RegExp][]} */
|
|
642
|
-
const pairs = [
|
|
632
|
+
const focusedAppCandidates: PackageActivityInfo[] = [];
|
|
633
|
+
const currentFocusAppCandidates: PackageActivityInfo[] = [];
|
|
634
|
+
const pairs: [PackageActivityInfo[], RegExp][] = [
|
|
643
635
|
[focusedAppCandidates, focusedAppRe],
|
|
644
636
|
[currentFocusAppCandidates, currentFocusAppRe]
|
|
645
637
|
];
|
|
646
638
|
for (const [candidates, pattern] of pairs) {
|
|
647
|
-
let match;
|
|
639
|
+
let match: RegExpExecArray | null;
|
|
648
640
|
while ((match = pattern.exec(stdout))) {
|
|
649
641
|
candidates.push({
|
|
650
642
|
appPackage: match[1].trim(),
|
|
@@ -685,28 +677,26 @@ export async function getFocusedPackageAndActivity () {
|
|
|
685
677
|
/**
|
|
686
678
|
* Wait for the given activity to be focused/non-focused.
|
|
687
679
|
*
|
|
688
|
-
* @
|
|
689
|
-
* @param
|
|
690
|
-
* @param {string} activity - The name of the activity, belonging to that package,
|
|
680
|
+
* @param pkg - The name of the package to wait for.
|
|
681
|
+
* @param activity - The name of the activity, belonging to that package,
|
|
691
682
|
* to wait for.
|
|
692
|
-
* @param
|
|
683
|
+
* @param waitForStop - Whether to wait until the activity is focused (true)
|
|
693
684
|
* or is not focused (false).
|
|
694
|
-
* @param
|
|
695
|
-
* @throws {
|
|
685
|
+
* @param waitMs - Number of milliseconds to wait before timeout occurs.
|
|
686
|
+
* @throws {Error} If timeout happens.
|
|
696
687
|
*/
|
|
697
|
-
export async function waitForActivityOrNot (pkg, activity, waitForStop, waitMs = 20000) {
|
|
688
|
+
export async function waitForActivityOrNot (this: ADB, pkg: string, activity: string, waitForStop: boolean, waitMs: number = 20000): Promise<void> {
|
|
698
689
|
if (!pkg || !activity) {
|
|
699
690
|
throw new Error('Package and activity required.');
|
|
700
691
|
}
|
|
701
692
|
|
|
702
|
-
const splitNames = (
|
|
693
|
+
const splitNames = (names: string) => names.split(',').map(_.trim);
|
|
703
694
|
const allPackages = splitNames(pkg);
|
|
704
695
|
const allActivities = splitNames(activity);
|
|
705
696
|
|
|
706
|
-
const toFullyQualifiedActivityName = (
|
|
697
|
+
const toFullyQualifiedActivityName = (prefix: string, suffix: string) =>
|
|
707
698
|
`${prefix}${suffix}`.replace(/\/\.?/g, '.').replace(/\.{2,}/g, '.');
|
|
708
|
-
|
|
709
|
-
const possibleActivityNamesSet = new Set();
|
|
699
|
+
const possibleActivityNamesSet = new Set<string>();
|
|
710
700
|
for (const oneActivity of allActivities) {
|
|
711
701
|
if (oneActivity.startsWith('.')) {
|
|
712
702
|
// add the package name if activity is not full qualified
|
|
@@ -741,12 +731,13 @@ export async function waitForActivityOrNot (pkg, activity, waitForStop, waitMs =
|
|
|
741
731
|
);
|
|
742
732
|
|
|
743
733
|
const conditionFunc = async () => {
|
|
744
|
-
let appPackage;
|
|
745
|
-
let appActivity;
|
|
734
|
+
let appPackage: string | null | undefined;
|
|
735
|
+
let appActivity: string | null | undefined;
|
|
746
736
|
try {
|
|
747
737
|
({appPackage, appActivity} = await this.getFocusedPackageAndActivity());
|
|
748
738
|
} catch (e) {
|
|
749
|
-
|
|
739
|
+
const error = e as Error;
|
|
740
|
+
log.debug(error.message);
|
|
750
741
|
return false;
|
|
751
742
|
}
|
|
752
743
|
if (appActivity && appPackage) {
|
|
@@ -785,42 +776,38 @@ export async function waitForActivityOrNot (pkg, activity, waitForStop, waitMs =
|
|
|
785
776
|
/**
|
|
786
777
|
* Wait for the given activity to be focused
|
|
787
778
|
*
|
|
788
|
-
* @
|
|
789
|
-
* @param
|
|
790
|
-
* @param {string} act - The name of the activity, belonging to that package,
|
|
779
|
+
* @param pkg - The name of the package to wait for.
|
|
780
|
+
* @param act - The name of the activity, belonging to that package,
|
|
791
781
|
* to wait for.
|
|
792
|
-
* @param
|
|
793
|
-
* @throws {
|
|
782
|
+
* @param waitMs - Number of milliseconds to wait before timeout occurs.
|
|
783
|
+
* @throws {Error} If timeout happens.
|
|
794
784
|
*/
|
|
795
|
-
export async function waitForActivity (pkg, act, waitMs = 20000) {
|
|
785
|
+
export async function waitForActivity (this: ADB, pkg: string, act: string, waitMs: number = 20000): Promise<void> {
|
|
796
786
|
await this.waitForActivityOrNot(pkg, act, false, waitMs);
|
|
797
787
|
}
|
|
798
788
|
|
|
799
789
|
/**
|
|
800
790
|
* Wait for the given activity to be non-focused.
|
|
801
791
|
*
|
|
802
|
-
* @
|
|
803
|
-
* @param
|
|
804
|
-
* @param {string} act - The name of the activity, belonging to that package,
|
|
792
|
+
* @param pkg - The name of the package to wait for.
|
|
793
|
+
* @param act - The name of the activity, belonging to that package,
|
|
805
794
|
* to wait for.
|
|
806
|
-
* @param
|
|
807
|
-
* @throws {
|
|
795
|
+
* @param waitMs - Number of milliseconds to wait before timeout occurs.
|
|
796
|
+
* @throws {Error} If timeout happens.
|
|
808
797
|
*/
|
|
809
|
-
export async function waitForNotActivity (pkg, act, waitMs = 20000) {
|
|
798
|
+
export async function waitForNotActivity (this: ADB, pkg: string, act: string, waitMs: number = 20000): Promise<void> {
|
|
810
799
|
await this.waitForActivityOrNot(pkg, act, true, waitMs);
|
|
811
800
|
}
|
|
812
801
|
|
|
813
|
-
// #region Private functions
|
|
814
|
-
|
|
815
802
|
/**
|
|
816
803
|
* Builds command line representation for the given
|
|
817
804
|
* application startup options
|
|
818
805
|
*
|
|
819
|
-
* @param
|
|
820
|
-
* @param
|
|
821
|
-
* @returns
|
|
806
|
+
* @param startAppOptions - Application options mapping
|
|
807
|
+
* @param apiLevel - The actual OS API level
|
|
808
|
+
* @returns The actual command line array
|
|
822
809
|
*/
|
|
823
|
-
export function buildStartCmd (startAppOptions, apiLevel) {
|
|
810
|
+
export function buildStartCmd (startAppOptions: StartCmdOptions, apiLevel: number): string[] {
|
|
824
811
|
const {
|
|
825
812
|
user,
|
|
826
813
|
waitForLaunch,
|
|
@@ -860,70 +847,12 @@ export function buildStartCmd (startAppOptions, apiLevel) {
|
|
|
860
847
|
return cmd;
|
|
861
848
|
}
|
|
862
849
|
|
|
863
|
-
/**
|
|
864
|
-
*
|
|
865
|
-
* @param {string} value expect optionalIntentArguments to be a single string of the form:
|
|
866
|
-
* "-flag key"
|
|
867
|
-
* "-flag key value"
|
|
868
|
-
* or a combination of these (e.g., "-flag1 key1 -flag2 key2 value2")
|
|
869
|
-
* @returns {string[]}
|
|
870
|
-
*/
|
|
871
|
-
function parseOptionalIntentArguments(value) {
|
|
872
|
-
// take a string and parse out the part before any spaces, and anything after
|
|
873
|
-
// the first space
|
|
874
|
-
/** @type {(str: string) => string[]} */
|
|
875
|
-
const parseKeyValue = (str) => {
|
|
876
|
-
str = str.trim();
|
|
877
|
-
const spacePos = str.indexOf(' ');
|
|
878
|
-
if (spacePos < 0) {
|
|
879
|
-
return str.length ? [str] : [];
|
|
880
|
-
} else {
|
|
881
|
-
return [str.substring(0, spacePos).trim(), str.substring(spacePos + 1).trim()];
|
|
882
|
-
}
|
|
883
|
-
};
|
|
884
|
-
|
|
885
|
-
// cycle through the optionalIntentArguments and pull out the arguments
|
|
886
|
-
// add a space initially so flags can be distinguished from arguments that
|
|
887
|
-
// have internal hyphens
|
|
888
|
-
let optionalIntentArguments = ` ${value}`;
|
|
889
|
-
const re = / (-[^\s]+) (.+)/;
|
|
890
|
-
/** @type {string[]} */
|
|
891
|
-
const result = [];
|
|
892
|
-
while (true) {
|
|
893
|
-
const args = re.exec(optionalIntentArguments);
|
|
894
|
-
if (!args) {
|
|
895
|
-
if (optionalIntentArguments.length) {
|
|
896
|
-
// no more flags, so the remainder can be treated as 'key' or 'key value'
|
|
897
|
-
result.push(...parseKeyValue(optionalIntentArguments));
|
|
898
|
-
}
|
|
899
|
-
// we are done
|
|
900
|
-
return result;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// take the flag and see if it is at the beginning of the string
|
|
904
|
-
// if it is not, then it means we have been through already, and
|
|
905
|
-
// what is before the flag is the argument for the previous flag
|
|
906
|
-
const flag = args[1];
|
|
907
|
-
const flagPos = optionalIntentArguments.indexOf(flag);
|
|
908
|
-
if (flagPos !== 0) {
|
|
909
|
-
const prevArgs = optionalIntentArguments.substring(0, flagPos);
|
|
910
|
-
result.push(...parseKeyValue(prevArgs));
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// add the flag, as there are no more earlier arguments
|
|
914
|
-
result.push(flag);
|
|
915
|
-
|
|
916
|
-
// make optionalIntentArguments hold the remainder
|
|
917
|
-
optionalIntentArguments = args[2];
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
850
|
/**
|
|
922
851
|
* Parses the name of launchable package activity
|
|
923
852
|
* from dumpsys output.
|
|
924
853
|
*
|
|
925
|
-
* @param
|
|
926
|
-
* @returns
|
|
854
|
+
* @param dumpsys - The actual dumpsys output
|
|
855
|
+
* @returns Either the fully qualified
|
|
927
856
|
* activity name as a single list item or an empty list if nothing could be parsed.
|
|
928
857
|
* In Android 6 and older there is no reliable way to determine
|
|
929
858
|
* the category name for the given activity, so this API just
|
|
@@ -931,12 +860,12 @@ function parseOptionalIntentArguments(value) {
|
|
|
931
860
|
* with the expectation that the app manifest could be parsed next
|
|
932
861
|
* in order to determine category names for these.
|
|
933
862
|
*/
|
|
934
|
-
export function parseLaunchableActivityNames (dumpsys) {
|
|
863
|
+
export function parseLaunchableActivityNames (dumpsys: string): string[] {
|
|
935
864
|
const mainActivityNameRe = new RegExp(`^\\s*${_.escapeRegExp(MAIN_ACTION)}:$`);
|
|
936
865
|
const categoryNameRe = /^\s*Category:\s+"([a-zA-Z0-9._/-]+)"$/;
|
|
937
|
-
const blocks = [];
|
|
938
|
-
let blockStartIndent;
|
|
939
|
-
let block = [];
|
|
866
|
+
const blocks: string[][] = [];
|
|
867
|
+
let blockStartIndent: number | null | undefined;
|
|
868
|
+
let block: string[] = [];
|
|
940
869
|
for (const line of dumpsys.split('\n').map(_.trimEnd)) {
|
|
941
870
|
const currentIndent = line.length - _.trimStart(line).length;
|
|
942
871
|
if (mainActivityNameRe.test(line)) {
|
|
@@ -965,7 +894,7 @@ export function parseLaunchableActivityNames (dumpsys) {
|
|
|
965
894
|
blocks.push(block);
|
|
966
895
|
}
|
|
967
896
|
|
|
968
|
-
const result = [];
|
|
897
|
+
const result: string[] = [];
|
|
969
898
|
for (const item of blocks) {
|
|
970
899
|
let hasCategory = false;
|
|
971
900
|
let isLauncherCategory = false;
|
|
@@ -1004,48 +933,30 @@ export function parseLaunchableActivityNames (dumpsys) {
|
|
|
1004
933
|
/**
|
|
1005
934
|
* Check if the given string is a valid component name
|
|
1006
935
|
*
|
|
1007
|
-
* @param
|
|
1008
|
-
* @
|
|
936
|
+
* @param classString - The string to verify
|
|
937
|
+
* @returns The result of Regexp.exec operation
|
|
1009
938
|
* or _null_ if no matches are found
|
|
1010
939
|
*/
|
|
1011
|
-
export function matchComponentName (classString) {
|
|
940
|
+
export function matchComponentName (classString: string): RegExpExecArray | null {
|
|
1012
941
|
// some.package/some.package.Activity
|
|
1013
942
|
return /^[\p{L}0-9./_]+$/u.exec(classString);
|
|
1014
943
|
}
|
|
1015
944
|
|
|
1016
|
-
/**
|
|
1017
|
-
* Escapes special characters in command line arguments.
|
|
1018
|
-
* This is needed to avoid possible issues with how system `spawn`
|
|
1019
|
-
* call handles them.
|
|
1020
|
-
* See https://discuss.appium.io/t/how-to-modify-wd-proxy-and-uiautomator2-source-code-to-support-unicode/33466
|
|
1021
|
-
* for more details.
|
|
1022
|
-
*
|
|
1023
|
-
* @param {string} arg Non-escaped argument string
|
|
1024
|
-
* @returns The escaped argument
|
|
1025
|
-
*/
|
|
1026
|
-
function escapeShellArg (arg) {
|
|
1027
|
-
arg = `${arg}`;
|
|
1028
|
-
if (system.isWindows()) {
|
|
1029
|
-
return /[&|^\s]/.test(arg) ? `"${arg.replace(/"/g, '""')}"` : arg;
|
|
1030
|
-
}
|
|
1031
|
-
return arg.replace(/&/g, '\\&');
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
945
|
/**
|
|
1035
946
|
* Retrieves the list of permission names encoded in `dumpsys package` command output.
|
|
1036
947
|
*
|
|
1037
|
-
* @param
|
|
1038
|
-
* @param
|
|
1039
|
-
* @param
|
|
948
|
+
* @param dumpsysOutput - The actual command output.
|
|
949
|
+
* @param groupNames - The list of group names to list permissions for.
|
|
950
|
+
* @param grantedState - The expected state of `granted` attribute to filter with.
|
|
1040
951
|
* No filtering is done if the parameter is not set.
|
|
1041
|
-
* @returns
|
|
952
|
+
* @returns The list of matched permission names or an empty list if no matches were found.
|
|
1042
953
|
*/
|
|
1043
|
-
export function extractMatchingPermissions (dumpsysOutput, groupNames, grantedState = null) {
|
|
1044
|
-
const groupPatternByName = (groupName) => new RegExp(`^(\\s*${_.escapeRegExp(groupName)} permissions:[\\s\\S]+)`, 'm');
|
|
954
|
+
export function extractMatchingPermissions (dumpsysOutput: string, groupNames: string[], grantedState: boolean | null = null): string[] {
|
|
955
|
+
const groupPatternByName = (groupName: string) => new RegExp(`^(\\s*${_.escapeRegExp(groupName)} permissions:[\\s\\S]+)`, 'm');
|
|
1045
956
|
const indentPattern = /\S|$/;
|
|
1046
957
|
const permissionNamePattern = /android\.\w*\.?permission\.\w+/;
|
|
1047
958
|
const grantedStatePattern = /\bgranted=(\w+)/;
|
|
1048
|
-
const result = [];
|
|
959
|
+
const result: Array<{ permission: string; granted?: boolean }> = [];
|
|
1049
960
|
for (const groupName of groupNames) {
|
|
1050
961
|
const groupMatch = groupPatternByName(groupName).exec(dumpsysOutput);
|
|
1051
962
|
if (!groupMatch) {
|
|
@@ -1068,7 +979,7 @@ export function extractMatchingPermissions (dumpsysOutput, groupNames, grantedSt
|
|
|
1068
979
|
if (!permissionNameMatch) {
|
|
1069
980
|
continue;
|
|
1070
981
|
}
|
|
1071
|
-
const item = {
|
|
982
|
+
const item: { permission: string; granted?: boolean } = {
|
|
1072
983
|
permission: permissionNameMatch[0],
|
|
1073
984
|
};
|
|
1074
985
|
const grantedStateMatch = grantedStatePattern.exec(line);
|
|
@@ -1090,11 +1001,10 @@ export function extractMatchingPermissions (dumpsysOutput, groupNames, grantedSt
|
|
|
1090
1001
|
/**
|
|
1091
1002
|
* Broadcast a message to the given intent.
|
|
1092
1003
|
*
|
|
1093
|
-
* @
|
|
1094
|
-
* @
|
|
1095
|
-
* @throws {error} If intent name is not a valid class name.
|
|
1004
|
+
* @param intent - The name of the intent to broadcast to.
|
|
1005
|
+
* @throws {Error} If intent name is not a valid class name.
|
|
1096
1006
|
*/
|
|
1097
|
-
export async function broadcast (intent) {
|
|
1007
|
+
export async function broadcast (this: ADB, intent: string): Promise<void> {
|
|
1098
1008
|
if (!this.isValidClass(intent)) {
|
|
1099
1009
|
throw new Error(`Invalid intent ${intent}`);
|
|
1100
1010
|
}
|
|
@@ -1105,11 +1015,10 @@ export async function broadcast (intent) {
|
|
|
1105
1015
|
/**
|
|
1106
1016
|
* Get the list of process ids for the particular package on the device under test.
|
|
1107
1017
|
*
|
|
1108
|
-
* @
|
|
1109
|
-
* @
|
|
1110
|
-
* @returns {Promise<number[]>} The list of matched process IDs or an empty list.
|
|
1018
|
+
* @param pkg - The package name
|
|
1019
|
+
* @returns The list of matched process IDs or an empty list.
|
|
1111
1020
|
*/
|
|
1112
|
-
export async function listAppProcessIds (pkg) {
|
|
1021
|
+
export async function listAppProcessIds (this: ADB, pkg: string): Promise<number[]> {
|
|
1113
1022
|
log.debug(`Getting IDs of all '${pkg}' package`);
|
|
1114
1023
|
const pidRegex = new RegExp(`ProcessRecord\\{[\\w]+\\s+(\\d+):${_.escapeRegExp(pkg)}\\/`);
|
|
1115
1024
|
const processesInfo = await this.shell(['dumpsys', 'activity', 'processes']);
|
|
@@ -1124,25 +1033,101 @@ export async function listAppProcessIds (pkg) {
|
|
|
1124
1033
|
* Check whether the process with the particular name is running on the device
|
|
1125
1034
|
* under test.
|
|
1126
1035
|
*
|
|
1127
|
-
* @
|
|
1128
|
-
* @param {string} pkg - The id of the package to be checked.
|
|
1036
|
+
* @param pkg - The id of the package to be checked.
|
|
1129
1037
|
* @returns True if the given package is running.
|
|
1130
1038
|
*/
|
|
1131
|
-
export async function isAppRunning (pkg) {
|
|
1039
|
+
export async function isAppRunning (this: ADB, pkg: string): Promise<boolean> {
|
|
1132
1040
|
return !_.isEmpty(await this.listAppProcessIds(pkg));
|
|
1133
1041
|
}
|
|
1134
1042
|
|
|
1043
|
+
// Private methods
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Parses optional intent arguments from a string.
|
|
1047
|
+
*
|
|
1048
|
+
* @param value - Expect optionalIntentArguments to be a single string of the form:
|
|
1049
|
+
* "-flag key"
|
|
1050
|
+
* "-flag key value"
|
|
1051
|
+
* or a combination of these (e.g., "-flag1 key1 -flag2 key2 value2")
|
|
1052
|
+
* @returns Parsed arguments array
|
|
1053
|
+
*/
|
|
1054
|
+
function parseOptionalIntentArguments(value: string): string[] {
|
|
1055
|
+
// take a string and parse out the part before any spaces, and anything after
|
|
1056
|
+
// the first space
|
|
1057
|
+
const parseKeyValue = (str: string): string[] => {
|
|
1058
|
+
str = str.trim();
|
|
1059
|
+
const spacePos = str.indexOf(' ');
|
|
1060
|
+
if (spacePos < 0) {
|
|
1061
|
+
return str.length ? [str] : [];
|
|
1062
|
+
} else {
|
|
1063
|
+
return [str.substring(0, spacePos).trim(), str.substring(spacePos + 1).trim()];
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
// cycle through the optionalIntentArguments and pull out the arguments
|
|
1068
|
+
// add a space initially so flags can be distinguished from arguments that
|
|
1069
|
+
// have internal hyphens
|
|
1070
|
+
let optionalIntentArguments = ` ${value}`;
|
|
1071
|
+
const re = / (-[^\s]+) (.+)/;
|
|
1072
|
+
const result: string[] = [];
|
|
1073
|
+
while (true) {
|
|
1074
|
+
const args = re.exec(optionalIntentArguments);
|
|
1075
|
+
if (!args) {
|
|
1076
|
+
if (optionalIntentArguments.length) {
|
|
1077
|
+
// no more flags, so the remainder can be treated as 'key' or 'key value'
|
|
1078
|
+
result.push(...parseKeyValue(optionalIntentArguments));
|
|
1079
|
+
}
|
|
1080
|
+
// we are done
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// take the flag and see if it is at the beginning of the string
|
|
1085
|
+
// if it is not, then it means we have been through already, and
|
|
1086
|
+
// what is before the flag is the argument for the previous flag
|
|
1087
|
+
const flag = args[1];
|
|
1088
|
+
const flagPos = optionalIntentArguments.indexOf(flag);
|
|
1089
|
+
if (flagPos !== 0) {
|
|
1090
|
+
const prevArgs = optionalIntentArguments.substring(0, flagPos);
|
|
1091
|
+
result.push(...parseKeyValue(prevArgs));
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// add the flag, as there are no more earlier arguments
|
|
1095
|
+
result.push(flag);
|
|
1096
|
+
|
|
1097
|
+
// make optionalIntentArguments hold the remainder
|
|
1098
|
+
optionalIntentArguments = args[2];
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1135
1102
|
/**
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
* @
|
|
1143
|
-
* @
|
|
1144
|
-
* @property {string} [flags]
|
|
1145
|
-
* @property {string} [optionalIntentArguments]
|
|
1103
|
+
* Escapes special characters in command line arguments.
|
|
1104
|
+
* This is needed to avoid possible issues with how system `spawn`
|
|
1105
|
+
* call handles them.
|
|
1106
|
+
* See https://discuss.appium.io/t/how-to-modify-wd-proxy-and-uiautomator2-source-code-to-support-unicode/33466
|
|
1107
|
+
* for more details.
|
|
1108
|
+
*
|
|
1109
|
+
* @param arg - Non-escaped argument string
|
|
1110
|
+
* @returns The escaped argument
|
|
1146
1111
|
*/
|
|
1112
|
+
function escapeShellArg (arg: string): string {
|
|
1113
|
+
arg = `${arg}`;
|
|
1114
|
+
if (system.isWindows()) {
|
|
1115
|
+
return /[&|^\s]/.test(arg) ? `"${arg.replace(/"/g, '""')}"` : arg;
|
|
1116
|
+
}
|
|
1117
|
+
return arg.replace(/&/g, '\\&');
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Type definitions
|
|
1121
|
+
|
|
1122
|
+
export interface StartCmdOptions {
|
|
1123
|
+
user?: number | string;
|
|
1124
|
+
waitForLaunch?: boolean;
|
|
1125
|
+
pkg?: string;
|
|
1126
|
+
activity?: string;
|
|
1127
|
+
action?: string;
|
|
1128
|
+
category?: string;
|
|
1129
|
+
stopApp?: boolean;
|
|
1130
|
+
flags?: string;
|
|
1131
|
+
optionalIntentArguments?: string;
|
|
1132
|
+
}
|
|
1147
1133
|
|
|
1148
|
-
// #endregion
|