// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import * as path from "path"; import * as fs from "fs"; import * as os from "os"; import * as nls from "vscode-nls"; import { ILogger } from "../log/LogHelper"; import { CommandExecutor } from "../../common/commandExecutor"; import { ChildProcess, ISpawnResult } from "../../common/node/childProcess"; import { PromiseUtil } from "../../common/node/promise"; import { IDebuggableMobileTarget } from "../mobileTarget"; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone, })(); const localize = nls.loadMessageBundle(); // See android versions usage at: http://developer.android.com/about/dashboards/index.html export enum AndroidAPILevel { Marshmallow = 23, LOLLIPOP_MR1 = 22, LOLLIPOP = 21 /* Supports adb reverse */, KITKAT = 19, JELLY_BEAN_MR2 = 18, JELLY_BEAN_MR1 = 17, JELLY_BEAN = 16, ICE_CREAM_SANDWICH_MR1 = 15, GINGERBREAD_MR1 = 10, } enum KeyEvents { KEYCODE_BACK = 4, KEYCODE_DPAD_UP = 19, KEYCODE_DPAD_DOWN = 20, KEYCODE_DPAD_CENTER = 23, KEYCODE_MENU = 82, } export class AdbHelper { private nodeModulesRoot: string; private launchActivity: string; private childProcess: ChildProcess = new ChildProcess(); private commandExecutor: CommandExecutor; private adbExecutable: string = ""; private static readonly AndroidRemoteTargetPattern = /^((?:\d{1,3}\.){3}\d{1,3}:\d{1,5}|.*_adb-tls-con{2}ect\._tcp.*)$/gm; public static readonly AndroidSDKEmulatorPattern = /^emulator-\d{1,5}$/; constructor( projectRoot: string, nodeModulesRoot: string, logger?: ILogger, launchActivity: string = "MainActivity", ) { this.nodeModulesRoot = nodeModulesRoot; this.adbExecutable = this.getAdbPath(projectRoot, logger); this.commandExecutor = new CommandExecutor(this.nodeModulesRoot); this.launchActivity = launchActivity; } /** * Gets the list of Android connected devices and emulators. */ public async getConnectedTargets(): Promise { const output = await this.childProcess.execToString(`${this.adbExecutable} devices`); return this.parseConnectedTargets(output); } public async findOnlineTargetById( targetId: string, ): Promise { return (await this.getOnlineTargets()).find(target => target.id === targetId); } public async getAvdsNames(): Promise { const res = await this.childProcess.execToString("emulator -list-avds"); let emulatorsNames: string[] = []; if (res) { emulatorsNames = res.split(/\r?\n|\r/g); const indexOfBlank = emulatorsNames.indexOf(""); if (indexOfBlank >= 0) { emulatorsNames.splice(indexOfBlank, 1); } } return emulatorsNames; } public isRemoteTarget(id: string): boolean { return !!id.match(AdbHelper.AndroidRemoteTargetPattern); } public async getAvdNameById(emulatorId: string): Promise { try { const output = await this.childProcess.execToString( `${this.adbExecutable} -s ${emulatorId} emu avd name`, ); // The command returns the name of avd by id of this running emulator. // Return value example: // " // emuName // OK // " return output ? output.split(/\r?\n|\r/g)[0] : null; } catch { // If the command returned an error, it means that we could not find the emulator with the passed id return null; } } public setLaunchActivity(launchActivity: string): void { this.launchActivity = launchActivity; } /** * Broadcasts an intent to reload the application in debug mode. */ public async switchDebugMode( projectRoot: string, packageName: string, enable: boolean, debugTarget?: string, appIdSuffix?: string, ): Promise { const enableDebugCommand = `${this.adbExecutable} ${ debugTarget ? `-s ${debugTarget}` : "" } shell am broadcast -a "${packageName}.RELOAD_APP_ACTION" --ez jsproxy ${String(enable)}`; await new CommandExecutor(this.nodeModulesRoot, projectRoot).execute(enableDebugCommand); // We should stop and start application again after RELOAD_APP_ACTION, otherwise app going to hangs up await PromiseUtil.delay(200); // We need a little delay after broadcast command await this.stopApp(projectRoot, packageName, debugTarget, appIdSuffix); return this.launchApp(projectRoot, packageName, debugTarget, appIdSuffix); } /** * Sends an intent which launches the main activity of the application. */ public launchApp( projectRoot: string, packageName: string, debugTarget?: string, appIdSuffix?: string, ): Promise { const launchAppCommand = `${this.adbExecutable} ${ debugTarget ? `-s ${debugTarget}` : "" } shell am start -n ${packageName}${appIdSuffix ? `.${appIdSuffix}` : ""}/${packageName}.${ this.launchActivity }`; return new CommandExecutor(projectRoot).execute(launchAppCommand); } public stopApp( projectRoot: string, packageName: string, debugTarget?: string, appIdSuffix?: string, ): Promise { const stopAppCommand = `${this.adbExecutable} ${ debugTarget ? `-s ${debugTarget}` : "" } shell am force-stop ${packageName}${appIdSuffix ? `.${appIdSuffix}` : ""}`; return new CommandExecutor(projectRoot).execute(stopAppCommand); } public async apiVersion(deviceId: string): Promise { const output = await this.executeQuery(deviceId, [ "shell", "getprop", "ro.build.version.sdk", ]); return parseInt(output, 10); } public reverseAdb(deviceId: string, port: number): Promise { return this.execute(deviceId, ["reverse", `tcp:${port}`, `tcp:${port}`]); } public showDevMenu(deviceId?: string): Promise { const command = `${this.adbExecutable} ${ deviceId ? `-s ${deviceId}` : "" } shell input keyevent ${KeyEvents.KEYCODE_MENU}`; return this.commandExecutor.execute(command); } public reloadApp(deviceId?: string): Promise { const command = `${this.adbExecutable} ${ deviceId ? `-s ${deviceId}` : "" } shell input text "RR"`; return this.commandExecutor.execute(command); } public async getOnlineTargets(): Promise { const devices = await this.getConnectedTargets(); return devices.filter(device => device.isOnline); } public startLogCat(adbParameters: string[]): ISpawnResult { return this.childProcess.spawn(this.adbExecutable.replace(/"/g, ""), adbParameters); } public parseSdkLocation(fileContent: string, logger?: ILogger): string | null { const matches = fileContent.match(/^sdk\.dir\s*=(.+)$/m); if (!matches || !matches[1]) { if (logger) { logger.info( localize( "NoSdkDirFoundInLocalPropertiesFile", "No sdk.dir value found in local.properties file. Using Android SDK location from PATH.", ), ); } return null; } let sdkLocation = matches[1].trim(); if (os.platform() === "win32") { // For Windows we need to unescape files separators and drive letter separators sdkLocation = sdkLocation.replace(/\\\\/g, "\\").replace("\\:", ":"); } if (logger) { logger.info( localize( "UsindAndroidSDKLocationDefinedInLocalPropertiesFile", "Using Android SDK location defined in android/local.properties file: {0}.", sdkLocation, ), ); } return sdkLocation; } public getAdbPath(projectRoot: string, logger?: ILogger): string { const sdkLocation = this.getSdkLocationFromLocalPropertiesFile(projectRoot, logger); if (sdkLocation) { const localPropertiesSdkPath = path.join( sdkLocation as string, "platform-tools", os.platform() === "win32" ? "adb.exe" : "adb", ); const isExist = fs.existsSync(localPropertiesSdkPath); if (isExist) { return localPropertiesSdkPath; } if (logger) { logger.warning( localize( "LocalPropertiesAndroidSDKPathNotExistingInLocal", "Local.properties file has Andriod SDK path but cannot find it in your local, will switch to SDK PATH in environment variable. Please check Android SDK path in android/local.properties file.", ), ); } return "adb"; } return "adb"; } public executeShellCommand(deviceId: string, command: string): Promise { return this.childProcess.execFileToString(this.adbExecutable, [ "-s", deviceId, "shell", command, ]); } public installApplicationToEmulator(appPath: string): Promise { return this.childProcess.execFileToString("adb", ["install", appPath]); } public executeQuery(deviceId: string, args: string[]): Promise { return this.childProcess.execFileToString(this.adbExecutable, ["-s", deviceId, ...args]); } private parseConnectedTargets(input: string): IDebuggableMobileTarget[] { const result: IDebuggableMobileTarget[] = []; const regex = new RegExp("^(\\S+)\\t(\\S+)$", "mg"); let match = regex.exec(input); while (match != null) { result.push({ id: match[1], isOnline: match[2] === "device", isVirtualTarget: this.isVirtualTarget(match[1]), }); match = regex.exec(input); } return result; } public isVirtualTarget(id: string): boolean { return !!id.match(AdbHelper.AndroidSDKEmulatorPattern); } private execute(deviceId: string, args: string[]): Promise { const command = `${this.adbExecutable} -s "${deviceId}" ${args.join(" ")}`; return this.commandExecutor.execute(command); } private getSdkLocationFromLocalPropertiesFile( projectRoot: string, logger?: ILogger, ): string | null { const localPropertiesFilePath = path.join(projectRoot, "android", "local.properties"); if (!fs.existsSync(localPropertiesFilePath)) { if (logger) { logger.info( localize( "LocalPropertiesFileDoesNotExist", "local.properties file doesn't exist. Using Android SDK location from PATH.", ), ); } return null; } let fileContent: string; try { fileContent = fs.readFileSync(localPropertiesFilePath).toString(); } catch (e) { if (logger) { logger.error( localize( "CouldNotReadFrom", "Couldn't read from {0}.", localPropertiesFilePath, ), e as Error, ); logger.info( localize( "UsingAndroidSDKLocationFromPATH", "Using Android SDK location from PATH.", ), ); } return null; } return this.parseSdkLocation(fileContent, logger); } }