18d307f52SEvan Baconimport chalk from 'chalk'; 28d307f52SEvan Baconimport os from 'os'; 38d307f52SEvan Bacon 4*8a424bebSJames Ideimport { ADBServer } from './ADBServer'; 58d307f52SEvan Baconimport * as Log from '../../../log'; 68d307f52SEvan Baconimport { CommandError } from '../../../utils/errors'; 78d307f52SEvan Baconimport { learnMore } from '../../../utils/link'; 88d307f52SEvan Bacon 9474a7a4bSEvan Baconconst debug = require('debug')('expo:start:platforms:android:adb') as typeof console.log; 10474a7a4bSEvan Bacon 113d6e487dSEvan Baconexport enum DeviceABI { 123d6e487dSEvan Bacon // The arch specific android target platforms are soft-deprecated. 133d6e487dSEvan Bacon // Instead of using TargetPlatform as a combination arch + platform 143d6e487dSEvan Bacon // the code will be updated to carry arch information in [DarwinArch] 153d6e487dSEvan Bacon // and [AndroidArch]. 163d6e487dSEvan Bacon arm = 'arm', 173d6e487dSEvan Bacon arm64 = 'arm64', 183d6e487dSEvan Bacon x64 = 'x64', 193d6e487dSEvan Bacon x86 = 'x86', 203d6e487dSEvan Bacon armeabiV7a = 'armeabi-v7a', 213d6e487dSEvan Bacon armeabi = 'armeabi', 223d6e487dSEvan Bacon universal = 'universal', 233d6e487dSEvan Bacon} 243d6e487dSEvan Bacon 258d307f52SEvan Bacon/** Represents a connected Android device. */ 268d307f52SEvan Baconexport type Device = { 278d307f52SEvan Bacon /** Process ID. */ 288d307f52SEvan Bacon pid?: string; 298d307f52SEvan Bacon /** Name of the device, also used as the ID for opening devices. */ 308d307f52SEvan Bacon name: string; 318d307f52SEvan Bacon /** Is emulator or connected device. */ 328d307f52SEvan Bacon type: 'emulator' | 'device'; 338d307f52SEvan Bacon /** Is the device booted (emulator). */ 348d307f52SEvan Bacon isBooted: boolean; 358d307f52SEvan Bacon /** Is device authorized for developing. https://expo.fyi/authorize-android-device */ 368d307f52SEvan Bacon isAuthorized: boolean; 378d307f52SEvan Bacon}; 388d307f52SEvan Bacon 398d307f52SEvan Bacontype DeviceContext = Pick<Device, 'pid'>; 408d307f52SEvan Bacon 418d307f52SEvan Bacontype DeviceProperties = Record<string, string>; 428d307f52SEvan Bacon 438d307f52SEvan Baconconst CANT_START_ACTIVITY_ERROR = 'Activity not started, unable to resolve Intent'; 443d6e487dSEvan Bacon// http://developer.android.com/ndk/guides/abis.html 453d6e487dSEvan Baconconst PROP_CPU_NAME = 'ro.product.cpu.abi'; 463d6e487dSEvan Bacon 473d6e487dSEvan Baconconst PROP_CPU_ABI_LIST_NAME = 'ro.product.cpu.abilist'; 483d6e487dSEvan Bacon 493d6e487dSEvan Bacon// Can sometimes be null 503d6e487dSEvan Bacon// http://developer.android.com/ndk/guides/abis.html 513d6e487dSEvan Baconconst PROP_BOOT_ANIMATION_STATE = 'init.svc.bootanim'; 528d307f52SEvan Bacon 538d307f52SEvan Baconlet _server: ADBServer | null; 548d307f52SEvan Bacon 558d307f52SEvan Bacon/** Return the lazily loaded ADB server instance. */ 568d307f52SEvan Baconexport function getServer() { 578d307f52SEvan Bacon _server ??= new ADBServer(); 588d307f52SEvan Bacon return _server; 598d307f52SEvan Bacon} 608d307f52SEvan Bacon 618d307f52SEvan Bacon/** Logs an FYI message about authorizing your device. */ 628d307f52SEvan Baconexport function logUnauthorized(device: Device) { 638d307f52SEvan Bacon Log.warn( 648d307f52SEvan Bacon `\nThis computer is not authorized for developing on ${chalk.bold(device.name)}. ${chalk.dim( 658d307f52SEvan Bacon learnMore('https://expo.fyi/authorize-android-device') 668d307f52SEvan Bacon )}` 678d307f52SEvan Bacon ); 688d307f52SEvan Bacon} 698d307f52SEvan Bacon 708d307f52SEvan Bacon/** Returns true if the provided package name is installed on the provided Android device. */ 718d307f52SEvan Baconexport async function isPackageInstalledAsync( 728d307f52SEvan Bacon device: DeviceContext, 738d307f52SEvan Bacon androidPackage: string 748d307f52SEvan Bacon): Promise<boolean> { 758d307f52SEvan Bacon const packages = await getServer().runAsync( 768d307f52SEvan Bacon adbArgs(device.pid, 'shell', 'pm', 'list', 'packages', androidPackage) 778d307f52SEvan Bacon ); 788d307f52SEvan Bacon 798d307f52SEvan Bacon const lines = packages.split(/\r?\n/); 808d307f52SEvan Bacon for (let i = 0; i < lines.length; i++) { 818d307f52SEvan Bacon const line = lines[i].trim(); 828d307f52SEvan Bacon if (line === `package:${androidPackage}`) { 838d307f52SEvan Bacon return true; 848d307f52SEvan Bacon } 858d307f52SEvan Bacon } 868d307f52SEvan Bacon return false; 878d307f52SEvan Bacon} 888d307f52SEvan Bacon 898d307f52SEvan Bacon/** 908d307f52SEvan Bacon * @param device.pid Process ID of the Android device to launch. 918d307f52SEvan Bacon * @param props.launchActivity Activity to launch `[application identifier]/.[main activity name]`, ex: `com.bacon.app/.MainActivity` 928d307f52SEvan Bacon */ 938d307f52SEvan Baconexport async function launchActivityAsync( 948d307f52SEvan Bacon device: DeviceContext, 958d307f52SEvan Bacon { 968d307f52SEvan Bacon launchActivity, 978d307f52SEvan Bacon }: { 988d307f52SEvan Bacon launchActivity: string; 998d307f52SEvan Bacon } 1008d307f52SEvan Bacon) { 1018d307f52SEvan Bacon return openAsync( 1028d307f52SEvan Bacon adbArgs( 1038d307f52SEvan Bacon device.pid, 1048d307f52SEvan Bacon 'shell', 1058d307f52SEvan Bacon 'am', 1068d307f52SEvan Bacon 'start', 1078d307f52SEvan Bacon // FLAG_ACTIVITY_SINGLE_TOP -- If set, the activity will not be launched if it is already running at the top of the history stack. 1088d307f52SEvan Bacon '-f', 1098d307f52SEvan Bacon '0x20000000', 1108d307f52SEvan Bacon // Activity to open first: com.bacon.app/.MainActivity 1118d307f52SEvan Bacon '-n', 1128d307f52SEvan Bacon launchActivity 1138d307f52SEvan Bacon ) 1148d307f52SEvan Bacon ); 1158d307f52SEvan Bacon} 1168d307f52SEvan Bacon 1178d307f52SEvan Bacon/** 1188d307f52SEvan Bacon * @param device.pid Process ID of the Android device to launch. 1198d307f52SEvan Bacon * @param props.applicationId package name to launch. 1208d307f52SEvan Bacon */ 1218d307f52SEvan Baconexport async function openAppIdAsync( 1228d307f52SEvan Bacon device: DeviceContext, 1238d307f52SEvan Bacon { 1248d307f52SEvan Bacon applicationId, 1258d307f52SEvan Bacon }: { 1268d307f52SEvan Bacon applicationId: string; 1278d307f52SEvan Bacon } 1288d307f52SEvan Bacon) { 1298d307f52SEvan Bacon return openAsync( 1308d307f52SEvan Bacon adbArgs( 1318d307f52SEvan Bacon device.pid, 1328d307f52SEvan Bacon 'shell', 1338d307f52SEvan Bacon 'monkey', 1348d307f52SEvan Bacon '-p', 1358d307f52SEvan Bacon applicationId, 1368d307f52SEvan Bacon '-c', 1378d307f52SEvan Bacon 'android.intent.category.LAUNCHER', 1388d307f52SEvan Bacon '1' 1398d307f52SEvan Bacon ) 1408d307f52SEvan Bacon ); 1418d307f52SEvan Bacon} 1428d307f52SEvan Bacon 1438d307f52SEvan Bacon/** 1448d307f52SEvan Bacon * @param device.pid Process ID of the Android device to launch. 1458d307f52SEvan Bacon * @param props.url URL to launch. 1468d307f52SEvan Bacon */ 1478d307f52SEvan Baconexport async function openUrlAsync( 1488d307f52SEvan Bacon device: DeviceContext, 1498d307f52SEvan Bacon { 1508d307f52SEvan Bacon url, 1518d307f52SEvan Bacon }: { 1528d307f52SEvan Bacon url: string; 1538d307f52SEvan Bacon } 1548d307f52SEvan Bacon) { 1558d307f52SEvan Bacon return openAsync( 1568ce30dc2SEvan Bacon adbArgs( 1578ce30dc2SEvan Bacon device.pid, 1588ce30dc2SEvan Bacon 'shell', 1598ce30dc2SEvan Bacon 'am', 1608ce30dc2SEvan Bacon 'start', 1618ce30dc2SEvan Bacon '-a', 1628ce30dc2SEvan Bacon 'android.intent.action.VIEW', 1638ce30dc2SEvan Bacon '-d', 1648ce30dc2SEvan Bacon // ADB requires ampersands to be escaped. 1658ce30dc2SEvan Bacon url.replace(/&/g, String.raw`\&`) 1668ce30dc2SEvan Bacon ) 1678d307f52SEvan Bacon ); 1688d307f52SEvan Bacon} 1698d307f52SEvan Bacon 1708d307f52SEvan Bacon/** Runs a generic command watches for common errors in order to throw with an expected code. */ 1718d307f52SEvan Baconasync function openAsync(args: string[]): Promise<string> { 1728d307f52SEvan Bacon const results = await getServer().runAsync(args); 1738d307f52SEvan Bacon if ( 1748d307f52SEvan Bacon results.includes(CANT_START_ACTIVITY_ERROR) || 1758d307f52SEvan Bacon results.match(/Error: Activity class .* does not exist\./g) 1768d307f52SEvan Bacon ) { 1778d307f52SEvan Bacon throw new CommandError('APP_NOT_INSTALLED', results.substring(results.indexOf('Error: '))); 1788d307f52SEvan Bacon } 1798d307f52SEvan Bacon return results; 1808d307f52SEvan Bacon} 1818d307f52SEvan Bacon 1828d307f52SEvan Bacon/** Uninstall an app given its Android package name. */ 1838d307f52SEvan Baconexport async function uninstallAsync( 1848d307f52SEvan Bacon device: DeviceContext, 1858d307f52SEvan Bacon { appId }: { appId: string } 1868d307f52SEvan Bacon): Promise<string> { 1878d307f52SEvan Bacon return await getServer().runAsync(adbArgs(device.pid, 'uninstall', appId)); 1888d307f52SEvan Bacon} 1898d307f52SEvan Bacon 1908d307f52SEvan Bacon/** Get package info from an app based on its Android package name. */ 1918d307f52SEvan Baconexport async function getPackageInfoAsync( 1928d307f52SEvan Bacon device: DeviceContext, 1938d307f52SEvan Bacon { appId }: { appId: string } 1948d307f52SEvan Bacon): Promise<string> { 1958d307f52SEvan Bacon return await getServer().runAsync(adbArgs(device.pid, 'shell', 'dumpsys', 'package', appId)); 1968d307f52SEvan Bacon} 1978d307f52SEvan Bacon 1988d307f52SEvan Bacon/** Install an app on a connected device. */ 1998d307f52SEvan Baconexport async function installAsync(device: DeviceContext, { filePath }: { filePath: string }) { 2008d307f52SEvan Bacon // TODO: Handle the `INSTALL_FAILED_INSUFFICIENT_STORAGE` error. 2018d307f52SEvan Bacon return await getServer().runAsync(adbArgs(device.pid, 'install', '-r', '-d', filePath)); 2028d307f52SEvan Bacon} 2038d307f52SEvan Bacon 2048d307f52SEvan Bacon/** Format ADB args with process ID. */ 2058d307f52SEvan Baconexport function adbArgs(pid: Device['pid'], ...options: string[]): string[] { 2068d307f52SEvan Bacon const args = []; 2078d307f52SEvan Bacon if (pid) { 2088d307f52SEvan Bacon args.push('-s', pid); 2098d307f52SEvan Bacon } 2108d307f52SEvan Bacon return args.concat(options); 2118d307f52SEvan Bacon} 2128d307f52SEvan Bacon 2138d307f52SEvan Bacon// TODO: This is very expensive for some operations. 2148d307f52SEvan Baconexport async function getAttachedDevicesAsync(): Promise<Device[]> { 2158d307f52SEvan Bacon const output = await getServer().runAsync(['devices', '-l']); 2168d307f52SEvan Bacon 2178d307f52SEvan Bacon const splitItems = output.trim().replace(/\n$/, '').split(os.EOL); 2188d307f52SEvan Bacon // First line is `"List of devices attached"`, remove it 2198d307f52SEvan Bacon // @ts-ignore: todo 2208d307f52SEvan Bacon const attachedDevices: { 2218d307f52SEvan Bacon props: string[]; 2228d307f52SEvan Bacon type: Device['type']; 2238d307f52SEvan Bacon isAuthorized: Device['isAuthorized']; 2248d307f52SEvan Bacon }[] = splitItems 2258d307f52SEvan Bacon .slice(1, splitItems.length) 2268d307f52SEvan Bacon .map((line) => { 2278d307f52SEvan Bacon // unauthorized: ['FA8251A00719', 'unauthorized', 'usb:338690048X', 'transport_id:5'] 2288d307f52SEvan Bacon // authorized: ['FA8251A00719', 'device', 'usb:336592896X', 'product:walleye', 'model:Pixel_2', 'device:walleye', 'transport_id:4'] 2298d307f52SEvan Bacon // emulator: ['emulator-5554', 'offline', 'transport_id:1'] 2308d307f52SEvan Bacon const props = line.split(' ').filter(Boolean); 2318d307f52SEvan Bacon 2328d307f52SEvan Bacon const isAuthorized = props[1] !== 'unauthorized'; 2338d307f52SEvan Bacon const type = line.includes('emulator') ? 'emulator' : 'device'; 2348d307f52SEvan Bacon return { props, type, isAuthorized }; 2358d307f52SEvan Bacon }) 2368d307f52SEvan Bacon .filter(({ props: [pid] }) => !!pid); 2378d307f52SEvan Bacon 2388d307f52SEvan Bacon const devicePromises = attachedDevices.map<Promise<Device>>(async (props) => { 2398d307f52SEvan Bacon const { 2408d307f52SEvan Bacon type, 2418d307f52SEvan Bacon props: [pid, ...deviceInfo], 2428d307f52SEvan Bacon isAuthorized, 2438d307f52SEvan Bacon } = props; 2448d307f52SEvan Bacon 2458d307f52SEvan Bacon let name: string | null = null; 2468d307f52SEvan Bacon 2478d307f52SEvan Bacon if (type === 'device') { 2488d307f52SEvan Bacon if (isAuthorized) { 2498d307f52SEvan Bacon // Possibly formatted like `model:Pixel_2` 2508d307f52SEvan Bacon // Transform to `Pixel_2` 2518d307f52SEvan Bacon const modelItem = deviceInfo.find((info) => info.includes('model:')); 2528d307f52SEvan Bacon if (modelItem) { 2538d307f52SEvan Bacon name = modelItem.replace('model:', ''); 2548d307f52SEvan Bacon } 2558d307f52SEvan Bacon } 2568d307f52SEvan Bacon // unauthorized devices don't have a name available to read 2578d307f52SEvan Bacon if (!name) { 2588d307f52SEvan Bacon // Device FA8251A00719 2598d307f52SEvan Bacon name = `Device ${pid}`; 2608d307f52SEvan Bacon } 2618d307f52SEvan Bacon } else { 2628d307f52SEvan Bacon // Given an emulator pid, get the emulator name which can be used to start the emulator later. 2638d307f52SEvan Bacon name = (await getAdbNameForDeviceIdAsync({ pid })) ?? ''; 2648d307f52SEvan Bacon } 2658d307f52SEvan Bacon 2668d307f52SEvan Bacon return { 2678d307f52SEvan Bacon pid, 2688d307f52SEvan Bacon name, 2698d307f52SEvan Bacon type, 2708d307f52SEvan Bacon isAuthorized, 2718d307f52SEvan Bacon isBooted: true, 2728d307f52SEvan Bacon }; 2738d307f52SEvan Bacon }); 2748d307f52SEvan Bacon 2758d307f52SEvan Bacon return Promise.all(devicePromises); 2768d307f52SEvan Bacon} 2778d307f52SEvan Bacon 2788d307f52SEvan Bacon/** 2798d307f52SEvan Bacon * Return the Emulator name for an emulator ID, this can be used to determine if an emulator is booted. 2808d307f52SEvan Bacon * 2818d307f52SEvan Bacon * @param device.pid a value like `emulator-5554` from `abd devices` 2828d307f52SEvan Bacon */ 2838d307f52SEvan Baconexport async function getAdbNameForDeviceIdAsync(device: DeviceContext): Promise<string | null> { 2848d307f52SEvan Bacon const results = await getServer().runAsync(adbArgs(device.pid, 'emu', 'avd', 'name')); 2858d307f52SEvan Bacon 2868d307f52SEvan Bacon if (results.match(/could not connect to TCP port .*: Connection refused/)) { 2878d307f52SEvan Bacon // Can also occur when the emulator does not exist. 2888d307f52SEvan Bacon throw new CommandError('EMULATOR_NOT_FOUND', results); 2898d307f52SEvan Bacon } 2908d307f52SEvan Bacon 291cc6b646aSCedric van Putten return sanitizeAdbDeviceName(results) ?? null; 2928d307f52SEvan Bacon} 2938d307f52SEvan Bacon 2948d307f52SEvan Baconexport async function isDeviceBootedAsync({ 2958d307f52SEvan Bacon name, 2968d307f52SEvan Bacon}: { name?: string } = {}): Promise<Device | null> { 2978d307f52SEvan Bacon const devices = await getAttachedDevicesAsync(); 2988d307f52SEvan Bacon 2998d307f52SEvan Bacon if (!name) { 3008d307f52SEvan Bacon return devices[0] ?? null; 3018d307f52SEvan Bacon } 3028d307f52SEvan Bacon 3038d307f52SEvan Bacon return devices.find((device) => device.name === name) ?? null; 3048d307f52SEvan Bacon} 3058d307f52SEvan Bacon 3068d307f52SEvan Bacon/** 3078d307f52SEvan Bacon * Returns true when a device's splash screen animation has stopped. 3088d307f52SEvan Bacon * This can be used to detect when a device is fully booted and ready to use. 3098d307f52SEvan Bacon * 3108d307f52SEvan Bacon * @param pid 3118d307f52SEvan Bacon */ 3128d307f52SEvan Baconexport async function isBootAnimationCompleteAsync(pid?: string): Promise<boolean> { 3138d307f52SEvan Bacon try { 3148d307f52SEvan Bacon const props = await getPropertyDataForDeviceAsync({ pid }, PROP_BOOT_ANIMATION_STATE); 3158d307f52SEvan Bacon return !!props[PROP_BOOT_ANIMATION_STATE].match(/stopped/); 3168d307f52SEvan Bacon } catch { 3178d307f52SEvan Bacon return false; 3188d307f52SEvan Bacon } 3198d307f52SEvan Bacon} 3208d307f52SEvan Bacon 3213d6e487dSEvan Bacon/** Get a list of ABIs for the provided device. */ 3223d6e487dSEvan Baconexport async function getDeviceABIsAsync( 3233d6e487dSEvan Bacon device: Pick<Device, 'name' | 'pid'> 3243d6e487dSEvan Bacon): Promise<DeviceABI[]> { 3253d6e487dSEvan Bacon const cpuAbiList = (await getPropertyDataForDeviceAsync(device, PROP_CPU_ABI_LIST_NAME))[ 3263d6e487dSEvan Bacon PROP_CPU_ABI_LIST_NAME 3273d6e487dSEvan Bacon ]; 3283d6e487dSEvan Bacon 3293d6e487dSEvan Bacon if (cpuAbiList) { 3303d6e487dSEvan Bacon return cpuAbiList.trim().split(',') as DeviceABI[]; 3313d6e487dSEvan Bacon } 3323d6e487dSEvan Bacon 3333d6e487dSEvan Bacon const abi = (await getPropertyDataForDeviceAsync(device, PROP_CPU_NAME))[ 3343d6e487dSEvan Bacon PROP_CPU_NAME 3353d6e487dSEvan Bacon ] as DeviceABI; 3363d6e487dSEvan Bacon return [abi]; 3373d6e487dSEvan Bacon} 3383d6e487dSEvan Bacon 3398d307f52SEvan Baconexport async function getPropertyDataForDeviceAsync( 3408d307f52SEvan Bacon device: DeviceContext, 3418d307f52SEvan Bacon prop?: string 3428d307f52SEvan Bacon): Promise<DeviceProperties> { 3438d307f52SEvan Bacon // @ts-ignore 3448d307f52SEvan Bacon const propCommand = adbArgs(...[device.pid, 'shell', 'getprop', prop].filter(Boolean)); 3458d307f52SEvan Bacon try { 3468d307f52SEvan Bacon // Prevent reading as UTF8. 3478d307f52SEvan Bacon const results = await getServer().getFileOutputAsync(propCommand); 3488d307f52SEvan Bacon // Like: 3498d307f52SEvan Bacon // [wifi.direct.interface]: [p2p-dev-wlan0] 3508d307f52SEvan Bacon // [wifi.interface]: [wlan0] 3518d307f52SEvan Bacon 3528d307f52SEvan Bacon if (prop) { 353474a7a4bSEvan Bacon debug(`Property data: (device pid: ${device.pid}, prop: ${prop}, data: ${results})`); 3548d307f52SEvan Bacon return { 3558d307f52SEvan Bacon [prop]: results, 3568d307f52SEvan Bacon }; 3578d307f52SEvan Bacon } 3583d6e487dSEvan Bacon const props = parseAdbDeviceProperties(results); 3593d6e487dSEvan Bacon 360474a7a4bSEvan Bacon debug(`Parsed data:`, props); 3613d6e487dSEvan Bacon 3623d6e487dSEvan Bacon return props; 36329975bfdSEvan Bacon } catch (error: any) { 3648d307f52SEvan Bacon // TODO: Ensure error has message and not stderr 36529975bfdSEvan Bacon throw new CommandError(`Failed to get properties for device (${device.pid}): ${error.message}`); 3668d307f52SEvan Bacon } 3678d307f52SEvan Bacon} 3688d307f52SEvan Bacon 3698d307f52SEvan Baconfunction parseAdbDeviceProperties(devicePropertiesString: string) { 3708d307f52SEvan Bacon const properties: DeviceProperties = {}; 3718d307f52SEvan Bacon const propertyExp = /\[(.*?)\]: \[(.*?)\]/gm; 3728d307f52SEvan Bacon for (const match of devicePropertiesString.matchAll(propertyExp)) { 3738d307f52SEvan Bacon properties[match[1]] = match[2]; 3748d307f52SEvan Bacon } 3758d307f52SEvan Bacon return properties; 3768d307f52SEvan Bacon} 377cc6b646aSCedric van Putten 378cc6b646aSCedric van Putten/** 379cc6b646aSCedric van Putten * Sanitize the ADB device name to only get the actual device name. 380cc6b646aSCedric van Putten * On Windows, we need to do \r, \n, and \r\n filtering to get the name. 381cc6b646aSCedric van Putten */ 382cc6b646aSCedric van Puttenexport function sanitizeAdbDeviceName(deviceName: string) { 383cc6b646aSCedric van Putten return deviceName 384cc6b646aSCedric van Putten .trim() 385cc6b646aSCedric van Putten .split(/[\r\n]+/) 386cc6b646aSCedric van Putten .shift(); 387cc6b646aSCedric van Putten} 388