microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
69ad2ab3726f88dfef08acbd2de49fa1555573c9

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/android/androidPlatform.ts

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