microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
4cd259621ddfbd348fade892a2f3ee87fd1924c5

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/android/androidPlatform.ts

383lines · 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";
5import { MobilePlatformDeps, TargetType } from "../generalPlatform";
6import { IAndroidRunOptions, PlatformType } from "../launchArgs";
7import { Package } from "../../common/node/package";
8import { PackageNameResolver } from "./packageNameResolver";
9import { OutputVerifier, PatternToFailure } from "../../common/outputVerifier";
10import { TelemetryHelper } from "../../common/telemetryHelper";
11import { CommandExecutor } from "../../common/commandExecutor";
12import { LogCatMonitor } from "./logCatMonitor";
13import * as nls from "vscode-nls";
14import { InternalErrorCode } from "../../common/error/internalErrorCode";
15import { ErrorHelper } from "../../common/error/errorHelper";
16import { notNullOrUndefined } from "../../common/utils";
17import { PromiseUtil } from "../../common/node/promise";
18import { LogCatMonitorManager } from "./logCatMonitorManager";
19import { AndroidTarget, AndroidTargetManager } from "./androidTargetManager";
20import { AdbHelper, AndroidAPILevel } from "./adb";
21import { GeneralMobilePlatform } from "../generalMobilePlatform";
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 packageName: string;
63 private adbHelper: AdbHelper;
64 private logCatMonitor: LogCatMonitor | null = null;
65 private needsToLaunchApps: boolean = false;
66
67 protected targetManager: AndroidTargetManager;
68 protected target?: AndroidTarget;
69
70 // 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.
71 constructor(protected runOptions: IAndroidRunOptions, platformDeps: MobilePlatformDeps = {}) {
72 super(runOptions, platformDeps);
73 this.adbHelper = new AdbHelper(
74 this.runOptions.projectRoot,
75 runOptions.nodeModulesRoot,
76 this.logger,
77 );
78 this.targetManager = new AndroidTargetManager(this.adbHelper);
79 }
80
81 public showDevMenu(deviceId?: string): Promise<void> {
82 return this.adbHelper.showDevMenu(deviceId);
83 }
84
85 public reloadApp(deviceId?: string): Promise<void> {
86 return this.adbHelper.reloadApp(deviceId);
87 }
88
89 public async getTarget(): Promise<AndroidTarget> {
90 if (!this.target) {
91 const onlineTargets = await this.adbHelper.getOnlineTargets();
92 const target = await this.getTargetFromRunArgs();
93 if (target) {
94 this.target = target;
95 } else {
96 const onlineTargetsBySpecifiedType = onlineTargets.filter(target => {
97 switch (this.runOptions.target) {
98 case TargetType.Simulator:
99 return target.isVirtualTarget;
100 case TargetType.Device:
101 return !target.isVirtualTarget;
102 case undefined:
103 case "":
104 return true;
105 default:
106 return target.id === this.runOptions.target;
107 }
108 });
109 if (onlineTargetsBySpecifiedType.length) {
110 this.target = AndroidTarget.fromInterface(onlineTargetsBySpecifiedType[0]);
111 } else if (onlineTargets.length) {
112 this.logger.warning(
113 localize(
114 "ThereIsNoOnlineTargetWithSpecifiedTargetType",
115 "There is no any online target with specified target type '{0}'. Continue with any online target.",
116 this.runOptions.target,
117 ),
118 );
119 this.target = AndroidTarget.fromInterface(onlineTargets[0]);
120 } else {
121 throw ErrorHelper.getInternalError(
122 InternalErrorCode.AndroidThereIsNoAnyOnlineDebuggableTarget,
123 );
124 }
125 }
126 }
127 return this.target;
128 }
129
130 public async runApp(shouldLaunchInAllDevices: boolean = false): Promise<void> {
131 let extProps: any = {
132 platform: {
133 value: PlatformType.Android,
134 isPii: false,
135 },
136 };
137
138 if (this.runOptions.isDirect) {
139 extProps.isDirect = {
140 value: true,
141 isPii: false,
142 };
143 }
144
145 extProps = TelemetryHelper.addPlatformPropertiesToTelemetryProperties(
146 this.runOptions,
147 this.runOptions.reactNativeVersions,
148 extProps,
149 );
150
151 await TelemetryHelper.generate("AndroidPlatform.runApp", extProps, async () => {
152 const env = GeneralMobilePlatform.getEnvArgument(
153 process.env,
154 this.runOptions.env,
155 this.runOptions.envFile,
156 );
157
158 if (
159 !semver.valid(
160 this.runOptions.reactNativeVersions.reactNativeVersion,
161 ) /*Custom RN implementations should support this flag*/ ||
162 semver.gte(
163 this.runOptions.reactNativeVersions.reactNativeVersion,
164 AndroidPlatform.NO_PACKAGER_VERSION,
165 )
166 ) {
167 this.runArguments.push("--no-packager");
168 }
169
170 let mainActivity = GeneralMobilePlatform.getOptFromRunArgs(
171 this.runArguments,
172 "--main-activity",
173 );
174
175 if (mainActivity) {
176 this.adbHelper.setLaunchActivity(mainActivity);
177 } else if (notNullOrUndefined(this.runOptions.debugLaunchActivity)) {
178 this.runArguments.push("--main-activity", this.runOptions.debugLaunchActivity);
179 this.adbHelper.setLaunchActivity(this.runOptions.debugLaunchActivity);
180 }
181
182 const runAndroidSpawn = new CommandExecutor(
183 this.runOptions.nodeModulesRoot,
184 this.projectPath,
185 this.logger,
186 ).spawnReactCommand("run-android", this.runArguments, { env });
187 const output = new OutputVerifier(
188 () => Promise.resolve(AndroidPlatform.RUN_ANDROID_SUCCESS_PATTERNS),
189 () => Promise.resolve(AndroidPlatform.RUN_ANDROID_FAILURE_PATTERNS),
190 PlatformType.Android,
191 ).process(runAndroidSpawn);
192
193 let devicesIdsForLaunch: string[] = [];
194 const onlineTargetsIds = (await this.adbHelper.getOnlineTargets()).map(
195 target => target.id,
196 );
197 let targetId: string | undefined;
198 try {
199 try {
200 await output;
201 } finally {
202 targetId = await this.getTargetIdForRunApp(onlineTargetsIds);
203 this.packageName = await this.getPackageName();
204 devicesIdsForLaunch = [targetId];
205 }
206 } catch (error) {
207 if (!targetId) {
208 targetId = await this.getTargetIdForRunApp(onlineTargetsIds);
209 }
210 if (
211 error.message ===
212 ErrorHelper.getInternalError(
213 InternalErrorCode.AndroidMoreThanOneDeviceOrEmulator,
214 ).message &&
215 onlineTargetsIds.length >= 1 &&
216 targetId
217 ) {
218 /* If it failed due to multiple devices, we'll apply this workaround to make it work anyways */
219 this.needsToLaunchApps = true;
220 devicesIdsForLaunch = shouldLaunchInAllDevices ? onlineTargetsIds : [targetId];
221 } else {
222 throw error;
223 }
224 }
225
226 await PromiseUtil.forEach(devicesIdsForLaunch, deviceId =>
227 this.launchAppWithADBReverseAndLogCat(deviceId),
228 );
229 });
230 }
231
232 public async enableJSDebuggingMode(): Promise<void> {
233 return this.adbHelper.switchDebugMode(
234 this.runOptions.projectRoot,
235 this.packageName,
236 true,
237 (await this.getTarget()).id,
238 this.getAppIdSuffixFromRunArgumentsIfExists(),
239 );
240 }
241
242 public async disableJSDebuggingMode(): Promise<void> {
243 return this.adbHelper.switchDebugMode(
244 this.runOptions.projectRoot,
245 this.packageName,
246 false,
247 (await this.getTarget()).id,
248 this.getAppIdSuffixFromRunArgumentsIfExists(),
249 );
250 }
251
252 public prewarmBundleCache(): Promise<void> {
253 return this.packager.prewarmBundleCache(PlatformType.Android);
254 }
255
256 public getRunArguments(): string[] {
257 let runArguments: string[] = [];
258
259 if (this.runOptions.runArguments && this.runOptions.runArguments.length > 0) {
260 runArguments = this.runOptions.runArguments;
261 } else {
262 if (this.runOptions.variant) {
263 runArguments.push("--variant", this.runOptions.variant);
264 }
265 if (this.runOptions.target) {
266 if (
267 this.runOptions.target !== TargetType.Device &&
268 this.runOptions.target !== TargetType.Simulator
269 ) {
270 runArguments.push("--deviceId", this.runOptions.target);
271 }
272 }
273 }
274
275 return runArguments;
276 }
277
278 public dispose(): void {
279 if (this.logCatMonitor) {
280 LogCatMonitorManager.delMonitor(this.logCatMonitor.deviceId);
281 this.logCatMonitor = null;
282 }
283 }
284
285 public async getTargetFromRunArgs(): Promise<AndroidTarget | undefined> {
286 if (this.runOptions.runArguments && this.runOptions.runArguments.length) {
287 const deviceId = GeneralMobilePlatform.getOptFromRunArgs(
288 this.runOptions.runArguments,
289 "--deviceId",
290 );
291 if (deviceId) {
292 return new AndroidTarget(true, this.adbHelper.isVirtualTarget(deviceId), deviceId);
293 }
294 }
295 return undefined;
296 }
297
298 private async getTargetIdForRunApp(onlineTargetsIds: string[]): Promise<string> {
299 let deviceId: string | undefined;
300 if (this.runOptions.runArguments && this.runOptions.runArguments.length) {
301 deviceId = GeneralMobilePlatform.getOptFromRunArgs(
302 this.runOptions.runArguments,
303 "--deviceId",
304 );
305 }
306 return deviceId
307 ? deviceId
308 : this.runOptions.target &&
309 this.runOptions.target !== TargetType.Simulator &&
310 this.runOptions.target !== TargetType.Device &&
311 onlineTargetsIds.find(id => id === this.runOptions.target)
312 ? this.runOptions.target
313 : (await this.getTarget()).id;
314 }
315
316 private getAppIdSuffixFromRunArgumentsIfExists(): string | undefined {
317 const appIdSuffixIndex = this.runArguments.indexOf("--appIdSuffix");
318 if (appIdSuffixIndex > -1) {
319 return this.runArguments[appIdSuffixIndex + 1];
320 }
321 return undefined;
322 }
323
324 private async launchAppWithADBReverseAndLogCat(deviceId: string): Promise<void> {
325 await this.configureADBReverseWhenApplicable(deviceId);
326 if (this.needsToLaunchApps) {
327 await this.adbHelper.launchApp(this.runOptions.projectRoot, this.packageName, deviceId);
328 }
329 return this.startMonitoringLogCat(deviceId, this.runOptions.logCatArguments);
330 }
331
332 private async configureADBReverseWhenApplicable(deviceId: string): Promise<void> {
333 // For other emulators and devices we try to enable adb reverse
334 const apiVersion = await this.adbHelper.apiVersion(deviceId);
335 if (apiVersion >= AndroidAPILevel.LOLLIPOP) {
336 // If we support adb reverse
337 try {
338 this.adbHelper.reverseAdb(deviceId, Number(this.runOptions.packagerPort));
339 } catch (error) {
340 // "adb reverse" command could work incorrectly with remote devices, then skip the error and try to go on
341 if (
342 this.adbHelper.isRemoteTarget(deviceId) &&
343 error.message.includes(AndroidPlatform.RUN_ANDROID_FAILURE_PATTERNS[3].pattern)
344 ) {
345 this.logger.warning(error.message);
346 } else {
347 throw error;
348 }
349 }
350 } else {
351 this.logger.warning(
352 localize(
353 "DeviceSupportsOnlyAPILevel",
354 "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",
355 deviceId,
356 apiVersion,
357 AndroidAPILevel.LOLLIPOP,
358 ),
359 );
360 }
361 }
362
363 private async getPackageName(): Promise<string> {
364 const appName = await new Package(this.runOptions.projectRoot).name();
365 return new PackageNameResolver(appName).resolvePackageName(this.runOptions.projectRoot);
366 }
367
368 private startMonitoringLogCat(deviceId: string, logCatArguments: string[]): void {
369 LogCatMonitorManager.delMonitor(deviceId); // Stop previous logcat monitor if it's running
370
371 // this.logCatMonitor can be mutated, so we store it locally too
372 this.logCatMonitor = new LogCatMonitor(deviceId, this.adbHelper, logCatArguments);
373 LogCatMonitorManager.addMonitor(this.logCatMonitor);
374 this.logCatMonitor
375 .start() // The LogCat will continue running forever, so we don't wait for it
376 .catch(error => {
377 this.logger.warning(error);
378 this.logger.warning(
379 localize("ErrorWhileMonitoringLogCat", "Error while monitoring LogCat"),
380 );
381 }); // The LogCatMonitor failing won't stop the debugging experience
382 }
383}