microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.4.3

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/extension/android/androidPlatform.ts

227lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4import * as Q from "q";
5
6import {GeneralMobilePlatform, MobilePlatformDeps } from "../generalMobilePlatform";
7import {IAndroidRunOptions} from "../launchArgs";
8import {AdbHelper, AndroidAPILevel, IDevice} from "./adb";
9import {Package} from "../../common/node/package";
10import {PromiseUtil} from "../../common/node/promise";
11import {PackageNameResolver} from "./packageNameResolver";
12import {OutputVerifier, PatternToFailure} from "../../common/outputVerifier";
13import {TelemetryHelper} from "../../common/telemetryHelper";
14import {CommandExecutor} from "../../common/commandExecutor";
15import {LogCatMonitor} from "./logCatMonitor";
16
17/**
18 * Android specific platform implementation for debugging RN applications.
19 */
20export class AndroidPlatform extends GeneralMobilePlatform {
21 private static MULTIPLE_DEVICES_ERROR = "error: more than one device/emulator";
22
23 // We should add the common Android build/run errors we find to this list
24 private static RUN_ANDROID_FAILURE_PATTERNS: PatternToFailure[] = [{
25 pattern: "Failed to install on any devices",
26 message: "Could not install the app on any available device. Make sure you have a correctly"
27 + " configured device or emulator running. See https://facebook.github.io/react-native/docs/android-setup.html",
28 }, {
29 pattern: "com.android.ddmlib.ShellCommandUnresponsiveException",
30 message: "An Android shell command timed-out. Please retry the operation.",
31 }, {
32 pattern: "Android project not found",
33 message: "Android project not found.",
34
35 }, {
36 pattern: "error: more than one device/emulator",
37 message: AndroidPlatform.MULTIPLE_DEVICES_ERROR,
38 }, {
39 pattern: /^Error: Activity class \{.*\} does not exist\.$/m,
40 message: "Failed to launch the specified activity. Try running application manually and "
41 + "start debugging using 'Attach to packager' launch configuration.",
42 }];
43
44 private static RUN_ANDROID_SUCCESS_PATTERNS: string[] = ["BUILD SUCCESSFUL", "Starting the app", "Starting: Intent"];
45
46 private debugTarget: IDevice;
47 private devices: IDevice[];
48 private packageName: string;
49 private logCatMonitor: LogCatMonitor | null = null;
50
51 private needsToLaunchApps: boolean = false;
52 public static showDevMenu(deviceId?: string): Q.Promise<void> {
53 return AdbHelper.showDevMenu(deviceId);
54 }
55 public static reloadApp(deviceId?: string): Q.Promise<void> {
56 return AdbHelper.reloadApp(deviceId);
57 }
58
59 // 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.
60 constructor(protected runOptions: IAndroidRunOptions, platformDeps: MobilePlatformDeps = {}) {
61 super(runOptions, platformDeps);
62
63 if (this.runOptions.target === AndroidPlatform.simulatorString ||
64 this.runOptions.target === AndroidPlatform.deviceString) {
65
66 const message = `Target ${this.runOptions.target} is not supported for Android ` +
67 "platform. If you want to use particular device or simulator for launching " +
68 "Android app, please specify device id (as in 'adb devices' output) instead.";
69
70 this.logger.warning(message);
71 delete this.runOptions.target;
72 }
73 }
74
75 public runApp(shouldLaunchInAllDevices: boolean = false): Q.Promise<void> {
76 return TelemetryHelper.generate("AndroidPlatform.runApp", () => {
77 const runArguments = this.getRunArgument();
78 const runAndroidSpawn = new CommandExecutor(this.projectPath, this.logger).spawnReactCommand("run-android", runArguments);
79
80 const output = new OutputVerifier(
81 () =>
82 Q(AndroidPlatform.RUN_ANDROID_SUCCESS_PATTERNS),
83 () =>
84 Q(AndroidPlatform.RUN_ANDROID_FAILURE_PATTERNS)).process(runAndroidSpawn);
85
86 return output
87 .finally(() => {
88 return this.initializeTargetDevicesAndPackageName();
89 }).then(() => [this.debugTarget], reason => {
90 if (reason.message === AndroidPlatform.MULTIPLE_DEVICES_ERROR && this.devices.length > 1 && this.debugTarget) {
91 /* If it failed due to multiple devices, we'll apply this workaround to make it work anyways */
92 this.needsToLaunchApps = true;
93 return shouldLaunchInAllDevices
94 ? AdbHelper.getOnlineDevices()
95 : Q([this.debugTarget]);
96 } else {
97 return Q.reject<IDevice[]>(reason);
98 }
99 }).then(devices => {
100 return new PromiseUtil().forEach(devices, device => {
101 return this.launchAppWithADBReverseAndLogCat(device);
102 });
103 });
104 });
105 }
106
107 public enableJSDebuggingMode(): Q.Promise<void> {
108 return AdbHelper.switchDebugMode(this.runOptions.projectRoot, this.packageName, true, this.debugTarget.id);
109 }
110
111 public disableJSDebuggingMode(): Q.Promise<void> {
112 return AdbHelper.switchDebugMode(this.runOptions.projectRoot, this.packageName, false, this.debugTarget.id);
113 }
114
115 public prewarmBundleCache(): Q.Promise<void> {
116 return this.packager.prewarmBundleCache("android");
117 }
118
119 public getRunArgument(): string[] {
120 let runArguments: string[] = [];
121
122 if (this.runOptions.runArguments && this.runOptions.runArguments.length > 0) {
123 runArguments = this.runOptions.runArguments;
124 } else {
125 if (this.runOptions.variant) {
126 runArguments.push("--variant", this.runOptions.variant);
127 }
128 if (this.runOptions.target) {
129 runArguments.push("--deviceId", this.runOptions.target);
130 }
131 }
132
133 runArguments.push("--no-packager");
134
135 return runArguments;
136 }
137
138 private initializeTargetDevicesAndPackageName(): Q.Promise<void> {
139 return AdbHelper.getConnectedDevices().then(devices => {
140 this.devices = devices;
141 this.debugTarget = this.getTargetEmulator(devices);
142 return this.getPackageName().then(packageName => {
143 this.packageName = packageName;
144 });
145 });
146 }
147
148 private launchAppWithADBReverseAndLogCat(device: IDevice): Q.Promise<void> {
149 return Q({})
150 .then(() => {
151 return this.configureADBReverseWhenApplicable(device);
152 }).then(() => {
153 return this.needsToLaunchApps
154 ? AdbHelper.launchApp(this.runOptions.projectRoot, this.packageName, device.id)
155 : Q<void>(void 0);
156 }).then(() => {
157 return this.startMonitoringLogCat(device, this.runOptions.logCatArguments);
158 });
159 }
160
161 private configureADBReverseWhenApplicable(device: IDevice): Q.Promise<void> {
162 return Q({}) // For other emulators and devices we try to enable adb reverse
163 .then(() => AdbHelper.apiVersion(device.id))
164 .then(apiVersion => {
165 if (apiVersion >= AndroidAPILevel.LOLLIPOP) { // If we support adb reverse
166 return AdbHelper.reverseAdb(device.id, Number( this.runOptions.packagerPort));
167 } else {
168 this.logger.warning(`Device ${device.id} supports only API Level ${apiVersion}. `
169 + `Level ${AndroidAPILevel.LOLLIPOP} is needed to support port forwarding via adb reverse. `
170 + "For debugging to work you'll need <Shake or press menu button> for the dev menu, "
171 + "go into <Dev Settings> and configure <Debug Server host & port for Device> to be "
172 + "an IP address of your computer that the Device can reach. More info at: "
173 + "https://facebook.github.io/react-native/docs/debugging.html#debugging-react-native-apps");
174 return void 0;
175 }
176 });
177 }
178
179 private getPackageName(): Q.Promise<string> {
180 return new Package(this.runOptions.projectRoot).name().then(appName =>
181 new PackageNameResolver(appName).resolvePackageName(this.runOptions.projectRoot));
182 }
183
184 /**
185 * Returns the target emulator, using the following logic:
186 * * If an emulator is specified and it is connected, use that one.
187 * * Otherwise, use the first one in the list.
188 */
189 private getTargetEmulator(devices: IDevice[]): IDevice {
190 let activeFilterFunction = (device: IDevice) => {
191 return device.isOnline;
192 };
193
194 let targetFilterFunction = (device: IDevice) => {
195 return device.id === this.runOptions.target && activeFilterFunction(device);
196 };
197
198 if (this.runOptions && this.runOptions.target && devices) {
199 /* check if the specified target is active */
200 const targetDevice = devices.find(targetFilterFunction);
201 if (targetDevice) {
202 return targetDevice;
203 }
204 }
205
206 /* return the first active device in the list */
207 let activeDevices = devices && devices.filter(activeFilterFunction);
208 return activeDevices && activeDevices[0];
209 }
210
211 private startMonitoringLogCat(device: IDevice, logCatArguments: string): void {
212 this.stopMonitoringLogCat(); // Stop previous logcat monitor if it's running
213
214 // this.logCatMonitor can be mutated, so we store it locally too
215 this.logCatMonitor = new LogCatMonitor(device.id, logCatArguments);
216 this.logCatMonitor.start() // The LogCat will continue running forever, so we don't wait for it
217 .catch(error => this.logger.warning("Error while monitoring LogCat", error)) // The LogCatMonitor failing won't stop the debugging experience
218 .done();
219 }
220
221 private stopMonitoringLogCat(): void {
222 if (this.logCatMonitor) {
223 this.logCatMonitor.dispose();
224 this.logCatMonitor = null;
225 }
226 }
227}
228