@react-native-harness/platform-android 1.1.0-rc.2 → 1.1.0-rc.4

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.
Files changed (87) hide show
  1. package/README.md +9 -2
  2. package/dist/__tests__/adb.test.js +283 -10
  3. package/dist/__tests__/avd-config.test.d.ts +2 -0
  4. package/dist/__tests__/avd-config.test.d.ts.map +1 -0
  5. package/dist/__tests__/avd-config.test.js +174 -0
  6. package/dist/__tests__/ci-action.test.d.ts +2 -0
  7. package/dist/__tests__/ci-action.test.d.ts.map +1 -0
  8. package/dist/__tests__/ci-action.test.js +46 -0
  9. package/dist/__tests__/emulator-startup.test.d.ts +2 -0
  10. package/dist/__tests__/emulator-startup.test.d.ts.map +1 -0
  11. package/dist/__tests__/emulator-startup.test.js +19 -0
  12. package/dist/__tests__/environment.test.d.ts +2 -0
  13. package/dist/__tests__/environment.test.d.ts.map +1 -0
  14. package/dist/__tests__/environment.test.js +117 -0
  15. package/dist/__tests__/instance.test.d.ts +2 -0
  16. package/dist/__tests__/instance.test.d.ts.map +1 -0
  17. package/dist/__tests__/instance.test.js +423 -0
  18. package/dist/__tests__/targets.test.d.ts +2 -0
  19. package/dist/__tests__/targets.test.d.ts.map +1 -0
  20. package/dist/__tests__/targets.test.js +49 -0
  21. package/dist/adb.d.ts +23 -0
  22. package/dist/adb.d.ts.map +1 -1
  23. package/dist/adb.js +259 -16
  24. package/dist/app-monitor.d.ts.map +1 -1
  25. package/dist/app-monitor.js +27 -7
  26. package/dist/assertions.d.ts +5 -0
  27. package/dist/assertions.d.ts.map +1 -0
  28. package/dist/assertions.js +6 -0
  29. package/dist/avd-config.d.ts +41 -0
  30. package/dist/avd-config.d.ts.map +1 -0
  31. package/dist/avd-config.js +173 -0
  32. package/dist/config.d.ts +77 -0
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/config.js +5 -0
  35. package/dist/emulator-startup.d.ts +3 -0
  36. package/dist/emulator-startup.d.ts.map +1 -0
  37. package/dist/emulator-startup.js +17 -0
  38. package/dist/emulator.d.ts +6 -0
  39. package/dist/emulator.d.ts.map +1 -0
  40. package/dist/emulator.js +27 -0
  41. package/dist/environment.d.ts +31 -0
  42. package/dist/environment.d.ts.map +1 -0
  43. package/dist/environment.js +317 -0
  44. package/dist/errors.d.ts +7 -0
  45. package/dist/errors.d.ts.map +1 -0
  46. package/dist/errors.js +14 -0
  47. package/dist/factory.d.ts.map +1 -1
  48. package/dist/factory.js +3 -0
  49. package/dist/index.d.ts +3 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +3 -0
  52. package/dist/instance.d.ts +6 -0
  53. package/dist/instance.d.ts.map +1 -0
  54. package/dist/instance.js +232 -0
  55. package/dist/reader.d.ts +6 -0
  56. package/dist/reader.d.ts.map +1 -0
  57. package/dist/reader.js +57 -0
  58. package/dist/runner.d.ts +2 -2
  59. package/dist/runner.d.ts.map +1 -1
  60. package/dist/runner.js +9 -52
  61. package/dist/targets.d.ts +1 -1
  62. package/dist/targets.d.ts.map +1 -1
  63. package/dist/targets.js +4 -0
  64. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  65. package/dist/types.d.ts +381 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +107 -0
  68. package/package.json +4 -4
  69. package/src/__tests__/adb.test.ts +419 -15
  70. package/src/__tests__/avd-config.test.ts +206 -0
  71. package/src/__tests__/ci-action.test.ts +81 -0
  72. package/src/__tests__/emulator-startup.test.ts +32 -0
  73. package/src/__tests__/environment.test.ts +212 -0
  74. package/src/__tests__/instance.test.ts +610 -0
  75. package/src/__tests__/targets.test.ts +53 -0
  76. package/src/adb.ts +430 -28
  77. package/src/app-monitor.ts +56 -18
  78. package/src/avd-config.ts +290 -0
  79. package/src/config.ts +8 -0
  80. package/src/emulator-startup.ts +28 -0
  81. package/src/environment.ts +554 -0
  82. package/src/errors.ts +19 -0
  83. package/src/factory.ts +4 -0
  84. package/src/index.ts +7 -1
  85. package/src/instance.ts +380 -0
  86. package/src/runner.ts +19 -70
  87. package/src/targets.ts +18 -8
