329 lines
9.1 KiB
Plaintext
329 lines
9.1 KiB
Plaintext
/**
|
|
* @flow
|
|
*/
|
|
|
|
import _ from 'lodash';
|
|
import spawnAsync from '@expo/spawn-async';
|
|
import existsAsync from 'exists-async';
|
|
import mkdirp from 'mkdirp';
|
|
import path from 'path';
|
|
import semver from 'semver';
|
|
|
|
import * as Analytics from './Analytics';
|
|
import * as Binaries from './Binaries';
|
|
import Api from './Api';
|
|
import Logger from './Logger';
|
|
import NotificationCode from './NotificationCode';
|
|
import * as ProjectUtils from './project/ProjectUtils';
|
|
import * as ProjectSettings from './ProjectSettings';
|
|
import UserSettings from './UserSettings';
|
|
import * as UrlUtils from './UrlUtils';
|
|
|
|
let _lastUrl = null;
|
|
const BEGINNING_OF_ADB_ERROR_MESSAGE = 'error: ';
|
|
const CANT_START_ACTIVITY_ERROR = 'Activity not started, unable to resolve Intent';
|
|
|
|
export function isPlatformSupported() {
|
|
return (
|
|
process.platform === 'darwin' || process.platform === 'win32' || process.platform === 'linux'
|
|
);
|
|
}
|
|
|
|
async function _getAdbOutputAsync(args) {
|
|
await Binaries.addToPathAsync('adb');
|
|
|
|
try {
|
|
let result = await spawnAsync('adb', args);
|
|
return result.stdout;
|
|
} catch (e) {
|
|
let errorMessage = _.trim(e.stderr);
|
|
if (errorMessage.startsWith(BEGINNING_OF_ADB_ERROR_MESSAGE)) {
|
|
errorMessage = errorMessage.substring(BEGINNING_OF_ADB_ERROR_MESSAGE.length);
|
|
}
|
|
throw new Error(errorMessage);
|
|
}
|
|
}
|
|
|
|
// Device attached
|
|
async function _isDeviceAttachedAsync() {
|
|
let devices = await _getAdbOutputAsync(['devices']);
|
|
let lines = _.trim(devices).split(/\r?\n/);
|
|
// First line is "List of devices".
|
|
return lines.length > 1;
|
|
}
|
|
|
|
async function _isDeviceAuthorizedAsync() {
|
|
let devices = await _getAdbOutputAsync(['devices']);
|
|
let lines = _.trim(devices).split(/\r?\n/);
|
|
lines.shift();
|
|
let listOfDevicesWithoutFirstLine = lines.join('\n');
|
|
// result looks like "072c4cf200e333c7 device" when authorized
|
|
// and "072c4cf200e333c7 unauthorized" when not.
|
|
return listOfDevicesWithoutFirstLine.includes('device');
|
|
}
|
|
|
|
// Expo installed
|
|
async function _isExpoInstalledAsync() {
|
|
let packages = await _getAdbOutputAsync(['shell', 'pm', 'list', 'packages', '-f']);
|
|
let lines = packages.split(/\r?\n/);
|
|
for (let i = 0; i < lines.length; i++) {
|
|
let line = lines[i];
|
|
if (line.includes('host.exp.exponent.test')) {
|
|
continue;
|
|
}
|
|
|
|
if (line.includes('host.exp.exponent')) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async function _expoVersionAsync() {
|
|
let info = await _getAdbOutputAsync(['shell', 'dumpsys', 'package', 'host.exp.exponent']);
|
|
|
|
let regex = /versionName\=([0-9\.]+)/;
|
|
let regexMatch = regex.exec(info);
|
|
if (regexMatch.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
return regexMatch[1];
|
|
}
|
|
|
|
async function _checkExpoUpToDateAsync() {
|
|
let versions = await Api.versionsAsync();
|
|
let installedVersion = await _expoVersionAsync();
|
|
|
|
if (!installedVersion || semver.lt(installedVersion, versions.androidVersion)) {
|
|
Logger.notifications.warn(
|
|
{ code: NotificationCode.OLD_ANDROID_APP_VERSION },
|
|
'This version of the Expo app is out of date. Uninstall the app and run again to upgrade.'
|
|
);
|
|
}
|
|
}
|
|
|
|
function _apkCacheDirectory() {
|
|
let dotExpoHomeDirectory = UserSettings.dotExpoHomeDirectory();
|
|
let dir = path.join(dotExpoHomeDirectory, 'android-apk-cache');
|
|
mkdirp.sync(dir);
|
|
return dir;
|
|
}
|
|
|
|
async function _downloadApkAsync() {
|
|
let versions = await Api.versionsAsync();
|
|
let apkPath = path.join(_apkCacheDirectory(), `Exponent-${versions.androidVersion}.apk`);
|
|
|
|
if (await existsAsync(apkPath)) {
|
|
return apkPath;
|
|
}
|
|
|
|
await Api.downloadAsync(
|
|
versions.androidUrl,
|
|
path.join(_apkCacheDirectory(), `Exponent-${versions.androidVersion}.apk`)
|
|
);
|
|
return apkPath;
|
|
}
|
|
|
|
async function _installExpoAsync() {
|
|
Logger.global.info(`Downloading latest version of Expo`);
|
|
Logger.notifications.info({ code: NotificationCode.START_LOADING });
|
|
let path = await _downloadApkAsync();
|
|
Logger.notifications.info({ code: NotificationCode.STOP_LOADING });
|
|
Logger.global.info(`Installing Expo on device`);
|
|
Logger.notifications.info({ code: NotificationCode.START_LOADING });
|
|
let result = await _getAdbOutputAsync(['install', path]);
|
|
Logger.notifications.info({ code: NotificationCode.STOP_LOADING });
|
|
return result;
|
|
}
|
|
|
|
async function _uninstallExpoAsync() {
|
|
Logger.global.info('Uninstalling Expo from Android device.');
|
|
return await _getAdbOutputAsync(['uninstall', 'host.exp.exponent']);
|
|
}
|
|
|
|
export async function upgradeExpoAsync(): Promise<boolean> {
|
|
try {
|
|
await _assertDeviceReadyAsync();
|
|
|
|
await _uninstallExpoAsync();
|
|
let installResult = await _installExpoAsync();
|
|
if (installResult.status !== 0) {
|
|
return false;
|
|
}
|
|
|
|
if (_lastUrl) {
|
|
Logger.global.info(`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;
|
|
}
|
|
}
|
|
|
|
// Open Url
|
|
async function _assertDeviceReadyAsync() {
|
|
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.`;
|
|
|
|
if (!await _isDeviceAttachedAsync()) {
|
|
throw new Error(
|
|
`No Android device found. Please connect a device and follow the instructions here to enable USB debugging:\n${genymotionMessage}`
|
|
);
|
|
}
|
|
|
|
if (!await _isDeviceAuthorizedAsync()) {
|
|
throw new Error(
|
|
`This computer is not authorized to debug the device. Please follow the instructions here to enable USB debugging:\n${genymotionMessage}`
|
|
);
|
|
}
|
|
}
|
|
|
|
async function _openUrlAsync(url: string) {
|
|
let output = await _getAdbOutputAsync([
|
|
'shell',
|
|
'am',
|
|
'start',
|
|
'-a',
|
|
'android.intent.action.VIEW',
|
|
'-d',
|
|
url,
|
|
]);
|
|
if (output.includes(CANT_START_ACTIVITY_ERROR)) {
|
|
throw new Error(output.substring(output.indexOf('Error: ')));
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
async function openUrlAsync(url: string, isDetached: boolean = false) {
|
|
try {
|
|
await _assertDeviceReadyAsync();
|
|
|
|
let installedExpo = false;
|
|
if (!isDetached && !await _isExpoInstalledAsync()) {
|
|
await _installExpoAsync();
|
|
installedExpo = true;
|
|
}
|
|
|
|
if (!isDetached) {
|
|
_lastUrl = url;
|
|
_checkExpoUpToDateAsync(); // let this run in background
|
|
}
|
|
|
|
Logger.global.info(`Opening on Android device`);
|
|
try {
|
|
await _openUrlAsync(url);
|
|
} 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;
|
|
}
|
|
|
|
Analytics.logEvent('Open Url on Device', {
|
|
platform: 'android',
|
|
installedExpo,
|
|
});
|
|
} catch (e) {
|
|
e.message = `Error running adb: ${e.message}`;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export async function openProjectAsync(projectRoot: string) {
|
|
try {
|
|
await startAdbReverseAsync(projectRoot);
|
|
|
|
let projectUrl = await UrlUtils.constructManifestUrlAsync(projectRoot);
|
|
let { exp } = await ProjectUtils.readConfigJsonAsync(projectRoot);
|
|
|
|
await openUrlAsync(projectUrl, !!exp.isDetached);
|
|
return { success: true, error: null };
|
|
} catch (e) {
|
|
Logger.global.error(`Couldn't start project on Android: ${e.message}`);
|
|
return { success: false, error: e };
|
|
}
|
|
}
|
|
|
|
// Adb reverse
|
|
export async function startAdbReverseAsync(projectRoot: string) {
|
|
const packagerInfo = await ProjectSettings.readPackagerInfoAsync(projectRoot);
|
|
const expRc = await ProjectUtils.readExpRcAsync(projectRoot);
|
|
const userDefinedAdbReversePorts = expRc.extraAdbReversePorts || [];
|
|
|
|
let adbReversePorts = [
|
|
packagerInfo.packagerPort,
|
|
packagerInfo.expoServerPort,
|
|
...userDefinedAdbReversePorts,
|
|
];
|
|
|
|
for (let port of adbReversePorts) {
|
|
if (!await adbReverse(port)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export async function stopAdbReverseAsync(projectRoot: string) {
|
|
const packagerInfo = await ProjectSettings.readPackagerInfoAsync(projectRoot);
|
|
const expRc = await ProjectUtils.readExpRcAsync(projectRoot);
|
|
const userDefinedAdbReversePorts = expRc.extraAdbReversePorts || [];
|
|
|
|
let adbReversePorts = [
|
|
packagerInfo.packagerPort,
|
|
packagerInfo.expoServerPort,
|
|
...userDefinedAdbReversePorts,
|
|
];
|
|
|
|
for (let port of adbReversePorts) {
|
|
await adbReverseRemove(port);
|
|
}
|
|
}
|
|
|
|
async function adbReverse(port: number) {
|
|
if (!await _isDeviceAuthorizedAsync()) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await _getAdbOutputAsync(['reverse', `tcp:${port}`, `tcp:${port}`]);
|
|
return true;
|
|
} catch (e) {
|
|
Logger.global.warn(`Couldn't adb reverse: ${e.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function adbReverseRemove(port: number) {
|
|
if (!await _isDeviceAuthorizedAsync()) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await _getAdbOutputAsync(['reverse', '--remove', `tcp:${port}`]);
|
|
return true;
|
|
} catch (e) {
|
|
// Don't send this to warn because we call this preemptively sometimes
|
|
Logger.global.debug(`Couldn't adb reverse remove: ${e.message}`);
|
|
return false;
|
|
}
|
|
}
|