microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.7.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/android/androidPlatform.ts

367lines · 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 async resolveVirtualDevice(target: string): Promise<IAndroidEmulator | null> {
96 if (!target.includes("device")) {
97 const emulator = await this.emulatorManager.startEmulator(target);
98 if (emulator) {
99 GeneralMobilePlatform.setRunArgument(this.runArguments, "--deviceId", emulator.id);
100 }
101 return emulator;
102 } else {
103 return null;
104 }
105 }
106
107 public async runApp(shouldLaunchInAllDevices: boolean = false): Promise<void> {
108 let extProps: any = {
109 platform: {
110 value: PlatformType.Android,
111 isPii: false,
112 },
113 };
114
115 if (this.runOptions.isDirect) {
116 extProps.isDirect = {
117 value: true,
118 isPii: false,
119 };
120 }
121
122 extProps = TelemetryHelper.addPlatformPropertiesToTelemetryProperties(
123 this.runOptions,
124 this.runOptions.reactNativeVersions,
125 extProps,
126 );
127
128 await TelemetryHelper.generate("AndroidPlatform.runApp", extProps, async () => {
129 const env = GeneralMobilePlatform.getEnvArgument(
130 process.env,
131 this.runOptions.env,
132 this.runOptions.envFile,
133 );
134
135 if (
136 !semver.valid(
137 this.runOptions.reactNativeVersions.reactNativeVersion,
138 ) /*Custom RN implementations should support this flag*/ ||
139 semver.gte(
140 this.runOptions.reactNativeVersions.reactNativeVersion,
141 AndroidPlatform.NO_PACKAGER_VERSION,
142 )
143 ) {
144 this.runArguments.push("--no-packager");
145 }
146
147 let mainActivity = GeneralMobilePlatform.getOptFromRunArgs(
148 this.runArguments,
149 "--main-activity",
150 );
151
152 if (mainActivity) {
153 this.adbHelper.setLaunchActivity(mainActivity);
154 } else if (notNullOrUndefined(this.runOptions.debugLaunchActivity)) {
155 this.runArguments.push("--main-activity", this.runOptions.debugLaunchActivity);
156 this.adbHelper.setLaunchActivity(this.runOptions.debugLaunchActivity);
157 }
158
159 const runAndroidSpawn = new CommandExecutor(
160 this.runOptions.nodeModulesRoot,
161 this.projectPath,
162 this.logger,
163 ).spawnReactCommand("run-android", this.runArguments, { env });
164 const output = new OutputVerifier(
165 () => Promise.resolve(AndroidPlatform.RUN_ANDROID_SUCCESS_PATTERNS),
166 () => Promise.resolve(AndroidPlatform.RUN_ANDROID_FAILURE_PATTERNS),
167 PlatformType.Android,
168 ).process(runAndroidSpawn);
169
170 let devicesForLaunch = [];
171 try {
172 try {
173 await output;
174 } finally {
175 await this.initializeTargetDevicesAndPackageName();
176 devicesForLaunch = [this.debugTarget];
177 }
178 } catch (error) {
179 if (
180 error.message ===
181 ErrorHelper.getInternalError(
182 InternalErrorCode.AndroidMoreThanOneDeviceOrEmulator,
183 ).message &&
184 this.devices.length >= 1 &&
185 this.debugTarget
186 ) {
187 /* If it failed due to multiple devices, we'll apply this workaround to make it work anyways */
188 this.needsToLaunchApps = true;
189 devicesForLaunch = shouldLaunchInAllDevices
190 ? await this.adbHelper.getOnlineDevices()
191 : [this.debugTarget];
192 } else {
193 throw error;
194 }
195 }
196
197 await PromiseUtil.forEach(devicesForLaunch, device =>
198 this.launchAppWithADBReverseAndLogCat(device),
199 );
200 });
201 }
202
203 public enableJSDebuggingMode(): Promise<void> {
204 return this.adbHelper.switchDebugMode(
205 this.runOptions.projectRoot,
206 this.packageName,
207 true,
208 this.debugTarget.id,
209 this.getAppIdSuffixFromRunArgumentsIfExists(),
210 );
211 }
212
213 public disableJSDebuggingMode(): Promise<void> {
214 return this.adbHelper.switchDebugMode(
215 this.runOptions.projectRoot,
216 this.packageName,
217 false,
218 this.debugTarget.id,
219 this.getAppIdSuffixFromRunArgumentsIfExists(),
220 );
221 }
222
223 public prewarmBundleCache(): Promise<void> {
224 return this.packager.prewarmBundleCache(PlatformType.Android);
225 }
226
227 public getRunArguments(): string[] {
228 let runArguments: string[] = [];
229
230 if (this.runOptions.runArguments && this.runOptions.runArguments.length > 0) {
231 runArguments = this.runOptions.runArguments;
232 } else {
233 if (this.runOptions.variant) {
234 runArguments.push("--variant", this.runOptions.variant);
235 }
236 if (this.runOptions.target) {
237 if (
238 this.runOptions.target === AndroidPlatform.simulatorString ||
239 this.runOptions.target === AndroidPlatform.deviceString
240 ) {
241 const message = localize(
242 "TargetIsNotSupportedForAndroid",
243 "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.",
244 this.runOptions.target,
245 );
246 this.logger.warning(message);
247 } else {
248 runArguments.push("--deviceId", this.runOptions.target);
249 }
250 }
251 }
252
253 return runArguments;
254 }
255
256 public dispose(): void {
257 if (this.logCatMonitor) {
258 LogCatMonitorManager.delMonitor(this.logCatMonitor.deviceId);
259 this.logCatMonitor = null;
260 }
261 }
262
263 private getAppIdSuffixFromRunArgumentsIfExists(): string | undefined {
264 const appIdSuffixIndex = this.runArguments.indexOf("--appIdSuffix");
265 if (appIdSuffixIndex > -1) {
266 return this.runArguments[appIdSuffixIndex + 1];
267 }
268 return undefined;
269 }
270
271 private async initializeTargetDevicesAndPackageName(): Promise<void> {
272 this.devices = await this.adbHelper.getConnectedDevices();
273 this.debugTarget = this.getTargetEmulator(this.devices);
274 this.packageName = await this.getPackageName();
275 }
276
277 private async launchAppWithADBReverseAndLogCat(device: IAdbDevice): Promise<void> {
278 await this.configureADBReverseWhenApplicable(device);
279 if (this.needsToLaunchApps) {
280 await this.adbHelper.launchApp(
281 this.runOptions.projectRoot,
282 this.packageName,
283 device.id,
284 );
285 }
286 return this.startMonitoringLogCat(device, this.runOptions.logCatArguments);
287 }
288
289 private async configureADBReverseWhenApplicable(device: IAdbDevice): Promise<void> {
290 // For other emulators and devices we try to enable adb reverse
291 const apiVersion = await this.adbHelper.apiVersion(device.id);
292 if (apiVersion >= AndroidAPILevel.LOLLIPOP) {
293 // If we support adb reverse
294 try {
295 this.adbHelper.reverseAdb(device.id, Number(this.runOptions.packagerPort));
296 } catch (error) {
297 // "adb reverse" command could work incorrectly with remote devices, then skip the error and try to go on
298 if (
299 device.type === AdbDeviceType.RemoteDevice &&
300 error.message.includes(AndroidPlatform.RUN_ANDROID_FAILURE_PATTERNS[3].pattern)
301 ) {
302 this.logger.warning(error.message);
303 } else {
304 throw error;
305 }
306 }
307 } else {
308 this.logger.warning(
309 localize(
310 "DeviceSupportsOnlyAPILevel",
311 "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",
312 device.id,
313 apiVersion,
314 AndroidAPILevel.LOLLIPOP,
315 ),
316 );
317 }
318 }
319
320 private async getPackageName(): Promise<string> {
321 const appName = await new Package(this.runOptions.projectRoot).name();
322 return new PackageNameResolver(appName).resolvePackageName(this.runOptions.projectRoot);
323 }
324
325 /**
326 * Returns the target emulator, using the following logic:
327 * * If an emulator is specified and it is connected, use that one.
328 * * Otherwise, use the first one in the list.
329 */
330 private getTargetEmulator(devices: IAdbDevice[]): IAdbDevice {
331 let activeFilterFunction = (device: IAdbDevice) => {
332 return device.isOnline;
333 };
334
335 let targetFilterFunction = (device: IAdbDevice) => {
336 return device.id === this.runOptions.target && activeFilterFunction(device);
337 };
338
339 if (this.runOptions && this.runOptions.target && devices) {
340 /* check if the specified target is active */
341 const targetDevice = devices.find(targetFilterFunction);
342 if (targetDevice) {
343 return targetDevice;
344 }
345 }
346
347 /* return the first active device in the list */
348 let activeDevices = devices && devices.filter(activeFilterFunction);
349 return activeDevices && activeDevices[0];
350 }
351
352 private startMonitoringLogCat(device: IAdbDevice, logCatArguments: string[]): void {
353 LogCatMonitorManager.delMonitor(device.id); // Stop previous logcat monitor if it's running
354
355 // this.logCatMonitor can be mutated, so we store it locally too
356 this.logCatMonitor = new LogCatMonitor(device.id, this.adbHelper, logCatArguments);
357 LogCatMonitorManager.addMonitor(this.logCatMonitor);
358 this.logCatMonitor
359 .start() // The LogCat will continue running forever, so we don't wait for it
360 .catch(error => {
361 this.logger.warning(error);
362 this.logger.warning(
363 localize("ErrorWhileMonitoringLogCat", "Error while monitoring LogCat"),
364 );
365 }); // The LogCatMonitor failing won't stop the debugging experience
366 }
367}
368