package/src/adb.ts CHANGED
@@ -1,10 +1,169 @@
1
1
  import { type AndroidAppLaunchOptions } from '@react-native-harness/platforms';
2
2
  import { spawn, SubprocessError } from '@react-native-harness/tools';
3
+ import { spawn as nodeSpawn } from 'node:child_process';
4
+ import type { ChildProcessByStdio } from 'node:child_process';
5
+ import { access, rm } from 'node:fs/promises';
6
+ import type { Readable } from 'node:stream';
7
+ import {
8
+ ensureAndroidEmulatorAvailable,
9
+ ensureAndroidSdkPackages,
10
+ getAdbBinaryPath,
11
+ getAndroidSystemImagePackage,
12
+ getAvdManagerBinaryPath,
13
+ getEmulatorBinaryPath,
14
+ getHostAndroidSystemImageArch,
15
+ getRequiredAndroidSdkPackages,
16
+ } from './environment.js';
17
+ import {
18
+ getEmulatorStartupArgs,
19
+ type EmulatorBootMode,
20
+ } from './emulator-startup.js';
21
+
22
+ const wait = async (ms: number): Promise<void> => {
23
+ await new Promise((resolve) => {
24
+ setTimeout(resolve, ms);
25
+ });
26
+ };
27
+
28
+ const waitForAbort = (signal: AbortSignal): Promise<never> => {
29
+ if (signal.aborted) {
30
+ return Promise.reject(signal.reason);
31
+ }
32
+
33
+ return new Promise((_, reject) => {
34
+ signal.addEventListener(
35
+ 'abort',
36
+ () => {
37
+ reject(signal.reason);
38
+ },
39
+ { once: true },
40
+ );
41
+ });
42
+ };
43
+
44
+ const waitWithSignal = async (
45
+ ms: number,
46
+ signal: AbortSignal,
47
+ ): Promise<void> => {
48
+ if (signal.aborted) {
49
+ throw signal.reason;
50
+ }
51
+
52
+ await Promise.race([wait(ms), waitForAbort(signal)]);
53
+ };
54
+
55
+ const getAvdConfigPath = (name: string): string =>
56
+ `${
57
+ process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`
58
+ }/${name}.avd/config.ini`;
59
+
60
+ const EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS = 5000;
61
+ const EMULATOR_OUTPUT_BUFFER_LIMIT = 16 * 1024;
62
+
63
+ export const emulatorProcess = {
64
+ startDetachedProcess: (
65
+ file: string,
66
+ args: readonly string[],
67
+ ): ChildProcessByStdio<null, Readable, Readable> =>
68
+ nodeSpawn(file, args, {
69
+ detached: true,
70
+ stdio: ['ignore', 'pipe', 'pipe'],
71
+ }),
72
+ };
73
+
74
+ const appendBoundedOutput = (
75
+ output: string,
76
+ chunk: string,
77
+ limit: number = EMULATOR_OUTPUT_BUFFER_LIMIT,
78
+ ): string => {
79
+ const nextOutput = output + chunk;
80
+
81
+ if (nextOutput.length <= limit) {
82
+ return nextOutput;
83
+ }
84
+
85
+ return nextOutput.slice(-limit);
86
+ };
87
+
88
+ const formatEmulatorStartupError = ({
89
+ name,
90
+ stdout,
91
+ stderr,
92
+ exitCode,
93
+ signal,
94
+ error,
95
+ }: {
96
+ name: string;
97
+ stdout: string;
98
+ stderr: string;
99
+ exitCode?: number | null;
100
+ signal?: NodeJS.Signals | null;
101
+ error?: unknown;
102
+ }): Error => {
103
+ const sections = [`Failed to start Android emulator @${name}.`];
104
+
105
+ if (typeof exitCode === 'number') {
106
+ sections.push(`Exit code: ${exitCode}`);
107
+ }
108
+
109
+ if (signal) {
110
+ sections.push(`Signal: ${signal}`);
111
+ }
112
+
113
+ if (error instanceof Error) {
114
+ sections.push(`Cause: ${error.message}`);
115
+ }
116
+
117
+ const trimmedStdout = stdout.trim();
118
+ const trimmedStderr = stderr.trim();
119
+
120
+ if (trimmedStdout !== '') {
121
+ sections.push(`stdout:\n${trimmedStdout}`);
122
+ }
123
+
124
+ if (trimmedStderr !== '') {
125
+ sections.push(`stderr:\n${trimmedStderr}`);
126
+ }
127
+
128
+ return new Error(sections.join('\n\n'), {
129
+ cause: error instanceof Error ? error : undefined,
130
+ });
131
+ };
132
+
133
+ const ensureEmulatorInstalled = async (): Promise<string> => {
134
+ await ensureAndroidEmulatorAvailable();
135
+
136
+ const emulatorBinaryPath = getEmulatorBinaryPath();
137
+ await access(emulatorBinaryPath);
138
+ return emulatorBinaryPath;
139
+ };
140
+
141
+ export type CreateAvdOptions = {
142
+ name: string;
143
+ apiLevel: number;
144
+ profile: string;
145
+ diskSize: string;
146
+ heapSize: string;
147
+ };
148
+
149
+ export const getRequiredEmulatorPackages = (apiLevel: number): string[] => {
150
+ return getRequiredAndroidSdkPackages({
151
+ apiLevel,
152
+ includeEmulator: true,
153
+ architecture: getHostAndroidSystemImageArch(),
154
+ });
155
+ };
156
+
157
+ export const verifyAndroidEmulatorSdk = async (
158
+ apiLevel: number,
159
+ ): Promise<void> => {
160
+ await ensureAndroidSdkPackages(getRequiredEmulatorPackages(apiLevel));
161
+ };
3
162
 
4
163
  export const getStartAppArgs = (
5
164
  bundleId: string,
6
165
  activityName: string,
7
- options?: AndroidAppLaunchOptions
166
+ options?: AndroidAppLaunchOptions,
8
167
  ): string[] => {
9
168
  const args = [
10
169
  'shell',
@@ -33,7 +192,7 @@ export const getStartAppArgs = (
33
192
 
34
193
  if (!Number.isSafeInteger(value)) {
35
194
  throw new Error(
36
- `Android app launch option "${key}" must be a safe integer.`
195
+ `Android app launch option "${key}" must be a safe integer.`,
37
196
  );
38
197
  }
39
198
 
@@ -45,9 +204,9 @@ export const getStartAppArgs = (
45
204
 
46
205
  export const isAppInstalled = async (
47
206
  adbId: string,
48
- bundleId: string
207
+ bundleId: string,
49
208
  ): Promise<boolean> => {
50
- const { stdout } = await spawn('adb', [
209
+ const { stdout } = await spawn(getAdbBinaryPath(), [
51
210
  '-s',
52
211
  adbId,
53
212
  'shell',
@@ -62,9 +221,9 @@ export const isAppInstalled = async (
62
221
  export const reversePort = async (
63
222
  adbId: string,
64
223
  port: number,
65
- hostPort: number = port
224
+ hostPort: number = port,
66
225
  ): Promise<void> => {
67
- await spawn('adb', [
226
+ await spawn(getAdbBinaryPath(), [
68
227
  '-s',
69
228
  adbId,
70
229
  'reverse',
@@ -75,22 +234,33 @@ export const reversePort = async (
75
234
 
76
235
  export const stopApp = async (
77
236
  adbId: string,
78
- bundleId: string
237
+ bundleId: string,
79
238
  ): Promise<void> => {
80
- await spawn('adb', ['-s', adbId, 'shell', 'am', 'force-stop', bundleId]);
239
+ await spawn(getAdbBinaryPath(), [
240
+ '-s',
241
+ adbId,
242
+ 'shell',
243
+ 'am',
244
+ 'force-stop',
245
+ bundleId,
246
+ ]);
81
247
  };
82
248
 
83
249
  export const startApp = async (
84
250
  adbId: string,
85
251
  bundleId: string,
86
252
  activityName: string,
87
- options?: AndroidAppLaunchOptions
253
+ options?: AndroidAppLaunchOptions,
88
254
  ): Promise<void> => {
89
- await spawn('adb', ['-s', adbId, ...getStartAppArgs(bundleId, activityName, options)]);
255
+ await spawn(getAdbBinaryPath(), [
256
+ '-s',
257
+ adbId,
258
+ ...getStartAppArgs(bundleId, activityName, options),
259
+ ]);
90
260
  };
91
261
 
92
262
  export const getDeviceIds = async (): Promise<string[]> => {
93
- const { stdout } = await spawn('adb', ['devices']);
263
+ const { stdout } = await spawn(getAdbBinaryPath(), ['devices']);
94
264
  return stdout
95
265
  .split('\n')
96
266
  .slice(1) // Skip header
@@ -99,17 +269,23 @@ export const getDeviceIds = async (): Promise<string[]> => {
99
269
  };
100
270
 
101
271
  export const getEmulatorName = async (
102
- adbId: string
272
+ adbId: string,
103
273
  ): Promise<string | null> => {
104
- const { stdout } = await spawn('adb', ['-s', adbId, 'emu', 'avd', 'name']);
274
+ const { stdout } = await spawn(getAdbBinaryPath(), [
275
+ '-s',
276
+ adbId,
277
+ 'emu',
278
+ 'avd',
279
+ 'name',
280
+ ]);
105
281
  return stdout.split('\n')[0].trim() || null;
106
282
  };
107
283
 
108
284
  export const getShellProperty = async (
109
285
  adbId: string,
110
- property: string
286
+ property: string,
111
287
  ): Promise<string | null> => {
112
- const { stdout } = await spawn('adb', [
288
+ const { stdout } = await spawn(getAdbBinaryPath(), [
113
289
  '-s',
114
290
  adbId,
115
291
  'shell',
@@ -119,13 +295,17 @@ export const getShellProperty = async (
119
295
  return stdout.trim() || null;
120
296
  };
121
297
 
298
+ const isTransientAdbShellFailure = (error: unknown): boolean => {
299
+ return error instanceof SubprocessError && error.exitCode === 1;
300
+ };
301
+
122
302
  export type DeviceInfo = {
123
303
  manufacturer: string | null;
124
304
  model: string | null;
125
305
  };
126
306
 
127
307
  export const getDeviceInfo = async (
128
- adbId: string
308
+ adbId: string,
129
309
  ): Promise<DeviceInfo | null> => {
130
310
  const manufacturer = await getShellProperty(adbId, 'ro.product.manufacturer');
131
311
  const model = await getShellProperty(adbId, 'ro.product.model');
@@ -133,20 +313,241 @@ export const getDeviceInfo = async (
133
313
  };
134
314
 
135
315
  export const isBootCompleted = async (adbId: string): Promise<boolean> => {
136
- const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
137
- return bootCompleted === '1';
316
+ try {
317
+ const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
318
+ return bootCompleted === '1';
319
+ } catch (error) {
320
+ if (isTransientAdbShellFailure(error)) {
321
+ return false;
322
+ }
323
+
324
+ throw error;
325
+ }
138
326
  };
139
327
 
140
328
  export const stopEmulator = async (adbId: string): Promise<void> => {
141
- await spawn('adb', ['-s', adbId, 'emu', 'kill']);
329
+ await spawn(getAdbBinaryPath(), ['-s', adbId, 'emu', 'kill']);
330
+ };
331
+
332
+ export const installApp = async (
333
+ adbId: string,
334
+ appPath: string,
335
+ ): Promise<void> => {
336
+ await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]);
337
+ };
338
+
339
+ export const hasAvd = async (name: string): Promise<boolean> => {
340
+ const avds = await getAvds();
341
+ return avds.includes(name);
342
+ };
343
+
344
+ export const createAvd = async ({
345
+ name,
346
+ apiLevel,
347
+ profile,
348
+ diskSize,
349
+ heapSize,
350
+ }: CreateAvdOptions): Promise<void> => {
351
+ const systemImagePackage = getAndroidSystemImagePackage(
352
+ apiLevel,
353
+ getHostAndroidSystemImageArch(),
354
+ );
355
+
356
+ await verifyAndroidEmulatorSdk(apiLevel);
357
+ await spawn('bash', [
358
+ '-lc',
359
+ `printf 'no\n' | "${getAvdManagerBinaryPath()}" create avd --force --name "${name}" --package "${systemImagePackage}" --device "${profile}"`,
360
+ ]);
361
+ await spawn('bash', [
362
+ '-lc',
363
+ `printf '%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' >> "${getAvdConfigPath(
364
+ name,
365
+ )}"`,
366
+ ]);
367
+ };
368
+
369
+ export const deleteAvd = async (name: string): Promise<void> => {
370
+ await rm(
371
+ `${
372
+ process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`
373
+ }/${name}.avd`,
374
+ {
375
+ force: true,
376
+ recursive: true,
377
+ },
378
+ );
379
+ await rm(
380
+ `${
381
+ process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`
382
+ }/${name}.ini`,
383
+ {
384
+ force: true,
385
+ },
386
+ );
387
+ };
388
+
389
+ export const startEmulator = async (
390
+ name: string,
391
+ mode: EmulatorBootMode = 'default-boot',
392
+ ): Promise<void> => {
393
+ const emulatorBinaryPath = await ensureEmulatorInstalled();
394
+ const childProcess = emulatorProcess.startDetachedProcess(
395
+ emulatorBinaryPath,
396
+ getEmulatorStartupArgs(name, mode),
397
+ );
398
+
399
+ let stdout = '';
400
+ let stderr = '';
401
+
402
+ childProcess.stdout?.setEncoding('utf8');
403
+ childProcess.stderr?.setEncoding('utf8');
404
+
405
+ const onStdout = (chunk: string | Buffer) => {
406
+ stdout = appendBoundedOutput(stdout, chunk.toString());
407
+ };
408
+ const onStderr = (chunk: string | Buffer) => {
409
+ stderr = appendBoundedOutput(stderr, chunk.toString());
410
+ };
411
+
412
+ childProcess.stdout?.on('data', onStdout);
413
+ childProcess.stderr?.on('data', onStderr);
414
+
415
+ const startupAbortController = new AbortController();
416
+ const cleanup = () => {
417
+ startupAbortController.abort();
418
+ childProcess.stdout?.off('data', onStdout);
419
+ childProcess.stderr?.off('data', onStderr);
420
+ childProcess.removeAllListeners('error');
421
+ childProcess.removeAllListeners('close');
422
+ };
423
+
424
+ const earlyExit = new Promise<never>((_, reject) => {
425
+ childProcess.once('error', (error) => {
426
+ reject(
427
+ formatEmulatorStartupError({
428
+ name,
429
+ stdout,
430
+ stderr,
431
+ error,
432
+ }),
433
+ );
434
+ });
435
+
436
+ childProcess.once('close', (exitCode, signal) => {
437
+ reject(
438
+ formatEmulatorStartupError({
439
+ name,
440
+ stdout,
441
+ stderr,
442
+ exitCode,
443
+ signal,
444
+ }),
445
+ );
446
+ });
447
+ });
448
+
449
+ const observedBoot = waitForEmulator(name, startupAbortController.signal)
450
+ .then(() => 'booted' as const)
451
+ .catch((error: unknown) => {
452
+ if (startupAbortController.signal.aborted) {
453
+ return 'aborted' as const;
454
+ }
455
+
456
+ throw error;
457
+ });
458
+
459
+ const observationTimeout = wait(EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS).then(
460
+ () => 'timeout' as const,
461
+ );
462
+
463
+ try {
464
+ await Promise.race([earlyExit, observedBoot, observationTimeout]);
465
+ } finally {
466
+ cleanup();
467
+ }
468
+
469
+ childProcess.stdout?.destroy();
470
+ childProcess.stderr?.destroy();
471
+ childProcess.unref();
472
+ };
473
+
474
+ export const waitForEmulator = async (
475
+ name: string,
476
+ signal: AbortSignal,
477
+ ): Promise<string> => {
478
+ while (!signal.aborted) {
479
+ const adbIds = await getDeviceIds();
480
+
481
+ for (const adbId of adbIds) {
482
+ if (!adbId.startsWith('emulator-')) {
483
+ continue;
484
+ }
485
+
486
+ const emulatorName = await getEmulatorName(adbId);
487
+
488
+ if (emulatorName === name) {
489
+ return adbId;
490
+ }
491
+ }
492
+
493
+ await waitWithSignal(1000, signal);
494
+ }
495
+
496
+ throw signal.reason;
497
+ };
498
+
499
+ export const waitForEmulatorDisconnect = async (
500
+ adbId: string,
501
+ signal: AbortSignal,
502
+ ): Promise<void> => {
503
+ while (!signal.aborted) {
504
+ const adbIds = await getDeviceIds();
505
+
506
+ if (!adbIds.includes(adbId)) {
507
+ return;
508
+ }
509
+
510
+ await waitWithSignal(1000, signal);
511
+ }
512
+
513
+ throw signal.reason;
514
+ };
515
+
516
+ export const waitForBoot = async (
517
+ name: string,
518
+ signal: AbortSignal,
519
+ ): Promise<string> => {
520
+ while (!signal.aborted) {
521
+ const adbIds = await getDeviceIds();
522
+
523
+ for (const adbId of adbIds) {
524
+ if (!adbId.startsWith('emulator-')) {
525
+ continue;
526
+ }
527
+
528
+ const emulatorName = await getEmulatorName(adbId);
529
+
530
+ if (emulatorName !== name) {
531
+ continue;
532
+ }
533
+
534
+ if (await isBootCompleted(adbId)) {
535
+ return adbId;
536
+ }
537
+ }
538
+
539
+ await waitWithSignal(1000, signal);
540
+ }
541
+
542
+ throw signal.reason;
142
543
  };
