@react-native-harness/platform-android 1.1.0 → 1.2.0-rc.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.
@@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events';
2
2
  import { PassThrough } from 'node:stream';
3
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
4
4
  import { SubprocessError } from '@react-native-harness/tools';
5
- import { createAvd, deleteAvd, emulatorProcess, getAppUid, getLogcatTimestamp, getStartAppArgs, hasAvd, installApp, startEmulator, waitForBoot, waitForEmulatorDisconnect, } from '../adb.js';
5
+ import { createAvd, deleteAvd, emulatorProcess, getAppUid, getLogcatTimestamp, getStartAppArgs, hasAvd, installApp, startEmulator, uninstallApp, waitForBoot, waitForEmulatorDisconnect, } from '../adb.js';
6
6
  import * as tools from '@react-native-harness/tools';
7
7
  import * as environment from '../environment.js';
8
8
  import * as avdConfig from '../avd-config.js';
@@ -101,6 +101,18 @@ describe('getStartAppArgs', () => {
101
101
  '/tmp/app.apk',
102
102
  ]);
103
103
  });
104
+ it('uninstalls the app via adb', async () => {
105
+ const spawnSpy = vi
106
+ .spyOn(tools, 'spawn')
107
+ .mockResolvedValueOnce({});
108
+ await uninstallApp('emulator-5554', 'com.example.app');
109
+ expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
110
+ '-s',
111
+ 'emulator-5554',
112
+ 'uninstall',
113
+ 'com.example.app',
114
+ ]);
115
+ });
104
116
  it('creates an AVD and appends config overrides', async () => {
105
117
  vi.spyOn(avdConfig, 'readAvdConfig').mockResolvedValue({});
106
118
  const spawnSpy = vi
@@ -414,4 +426,84 @@ describe('getStartAppArgs', () => {
414
426
  await expect(waitPromise).resolves.toBeUndefined();
415
427
  expect(spawnSpy).toHaveBeenCalledTimes(2);
416
428
  });
429
+ it('grants requested dangerous permissions to an app', async () => {
430
+ const { grantPermissions } = await import('../adb.js');
431
+ const spawnSpy = vi
432
+ .spyOn(tools, 'spawn')
433
+ .mockResolvedValueOnce({
434
+ stdout: 'package:com.example.app\n',
435
+ })
436
+ .mockResolvedValueOnce({
437
+ stdout: `requested permissions:\n android.permission.CAMERA\n android.permission.INTERNET\n android.permission.ACCESS_FINE_LOCATION\ninstall permissions:\n`,
438
+ })
439
+ .mockResolvedValueOnce({
440
+ stdout: `Dangerous Permissions:\n permission:android.permission.CAMERA\n permission:android.permission.ACCESS_FINE_LOCATION\n`,
441
+ })
442
+ .mockResolvedValue({
443
+ stdout: '',
444
+ });
445
+ await grantPermissions('emulator-5554', 'com.example.app');
446
+ expect(spawnSpy).toHaveBeenCalledTimes(5);
447
+ expect(spawnSpy).toHaveBeenNthCalledWith(1, expect.any(String), [
448
+ '-s',
449
+ 'emulator-5554',
450
+ 'shell',
451
+ 'pm',
452
+ 'list',
453
+ 'packages',
454
+ 'com.example.app',
455
+ ]);
456
+ expect(spawnSpy).toHaveBeenNthCalledWith(2, expect.any(String), [
457
+ '-s',
458
+ 'emulator-5554',
459
+ 'shell',
460
+ 'dumpsys',
461
+ 'package',
462
+ 'com.example.app',
463
+ ]);
464
+ expect(spawnSpy).toHaveBeenNthCalledWith(3, expect.any(String), [
465
+ '-s',
466
+ 'emulator-5554',
467
+ 'shell',
468
+ 'pm',
469
+ 'list',
470
+ 'permissions',
471
+ '-g',
472
+ '-d',
473
+ ]);
474
+ expect(spawnSpy).toHaveBeenNthCalledWith(4, expect.any(String), [
475
+ '-s',
476
+ 'emulator-5554',
477
+ 'shell',
478
+ 'pm',
479
+ 'grant',
480
+ 'com.example.app',
481
+ 'android.permission.CAMERA',
482
+ ]);
483
+ expect(spawnSpy).toHaveBeenNthCalledWith(5, expect.any(String), [
484
+ '-s',
485
+ 'emulator-5554',
486
+ 'shell',
487
+ 'pm',
488
+ 'grant',
489
+ 'com.example.app',
490
+ 'android.permission.ACCESS_FINE_LOCATION',
491
+ ]);
492
+ });
493
+ it('does nothing when the app has no grantable dangerous permissions', async () => {
494
+ const { grantPermissions } = await import('../adb.js');
495
+ const spawnSpy = vi
496
+ .spyOn(tools, 'spawn')
497
+ .mockResolvedValueOnce({
498
+ stdout: 'package:com.example.app\n',
499
+ })
500
+ .mockResolvedValueOnce({
501
+ stdout: `requested permissions:\n android.permission.INTERNET\ninstall permissions:\n`,
502
+ })
503
+ .mockResolvedValueOnce({
504
+ stdout: `Dangerous Permissions:\n permission:android.permission.CAMERA\n`,
505
+ });
506
+ await grantPermissions('emulator-5554', 'com.example.app');
507
+ expect(spawnSpy).toHaveBeenCalledTimes(3);
508
+ });
417
509
  });
