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