143
544
 
144
545
  export const isAppRunning = async (
145
546
  adbId: string,
146
- bundleId: string
547
+ bundleId: string,
147
548
  ): Promise<boolean> => {
148
549
  try {
149
- const { stdout } = await spawn('adb', [
550
+ const { stdout } = await spawn(getAdbBinaryPath(), [
150
551
  '-s',
151
552
  adbId,
152
553
  'shell',
@@ -165,9 +566,9 @@ export const isAppRunning = async (
165
566
 
166
567
  export const getAppUid = async (
167
568
  adbId: string,
168
- bundleId: string
569
+ bundleId: string,
169
570
  ): Promise<number> => {
170
- const { stdout } = await spawn('adb', [
571
+ const { stdout } = await spawn(getAdbBinaryPath(), [
171
572
  '-s',
172
573
  adbId,
173
574
  'shell',
@@ -190,9 +591,9 @@ export const getAppUid = async (
190
591
 
191
592
  export const setHideErrorDialogs = async (
192
593
  adbId: string,
193
- hide: boolean
594
+ hide: boolean,
194
595
  ): Promise<void> => {
195
- await spawn('adb', [
596
+ await spawn(getAdbBinaryPath(), [
196
597
  '-s',
197
598
  adbId,
198
599
  'shell',
@@ -205,7 +606,7 @@ export const setHideErrorDialogs = async (
205
606
  };
206
607
 
207
608
  export const getLogcatTimestamp = async (adbId: string): Promise<string> => {
208
- const { stdout } = await spawn('adb', [
609
+ const { stdout } = await spawn(getAdbBinaryPath(), [
209
610
  '-s',
210
611
  adbId,
211
612
  'shell',
@@ -218,7 +619,8 @@ export const getLogcatTimestamp = async (adbId: string): Promise<string> => {
218
619
 
219
620
  export const getAvds = async (): Promise<string[]> => {
220
621
  try {
221
- const { stdout } = await spawn('emulator', ['-list-avds']);
622
+ const emulatorBinaryPath = await ensureEmulatorInstalled();
623
+ const { stdout } = await spawn(emulatorBinaryPath, ['-list-avds']);
222
624
  return stdout
223
625
  .split('\n')
224
626
  .map((line) => line.trim())
@@ -235,7 +637,7 @@ export type AdbDevice = {
235
637
  };
236
638
 
237
639
  export const getConnectedDevices = async (): Promise<AdbDevice[]> => {
238
- const { stdout } = await spawn('adb', ['devices', '-l']);
640
+ const { stdout } = await spawn(getAdbBinaryPath(), ['devices', '-l']);
239
641
  const lines = stdout.split('\n').slice(1);
240
642
  const devices: AdbDevice[] = [];
241
643