microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
ffdf159274093d12e32a09aafae6d0de3099bb4a

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/android/androidPlatform.ts

389lines · 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 semver from "semver";
5
6import { GeneralMobilePlatform, MobilePlatformDeps } from "../generalMobilePlatform";
7import { IAndroidRunOptions, PlatformType } from "../launchArgs";
8import { AdbHelper, AndroidAPILevel, IAdbDevice, AdbDeviceType } from "./adb";
9import { Package } from "../../common/node/package";
10import { PackageNameResolver } from "./packageNameResolver";
11import { OutputVerifier, PatternToFailure } from "../../common/outputVerifier";
12import { TelemetryHelper } from "../../common/telemetryHelper";
13import { CommandExecutor } from "../../common/commandExecutor";
14import { LogCatMonitor } from "./logCatMonitor";
15import * as nls from "vscode-nls";
16import { InternalErrorCode } from "../../common/error/internalErrorCode";
17import { ErrorHelper } from "../../common/error/errorHelper";
18import { notNullOrUndefined } from "../../common/utils";
19import { PromiseUtil } from "../../common/node/promise";
20import { AndroidEmulatorManager, IAndroidEmulator } from "./androidEmulatorManager";
21import { LogCatMonitorManager } from "./logCatMonitorManager";
22nls.config({
23 messageFormat: nls.MessageFormat.bundle,
24 bundleFormat: nls.BundleFormat.standalone,
25})();
26const localize = nls.loadMessageBundle();
27
28/**
29 * Android specific platform implementation for debugging RN applications.
30 */
31export class AndroidPlatform extends GeneralMobilePlatform {
32 // We should add the common Android build/run errors we find to this list
33 private static RUN_ANDROID_FAILURE_PATTERNS: PatternToFailure[] = [
34 {
35 pattern: "Failed to install on any devices",
36 errorCode: InternalErrorCode.AndroidCouldNotInstallTheAppOnAnyAvailibleDevice,
37 },
38 {
39 pattern: "com.android.ddmlib.ShellCommandUnresponsiveException",
40 errorCode: InternalErrorCode.AndroidShellCommandTimedOut,
41 },
42 {
43 pattern: "Android project not found",
44 errorCode: InternalErrorCode.AndroidProjectNotFound,
45 },
46 {
47 pattern: "error: more than one device/emulator",
48 errorCode: InternalErrorCode.AndroidMoreThanOneDeviceOrEmulator,
49 },
50 {
51 pattern: /^Error: Activity class \{.*\} does not exist\.$/m,
52 errorCode: InternalErrorCode.AndroidFailedToLaunchTheSpecifiedActivity,
53 },
54 ];
55
56 private static RUN_ANDROID_SUCCESS_PATTERNS: string[] = [
57 "BUILD SUCCESSFUL",
58 "Starting the app",
59 "Starting: Intent",
60 ];
61
62 private debugTarget: IAdbDevice;
63 private devices: IAdbDevice[];
64 private packageName: string;
65 private adbHelper: AdbHelper;
66 private emulatorManager: AndroidEmulatorManager;
67 private logCatMonitor: LogCatMonitor | null = null;
68
69 private needsToLaunchApps: boolean = false;
70
71 public showDevMenu(deviceId?: string): Promise<void> {
72 return this.adbHelper.showDevMenu(deviceId);
73 }
74
75 public reloadApp(deviceId?: string): Promise<void> {
76 return this.adbHelper.reloadApp(deviceId);
77 }
78
79 // 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.
80 constructor(protected runOptions: IAndroidRunOptions, platformDeps: MobilePlatformDeps = {}) {
81 super(runOptions, platformDeps);
82 this.adbHelper = new AdbHelper(
83 this.runOptions.projectRoot,
84 runOptions.nodeModulesRoot,
85 this.logger,
86 );
87 this.emulatorManager = new AndroidEmulatorManager(this.adbHelper);
88 }
89
90 // TODO: remove this method when sinon will be updated to upper version. Now it is used for tests only.
91 public setAdbHelper(adbHelper: AdbHelper): void {
92 this.adbHelper = adbHelper;
93 }
94
95 public resolveVirtualDevice(target: string): Promise<IAndroidEmulator | null> {
96 if (!target.includes("device")) {
97 return this.emulatorManager
98 .startEmulator(target)
99 .then((emulator: IAndroidEmulator | null) => {
100 if (emulator) {
101 GeneralMobilePlatform.setRunArgument(
102 this.runArguments,
103 "--deviceId",
104 emulator.id,
105 );
106 }
107 return emulator;
108 });
109 } else {
110 return Promise.resolve(null);
111 }
112 }
113
114 public runApp(shouldLaunchInAllDevices: boolean = false): Promise<void> {
115 let extProps: any = {
116 platform: {
117 value: PlatformType.Android,
118 isPii: false,
119 },
120 };
121
122 if (this.runOptions.isDirect) {
123 extProps.isDirect = {
124 value: true,
125 isPii: false,
126 };
127 }
128
129 extProps = TelemetryHelper.addPlatformPropertiesToTelemetryProperties(
130 this.runOptions,
131 this.runOptions.reactNativeVersions,
132 extProps,
133 );
134
135 return TelemetryHelper.generate("AndroidPlatform.runApp", extProps, () => {
136 const env = GeneralMobilePlatform.getEnvArgument(
137 process.env,
138 this.runOptions.env,
139 this.runOptions.envFile,
140 );
141
142 if (
143 !semver.valid(
144 this.runOptions.reactNativeVersions.reactNativeVersion,
145 ) /*Custom RN implementations should support this flag*/ ||
146 semver.gte(
147 this.runOptions.reactNativeVersions.reactNativeVersion,
148 AndroidPlatform.NO_PACKAGER_VERSION,
149 )
150 ) {
151 this.runArguments.push("--no-packager");
152 }
153
154 let mainActivity = GeneralMobilePlatform.getOptFromRunArgs(
155 this.runArguments,
156 "--main-activity",
157 );
158
159 if (mainActivity) {
160 this.adbHelper.setLaunchActivity(mainActivity);
161 } else if (notNullOrUndefined(this.runOptions.debugLaunchActivity)) {
162 this.runArguments.push("--main-activity", this.runOptions.debugLaunchActivity);
163 this.adbHelper.setLaunchActivity(this.runOptions.debugLaunchActivity);
164 }
165
166 const runAndroidSpawn = new CommandExecutor(
167 this.runOptions.nodeModulesRoot,
168 this.projectPath,
169 this.logger,
170 ).spawnReactCommand("run-android", this.runArguments, { env });
171 const output = new OutputVerifier(
172 () => Promise.resolve(AndroidPlatform.RUN_ANDROID_SUCCESS_PATTERNS),
173 () => Promise.resolve(AndroidPlatform.RUN_ANDROID_FAILURE_PATTERNS),
174 PlatformType.Android,
175 ).process(runAndroidSpawn);
176
177 return output
178 .finally(() => {
179 return this.initializeTargetDevicesAndPackageName();
180 })
181 .then(
182 () => [this.debugTarget],
183 reason => {
184 if (
185 reason.message ===
186 ErrorHelper.getInternalError(
187 InternalErrorCode.AndroidMoreThanOneDeviceOrEmulator,
188 ).message &&
189 this.devices.length >= 1 &&
190 this.debugTarget
191 ) {
192 /* If it failed due to multiple devices, we'll apply this workaround to make it work anyways */
193 this.needsToLaunchApps = true;
194 return shouldLaunchInAllDevices
195 ? this.adbHelper.getOnlineDevices()
196 : Promise.resolve([this.debugTarget]);
197 } else {
198 return Promise.reject<IAdbDevice[]>(reason);
199 }
200 },
201 )
202 .then(devices => {
203 return new PromiseUtil().forEach(devices, device => {
204 return this.launchAppWithADBReverseAndLogCat(device);
205 });
206 });
207 });
208 }
209
210 public enableJSDebuggingMode(): Promise<void> {
211 return this.adbHelper.switchDebugMode(
212 this.runOptions.projectRoot,
213 this.packageName,
214 true,
215 this.debugTarget.id,
216 this.getAppIdSuffixFromRunArgumentsIfExists(),
217 );
218 }
219
220 public disableJSDebuggingMode(): Promise<void> {
221 return this.adbHelper.switchDebugMode(
222 this.runOptions.projectRoot,
223 this.packageName,
224 false,
225 this.debugTarget.id,
226 this.getAppIdSuffixFromRunArgumentsIfExists(),
227 );
228 }
229
230 public prewarmBundleCache(): Promise<void> {
231 return this.packager.prewarmBundleCache(PlatformType.Android);
232 }
233
234 public getRunArguments(): string[] {
235 let runArguments: string[] = [];
236
237 if (this.runOptions.runArguments && this.runOptions.runArguments.length > 0) {
238 runArguments = this.runOptions.runArguments;
239 } else {
240 if (this.runOptions.variant) {
241 runArguments.push("--variant", this.runOptions.variant);
242 }
243 if (this.runOptions.target) {
244 if (
245 this.runOptions.target === AndroidPlatform.simulatorString ||
246 this.runOptions.target === AndroidPlatform.deviceString
247 ) {
248 const message = localize(
249 "TargetIsNotSupportedForAndroid",
250 "Target {0} is not supported for Android platform. \n If you want to use particular device or simulator for launching Android app,\n please specify device id (as in 'adb devices' output) instead.",
251 this.runOptions.target,
252 );
253 this.logger.warning(message);
254 } else {
255 runArguments.push("--deviceId", this.runOptions.target);
256 }
257 }
258 }
259
260 return runArguments;
261 }
262
263 public dispose(): void {
264 if (this.logCatMonitor) {
265 LogCatMonitorManager.delMonitor(this.logCatMonitor.deviceId);
266 this.logCatMonitor = null;
267 }
268 }
269
270 private getAppIdSuffixFromRunArgumentsIfExists(): string | undefined {
271 const appIdSuffixIndex = this.runArguments.indexOf("--appIdSuffix");
272 if (appIdSuffixIndex > -1) {
273 return this.runArguments[appIdSuffixIndex + 1];
274 }
275 return undefined;
276 }
277
278 private initializeTargetDevicesAndPackageName(): Promise<void> {
279 return this.adbHelper.getConnectedDevices().then(devices => {
280 this.devices = devices;
281 this.debugTarget = this.getTargetEmulator(devices);
282 return this.getPackageName().then(packageName => {
283 this.packageName = packageName;
284 });
285 });
286 }
287
288 private launchAppWithADBReverseAndLogCat(device: IAdbDevice): Promise<void> {
289 return this.configureADBReverseWhenApplicable(device)
290 .then(() => {
291 return this.needsToLaunchApps
292 ? this.adbHelper.launchApp(
293 this.runOptions.projectRoot,
294 this.packageName,
295 device.id,
296 )
297 : Promise.resolve();
298 })
299 .then(() => {
300 return this.startMonitoringLogCat(device, this.runOptions.logCatArguments);
301 });
302 }
303
304 private configureADBReverseWhenApplicable(device: IAdbDevice): Promise<void> {
305 return Promise.resolve() // For other emulators and devices we try to enable adb reverse
306 .then(() => this.adbHelper.apiVersion(device.id))
307 .then(apiVersion => {
308 if (apiVersion >= AndroidAPILevel.LOLLIPOP) {
309 // If we support adb reverse
310 return this.adbHelper
311 .reverseAdb(device.id, Number(this.runOptions.packagerPort))
312 .catch(err => {
313 // "adb reverse" command could work incorrectly with remote devices, then skip the error and try to go on
314 if (
315 device.type === AdbDeviceType.RemoteDevice &&
316 err.message.includes(
317 AndroidPlatform.RUN_ANDROID_FAILURE_PATTERNS[3].pattern,
318 )
319 ) {
320 this.logger.warning(err.message);
321 } else {
322 throw err;
323 }
324 });
325 } else {
326 const message = localize(
327 "DeviceSupportsOnlyAPILevel",
328 "Device {0} supports only API Level {1}. \n Level {2} is needed to support port forwarding via adb reverse. \n For debugging to work you'll need <Shake or press menu button> for the dev menu, \n go into <Dev Settings> and configure <Debug Server host & port for Device> to be \n an IP address of your computer that the Device can reach. More info at: \n https://facebook.github.io/react-native/docs/debugging.html#debugging-react-native-apps",
329 device.id,
330 apiVersion,
331 AndroidAPILevel.LOLLIPOP,
332 );
333 this.logger.warning(message);
334 return void 0;
335 }
336 });
337 }
338
339 private getPackageName(): Promise<string> {
340 return new Package(this.runOptions.projectRoot)
341 .name()
342 .then(appName =>
343 new PackageNameResolver(appName).resolvePackageName(this.runOptions.projectRoot),
344 );
345 }
346
347 /**
348 * Returns the target emulator, using the following logic:
349 * * If an emulator is specified and it is connected, use that one.
350 * * Otherwise, use the first one in the list.
351 */
352 private getTargetEmulator(devices: IAdbDevice[]): IAdbDevice {
353 let activeFilterFunction = (device: IAdbDevice) => {
354 return device.isOnline;
355 };
356
357 let targetFilterFunction = (device: IAdbDevice) => {
358 return device.id === this.runOptions.target && activeFilterFunction(device);
359 };
360
361 if (this.runOptions && this.runOptions.target && devices) {
362 /* check if the specified target is active */
363 const targetDevice = devices.find(targetFilterFunction);
364 if (targetDevice) {
365 return targetDevice;
366 }
367 }
368
369 /* return the first active device in the list */
370 let activeDevices = devices && devices.filter(activeFilterFunction);
371 return activeDevices && activeDevices[0];
372 }
373
374 private startMonitoringLogCat(device: IAdbDevice, logCatArguments: string[]): void {
375 LogCatMonitorManager.delMonitor(device.id); // Stop previous logcat monitor if it's running
376
377 // this.logCatMonitor can be mutated, so we store it locally too
378 this.logCatMonitor = new LogCatMonitor(device.id, this.adbHelper, logCatArguments);
379 LogCatMonitorManager.addMonitor(this.logCatMonitor);
380 this.logCatMonitor
381 .start() // The LogCat will continue running forever, so we don't wait for it
382 .catch(error => {
383 this.logger.warning(error);
384 this.logger.warning(
385 localize("ErrorWhileMonitoringLogCat", "Error while monitoring LogCat"),
386 );
387 }); // The LogCatMonitor failing won't stop the debugging experience
388 }
389}
390