18d307f52SEvan Baconimport assert from 'assert';
28d307f52SEvan Baconimport chalk from 'chalk';
38d307f52SEvan Bacon
4*8a424bebSJames Ideimport { activateWindowAsync } from './activateWindow';
5*8a424bebSJames Ideimport * as AndroidDebugBridge from './adb';
6*8a424bebSJames Ideimport { startDeviceAsync } from './emulator';
7*8a424bebSJames Ideimport { getDevicesAsync } from './getDevices';
8*8a424bebSJames Ideimport { promptForDeviceAsync } from './promptAndroidDevice';
98d307f52SEvan Baconimport * as Log from '../../../log';
108d307f52SEvan Baconimport { AbortCommandError, CommandError } from '../../../utils/errors';
118d307f52SEvan Baconimport { validateUrl } from '../../../utils/url';
128d307f52SEvan Baconimport { DeviceManager } from '../DeviceManager';
138d307f52SEvan Baconimport { ExpoGoInstaller } from '../ExpoGoInstaller';
148d307f52SEvan Baconimport { BaseResolveDeviceProps } from '../PlatformManager';
158d307f52SEvan Bacon
168d307f52SEvan Baconconst EXPO_GO_APPLICATION_IDENTIFIER = 'host.exp.exponent';
178d307f52SEvan Bacon
188d307f52SEvan Baconexport class AndroidDeviceManager extends DeviceManager<AndroidDebugBridge.Device> {
193d6e487dSEvan Bacon  static async resolveFromNameAsync(name: string): Promise<AndroidDeviceManager> {
203d6e487dSEvan Bacon    const devices = await getDevicesAsync();
213d6e487dSEvan Bacon    const device = devices.find((device) => device.name === name);
223d6e487dSEvan Bacon
233d6e487dSEvan Bacon    if (!device) {
243d6e487dSEvan Bacon      throw new CommandError('Could not find device with name: ' + name);
253d6e487dSEvan Bacon    }
263d6e487dSEvan Bacon    return AndroidDeviceManager.resolveAsync({ device, shouldPrompt: false });
273d6e487dSEvan Bacon  }
283d6e487dSEvan Bacon
298d307f52SEvan Bacon  static async resolveAsync({
308d307f52SEvan Bacon    device,
318d307f52SEvan Bacon    shouldPrompt,
328d307f52SEvan Bacon  }: BaseResolveDeviceProps<AndroidDebugBridge.Device> = {}): Promise<AndroidDeviceManager> {
338d307f52SEvan Bacon    if (device) {
348d307f52SEvan Bacon      const manager = new AndroidDeviceManager(device);
358d307f52SEvan Bacon      if (!(await manager.attemptToStartAsync())) {
368d307f52SEvan Bacon        throw new AbortCommandError();
378d307f52SEvan Bacon      }
388d307f52SEvan Bacon      return manager;
398d307f52SEvan Bacon    }
408d307f52SEvan Bacon
418d307f52SEvan Bacon    const devices = await getDevicesAsync();
428d307f52SEvan Bacon    const _device = shouldPrompt ? await promptForDeviceAsync(devices) : devices[0];
438d307f52SEvan Bacon    return AndroidDeviceManager.resolveAsync({ device: _device, shouldPrompt: false });
448d307f52SEvan Bacon  }
458d307f52SEvan Bacon
468d307f52SEvan Bacon  get name() {
478d307f52SEvan Bacon    // TODO: Maybe strip `_` from the device name?
488d307f52SEvan Bacon    return this.device.name;
498d307f52SEvan Bacon  }
508d307f52SEvan Bacon
518d307f52SEvan Bacon  get identifier(): string {
528d307f52SEvan Bacon    return this.device.pid ?? 'unknown';
538d307f52SEvan Bacon  }
548d307f52SEvan Bacon
558d307f52SEvan Bacon  async getAppVersionAsync(applicationId: string): Promise<string | null> {
568d307f52SEvan Bacon    const info = await AndroidDebugBridge.getPackageInfoAsync(this.device, {
578d307f52SEvan Bacon      appId: applicationId,
588d307f52SEvan Bacon    });
598d307f52SEvan Bacon
608d307f52SEvan Bacon    const regex = /versionName=([0-9.]+)/;
618d307f52SEvan Bacon    return regex.exec(info)?.[1] ?? null;
628d307f52SEvan Bacon  }
638d307f52SEvan Bacon
648d307f52SEvan Bacon  protected async attemptToStartAsync(): Promise<AndroidDebugBridge.Device | null> {
658d307f52SEvan Bacon    // TODO: Add a light-weight method for checking since a device could disconnect.
668d307f52SEvan Bacon    if (!(await AndroidDebugBridge.isDeviceBootedAsync(this.device))) {
678d307f52SEvan Bacon      this.device = await startDeviceAsync(this.device);
688d307f52SEvan Bacon    }
698d307f52SEvan Bacon
703d6e487dSEvan Bacon    if (this.device.isAuthorized === false) {
718d307f52SEvan Bacon      AndroidDebugBridge.logUnauthorized(this.device);
728d307f52SEvan Bacon      return null;
738d307f52SEvan Bacon    }
748d307f52SEvan Bacon
758d307f52SEvan Bacon    return this.device;
768d307f52SEvan Bacon  }
778d307f52SEvan Bacon
788d307f52SEvan Bacon  async startAsync(): Promise<AndroidDebugBridge.Device> {
798d307f52SEvan Bacon    const device = await this.attemptToStartAsync();
808d307f52SEvan Bacon    assert(device, `Failed to boot emulator.`);
818d307f52SEvan Bacon    return this.device;
828d307f52SEvan Bacon  }
838d307f52SEvan Bacon
848d307f52SEvan Bacon  async installAppAsync(binaryPath: string) {
858d307f52SEvan Bacon    await AndroidDebugBridge.installAsync(this.device, {
868d307f52SEvan Bacon      filePath: binaryPath,
878d307f52SEvan Bacon    });
888d307f52SEvan Bacon  }
898d307f52SEvan Bacon
908d307f52SEvan Bacon  async uninstallAppAsync(appId: string) {
918d307f52SEvan Bacon    // we need to check if the app is installed, else we might bump into "Failure [DELETE_FAILED_INTERNAL_ERROR]"
928d307f52SEvan Bacon    const isInstalled = await this.isAppInstalledAsync(appId);
938d307f52SEvan Bacon    if (!isInstalled) {
948d307f52SEvan Bacon      return;
958d307f52SEvan Bacon    }
968d307f52SEvan Bacon
978d307f52SEvan Bacon    try {
988d307f52SEvan Bacon      await AndroidDebugBridge.uninstallAsync(this.device, {
998d307f52SEvan Bacon        appId,
1008d307f52SEvan Bacon      });
1018d307f52SEvan Bacon    } catch (e) {
1028d307f52SEvan Bacon      Log.error(
1038d307f52SEvan Bacon        `Could not uninstall app "${appId}" from your device, please uninstall it manually and try again.`
1048d307f52SEvan Bacon      );
1058d307f52SEvan Bacon      throw e;
1068d307f52SEvan Bacon    }
1078d307f52SEvan Bacon  }
1088d307f52SEvan Bacon
1098d307f52SEvan Bacon  /**
1108d307f52SEvan Bacon   * @param launchActivity Activity to launch `[application identifier]/.[main activity name]`, ex: `com.bacon.app/.MainActivity`
1118d307f52SEvan Bacon   */
1128d307f52SEvan Bacon  async launchActivityAsync(launchActivity: string): Promise<string> {
1138d307f52SEvan Bacon    try {
1148d307f52SEvan Bacon      return await AndroidDebugBridge.launchActivityAsync(this.device, {
1158d307f52SEvan Bacon        launchActivity,
1168d307f52SEvan Bacon      });
11729975bfdSEvan Bacon    } catch (error: any) {
1188d307f52SEvan Bacon      let errorMessage = `Couldn't open Android app with activity "${launchActivity}" on device "${this.name}".`;
1198d307f52SEvan Bacon      if (error instanceof CommandError && error.code === 'APP_NOT_INSTALLED') {
1208d307f52SEvan Bacon        errorMessage += `\nThe app might not be installed, try installing it with: ${chalk.bold(
1218d307f52SEvan Bacon          `npx expo run:android -d ${this.name}`
1228d307f52SEvan Bacon        )}`;
1238d307f52SEvan Bacon      }
1248d307f52SEvan Bacon      errorMessage += chalk.gray(`\n${error.message}`);
1258d307f52SEvan Bacon      error.message = errorMessage;
1268d307f52SEvan Bacon      throw error;
1278d307f52SEvan Bacon    }
1288d307f52SEvan Bacon  }
1298d307f52SEvan Bacon
1308d307f52SEvan Bacon  async isAppInstalledAsync(applicationId: string) {
1318d307f52SEvan Bacon    return await AndroidDebugBridge.isPackageInstalledAsync(this.device, applicationId);
1328d307f52SEvan Bacon  }
1338d307f52SEvan Bacon
1348d307f52SEvan Bacon  async openUrlAsync(url: string) {
1358d307f52SEvan Bacon    // Non-compliant URLs will be treated as application identifiers.
1368d307f52SEvan Bacon    if (!validateUrl(url, { requireProtocol: true })) {
1378d307f52SEvan Bacon      await this.launchActivityAsync(url);
1388d307f52SEvan Bacon      return;
1398d307f52SEvan Bacon    }
1408d307f52SEvan Bacon
1418d307f52SEvan Bacon    const parsed = new URL(url);
1428d307f52SEvan Bacon
1438d307f52SEvan Bacon    if (parsed.protocol === 'exp:') {
1448d307f52SEvan Bacon      // NOTE(brentvatne): temporary workaround! launch Expo Go first, then
1458d307f52SEvan Bacon      // launch the project!
1468d307f52SEvan Bacon      // https://github.com/expo/expo/issues/7772
1478d307f52SEvan Bacon      // adb shell monkey -p host.exp.exponent -c android.intent.category.LAUNCHER 1
1488d307f52SEvan Bacon      // Note: this is not needed in Expo Development Client, it only applies to Expo Go
1498d307f52SEvan Bacon      await AndroidDebugBridge.openAppIdAsync(
1508d307f52SEvan Bacon        { pid: this.device.pid },
1518d307f52SEvan Bacon        { applicationId: EXPO_GO_APPLICATION_IDENTIFIER }
1528d307f52SEvan Bacon      );
1538d307f52SEvan Bacon    }
1548d307f52SEvan Bacon
1558d307f52SEvan Bacon    await AndroidDebugBridge.openUrlAsync({ pid: this.device.pid }, { url });
1568d307f52SEvan Bacon  }
1578d307f52SEvan Bacon
1588d307f52SEvan Bacon  async activateWindowAsync() {
1598d307f52SEvan Bacon    // Bring the emulator window to the front on macos devices.
1608d307f52SEvan Bacon    await activateWindowAsync(this.device);
1618d307f52SEvan Bacon  }
1628d307f52SEvan Bacon
1638d307f52SEvan Bacon  async ensureExpoGoAsync(sdkVersion?: string): Promise<boolean> {
1648d307f52SEvan Bacon    const installer = new ExpoGoInstaller('android', EXPO_GO_APPLICATION_IDENTIFIER, sdkVersion);
1658d307f52SEvan Bacon    return installer.ensureAsync(this);
1668d307f52SEvan Bacon  }
1678d307f52SEvan Bacon}
168