Skip to content

Commit

Permalink
[Android] Add reading SDK location from local.properties (#754)
Browse files Browse the repository at this point in the history
* Add support for using sdk location from local.properties

* Add additional logging

* Upgrade getConnectedDevices method

* Remove extracode

* fix tests

* Make changes after review

* Fix tests

* Remove fsPath usage from reloadApp and showDevMenu
  • Loading branch information
ruslan-bikkinin authored Jul 25, 2018
1 parent 77b86bf commit 43e1a99
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 90 deletions.
107 changes: 79 additions & 28 deletions src/extension/android/adb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@

import * as Q from "q";

import {ChildProcess} from "../../common/node/childProcess";
import {CommandExecutor} from "../../common/commandExecutor";
import { ChildProcess, ISpawnResult } from "../../common/node/childProcess";
import { CommandExecutor } from "../../common/commandExecutor";
import * as path from "path";
import * as fs from "fs";
import { ILogger } from "../log/LogHelper";

// See android versions usage at: http://developer.android.com/about/dashboards/index.html
export enum AndroidAPILevel {
Expand Down Expand Up @@ -41,14 +44,23 @@ export interface IDevice {
const AndroidSDKEmulatorPattern = /^emulator-\d{1,5}$/;

export class AdbHelper {
private static childProcess: ChildProcess = new ChildProcess();
private static commandExecutor: CommandExecutor = new CommandExecutor();
private childProcess: ChildProcess = new ChildProcess();
private commandExecutor: CommandExecutor = new CommandExecutor();
private adbExecutable: string = "";

constructor(projectRoot: string, logger?: ILogger) {

// Trying to read sdk location from local.properties file and if we succueded then
// we would run adb from inside it, otherwise we would rely to PATH
const sdkLocation = this.getSdkLocationFromLocalPropertiesFile(projectRoot, logger);
this.adbExecutable = sdkLocation ? `${path.join(sdkLocation, "platform-tools", "adb")}` : "adb";
}

/**
* Gets the list of Android connected devices and emulators.
*/
public static getConnectedDevices(): Q.Promise<IDevice[]> {
return this.childProcess.execToString("adb devices")
public getConnectedDevices(): Q.Promise<IDevice[]> {
return this.childProcess.execToString(`${this.adbExecutable} devices`)
.then(output => {
return this.parseConnectedDevices(output);
});
Expand All @@ -57,8 +69,8 @@ export class AdbHelper {
/**
* Broadcasts an intent to reload the application in debug mode.
*/
public static switchDebugMode(projectRoot: string, packageName: string, enable: boolean, debugTarget?: string): Q.Promise<void> {
let enableDebugCommand = `adb ${debugTarget ? "-s " + debugTarget : ""} shell am broadcast -a "${packageName}.RELOAD_APP_ACTION" --ez jsproxy ${enable}`;
public switchDebugMode(projectRoot: string, packageName: string, enable: boolean, debugTarget?: string): Q.Promise<void> {
let enableDebugCommand = `${this.adbExecutable} ${debugTarget ? "-s " + debugTarget : ""} shell am broadcast -a "${packageName}.RELOAD_APP_ACTION" --ez jsproxy ${enable}`;
return new CommandExecutor(projectRoot).execute(enableDebugCommand)
.then(() => { // We should stop and start application again after RELOAD_APP_ACTION, otherwise app going to hangs up
let deferred = Q.defer();
Expand All @@ -79,48 +91,52 @@ export class AdbHelper {
/**
* Sends an intent which launches the main activity of the application.
*/
public static launchApp(projectRoot: string, packageName: string, debugTarget?: string): Q.Promise<void> {
let launchAppCommand = `adb ${debugTarget ? "-s " + debugTarget : ""} shell am start -n ${packageName}/.MainActivity`;
public launchApp(projectRoot: string, packageName: string, debugTarget?: string): Q.Promise<void> {
let launchAppCommand = `${this.adbExecutable} ${debugTarget ? "-s " + debugTarget : ""} shell am start -n ${packageName}/.MainActivity`;
return new CommandExecutor(projectRoot).execute(launchAppCommand);
}

public static stopApp(projectRoot: string, packageName: string, debugTarget?: string): Q.Promise<void> {
let stopAppCommand = `adb ${debugTarget ? "-s " + debugTarget : ""} shell am force-stop ${packageName}`;
public stopApp(projectRoot: string, packageName: string, debugTarget?: string): Q.Promise<void> {
let stopAppCommand = `${this.adbExecutable} ${debugTarget ? "-s " + debugTarget : ""} shell am force-stop ${packageName}`;
return new CommandExecutor(projectRoot).execute(stopAppCommand);
}

public static apiVersion(deviceId: string): Q.Promise<AndroidAPILevel> {
public apiVersion(deviceId: string): Q.Promise<AndroidAPILevel> {
return this.executeQuery(deviceId, "shell getprop ro.build.version.sdk").then(output =>
parseInt(output, 10));
}

public static reverseAdb(deviceId: string, packagerPort: number): Q.Promise<void> {
public reverseAdb(deviceId: string, packagerPort: number): Q.Promise<void> {
return this.execute(deviceId, `reverse tcp:${packagerPort} tcp:${packagerPort}`);
}

public static showDevMenu(deviceId?: string): Q.Promise<void> {
let command = `adb ${deviceId ? "-s " + deviceId : ""} shell input keyevent ${KeyEvents.KEYCODE_MENU}`;
public showDevMenu(deviceId?: string): Q.Promise<void> {
let command = `${this.adbExecutable} ${deviceId ? "-s " + deviceId : ""} shell input keyevent ${KeyEvents.KEYCODE_MENU}`;
return this.commandExecutor.execute(command);
}

public static reloadApp(deviceId?: string): Q.Promise<void> {
public reloadApp(deviceId?: string): Q.Promise<void> {
let commands = [
`adb ${deviceId ? "-s " + deviceId : ""} shell input keyevent ${KeyEvents.KEYCODE_MENU}`,
`adb ${deviceId ? "-s " + deviceId : ""} shell input keyevent ${KeyEvents.KEYCODE_DPAD_UP}`,
`adb ${deviceId ? "-s " + deviceId : ""} shell input keyevent ${KeyEvents.KEYCODE_DPAD_CENTER}`,
`${this.adbExecutable} ${deviceId ? "-s " + deviceId : ""} shell input keyevent ${KeyEvents.KEYCODE_MENU}`,
`${this.adbExecutable} ${deviceId ? "-s " + deviceId : ""} shell input keyevent ${KeyEvents.KEYCODE_DPAD_UP}`,
`${this.adbExecutable} ${deviceId ? "-s " + deviceId : ""} shell input keyevent ${KeyEvents.KEYCODE_DPAD_CENTER}`,
];

return this.executeChain(commands);
}

public static getOnlineDevices(): Q.Promise<IDevice[]> {
public getOnlineDevices(): Q.Promise<IDevice[]> {
return this.getConnectedDevices().then(devices => {
return devices.filter(device =>
device.isOnline);
});
}

private static parseConnectedDevices(input: string): IDevice[] {
public startLogCat(adbParameters: string[]): ISpawnResult {
return new ChildProcess().spawn(`${this.adbExecutable}`, adbParameters);
}

private parseConnectedDevices(input: string): IDevice[] {
let result: IDevice[] = [];
let regex = new RegExp("^(\\S+)\\t(\\S+)$", "mg");
let match = regex.exec(input);
Expand All @@ -131,27 +147,62 @@ export class AdbHelper {
return result;
}

private static extractDeviceType(id: string): DeviceType {
private extractDeviceType(id: string): DeviceType {
return id.match(AndroidSDKEmulatorPattern)
? DeviceType.AndroidSdkEmulator
: DeviceType.Other;
}

private static executeQuery(deviceId: string, command: string): Q.Promise<string> {
private executeQuery(deviceId: string, command: string): Q.Promise<string> {
return this.childProcess.execToString(this.generateCommandForDevice(deviceId, command));
}

private static execute(deviceId: string, command: string): Q.Promise<void> {
private execute(deviceId: string, command: string): Q.Promise<void> {
return this.commandExecutor.execute(this.generateCommandForDevice(deviceId, command));
}

private static executeChain(commands: string[]): Q.Promise<any> {
private executeChain(commands: string[]): Q.Promise<any> {
return commands.reduce((promise, command) => {
return promise.then(() => this.commandExecutor.execute(command));
}, Q(void 0));
}

private static generateCommandForDevice(deviceId: string, adbCommand: string): string {
return `adb -s "${deviceId}" ${adbCommand}`;
private generateCommandForDevice(deviceId: string, adbCommand: string): string {
return `${this.adbExecutable} -s "${deviceId}" ${adbCommand}`;
}

private getSdkLocationFromLocalPropertiesFile(projectRoot: string, logger?: ILogger): string | null {
const localPropertiesFilePath = path.join(projectRoot, "android", "local.properties");
if (!fs.existsSync(localPropertiesFilePath)) {
if (logger) {
logger.info(`local.properties file doesn't exist. Using Android SDK location from PATH.`);
}
return null;
}

let fileContent;
try {
fileContent = fs.readFileSync(localPropertiesFilePath).toString();
} catch (e) {
if (logger) {
logger.error(`Could read from ${localPropertiesFilePath}.`, e, e.stack);
logger.info(`Using Android SDK location from PATH.`);
}
return null;
}
const matches = fileContent.match(/^sdk\.dir=(.+)$/m);
if (!matches || !matches[1]) {
if (logger) {
logger.info(`No sdk.dir value found in local.properties file. Using Android SDK location from PATH.`);
}
return null;
}

const sdkLocation = matches[1].trim();
if (logger) {
logger.info(`Using Android SDK location defined in android/local.properties file: ${sdkLocation}.`);
}

return sdkLocation;
}
}
34 changes: 22 additions & 12 deletions src/extension/android/androidPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,16 @@ export class AndroidPlatform extends GeneralMobilePlatform {
private devices: IDevice[];
private packageName: string;
private logCatMonitor: LogCatMonitor | null = null;
private adbHelper: AdbHelper;

private needsToLaunchApps: boolean = false;
public static showDevMenu(deviceId?: string): Q.Promise<void> {
return AdbHelper.showDevMenu(deviceId);

public showDevMenu(deviceId?: string): Q.Promise<void> {
return this.adbHelper.showDevMenu(deviceId);
}
public static reloadApp(deviceId?: string): Q.Promise<void> {
return AdbHelper.reloadApp(deviceId);

public reloadApp(deviceId?: string): Q.Promise<void> {
return this.adbHelper.reloadApp(deviceId);
}

// We set remoteExtension = null so that if there is an instance of androidPlatform that wants to have it's custom remoteExtension it can. This is specifically useful for tests.
Expand All @@ -72,6 +75,13 @@ export class AndroidPlatform extends GeneralMobilePlatform {
this.logger.warning(message);
delete this.runOptions.target;
}

this.adbHelper = new AdbHelper(this.runOptions.projectRoot, this.logger);
}

// TODO: remove this method when sinon will be updated to upper version. Now it is used for tests only.
public setAdbHelper(adbHelper: AdbHelper) {
this.adbHelper = adbHelper;
}

public runApp(shouldLaunchInAllDevices: boolean = false): Q.Promise<void> {
Expand Down Expand Up @@ -108,7 +118,7 @@ export class AndroidPlatform extends GeneralMobilePlatform {
/* If it failed due to multiple devices, we'll apply this workaround to make it work anyways */
this.needsToLaunchApps = true;
return shouldLaunchInAllDevices
? AdbHelper.getOnlineDevices()
? this.adbHelper.getOnlineDevices()
: Q([this.debugTarget]);
} else {
return Q.reject<IDevice[]>(reason);
Expand All @@ -123,11 +133,11 @@ export class AndroidPlatform extends GeneralMobilePlatform {
}

public enableJSDebuggingMode(): Q.Promise<void> {
return AdbHelper.switchDebugMode(this.runOptions.projectRoot, this.packageName, true, this.debugTarget.id);
return this.adbHelper.switchDebugMode(this.runOptions.projectRoot, this.packageName, true, this.debugTarget.id);
}

public disableJSDebuggingMode(): Q.Promise<void> {
return AdbHelper.switchDebugMode(this.runOptions.projectRoot, this.packageName, false, this.debugTarget.id);
return this.adbHelper.switchDebugMode(this.runOptions.projectRoot, this.packageName, false, this.debugTarget.id);
}

public prewarmBundleCache(): Q.Promise<void> {
Expand All @@ -152,7 +162,7 @@ export class AndroidPlatform extends GeneralMobilePlatform {
}

private initializeTargetDevicesAndPackageName(): Q.Promise<void> {
return AdbHelper.getConnectedDevices().then(devices => {
return this.adbHelper.getConnectedDevices().then(devices => {
this.devices = devices;
this.debugTarget = this.getTargetEmulator(devices);
return this.getPackageName().then(packageName => {
Expand All @@ -167,7 +177,7 @@ export class AndroidPlatform extends GeneralMobilePlatform {
return this.configureADBReverseWhenApplicable(device);
}).then(() => {
return this.needsToLaunchApps
? AdbHelper.launchApp(this.runOptions.projectRoot, this.packageName, device.id)
? this.adbHelper.launchApp(this.runOptions.projectRoot, this.packageName, device.id)
: Q<void>(void 0);
}).then(() => {
return this.startMonitoringLogCat(device, this.runOptions.logCatArguments);
Expand All @@ -176,10 +186,10 @@ export class AndroidPlatform extends GeneralMobilePlatform {

private configureADBReverseWhenApplicable(device: IDevice): Q.Promise<void> {
return Q({}) // For other emulators and devices we try to enable adb reverse
.then(() => AdbHelper.apiVersion(device.id))
.then(() => this.adbHelper.apiVersion(device.id))
.then(apiVersion => {
if (apiVersion >= AndroidAPILevel.LOLLIPOP) { // If we support adb reverse
return AdbHelper.reverseAdb(device.id, Number( this.runOptions.packagerPort));
return this.adbHelper.reverseAdb(device.id, Number(this.runOptions.packagerPort));
} else {
this.logger.warning(`Device ${device.id} supports only API Level ${apiVersion}. `
+ `Level ${AndroidAPILevel.LOLLIPOP} is needed to support port forwarding via adb reverse. `
Expand Down Expand Up @@ -228,7 +238,7 @@ export class AndroidPlatform extends GeneralMobilePlatform {
this.stopMonitoringLogCat(); // Stop previous logcat monitor if it's running

// this.logCatMonitor can be mutated, so we store it locally too
this.logCatMonitor = new LogCatMonitor(device.id, logCatArguments);
this.logCatMonitor = new LogCatMonitor(device.id, logCatArguments, this.adbHelper);
this.logCatMonitor.start() // The LogCat will continue running forever, so we don't wait for it
.catch(error => this.logger.warning("Error while monitoring LogCat", error)) // The LogCatMonitor failing won't stop the debugging experience
.done();
Expand Down
11 changes: 6 additions & 5 deletions src/extension/android/logCatMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import * as Q from "q";
import * as vscode from "vscode";

import { ChildProcess, ISpawnResult } from "../../common/node/childProcess";
import { ISpawnResult } from "../../common/node/childProcess";
import { OutputChannelLogger } from "../log/OutputChannelLogger";
import { ExecutionsFilterBeforeTimestamp } from "../../common/executionsLimiter";
import { AdbHelper } from "./adb";

/* This class will print the LogCat messages to an Output Channel. The configuration for logcat can be cutomized in
the .vscode/launch.json file by defining a setting named logCatArguments for the configuration being used. The
Expand All @@ -18,28 +19,28 @@ import { ExecutionsFilterBeforeTimestamp } from "../../common/executionsLimiter"
export class LogCatMonitor implements vscode.Disposable {
private static DEFAULT_PARAMETERS = ["*:S", "ReactNative:V", "ReactNativeJS:V"];

private _childProcess: ChildProcess;
private _logger: OutputChannelLogger;

private _deviceId: string;
private _userProvidedLogCatArguments: any; // This is user input, we don't know what's here

private _logCatSpawn: ISpawnResult | null;
private adbHelper: AdbHelper;

constructor(deviceId: string, userProvidedLogCatArguments: string, { childProcess = new ChildProcess() } = {}) {
constructor(deviceId: string, userProvidedLogCatArguments: string, adbHelper: AdbHelper) {
this._deviceId = deviceId;
this._userProvidedLogCatArguments = userProvidedLogCatArguments;
this._childProcess = childProcess;

this._logger = OutputChannelLogger.getChannel(`LogCat - ${deviceId}`);
this.adbHelper = adbHelper;
}

public start(): Q.Promise<void> {
const logCatArguments = this.getLogCatArguments();
const adbParameters = ["-s", this._deviceId, "logcat"].concat(logCatArguments);
this._logger.debug(`Monitoring LogCat for device ${this._deviceId} with arguments: ${logCatArguments}`);

this._logCatSpawn = new ChildProcess().spawn("adb", adbParameters);
this._logCatSpawn = this.adbHelper.startLogCat(adbParameters);

/* LogCat has a buffer and prints old messages when first called. To ignore them,
we won't print messages for the first 0.5 seconds */
Expand Down
Loading

0 comments on commit 43e1a99

Please sign in to comment.