@@ -7,6 +7,7 @@ describe('emulator startup modes', () => {
7
7
  '-no-snapshot-load',
8
8
  '-no-snapshot-save',
9
9
  ]));
10
+ expect(getEmulatorStartupArgs('Pixel_8_API_35', 'default-boot')).not.toEqual(expect.arrayContaining(['-camera-back', 'none']));
10
11
  });
11
12
  it('builds clean snapshot generation args', () => {
12
13
  expect(getEmulatorStartupArgs('Pixel_8_API_35', 'clean-snapshot-generation')).toEqual(expect.arrayContaining(['@Pixel_8_API_35', '-no-snapshot-load']));
@@ -293,6 +293,42 @@ describe('Android platform instance', () => {
293
293
  expect(installApp).toHaveBeenCalledWith('emulator-5554', appPath);
294
294
  fs.rmSync(appPath, { force: true });
295
295
  });
296
+ it('reinstalls the app from HARNESS_APP_PATH when already installed', async () => {
297
+ const appPath = path.join(os.tmpdir(), 'HarnessPlayground-installed.apk');
298
+ fs.writeFileSync(appPath, 'apk');
299
+ vi.stubEnv('HARNESS_APP_PATH', appPath);
300
+ vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
301
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
302
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
303
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
304
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
305
+ const uninstallApp = vi
306
+ .spyOn(adb, 'uninstallApp')
307
+ .mockResolvedValue(undefined);
308
+ const installApp = vi.spyOn(adb, 'installApp').mockResolvedValue(undefined);
309
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
310
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
311
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
312
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
313
+ await expect(getAndroidEmulatorPlatformInstance({
314
+ name: 'android',
315
+ device: {
316
+ type: 'emulator',
317
+ name: 'Pixel_8_API_35',
318
+ avd: {
319
+ apiLevel: 35,
320
+ profile: 'pixel_8',
321
+ diskSize: '1G',
322
+ heapSize: '1G',
323
+ },
324
+ },
325
+ bundleId: 'com.harnessplayground',
326
+ activityName: '.MainActivity',
327
+ }, harnessConfig, init)).resolves.toBeDefined();
328
+ expect(uninstallApp).toHaveBeenCalledWith('emulator-5554', 'com.harnessplayground');
329
+ expect(installApp).toHaveBeenCalledWith('emulator-5554', appPath);
330
+ fs.rmSync(appPath, { force: true });
331
+ });
296
332
  it('throws a HarnessAppPathError when HARNESS_APP_PATH is missing', async () => {
297
333
  vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
298
334
  vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
@@ -420,4 +456,98 @@ describe('Android platform instance', () => {
420
456
  expect(appMonitor.addListener(listener)).toBeUndefined();
421
457
  expect(appMonitor.removeListener(listener)).toBeUndefined();
422
458
  });
459
+ it('grants permissions when permissions are enabled for emulator', async () => {
460
+ vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
461
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
462
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
463
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
464
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
465
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
466
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
467
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
468
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
469
+ const grantPermissions = vi
470
+ .spyOn(adb, 'grantPermissions')
471
+ .mockResolvedValue(undefined);
472
+ const harnessConfigWithPermissions = {
473
+ ...harnessConfig,
474
+ permissions: true,
475
+ };
476
+ await getAndroidEmulatorPlatformInstance({
477
+ name: 'android',
478
+ device: {
479
+ type: 'emulator',
480
+ name: 'Pixel_8_API_35',
481
+ avd: {
482
+ apiLevel: 35,
483
+ profile: 'pixel_8',
484
+ diskSize: '1G',
485
+ heapSize: '1G',
486
+ },
487
+ },
488
+ bundleId: 'com.harnessplayground',
489
+ activityName: '.MainActivity',
490
+ }, harnessConfigWithPermissions, init);
491
+ expect(grantPermissions).toHaveBeenCalledWith('emulator-5554', 'com.harnessplayground');
492
+ });
493
+ it('does not grant permissions when permissions are disabled for emulator', async () => {
494
+ vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
495
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
496
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
497
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
498
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
499
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
500
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
501
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
502
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
503
+ const grantPermissions = vi
504
+ .spyOn(adb, 'grantPermissions')
505
+ .mockResolvedValue(undefined);
506
+ await getAndroidEmulatorPlatformInstance({
507
+ name: 'android',
508
+ device: {
509
+ type: 'emulator',
510
+ name: 'Pixel_8_API_35',
511
+ avd: {
512
+ apiLevel: 35,
513
+ profile: 'pixel_8',
514
+ diskSize: '1G',
515
+ heapSize: '1G',
516
+ },
517
+ },
518
+ bundleId: 'com.harnessplayground',
519
+ activityName: '.MainActivity',
520
+ }, harnessConfig, init);
521
+ expect(grantPermissions).not.toHaveBeenCalled();
522
+ });
523
+ it('grants permissions when permissions are enabled for physical device', async () => {
524
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['012345']);
525
+ vi.spyOn(adb, 'getDeviceInfo').mockResolvedValue({
526
+ manufacturer: 'motorola',
527
+ model: 'moto g72',
528
+ });
529
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
530
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
531
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
532
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
533
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
534
+ const grantPermissions = vi
535
+ .spyOn(adb, 'grantPermissions')
536
+ .mockResolvedValue(undefined);
537
+ const harnessConfigWithPermissions = {
538
+ ...harnessConfig,
539
+ permissions: true,
540
+ };
541
+ await getAndroidPhysicalDevicePlatformInstance({
542
+ name: 'android-device',
543
+ device: {
544
+ type: 'physical',
545
+ manufacturer: 'motorola',
546
+ model: 'moto g72',
547
+ },
548
+ bundleId: 'com.harnessplayground',
549
+ activityName: '.MainActivity',
550
+ }, harnessConfigWithPermissions);
551
+ expect(grantPermissions).toHaveBeenCalledWith('012345', 'com.harnessplayground');
552
+ });
423
553
  });
@@ -0,0 +1,16 @@
1
+ export declare class AdbError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class AdbDeviceNotFoundError extends AdbError {
5
+ constructor(adbId: string);
6
+ }
7
+ export declare class AdbAppNotInstalledError extends AdbError {
8
+ constructor(bundleId: string, adbId: string);
9
+ }
10
+ export declare class AdbPermissionGrantError extends AdbError {
11
+ constructor(bundleId: string, permissions: string[], adbId: string);
12
+ }
13
+ export declare class AdbBinaryNotFoundError extends AdbError {
14
+ constructor();
15
+ }
16
+ //# sourceMappingURL=adb-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adb-errors.d.ts","sourceRoot":"","sources":["../src/adb-errors.ts"],"names":[],"mappings":"AAAA,qBAAa,QAAS,SAAQ,KAAK;gBACrB,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,sBAAuB,SAAQ,QAAQ;gBACtC,KAAK,EAAE,MAAM;CAO1B;AAED,qBAAa,uBAAwB,SAAQ,QAAQ;gBACvC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAO5C;AAED,qBAAa,uBAAwB,SAAQ,QAAQ;gBACvC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM;CAQnE;AAED,qBAAa,sBAAuB,SAAQ,QAAQ;;CAQnD"}
@@ -0,0 +1,35 @@
1
+ export class AdbError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'AdbError';
5
+ }
6
+ }
7
+ export class AdbDeviceNotFoundError extends AdbError {
8
+ constructor(adbId) {
9
+ super(`Android device "${adbId}" not found or not connected. ` +
10
+ `Run "adb devices" to see available devices.`);
11
+ this.name = 'AdbDeviceNotFoundError';
12
+ }
13
+ }
14
+ export class AdbAppNotInstalledError extends AdbError {
15
+ constructor(bundleId, adbId) {
16
+ super(`App "${bundleId}" is not installed on device "${adbId}". ` +
17
+ `Install the app before running tests.`);
18
+ this.name = 'AdbAppNotInstalledError';
19
+ }
20
+ }
21
+ export class AdbPermissionGrantError extends AdbError {
22
+ constructor(bundleId, permissions, adbId) {
23
+ const permissionList = permissions.join(', ');
24
+ super(`Failed to grant permissions [${permissionList}] to "${bundleId}" on device "${adbId}". ` +
25
+ `Verify the app is installed and the device supports these permissions.`);
26
+ this.name = 'AdbPermissionGrantError';
27
+ }
28
+ }
29
+ export class AdbBinaryNotFoundError extends AdbError {
30
+ constructor() {
31
+ super(`adb binary not found or not accessible. ` +
32
+ `Ensure Android SDK is properly installed and ANDROID_HOME is set.`);
33
+ this.name = 'AdbBinaryNotFoundError';
34
+ }
35
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"adb-id.d.ts","sourceRoot":"","sources":["../src/adb-id.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,aAAa,EACd,MAAM,aAAa,CAAC;AAErB,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,KAAG,OAE/C,CAAC;AAEF,eAAO,MAAM,QAAQ,GACnB,QAAQ,aAAa,KACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CA+BvB,CAAC"}
1
+ {"version":3,"file":"adb-id.d.ts","sourceRoot":"","sources":["../src/adb-id.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,aAAa,EACd,MAAM,aAAa,CAAC;AAErB,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,KAAG,OAE/C,CAAC;AAEF,eAAO,MAAM,QAAQ,GACnB,QAAQ,aAAa,KACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAmCvB,CAAC"}
package/dist/adb-id.js CHANGED
@@ -7,6 +7,9 @@ export const getAdbId = async (device) => {
7
7
  const adbIds = await adb.getDeviceIds();
8
8
  for (const adbId of adbIds) {
9
9
  if (isAndroidDeviceEmulator(device)) {
10
+ if (!isAdbIdEmulator(adbId)) {
11
+ continue;
12
+ }
10
13
  const emulatorName = await adb.getEmulatorName(adbId);
11
14
  if (emulatorName === device.name) {
12
15
  return adbId;
package/dist/adb.d.ts CHANGED
@@ -30,6 +30,7 @@ export declare const getDeviceInfo: (adbId: string) => Promise<DeviceInfo | null
30
30
  export declare const isBootCompleted: (adbId: string) => Promise<boolean>;
31
31
  export declare const stopEmulator: (adbId: string) => Promise<void>;
32
32
  export declare const installApp: (adbId: string, appPath: string) => Promise<void>;
33
+ export declare const uninstallApp: (adbId: string, bundleId: string) => Promise<void>;
33
34
  export declare const hasAvd: (name: string) => Promise<boolean>;
34
35
  export declare const createAvd: ({ name, apiLevel, profile, diskSize, heapSize, }: CreateAvdOptions) => Promise<void>;
35
36
  export declare const deleteAvd: (name: string) => Promise<void>;
@@ -48,4 +49,5 @@ export type AdbDevice = {
48
49
  manufacturer: string;
49
50
  };
50
51
  export declare const getConnectedDevices: () => Promise<AdbDevice[]>;
52
+ export declare const grantPermissions: (adbId: string, bundleId: string) => Promise<void>;
51
53
  //# sourceMappingURL=adb.d.ts.map
package/dist/adb.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"adb.d.ts","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adb.d.ts","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"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"}
package/dist/adb.js CHANGED
@@ -1,9 +1,10 @@
1
- import { spawn, SubprocessError } from '@react-native-harness/tools';
1
+ import { logger, spawn, SubprocessError } from '@react-native-harness/tools';
2
2
  import { spawn as nodeSpawn } from 'node:child_process';
3
3
  import { access, rm } from 'node:fs/promises';
4
4
  import { getAvdConfigPath, getAvdDirectory, readAvdConfig, } from './avd-config.js';
5
5
  import { ensureAndroidEmulatorAvailable, ensureAndroidSdkPackages, getAdbBinaryPath, getAndroidSystemImagePackage, getAvdManagerBinaryPath, getEmulatorBinaryPath, getHostAndroidSystemImageArch, getRequiredAndroidSdkPackages, } from './environment.js';
6
6
  import { getEmulatorStartupArgs, } from './emulator-startup.js';
7
+ import { AdbAppNotInstalledError, AdbPermissionGrantError, } from './adb-errors.js';
7
8
  const wait = async (ms) => {
8
9
  await new Promise((resolve) => {
9
10
  setTimeout(resolve, ms);
@@ -27,6 +28,7 @@ const waitWithSignal = async (ms, signal) => {
27
28
  };
28
29
  const EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS = 5000;
29
30
  const EMULATOR_OUTPUT_BUFFER_LIMIT = 16 * 1024;
31
+ const androidAdbLogger = logger.child('android-adb');
30
32
  export const emulatorProcess = {
31
33
  startDetachedProcess: (file, args) => nodeSpawn(file, args, {
32
34
  detached: true,
@@ -219,6 +221,61 @@ export const getDeviceInfo = async (adbId) => {
219
221
  const model = await getShellProperty(adbId, 'ro.product.model');
220
222
  return { manufacturer, model };
221
223
  };
224
+ const getRequestedPermissions = async (adbId, bundleId) => {
225
+ const { stdout } = await spawn(getAdbBinaryPath(), [
226
+ '-s',
227
+ adbId,
228
+ 'shell',
229
+ 'dumpsys',
230
+ 'package',
231
+ bundleId,
232
+ ]);
233
+ const requestedPermissions = new Set();
234
+ const lines = stdout.split('\n');
235
+ let inRequestedPermissionsSection = false;
236
+ for (const line of lines) {
237
+ const trimmedLine = line.trim();
238
+ if (trimmedLine === 'requested permissions:') {
239
+ inRequestedPermissionsSection = true;
240
+ continue;
241
+ }
242
+ if (!inRequestedPermissionsSection) {
243
+ continue;
244
+ }
245
+ if (trimmedLine === '') {
246
+ continue;
247
+ }
248
+ if (trimmedLine.endsWith(':')) {
249
+ break;
250
+ }
251
+ if (/^[a-zA-Z0-9_.]+$/.test(trimmedLine)) {
252
+ requestedPermissions.add(trimmedLine);
253
+ continue;
254
+ }
255
+ break;
256
+ }
257
+ return [...requestedPermissions];
258
+ };
259
+ const getDangerousPermissions = async (adbId) => {
260
+ const { stdout } = await spawn(getAdbBinaryPath(), [
261
+ '-s',
262
+ adbId,
263
+ 'shell',
264
+ 'pm',
265
+ 'list',
266
+ 'permissions',
267
+ '-g',
268
+ '-d',
269
+ ]);
270
+ const dangerousPermissions = new Set();
271
+ for (const match of stdout.matchAll(/permission:([a-zA-Z0-9_.]+)/g)) {
272
+ const permission = match[1]?.trim();
273
+ if (permission) {
274
+ dangerousPermissions.add(permission);
275
+ }
276
+ }
277
+ return dangerousPermissions;
278
+ };
222
279
  export const isBootCompleted = async (adbId) => {
223
280
  try {
224
281
  const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
@@ -237,6 +294,9 @@ export const stopEmulator = async (adbId) => {
237
294
  export const installApp = async (adbId, appPath) => {
238
295
  await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]);
239
296
  };
297
+ export const uninstallApp = async (adbId, bundleId) => {
298
+ await spawn(getAdbBinaryPath(), ['-s', adbId, 'uninstall', bundleId]);
299
+ };
240
300
  export const hasAvd = async (name) => {
241
301
  const avds = await getAvds();
242
302
  return avds.includes(name);
@@ -468,3 +528,62 @@ export const getConnectedDevices = async () => {
468
528
  }
469
529
  return devices;
470
530
  };
531
+ export const grantPermissions = async (adbId, bundleId) => {
532
+ androidAdbLogger.debug('grantPermissions:start %o', {
533
+ adbId,
534
+ bundleId,
535
+ });
536
+ const isInstalled = await isAppInstalled(adbId, bundleId);
537
+ if (!isInstalled) {
538
+ throw new AdbAppNotInstalledError(bundleId, adbId);
539
+ }
540
+ const [requestedPermissions, dangerousPermissions] = await Promise.all([
541
+ getRequestedPermissions(adbId, bundleId),
542
+ getDangerousPermissions(adbId),
543
+ ]);
544
+ const permissions = requestedPermissions.filter((permission) => dangerousPermissions.has(permission));
545
+ androidAdbLogger.debug('grantPermissions:resolved %o', {
546
+ adbId,
547
+ bundleId,
548
+ requestedPermissions,
549
+ permissions,
550
+ });
551
+ if (permissions.length === 0) {
552
+ androidAdbLogger.debug('grantPermissions:skip %o', {
553
+ adbId,
554
+ bundleId,
555
+ });
556
+ return;
557
+ }
558
+ const grantCommands = permissions.map((permission) => [
559
+ '-s',
560
+ adbId,
561
+ 'shell',
562
+ 'pm',
563
+ 'grant',
564
+ bundleId,
565
+ permission,
566
+ ]);
567
+ try {
568
+ androidAdbLogger.debug('grantPermissions:commands %o', {
569
+ adbId,
570
+ bundleId,
571
+ grantCommands,
572
+ });
573
+ await Promise.all(grantCommands.map((args) => spawn(getAdbBinaryPath(), args)));
574
+ androidAdbLogger.debug('grantPermissions:success %o', {
575
+ adbId,
576
+ bundleId,
577
+ permissions,
578
+ });
579
+ }
580
+ catch (error) {
581
+ androidAdbLogger.debug('grantPermissions:error %o', {
582
+ adbId,
583
+ bundleId,
584
+ permissions,
585
+ error,
586
+ });
587
+ throw new AdbPermissionGrantError(bundleId, permissions, adbId);
588
+ }
589
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"emulator-startup.d.ts","sourceRoot":"","sources":["../src/emulator-startup.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,2BAA2B,GAC3B,gBAAgB,CAAC;AAYrB,eAAO,MAAM,sBAAsB,GACjC,MAAM,MAAM,EACZ,MAAM,gBAAgB,KACrB,MAAM,EASR,CAAC"}
1
+ {"version":3,"file":"emulator-startup.d.ts","sourceRoot":"","sources":["../src/emulator-startup.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,2BAA2B,GAC3B,gBAAgB,CAAC;AAUrB,eAAO,MAAM,sBAAsB,GACjC,MAAM,MAAM,EACZ,MAAM,gBAAgB,KACrB,MAAM,EASR,CAAC"}
@@ -4,8 +4,6 @@ const COMMON_EMULATOR_ARGS = [
4
4
  'swiftshader_indirect',
5
5
  '-noaudio',
6
6
  '-no-boot-anim',
7
- '-camera-back',
8
- 'none',
9
7
  ];
10
8
  export const getEmulatorStartupArgs = (name, mode) => {
11
9
  const modeArgs = mode === 'clean-snapshot-generation'
@@ -1 +1 @@
1
- {"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../src/instance.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,0BAA0B,EAC/B,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE5E,OAAO,EACL,qBAAqB,EAGtB,MAAM,aAAa,CAAC;AAkKrB,eAAO,MAAM,kCAAkC,GAC7C,QAAQ,qBAAqB,EAC7B,eAAe,aAAa,EAC5B,MAAM,0BAA0B,KAC/B,OAAO,CAAC,qBAAqB,CAkI/B,CAAC;AAEF,eAAO,MAAM,wCAAwC,GACnD,QAAQ,qBAAqB,EAC7B,eAAe,aAAa,KAC3B,OAAO,CAAC,qBAAqB,CAiE/B,CAAC"}
1
+ {"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../src/instance.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,0BAA0B,EAC/B,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE5E,OAAO,EACL,qBAAqB,EAGtB,MAAM,aAAa,CAAC;AAgLrB,eAAO,MAAM,kCAAkC,GAC7C,QAAQ,qBAAqB,EAC7B,eAAe,aAAa,EAC5B,MAAM,0BAA0B,KAC/B,OAAO,CAAC,qBAAqB,CA2I/B,CAAC;AAEF,eAAO,MAAM,wCAAwC,GACnD,QAAQ,qBAAqB,EAC7B,eAAe,aAAa,KAC3B,OAAO,CAAC,qBAAqB,CAsE/B,CAAC"}
package/dist/instance.js CHANGED
@@ -29,6 +29,16 @@ const getHarnessAppPath = () => {
29
29
  }
30
30
  return appPath;
31
31
  };
32
+ const getOptionalHarnessAppPath = () => {
33
+ const appPath = process.env.HARNESS_APP_PATH;
34
+ if (!appPath) {
35
+ return undefined;
36
+ }
37
+ if (!fs.existsSync(appPath)) {
38
+ throw new HarnessAppPathError('invalid', appPath);
39
+ }
40
+ return appPath;
41
+ };
32
42
  const configureAndroidRuntime = async (adbId, config, harnessConfig) => {
33
43
  const metroPort = harnessConfig.metroPort;
34
44
  await Promise.all([
@@ -98,6 +108,7 @@ const prepareCachedAvd = async ({ emulatorConfig, signal, }) => {
98
108
  export const getAndroidEmulatorPlatformInstance = async (config, harnessConfig, init) => {
99
109
  assertAndroidDeviceEmulator(config.device);
100
110
  const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true;
111
+ const permissionsEnabled = harnessConfig.permissions ?? false;
101
112
  const emulatorConfig = config.device;
102
113
  const emulatorName = emulatorConfig.name;
103
114
  const avdConfig = emulatorConfig.avd;
@@ -141,11 +152,19 @@ export const getAndroidEmulatorPlatformInstance = async (config, harnessConfig,
141
152
  }
142
153
  androidInstanceLogger.debug('waiting for Android emulator %s to finish booting', adbId);
143
154
  const isInstalled = await adb.isAppInstalled(adbId, config.bundleId);
144
- if (!isInstalled) {
145
- const appPath = getHarnessAppPath();
155
+ const appPath = getOptionalHarnessAppPath();
156
+ if (isInstalled && appPath) {
157
+ await adb.uninstallApp(adbId, config.bundleId);
146
158
  await adb.installApp(adbId, appPath);
147
159
  }
160
+ else if (!isInstalled) {
161
+ const installPath = appPath ?? getHarnessAppPath();
162
+ await adb.installApp(adbId, installPath);
163
+ }
148
164
  const appUid = await configureAndroidRuntime(adbId, config, harnessConfig);
165
+ if (permissionsEnabled) {
166
+ await adb.grantPermissions(adbId, config.bundleId);
167
+ }
149
168
  return {
150
169
  startApp: async (options) => {
151
170
  await adb.startApp(adbId, config.bundleId, config.activityName, options ??
@@ -187,6 +206,7 @@ export const getAndroidEmulatorPlatformInstance = async (config, harnessConfig,
187
206
  export const getAndroidPhysicalDevicePlatformInstance = async (config, harnessConfig) => {
188
207
  assertAndroidDevicePhysical(config.device);
189
208
  const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true;
209
+ const permissionsEnabled = harnessConfig.permissions ?? false;
190
210
  const adbId = await getAdbId(config.device);
191
211
  if (!adbId) {
192
212
  throw new DeviceNotFoundError(getDeviceName(config.device));
@@ -196,6 +216,9 @@ export const getAndroidPhysicalDevicePlatformInstance = async (config, harnessCo
196
216
  throw new AppNotInstalledError(config.bundleId, getDeviceName(config.device));
197
217
  }
198
218
  const appUid = await configureAndroidRuntime(adbId, config, harnessConfig);
219
+ if (permissionsEnabled) {
220
+ await adb.grantPermissions(adbId, config.bundleId);
221
+ }
199
222
  return {
200
223
  startApp: async (options) => {
201
224
  await adb.startApp(adbId, config.bundleId, config.activityName, options ??