1import spawnAsync from '@expo/spawn-async';
2import chalk from 'chalk';
3import { spawn } from 'child_process';
4import os from 'os';
5
6import * as Log from '../../../log';
7import { Device, getAttachedDevicesAsync, isBootAnimationCompleteAsync } from './adb';
8
9export const EMULATOR_MAX_WAIT_TIMEOUT = 60 * 1000 * 3;
10
11export function whichEmulator(): string {
12  // https://developer.android.com/studio/command-line/variables
13  // TODO: Add ANDROID_SDK_ROOT support as well https://github.com/expo/expo/pull/16516#discussion_r820037917
14  if (process.env.ANDROID_HOME) {
15    return `${process.env.ANDROID_HOME}/emulator/emulator`;
16  }
17  return 'emulator';
18}
19
20/** Returns a list of emulator names. */
21export async function listAvdsAsync(): Promise<Device[]> {
22  try {
23    const { stdout } = await spawnAsync(whichEmulator(), ['-list-avds']);
24    return stdout
25      .split(os.EOL)
26      .filter(Boolean)
27      .map((name) => ({
28        name,
29        type: 'emulator',
30        // unsure from this
31        isBooted: false,
32        isAuthorized: true,
33      }));
34  } catch {
35    return [];
36  }
37}
38
39/** Start an Android device and wait until it is booted. */
40export async function startDeviceAsync(
41  device: Pick<Device, 'name'>,
42  {
43    timeout = EMULATOR_MAX_WAIT_TIMEOUT,
44    interval = 1000,
45  }: {
46    /** Time in milliseconds to wait before asserting a timeout error. */
47    timeout?: number;
48    interval?: number;
49  } = {}
50): Promise<Device> {
51  Log.log(`\u203A Opening emulator ${chalk.bold(device.name)}`);
52
53  // Start a process to open an emulator
54  const emulatorProcess = spawn(
55    whichEmulator(),
56    [
57      `@${device.name}`,
58      // disable animation for faster boot -- this might make it harder to detect if it mounted properly tho
59      //'-no-boot-anim'
60    ],
61    {
62      stdio: 'ignore',
63      detached: true,
64    }
65  );
66
67  emulatorProcess.unref();
68
69  return new Promise<Device>((resolve, reject) => {
70    const waitTimer = setInterval(async () => {
71      try {
72        const bootedDevices = await getAttachedDevicesAsync();
73        const connected = bootedDevices.find(({ name }) => name === device.name);
74        if (connected) {
75          const isBooted = await isBootAnimationCompleteAsync(connected.pid);
76          if (isBooted) {
77            stopWaiting();
78            resolve(connected);
79          }
80        }
81      } catch (error) {
82        stopWaiting();
83        reject(error);
84      }
85    }, interval);
86
87    // Reject command after timeout
88    const maxTimer = setTimeout(() => {
89      const manualCommand = `${whichEmulator()} @${device.name}`;
90      stopWaitingAndReject(
91        `It took too long to start the Android emulator: ${device.name}. You can try starting the emulator manually from the terminal with: ${manualCommand}`
92      );
93    }, timeout);
94
95    const stopWaiting = () => {
96      clearTimeout(maxTimer);
97      clearInterval(waitTimer);
98    };
99
100    const stopWaitingAndReject = (message: string) => {
101      stopWaiting();
102      reject(new Error(message));
103    };
104
105    emulatorProcess.on('error', ({ message }) => stopWaitingAndReject(message));
106
107    emulatorProcess.on('exit', () => {
108      const manualCommand = `${whichEmulator()} @${device.name}`;
109      stopWaitingAndReject(
110        `The emulator (${device.name}) quit before it finished opening. You can try starting the emulator manually from the terminal with: ${manualCommand}`
111      );
112    });
113  });
114}
115