@react-native-harness/platform-android 1.1.0-rc.4 → 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.
package/dist/adb.js CHANGED
@@ -1,8 +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
+ import { getAvdConfigPath, getAvdDirectory, readAvdConfig, } from './avd-config.js';
4
5
  import { ensureAndroidEmulatorAvailable, ensureAndroidSdkPackages, getAdbBinaryPath, getAndroidSystemImagePackage, getAvdManagerBinaryPath, getEmulatorBinaryPath, getHostAndroidSystemImageArch, getRequiredAndroidSdkPackages, } from './environment.js';
5
6
  import { getEmulatorStartupArgs, } from './emulator-startup.js';
7
+ import { AdbAppNotInstalledError, AdbPermissionGrantError, } from './adb-errors.js';
6
8
  const wait = async (ms) => {
7
9
  await new Promise((resolve) => {
8
10
  setTimeout(resolve, ms);
@@ -24,9 +26,9 @@ const waitWithSignal = async (ms, signal) => {
24
26
  }
25
27
  await Promise.race([wait(ms), waitForAbort(signal)]);
26
28
  };
27
- const getAvdConfigPath = (name) => `${process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`}/${name}.avd/config.ini`;
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,
@@ -79,6 +81,43 @@ export const getRequiredEmulatorPackages = (apiLevel) => {
79
81
  export const verifyAndroidEmulatorSdk = async (apiLevel) => {
80
82
  await ensureAndroidSdkPackages(getRequiredEmulatorPackages(apiLevel));
81
83
  };
84
+ const listAvdProfiles = async () => {
85
+ const { stdout } = await spawn(getAvdManagerBinaryPath(), ['list', 'device']);
86
+ return stdout
87
+ .split('\n')
88
+ .map((line) => line.match(/^id:\s+\d+\s+or\s+"([^"]+)"/i)?.[1]?.trim())
89
+ .filter((profile) => profile != null && profile !== '');
90
+ };
91
+ const ensureAvdProfileAvailable = async (profile) => {
92
+ const availableProfiles = await listAvdProfiles();
93
+ if (availableProfiles.includes(profile)) {
94
+ return;
95
+ }
96
+ const availableProfilesList = availableProfiles.length > 0
97
+ ? availableProfiles.join(', ')
98
+ : 'None reported by avdmanager.';
99
+ throw new Error(`Android AVD profile "${profile}" is not available on this machine. Available profiles: ${availableProfilesList}`);
100
+ };
101
+ const ensureAvdConfigExists = async (name) => {
102
+ const configPath = getAvdConfigPath(name);
103
+ if ((await readAvdConfig(name)) == null) {
104
+ throw new Error(`Android AVD "${name}" was created, but config.ini was not found at ${configPath}.`);
105
+ }
106
+ return configPath;
107
+ };
108
+ const getAvdIniPath = (name) => {
109
+ const avdHome = process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`;
110
+ return `${avdHome}/${name}.ini`;
111
+ };
112
+ const ensureAvdIniExists = async ({ name, apiLevel, }) => {
113
+ const iniPath = getAvdIniPath(name);
114
+ const avdDirectory = getAvdDirectory(name);
115
+ await spawn('bash', [
116
+ '-lc',
117
+ `printf '%s\n%s\n%s\n%s\n' 'avd.ini.encoding=UTF-8' 'path=${avdDirectory}' 'path.rel=avd/${name}.avd' 'target=android-${apiLevel}' > "${iniPath}"`,
118
+ ]);
119
+ return iniPath;
120
+ };
82
121
  export const getStartAppArgs = (bundleId, activityName, options) => {
83
122
  const args = [
84
123
  'shell',
@@ -182,6 +221,61 @@ export const getDeviceInfo = async (adbId) => {
182
221
  const model = await getShellProperty(adbId, 'ro.product.model');
183
222
  return { manufacturer, model };
184
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
+ };
185
279
  export const isBootCompleted = async (adbId) => {
186
280
  try {
187
281
  const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
@@ -200,6 +294,9 @@ export const stopEmulator = async (adbId) => {
200
294
  export const installApp = async (adbId, appPath) => {
201
295
  await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]);
202
296
  };
297
+ export const uninstallApp = async (adbId, bundleId) => {
298
+ await spawn(getAdbBinaryPath(), ['-s', adbId, 'uninstall', bundleId]);
299
+ };
203
300
  export const hasAvd = async (name) => {
204
301
  const avds = await getAvds();
205
302
  return avds.includes(name);
@@ -207,13 +304,16 @@ export const hasAvd = async (name) => {
207
304
  export const createAvd = async ({ name, apiLevel, profile, diskSize, heapSize, }) => {
208
305
  const systemImagePackage = getAndroidSystemImagePackage(apiLevel, getHostAndroidSystemImageArch());
209
306
  await verifyAndroidEmulatorSdk(apiLevel);
307
+ await ensureAvdProfileAvailable(profile);
210
308
  await spawn('bash', [
211
309
  '-lc',
212
- `printf 'no\n' | "${getAvdManagerBinaryPath()}" create avd --force --name "${name}" --package "${systemImagePackage}" --device "${profile}"`,
310
+ `printf 'no\n' | "${getAvdManagerBinaryPath()}" create avd --force --name "${name}" --package "${systemImagePackage}" --device "${profile}" -p "${getAvdDirectory(name)}"`,
213
311
  ]);
312
+ await ensureAvdIniExists({ name, apiLevel });
313
+ const configPath = await ensureAvdConfigExists(name);
214
314
  await spawn('bash', [
215
315
  '-lc',
216
- `printf '%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' >> "${getAvdConfigPath(name)}"`,
316
+ `printf '%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' >> "${configPath}"`,
217
317
  ]);
218
318
  };
219
319
  export const deleteAvd = async (name) => {
@@ -428,3 +528,62 @@ export const getConnectedDevices = async () => {
428
528
  }
429
529
  return devices;
430
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
+ };
package/dist/config.d.ts CHANGED
@@ -68,8 +68,8 @@ export declare const AndroidEmulatorSchema: z.ZodObject<{
68
68
  } | undefined;
69
69
  }>>;
70
70
  }, "strip", z.ZodTypeAny, {
71
- name: string;
72
71
  type: "emulator";
72
+ name: string;
73
73
  avd?: {
74
74
  apiLevel: number;
75
75
  profile: string;
@@ -80,8 +80,8 @@ export declare const AndroidEmulatorSchema: z.ZodObject<{
80
80
  } | undefined;
81
81
  } | undefined;
82
82
  }, {
83
- name: string;
84
83
  type: "emulator";
84
+ name: string;
85
85
  avd?: {
86
86
  apiLevel: number;
87
87
  profile: string;
@@ -97,13 +97,13 @@ export declare const PhysicalAndroidDeviceSchema: z.ZodObject<{
97
97
  manufacturer: z.ZodString;
98
98
  model: z.ZodString;
99
99
  }, "strip", z.ZodTypeAny, {
100
+ type: "physical";
100
101
  manufacturer: string;
101
102
  model: string;
102
- type: "physical";
103
103
  }, {
104
+ type: "physical";
104
105
  manufacturer: string;
105
106
  model: string;
106
- type: "physical";
107
107
  }>;
108
108
  export declare const AndroidDeviceSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
109
109
  type: z.ZodLiteral<"emulator">;
@@ -138,8 +138,8 @@ export declare const AndroidDeviceSchema: z.ZodDiscriminatedUnion<"type", [z.Zod
138
138
  } | undefined;
139
139
  }>>;
140
140
  }, "strip", z.ZodTypeAny, {
141
- name: string;
142
141
  type: "emulator";
142
+ name: string;
143
143
  avd?: {
144
144
  apiLevel: number;
145
145
  profile: string;
@@ -150,8 +150,8 @@ export declare const AndroidDeviceSchema: z.ZodDiscriminatedUnion<"type", [z.Zod
150
150
  } | undefined;
151
151
  } | undefined;
152
152
  }, {
153
- name: string;
154
153
  type: "emulator";
154
+ name: string;
155
155
  avd?: {
156
156
  apiLevel: number;
157
157
  profile: string;
@@ -166,13 +166,13 @@ export declare const AndroidDeviceSchema: z.ZodDiscriminatedUnion<"type", [z.Zod
166
166
  manufacturer: z.ZodString;
167
167
  model: z.ZodString;
168
168
  }, "strip", z.ZodTypeAny, {
169
+ type: "physical";
169
170
  manufacturer: string;
170
171
  model: string;
171
- type: "physical";
172
172
  }, {
173
+ type: "physical";
173
174
  manufacturer: string;
174
175
  model: string;
175
- type: "physical";
176
176
  }>]>;
177
177
  export declare const AndroidPlatformConfigSchema: z.ZodObject<{
178
178
  name: z.ZodString;
@@ -209,8 +209,8 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
209
209
  } | undefined;
210
210
  }>>;
211
211
  }, "strip", z.ZodTypeAny, {
212
- name: string;
213
212
  type: "emulator";
213
+ name: string;
214
214
  avd?: {
215
215
  apiLevel: number;
216
216
  profile: string;
@@ -221,8 +221,8 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
221
221
  } | undefined;
222
222
  } | undefined;
223
223
  }, {
224
- name: string;
225
224
  type: "emulator";
225
+ name: string;
226
226
  avd?: {
227
227
  apiLevel: number;
228
228
  profile: string;
@@ -237,13 +237,13 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
237
237
  manufacturer: z.ZodString;
238
238
  model: z.ZodString;
239
239
  }, "strip", z.ZodTypeAny, {
240
+ type: "physical";
240
241
  manufacturer: string;
241
242
  model: string;
242
- type: "physical";
243
243
  }, {
244
+ type: "physical";
244
245
  manufacturer: string;
245
246
  model: string;
246
- type: "physical";
247
247
  }>]>;
248
248
  bundleId: z.ZodString;
249
249
  activityName: z.ZodDefault<z.ZodString>;
@@ -257,8 +257,8 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
257
257
  }, "strip", z.ZodTypeAny, {
258
258
  name: string;
259
259
  device: {
260
- name: string;
261
260
  type: "emulator";
261
+ name: string;
262
262
  avd?: {
263
263
  apiLevel: number;
264
264
  profile: string;
@@ -269,9 +269,9 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
269
269
  } | undefined;
270
270
  } | undefined;
271
271
  } | {
272
+ type: "physical";
272
273
  manufacturer: string;
273
274
  model: string;
274
- type: "physical";
275
275
  };
276
276
  bundleId: string;
277
277
  activityName: string;
@@ -281,8 +281,8 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
281
281
  }, {
282
282
  name: string;
283
283
  device: {
284
- name: string;
285
284
  type: "emulator";
285
+ name: string;
286
286
  avd?: {
287
287
  apiLevel: number;
288
288
  profile: string;
@@ -293,9 +293,9 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
293
293
  } | undefined;
294
294
  } | undefined;
295
295
  } | {
296
+ type: "physical";
296
297
  manufacturer: string;
297
298
  model: string;
298
- type: "physical";
299
299
  };
300
300
  bundleId: string;
301
301
  activityName?: string | undefined;
@@ -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 ??