microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
db6fd42a8f9de3bdadf99868ae4ea9b3cf8796d8

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/android/androidPlatform.ts

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