import { ExpoConfig , getConfig , readExpRcAsync } from '@expo/config' ; import { AndroidConfig } from '@expo/config-plugins' ; import * as osascript from '@expo/osascript' ; import spawnAsync from '@expo/spawn-async' ; import chalk from 'chalk' ; import child_process , { execFileSync } from 'child_process' ; import trim from 'lodash/trim' ; import os from 'os' ; import prompts from 'prompts' ; import semver from 'semver' ; import { Analytics , Binaries , downloadApkAsync , Env , ImageUtils , isDevClientPackageInstalled , learnMore , LoadingEvent , Logger , ProjectSettings , Prompts , UrlUtils , Versions , Webpack , XDLError , } from './internal' ; export type Device = { pid ? : string ; name : string ; type : 'emulator' | 'device' ; isBooted : boolean ; isAuthorized : boolean ; } ; let _lastUrl = null ; let _isAdbOwner = null ; const BEGINNING_OF_ADB_ERROR_MESSAGE = 'error: ' ; const CANT_START_ACTIVITY_ERROR = 'Activity not started, unable to resolve Intent' ; const INSTALL_WARNING_TIMEOUT = 60 * 1000 ; const EMULATOR_MAX_WAIT_TIMEOUT = 60 * 1000 * 3 ; function whichEmulator ( ) { if ( process . env . ANDROID_HOME ) { return `${ process . env . ANDROID_HOME }/emulator/emulator` ; } return 'emulator' ; } function whichADB ( ) { if ( process . env . ANDROID_HOME ) { return `${ process . env . ANDROID_HOME }/platform-tools/adb` ; } return 'adb' ; } /**
 * Returns a list of emulator names.
 */ async function getEmulatorsAsync ( ) { try { const { stdout } = await spawnAsync ( whichEmulator ( ) , [ '-list-avds' ] ) ; return stdout . split ( os . EOL ) . filter ( Boolean ) . map ( name => ( { name , type : 'emulator' , isBooted : false , isAuthorized : true } ) ) ; } catch { return [ ] ; } } param emulatorId async function getAbdNameForEmulatorIdAsync ( emulatorId ) { return ( trim ( await getAdbOutputAsync ( [ '-s' , emulatorId , 'emu' , 'avd' , 'name' ] ) ) . split ( /\r?\n/ ) . shift ( ) ?? null ) ; } export async function getAllAvailableDevicesAsync ( ) { const bootedDevices = await getAttachedDevicesAsync ( ) ; const data = await getEmulatorsAsync ( ) ; const connectedNames = bootedDevices . map ( ( { name } ) => name ) ; const offlineEmulators = data . filter ( ( { name } ) => ! connectedNames . includes ( name ) ) . map ( ( { name , type } ) => { return { name , type , isBooted : false , isAuthorized : true } ; } ) ; const allDevices = bootedDevices . concat ( offlineEmulators ) ; if ( ! allDevices . length ) { const genymotionMessage = `https://developer.android.com/studio/run/device.html#developer-device-options. If you are using Genymotion go to Settings -> ADB, select "Use custom Android SDK tools", and point it at your Android SDK directory.` ; throw new Error ( `No Android connected device found, and no emulators could be started automatically.\nPlease connect a device or create an emulator (https://docs.expo.dev/workflow/android-studio-emulator).\nThen follow the instructions here to enable USB debugging:\n${ genymotionMessage }` ) ; } return allDevices ; } param pid async function isBootAnimationCompleteAsync ( pid ? ) { try { const props = await getPropertyDataForDeviceAsync ( { pid } , PROP_BOOT_ANIMATION_STATE ) ; return ! ! props [ PROP_BOOT_ANIMATION_STATE ] . match ( /stopped/ ) ; } catch { return false ; } } async function startEmulatorAsync ( device ) { Logger . global . info ( `\u203A Opening emulator ${ chalk . bold ( device . name ) }` ) ; const emulatorProcess = child_process . spawn ( whichEmulator ( ) , [ `@${ device . name }` , ] , { stdio : 'ignore' , detached : true } ) ; emulatorProcess . unref ( ) ; return new Promise < Device > ( ( resolve , reject ) => { const waitTimer = setInterval ( async ( ) => { const bootedDevices = await getAttachedDevicesAsync ( ) ; const connected = bootedDevices . find ( ( { name } ) => name === device . name ) ; if ( connected ) { const isBooted = await isBootAnimationCompleteAsync ( connected . pid ) ; if ( isBooted ) { stopWaiting ( ) ; resolve ( connected ) ; } } } , 1000 ) ; const maxTimer = setTimeout ( ( ) => { const manualCommand = `${ whichEmulator ( ) } @${ device . name }` ; stopWaitingAndReject ( `It took too long to start the Android emulator: ${ device . name }. You can try starting the emulator manually from the terminal with: ${ manualCommand }` ) ; } , EMULATOR_MAX_WAIT_TIMEOUT ) ; const stopWaiting = ( ) => { clearTimeout ( maxTimer ) ; clearInterval ( waitTimer ) ; } ; const stopWaitingAndReject = ( message ) => { stopWaiting ( ) ; reject ( new Error ( message ) ) ; clearInterval ( waitTimer ) ; } ; emulatorProcess . on ( 'error' , ( { message } ) => stopWaitingAndReject ( message ) ) ; emulatorProcess . on ( 'exit' , ( ) => { const manualCommand = `${ whichEmulator ( ) } @${ device . name }` ; stopWaitingAndReject ( `The emulator (${ device . name }) quit before it finished opening. You can try starting the emulator manually from the terminal with: ${ manualCommand }` ) ; } ) ; } ) ; } export async function getAttachedDevicesAsync ( ) { const output = await getAdbOutputAsync ( [ 'devices' , '-l' ] ) ; const splitItems = output . trim ( ) . replace ( /\n$/ , '' ) . split ( os . EOL ) ; const attachedDevices = splitItems . slice ( 1 , splitItems . length ) . map ( line => { const props = line . split ( ' ' ) . filter ( Boolean ) ; const isAuthorized = props [ 1 ] !== 'unauthorized' ; const type = line . includes ( 'emulator' ) ? 'emulator' : 'device' ; return { props , type , isAuthorized } ; } ) . filter ( ( { props : [ pid ] } ) => ! ! pid ) ; const devicePromises = attachedDevices . map < Promise < Device > > ( async ( props ) => { const { type , props : [ pid , ... deviceInfo ] , isAuthorized , } = props ; let name = null ; if ( type === 'device' ) { if ( isAuthorized ) { const modelItem = deviceInfo . find ( info => info . includes ( 'model:' ) ) ; if ( modelItem ) { name = modelItem . replace ( 'model:' , '' ) ; } } if ( ! name ) { name = `Device ${ pid }` ; } } else { name = ( await getAbdNameForEmulatorIdAsync ( pid ) ) ?? '' ; } return { pid , name , type , isAuthorized , isBooted : true } ; } ) ; return Promise . all ( devicePromises ) ; } export function isPlatformSupported ( ) { return ( process . platform === 'darwin' || process . platform === 'win32' || process . platform === 'linux' ) ; } async function adbAlreadyRunning ( adb ) { try { const result = await spawnAsync ( adb , [ 'start-server' ] ) ; const lines = trim ( result . stderr ) . split ( /\r?\n/ ) ; return lines . includes ( '* daemon started successfully' ) === false ; } catch ( e ) { let errorMessage = trim ( e . stderr || e . stdout ) ; if ( errorMessage . startsWith ( BEGINNING_OF_ADB_ERROR_MESSAGE ) ) { errorMessage = errorMessage . substring ( BEGINNING_OF_ADB_ERROR_MESSAGE . length ) ; } e . message = errorMessage ; throw e ; } } export async function getAdbOutputAsync ( args ) { await Binaries . addToPathAsync ( 'adb' ) ; const adb = whichADB ( ) ; if ( _isAdbOwner === null ) { const alreadyRunning = await adbAlreadyRunning ( adb ) ; _isAdbOwner = alreadyRunning === false ; } if ( Env . isDebug ( ) ) { Logger . global . info ( [ adb , ... args ] . join ( ' ' ) ) ; } try { const result = await spawnAsync ( adb , args ) ; return result . output . join ( '\n' ) ; } catch ( e ) { if ( e . signal === 'SIGINT' ) { e . isAbortError = true ; } let errorMessage = ( e . stderr || e . stdout || e . message ) . trim ( ) ; if ( errorMessage . startsWith ( BEGINNING_OF_ADB_ERROR_MESSAGE ) ) { errorMessage = errorMessage . substring ( BEGINNING_OF_ADB_ERROR_MESSAGE . length ) ; } e . message = errorMessage ; throw e ; } } export async function getAdbFileOutputAsync ( args , encoding ? ) { await Binaries . addToPathAsync ( 'adb' ) ; const adb = whichADB ( ) ; if ( _isAdbOwner === null ) { const alreadyRunning = await adbAlreadyRunning ( adb ) ; _isAdbOwner = alreadyRunning === false ; } try { return await execFileSync ( adb , args , { encoding , stdio : 'pipe' } ) ; } catch ( e ) { let errorMessage = ( e . stderr || e . stdout || e . message ) . trim ( ) ; if ( errorMessage . startsWith ( BEGINNING_OF_ADB_ERROR_MESSAGE ) ) { errorMessage = errorMessage . substring ( BEGINNING_OF_ADB_ERROR_MESSAGE . length ) ; } e . message = errorMessage ; throw e ; } } async function _isDeviceAuthorizedAsync ( device ) { return device . isAuthorized ; } async function isInstalledAsync ( device , androidPackage ) { const packages = await getAdbOutputAsync ( adbPidArgs ( device . pid , 'shell' , 'pm' , 'list' , 'packages' , androidPackage ) ) ; const lines = packages . split ( /\r?\n/ ) ; for ( let i = 0 ; i < lines . length ; i ++ ) { const line = lines [ i ] . trim ( ) ; if ( line === `package:${ androidPackage }` ) { return true ; } } return false ; } async function _isExpoInstalledAsync ( device ) { return await isInstalledAsync ( device , 'host.exp.exponent' ) ; } async function ensureDevClientInstalledAsync ( device , applicationId ) { if ( ! ( await isInstalledAsync ( device , applicationId ) ) ) { throw new Error ( `The development client (${ applicationId }) for this project is not installed. ` + `Please build and install the client on the device first.\n${ learnMore ( 'https://docs.expo.dev/clients/distribution-for-android/' ) }` ) ; } } async function getExpoVersionAsync ( device ) { const info = await getAdbOutputAsync ( adbPidArgs ( device . pid , 'shell' , 'dumpsys' , 'package' , 'host.exp.exponent' ) ) ; const regex = /versionName=([0-9.]+)/ ; const regexMatch = regex . exec ( info ) ; if ( ! regexMatch || regexMatch . length < 2 ) { return null ; } return regexMatch [ 1 ] ; } async function isClientOutdatedAsync ( device , sdkVersion ? ) { const versions = await Versions . versionsAsync ( ) ; const clientForSdk = await getClientForSDK ( sdkVersion ) ; const latestVersionForSdk = clientForSdk ?. version ?? versions . androidVersion ; const installedVersion = await getExpoVersionAsync ( device ) ; return ! installedVersion || semver . lt ( installedVersion , latestVersionForSdk ) ; } export async function installExpoAsync ( { device , url , version , } ) { let warningTimer ; const setWarningTimer = ( ) => { if ( warningTimer ) { clearTimeout ( warningTimer ) ; } return setTimeout ( ( ) => { Logger . global . info ( '' ) ; Logger . global . info ( 'This download is taking longer than expected. You can also try downloading the clients from the website at https://expo.dev/tools' ) ; } , INSTALL_WARNING_TIMEOUT ) ; } ; Logger . notifications . info ( { code : LoadingEvent . START_PROGRESS_BAR } , 'Downloading the Expo Go app [:bar] :percent :etas' ) ; warningTimer = setWarningTimer ( ) ; const path = await downloadApkAsync ( url , progress => { Logger . notifications . info ( { code : LoadingEvent . TICK_PROGRESS_BAR } , progress ) ; } ) ; Logger . notifications . info ( { code : LoadingEvent . STOP_PROGRESS_BAR } ) ; const message = version ? `Installing Expo Go ${ version } on ${ device . name }` : `Installing Expo Go on ${ device . name }` ; Logger . notifications . info ( { code : LoadingEvent . START_LOADING } , message ) ; warningTimer = setWarningTimer ( ) ; const result = await installOnDeviceAsync ( device , { binaryPath : path } ) ; Logger . notifications . info ( { code : LoadingEvent . STOP_LOADING } ) ; clearTimeout ( warningTimer ) ; return result ; } export async function installOnDeviceAsync ( device , { binaryPath } ) { return await getAdbOutputAsync ( adbPidArgs ( device . pid , 'install' , '-r' , '-d' , binaryPath ) ) ; } export async function isDeviceBootedAsync ( { name , } = { } ) { const devices = await getAttachedDevicesAsync ( ) ; if ( ! name ) { return devices [ 0 ] ?? null ; } return devices . find ( device => device . name === name ) ?? null ; } export async function uninstallExpoAsync ( device ) { Logger . global . info ( 'Uninstalling Expo Go from Android device.' ) ; const isInstalled = await _isExpoInstalledAsync ( device ) ; if ( ! isInstalled ) { return ; } try { return await getAdbOutputAsync ( adbPidArgs ( device . pid , 'uninstall' , 'host.exp.exponent' ) ) ; } catch ( e ) { Logger . global . error ( 'Could not uninstall Expo Go from your device, please uninstall Expo Go manually and try again.' ) ; throw e ; } } export async function upgradeExpoAsync ( { url , version , device , } = { } ) { try { if ( ! device ) { device = ( await getAttachedDevicesAsync ( ) ) [ 0 ] ; if ( ! device ) { throw new Error ( 'no devices connected' ) ; } } device = await attemptToStartEmulatorOrAssertAsync ( device ) ; if ( ! device ) { return false ; } await uninstallExpoAsync ( device ) ; await installExpoAsync ( { device , url , version } ) ; if ( _lastUrl ) { Logger . global . info ( `\u203A Opening ${ _lastUrl } in Expo.` ) ; await getAdbOutputAsync ( [ 'shell' , 'am' , 'start' , '-a' , 'android.intent.action.VIEW' , '-d' , _lastUrl , ] ) ; _lastUrl = null ; } return true ; } catch ( e ) { Logger . global . error ( e . message ) ; return false ; } } async function _openUrlAsync ( { pid , url , applicationId , } ) { if ( applicationId === 'host.exp.exponent' ) { const openClient = await getAdbOutputAsync ( adbPidArgs ( pid , 'shell' , 'monkey' , '-p' , applicationId , '-c' , 'android.intent.category.LAUNCHER' , '1' ) ) ; if ( openClient . includes ( CANT_START_ACTIVITY_ERROR ) ) { throw new Error ( openClient . substring ( openClient . indexOf ( 'Error: ' ) ) ) ; } } const openProject = await getAdbOutputAsync ( adbPidArgs ( pid , 'shell' , 'am' , 'start' , '-a' , 'android.intent.action.VIEW' , '-d' , url ) ) ; if ( openProject . includes ( CANT_START_ACTIVITY_ERROR ) ) { throw new Error ( openProject . substring ( openProject . indexOf ( 'Error: ' ) ) ) ; } return openProject ; } function getUnixPID ( port ) { return execFileSync ( 'lsof' , [ `-i:${ port }` , '-P' , '-t' , '-sTCP:LISTEN' ] , { encoding : 'utf8' , stdio : [ 'pipe' , 'pipe' , 'ignore' ] } ) . split ( '\n' ) [ 0 ] . trim ( ) ; } export async function activateEmulatorWindowAsync ( device ) { if ( process . platform !== 'darwin' || device . type !== 'emulator' ) { return ; } const androidPid = device . pid ! . match ( /-(\d+)/ ) ?. [ 1 ] ; if ( ! androidPid ) { return ; } const pid = getUnixPID ( androidPid ) ; try { await osascript . execAsync ( `
  tell application "System Events"
    set frontmost of the first process whose unix id is ${ pid } to true
  end tell` ) ; } catch { } } param device param props . launchActivity export async function openAppAsync ( device , { launchActivity , } ) { const openProject = await getAdbOutputAsync ( adbPidArgs ( device . pid , 'shell' , 'am' , 'start' , '-a' , 'android.intent.action.RUN' , '-f' , '0x20000000' , '-n' , launchActivity ) ) ; if ( openProject . match ( /Error: Activity class .* does not exist./g ) ) { throw new XDLError ( 'APP_NOT_INSTALLED' , openProject . substring ( openProject . indexOf ( 'Error: ' ) ) ) ; } await activateEmulatorWindowAsync ( device ) ; return openProject ; } export async function attemptToStartEmulatorOrAssertAsync ( device ) { if ( ! ( await isDeviceBootedAsync ( device ) ) ) { device = await startEmulatorAsync ( device ) ; } if ( ! ( await _isDeviceAuthorizedAsync ( device ) ) ) { logUnauthorized ( device ) ; return null ; } return device ; } function logUnauthorized ( device ) { Logger . global . warn ( `\nThis computer is not authorized for developing on ${ chalk . bold ( device . name ) }. ${ chalk . dim ( learnMore ( 'https://expo.fyi/authorize-android-device' ) ) }` ) ; } const hasPromptedToUpgrade = { } ; async function isManagedProjectAsync ( projectRoot ) { try { await AndroidConfig . Paths . getProjectPathOrThrowAsync ( projectRoot ) ; return false ; } catch { return true ; } } async function openUrlAsync ( { url , device , isDetached = false , sdkVersion , devClient = false , exp , projectRoot , } ) { const bootedDevice = await attemptToStartEmulatorOrAssertAsync ( device ) ; if ( ! bootedDevice ) { return ; } Logger . global . info ( `\u203A Opening ${ chalk . underline ( url ) } on ${ chalk . bold ( bootedDevice . name ) }` ) ; await activateEmulatorWindowAsync ( bootedDevice ) ; device = bootedDevice ; let installedExpo = false ; let clientApplicationId = 'host.exp.exponent' ; try { if ( devClient ) { let applicationId ; const isManaged = await isManagedProjectAsync ( projectRoot ) ; if ( isManaged ) { applicationId = exp ?. android ?. package ; if ( ! applicationId ) { throw new Error ( `Could not find property android.package in app.config.js/app.json. This setting is required to launch the app.` ) ; } } else { applicationId = await resolveApplicationIdAsync ( projectRoot ) ; if ( ! applicationId ) { throw new Error ( `Could not find applicationId in ${ AndroidConfig . Paths . getAppBuildGradleFilePath ( projectRoot ) }` ) ; } } clientApplicationId = applicationId ; await ensureDevClientInstalledAsync ( device , clientApplicationId ) ; } else if ( ! isDetached ) { let shouldInstall = ! ( await _isExpoInstalledAsync ( device ) ) ; const promptKey = device . pid ?? 'unknown' ; if ( ! shouldInstall && ! hasPromptedToUpgrade [ promptKey ] && ( await isClientOutdatedAsync ( device , sdkVersion ) ) ) { hasPromptedToUpgrade [ promptKey ] = true ; const confirm = await Prompts . confirmAsync ( { initial : true , message : `Expo Go on ${ device . name } (${ device . type }) is outdated, would you like to upgrade?` } ) ; if ( confirm ) { await uninstallExpoAsync ( device ) ; shouldInstall = true ; } } if ( shouldInstall ) { const androidClient = await getClientForSDK ( sdkVersion ) ; await installExpoAsync ( { device , ... androidClient } ) ; installedExpo = true ; } _lastUrl = url ; } try { await _openUrlAsync ( { pid : device . pid ! , url , applicationId : clientApplicationId } ) ; } catch ( e ) { if ( isDetached ) { e . message = `Error running app. Have you installed the app already using Android Studio? Since you are detached you must build manually. ${ e . message }` ; } else { e . message = `Error running app. ${ e . message }` ; } throw e ; } if ( device . type === 'emulator' ) { } Analytics . logEvent ( 'Open Url on Device' , { platform : 'android' , installedExpo } ) ; } catch ( e ) { e . message = `Error running adb: ${ e . message }` ; throw e ; } } async function getClientForSDK ( sdkVersionString ? ) { if ( ! sdkVersionString ) { return null ; } const sdkVersion = ( await Versions . sdkVersionsAsync ( ) ) [ sdkVersionString ] ; if ( ! sdkVersion ) { return null ; } return { url : sdkVersion . androidClientUrl , version : sdkVersion . androidClientVersion } ; } export async function resolveApplicationIdAsync ( projectRoot ) { try { const applicationIdFromGradle = await AndroidConfig . Package . getApplicationIdAsync ( projectRoot ) ; if ( applicationIdFromGradle ) { return applicationIdFromGradle ; } } catch { } try { const filePath = await AndroidConfig . Paths . getAndroidManifestAsync ( projectRoot ) ; const androidManifest = await AndroidConfig . Manifest . readAndroidManifestAsync ( filePath ) ; await AndroidConfig . Manifest . getMainActivityOrThrow ( androidManifest ) ; if ( androidManifest . manifest ?. $ ?. package ) { return androidManifest . manifest . $ . package ; } } catch { } const { exp } = getConfig ( projectRoot , { skipSDKVersionRequirement : true } ) ; return exp . android ?. package ?? null ; } async function constructDeepLinkAsync ( projectRoot , scheme ? , devClient ? ) { if ( process . env [ 'EXPO_ENABLE_INTERSTITIAL_PAGE' ] && ! devClient && isDevClientPackageInstalled ( projectRoot ) ) { return UrlUtils . constructLoadingUrlAsync ( projectRoot , 'android' ) ; } else { return await UrlUtils . constructDeepLinkAsync ( projectRoot , { scheme } ) . catch ( e => { if ( devClient ) { return null ; } throw e ; } ) ; } } export async function openProjectAsync ( { projectRoot , shouldPrompt , devClient = false , device , scheme , applicationId , launchActivity , } ) { await startAdbReverseAsync ( projectRoot ) ; const projectUrl = await constructDeepLinkAsync ( projectRoot , scheme , devClient ) ; const { exp } = getConfig ( projectRoot , { skipSDKVersionRequirement : true } ) ; if ( device ) { const booted = await attemptToStartEmulatorOrAssertAsync ( device ) ; if ( ! booted ) { return { success : false , error : 'escaped' } ; } device = booted ; } else { const devices = await getAllAvailableDevicesAsync ( ) ; let booted = devices [ 0 ] ; if ( shouldPrompt ) { booted = await promptForDeviceAsync ( devices ) ; } if ( ! booted ) { return { success : false , error : 'escaped' } ; } device = booted ; } if ( ! projectUrl ) { if ( ! launchActivity ) { applicationId = applicationId ?? ( await resolveApplicationIdAsync ( projectRoot ) ) ; if ( ! applicationId ) { return { success : false , error : 'Cannot resolve application identifier or URI scheme to open the native Android app.\nBuild the native app with `expo run:android` or `eas build -p android`' } ; } launchActivity = `${ applicationId }/.MainActivity` ; } try { await openAppAsync ( device , { launchActivity } ) ; } catch ( error ) { let errorMessage = `Couldn't open Android app with activity "${ launchActivity }" on device "${ device . name }".` ; if ( error instanceof XDLError && error . code === 'APP_NOT_INSTALLED' ) { errorMessage += `\nThe app might not be installed, try installing it with: ${ chalk . bold ( `expo run:android -d ${ device . name }` ) }` ; } errorMessage += chalk . gray ( `\n${ error . message }` ) ; error . message = errorMessage ; return { success : false , error } ; } return { success : true , url : '' } ; } try { await openUrlAsync ( { url : projectUrl , device , isDetached : ! ! exp . isDetached , sdkVersion : exp . sdkVersion , devClient , exp , projectRoot } ) ; return { success : true , url : projectUrl } ; } catch ( e ) { if ( e . isAbortError ) { return { success : false , error : 'escaped' } ; } else { e . message = `Couldn't start project on Android: ${ e . message }` ; } return { success : false , error : e } ; } } export async function openWebProjectAsync ( { projectRoot , shouldPrompt , } ) { try { await startAdbReverseAsync ( projectRoot ) ; const projectUrl = await Webpack . getUrlAsync ( projectRoot ) ; if ( projectUrl === null ) { return { success : false , error : `The web project has not been started yet` } ; } const devices = await getAllAvailableDevicesAsync ( ) ; let device = devices [ 0 ] ; if ( shouldPrompt ) { device = await promptForDeviceAsync ( devices ) ; } if ( ! device ) { return { success : false , error : 'escaped' } ; } await openUrlAsync ( { url : projectUrl , device , isDetached : true , projectRoot } ) ; return { success : true , url : projectUrl } ; } catch ( e ) { return { success : false , error : `Couldn't open the web project on Android: ${ e . message }` } ; } } export async function startAdbReverseAsync ( projectRoot ) { const packagerInfo = await ProjectSettings . readPackagerInfoAsync ( projectRoot ) ; const expRc = await readExpRcAsync ( projectRoot ) ; const userDefinedAdbReversePorts = expRc . extraAdbReversePorts || [ ] ; const adbReversePorts = [ packagerInfo . packagerPort , packagerInfo . expoServerPort , ... userDefinedAdbReversePorts , ] . filter ( Boolean ) ; const devices = await getAttachedDevicesAsync ( ) ; for ( const device of devices ) { for ( const port of adbReversePorts ) { if ( ! ( await adbReverse ( { device , port } ) ) ) { return false ; } } } return true ; } export async function stopAdbReverseAsync ( projectRoot ) { const packagerInfo = await ProjectSettings . readPackagerInfoAsync ( projectRoot ) ; const expRc = await readExpRcAsync ( projectRoot ) ; const userDefinedAdbReversePorts = expRc . extraAdbReversePorts || [ ] ; const adbReversePorts = [ packagerInfo . packagerPort , packagerInfo . expoServerPort , ... userDefinedAdbReversePorts , ] . filter ( Boolean ) ; const devices = await getAttachedDevicesAsync ( ) ; for ( const device of devices ) { for ( const port of adbReversePorts ) { await adbReverseRemove ( { device , port } ) ; } } } async function adbReverse ( { device , port } ) { if ( ! ( await _isDeviceAuthorizedAsync ( device ) ) ) { return false ; } try { await getAdbOutputAsync ( adbPidArgs ( device . pid , 'reverse' , `tcp:${ port }` , `tcp:${ port }` ) ) ; return true ; } catch ( e ) { Logger . global . warn ( `Couldn't adb reverse: ${ e . message }` ) ; return false ; } } async function adbReverseRemove ( { device , port , } ) { if ( ! ( await _isDeviceAuthorizedAsync ( device ) ) ) { return false ; } try { await getAdbOutputAsync ( adbPidArgs ( device . pid , 'reverse' , '--remove' , `tcp:${ port }` ) ) ; return true ; } catch ( e ) { Logger . global . debug ( `Couldn't adb reverse remove: ${ e . message }` ) ; return false ; } } function adbPidArgs ( pid , ... options ) { const args = [ ] ; if ( pid ) { args . push ( '-s' , pid ) ; } return args . concat ( options ) ; } type DPIConstraint = { dpi : 'mdpi' | 'hdpi' | 'xhdpi' | 'xxhdpi' | 'xxxhdpi' ; sizeMultiplier : number ; } ; const splashScreenDPIConstraints = [ { dpi : 'mdpi' , sizeMultiplier : 1 } , { dpi : 'hdpi' , sizeMultiplier : 1.5 } , { dpi : 'xhdpi' , sizeMultiplier : 2 } , { dpi : 'xxhdpi' , sizeMultiplier : 3 } , { dpi : 'xxxhdpi' , sizeMultiplier : 4 } , ] ; param projectRoot since export async function checkSplashScreenImages ( projectRoot ) { const { exp } = getConfig ( projectRoot ) ; if ( ! Versions . gteSdkVersion ( exp , '33.0.0' ) ) { return ; } const splashScreenMode = exp . android ?. splash ?. resizeMode ?? exp . splash ?. resizeMode ?? 'contain' ; if ( splashScreenMode === 'contain' || splashScreenMode === 'cover' ) { return ; } const generalSplashImagePath = exp . splash ?. image ; if ( ! generalSplashImagePath ) { Logger . global . warn ( `Couldn't read '${ chalk . italic ( 'splash.image' ) }' from ${ chalk . italic ( 'app.json' ) }. Provide asset that would serve as baseline splash image.` ) ; return ; } const generalSplashImage = await ImageUtils . getImageDimensionsAsync ( projectRoot , generalSplashImagePath ) ; if ( ! generalSplashImage ) { Logger . global . warn ( `Couldn't read dimensions of provided splash image '${ chalk . italic ( generalSplashImagePath ) }'. Does the file exist?` ) ; return ; } const androidSplash = exp . android ?. splash ; const androidSplashImages = [ ] ; for ( const { dpi , sizeMultiplier } of splashScreenDPIConstraints ) { const imageRelativePath = androidSplash ?. [ dpi ] ; if ( imageRelativePath ) { const splashImage = await ImageUtils . getImageDimensionsAsync ( projectRoot , imageRelativePath ) ; if ( ! splashImage ) { Logger . global . warn ( `Couldn't read dimensions of provided splash image '${ chalk . italic ( imageRelativePath ) }'. Does the file exist?` ) ; continue ; } const { width , height } = splashImage ; const expectedWidth = sizeMultiplier * generalSplashImage . width ; const expectedHeight = sizeMultiplier * generalSplashImage . height ; androidSplashImages . push ( { dpi , width , height , expectedWidth , expectedHeight , sizeMatches : width === expectedWidth && height === expectedHeight } ) ; } } if ( androidSplashImages . length === 0 ) { Logger . global . warn ( `Splash resizeMode is set to 'native', but you haven't provided any images for different DPIs.
Be aware that your splash image will be used as xxxhdpi asset and its ${ chalk . bold ( 'actual size will be different' ) } depending on device's DPI.
See https://docs.expo.dev/guides/splash-screens/#splash-screen-api-limitations-on-android for more information` ) ; return ; } if ( androidSplashImages . some ( ( { sizeMatches } ) => ! sizeMatches ) ) { Logger . global . warn ( `Splash resizeMode is set to 'native' and you've provided different images for different DPIs,
but their sizes mismatch expected ones: [dpi: provided (expected)] ${ androidSplashImages . map ( ( { dpi , width , height , expectedWidth , expectedHeight } ) => `${ dpi }: ${ width }x${ height } (${ expectedWidth }x${ expectedHeight })` ) . join ( ', ' ) }
See https://docs.expo.dev/guides/splash-screens/#splash-screen-api-limitations-on-android for more information` ) ; } } export async function maybeStopAdbDaemonAsync ( ) { if ( _isAdbOwner !== true ) { return false ; } try { await getAdbOutputAsync ( [ 'kill-server' ] ) ; return true ; } catch { return false ; } } function nameStyleForDevice ( device ) { const isActive = device . isBooted ; if ( ! isActive ) { return ( text ) => text ; } if ( device . isAuthorized ) { return chalk . bold ; } return ( text ) => chalk . bold ( chalk . gray ( text ) ) ; } export async function promptForDeviceAsync ( devices ) { Prompts . pauseInteractions ( ) ; const { value } = await prompts ( { type : 'autocomplete' , name : 'value' , limit : 11 , message : 'Select a device/emulator' , choices : devices . map ( item => { const format = nameStyleForDevice ( item ) ; const type = item . isAuthorized ? item . type : 'unauthorized' ; return { title : `${ format ( item . name ) } ${ chalk . dim ( `(${ type })` ) }` , value : item . name } ; } ) , suggest : ( input , choices ) => { const regex = new RegExp ( input , 'i' ) ; return choices . filter ( ( choice ) => regex . test ( choice . title ) ) ; } } ) ; Prompts . resumeInteractions ( ) ; const device = value ? devices . find ( ( { name } ) => name === value ) ! : null ; if ( device ?. isAuthorized === false ) { logUnauthorized ( device ) ; return null ; } return device ; } export enum DeviceABI { arm = 'arm' , arm64 = 'arm64' , x64 = 'x64' , x86 = 'x86' , armeabiV7a = 'armeabi-v7a' , armeabi = 'armeabi' , universal = 'universal' } type DeviceProperties = Record < string , string > ; const deviceProperties = { } ; const PROP_SDK_VERSION = 'ro.build.version.release' ; const PROP_API_VERSION = 'ro.build.version.sdk' ; const PROP_CPU_NAME = 'ro.product.cpu.abi' ; const PROP_CPU_ABILIST_NAME = 'ro.product.cpu.abilist' ; const PROP_BOOT_ANIMATION_STATE = 'init.svc.bootanim' ; const LOWEST_SUPPORTED_EXPO_API_VERSION = 21 ; returns export async function getDeviceSDKVersionAsync ( device ) { return await getPropertyForDeviceAsync ( device , PROP_SDK_VERSION ) ; } returns export async function getDeviceAPIVersionAsync ( device ) { const sdkVersion = ( await getPropertyForDeviceAsync ( device , PROP_API_VERSION ) ) ?? LOWEST_SUPPORTED_EXPO_API_VERSION ; return parseInt ( sdkVersion , 10 ) ; } export async function getDeviceABIsAsync ( device ) { const cpuAbilist = await getPropertyForDeviceAsync ( device , PROP_CPU_ABILIST_NAME ) ; if ( cpuAbilist ) { return cpuAbilist . trim ( ) . split ( ',' ) as DeviceABI [ ] ; } const abi = ( await getPropertyForDeviceAsync ( device , PROP_CPU_NAME ) ) as DeviceABI ; return [ abi ] ; } export async function getPropertyForDeviceAsync ( device , name , shouldRefresh ? ) { if ( shouldRefresh ) { delete deviceProperties [ device . name ] ; } if ( deviceProperties [ device . name ] == null ) { try { deviceProperties [ device . name ] = await getPropertyDataForDeviceAsync ( device ) ; } catch ( error ) { Logger . global . error ( `Failed to get properties for device "${ device . name }" (${ device . pid }): ${ error . message }` ) ; } } return deviceProperties [ device . name ] [ name ] ; } async function getPropertyDataForDeviceAsync ( device , prop ? ) { const propCommand = adbPidArgs ( ... [ device . pid , 'shell' , 'getprop' , prop ] . filter ( Boolean ) ) ; try { const results = ( await getAdbFileOutputAsync ( propCommand , 'latin1' ) ) . toString ( 'latin1' ) ; if ( prop ) { return { [ prop ] : results } ; } return parseAdbDeviceProperties ( results ) ; } catch ( error ) { throw new Error ( `Failed to get properties for device (${ device . pid }): ${ error . message }` ) ; } } export function parseAdbDeviceProperties ( devicePropertiesString ) { const properties = { } ; const propertyExp = /\[(.*?)\]: \[(.*?)\]/gm ; for ( const match of devicePropertiesString . matchAll ( propertyExp ) ) { properties [ match [ 1 ] ] = match [ 2 ] ; } return properties ; }