microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
866e020b2c664db8b8c5bf972901bc2fc25ddd93

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/android/androidPlatform.ts

384lines · 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 this.projectObserver?.updateRNAndroidHermesProjectState(true);
144 }
145
146 extProps = TelemetryHelper.addPlatformPropertiesToTelemetryProperties(
147 this.runOptions,
148 this.runOptions.reactNativeVersions,
149 extProps,
150 );
151
152 await TelemetryHelper.generate("AndroidPlatform.runApp", extProps, async () => {
153 const env = GeneralMobilePlatform.getEnvArgument(
154 process.env,
155 this.runOptions.env,
156 this.runOptions.envFile,
157 );
158
159 if (
160 !semver.valid(
161 this.runOptions.reactNativeVersions.reactNativeVersion,
162 ) /*Custom RN implementations should support this flag*/ ||
163 semver.gte(
164 this.runOptions.reactNativeVersions.reactNativeVersion,
165 AndroidPlatform.NO_PACKAGER_VERSION,
166 )
167 ) {
168 this.runArguments.push("--no-packager");
169 }
170
171 let mainActivity = GeneralMobilePlatform.getOptFromRunArgs(
172 this.runArguments,
173 "--main-activity",
174 );
175
176 if (mainActivity) {
177 this.adbHelper.setLaunchActivity(mainActivity);
178 } else if (notNullOrUndefined(this.runOptions.debugLaunchActivity)) {
179 this.runArguments.push("--main-activity", this.runOptions.debugLaunchActivity);
180 this.adbHelper.setLaunchActivity(this.runOptions.debugLaunchActivity);
181 }
182
183 const runAndroidSpawn = new CommandExecutor(
184 this.runOptions.nodeModulesRoot,
185 this.projectPath,
186 this.logger,
187 ).spawnReactCommand("run-android", this.runArguments, { env });
188 const output = new OutputVerifier(
189 () => Promise.resolve(AndroidPlatform.RUN_ANDROID_SUCCESS_PATTERNS),
190 () => Promise.resolve(AndroidPlatform.RUN_ANDROID_FAILURE_PATTERNS),
191 PlatformType.Android,
192 ).process(runAndroidSpawn);
193
194 let devicesIdsForLaunch: string[] = [];
195 const onlineTargetsIds = (await this.adbHelper.getOnlineTargets()).map(
196 target => target.id,
197 );
198 let targetId: string | undefined;
199 try {
200 try {
201 await output;
202 } finally {
203 targetId = await this.getTargetIdForRunApp(onlineTargetsIds);
204 this.packageName = await this.getPackageName();
205 devicesIdsForLaunch = [targetId];
206 }
207 } catch (error) {
208 if (!targetId) {
209 targetId = await this.getTargetIdForRunApp(onlineTargetsIds);
210 }
211 if (
212 error.message ===
213 ErrorHelper.getInternalError(
214 InternalErrorCode.AndroidMoreThanOneDeviceOrEmulator,
215 ).message &&
216 onlineTargetsIds.length >= 1 &&
217 targetId
218 ) {
219 /* If it failed due to multiple devices, we'll apply this workaround to make it work anyways */
220 this.needsToLaunchApps = true;
221 devicesIdsForLaunch = shouldLaunchInAllDevices ? onlineTargetsIds : [targetId];
222 } else {
223 throw error;
224 }
225 }
226
227 await PromiseUtil.forEach(devicesIdsForLaunch, deviceId =>
228 this.launchAppWithADBReverseAndLogCat(deviceId),
229 );
230 });
231 }
232
233 public async enableJSDebuggingMode(): Promise<void> {
234 return this.adbHelper.switchDebugMode(
235 this.runOptions.projectRoot,
236 this.packageName,
237 true,
238 (await this.getTarget()).id,
239 this.getAppIdSuffixFromRunArgumentsIfExists(),
240 );
241 }
242
243 public async disableJSDebuggingMode(): Promise<void> {
244 return this.adbHelper.switchDebugMode(
245 this.runOptions.projectRoot,
246 this.packageName,
247 false,
248 (await this.getTarget()).id,
249 this.getAppIdSuffixFromRunArgumentsIfExists(),
250 );
251 }
252
253 public prewarmBundleCache(): Promise<void> {
254 return this.packager.prewarmBundleCache(PlatformType.Android);
255 }
256
257 public getRunArguments(): string[] {
258 let runArguments: string[] = [];
259
260 if (this.runOptions.runArguments && this.runOptions.runArguments.length > 0) {
261 runArguments = this.runOptions.runArguments;
262 } else {
263 if (this.runOptions.variant) {
264 runArguments.push("--variant", this.runOptions.variant);
265 }
266 if (this.runOptions.target) {
267 if (
268 this.runOptions.target !== TargetType.Device &&
269 this.runOptions.target !== TargetType.Simulator
270 ) {
271 runArguments.push("--deviceId", this.runOptions.target);
272 }
273 }
274 }
275
276 return runArguments;
277 }
278
279 public dispose(): void {
280 if (this.logCatMonitor) {
281 LogCatMonitorManager.delMonitor(this.logCatMonitor.deviceId);
282 this.logCatMonitor = null;
283 }
284 }
285
286 public async getTargetFromRunArgs(): Promise<AndroidTarget | undefined> {
287 if (this.runOptions.runArguments && this.runOptions.runArguments.length) {
288 const deviceId = GeneralMobilePlatform.getOptFromRunArgs(
289 this.runOptions.runArguments,
290 "--deviceId",
291 );
292 if (deviceId) {
293 return new AndroidTarget(true, this.adbHelper.isVirtualTarget(deviceId), deviceId);
294 }
295 }
296 return undefined;
297 }
298
299 private async getTargetIdForRunApp(onlineTargetsIds: string[]): Promise<string> {
300 let deviceId: string | undefined;
301 if (this.runOptions.runArguments && this.runOptions.runArguments.length) {
302 deviceId = GeneralMobilePlatform.getOptFromRunArgs(
303 this.runOptions.runArguments,
304 "--deviceId",
305 );
306 }
307 return deviceId
308 ? deviceId
309 : this.runOptions.target &&
310 this.runOptions.target !== TargetType.Simulator &&
311 this.runOptions.target !== TargetType.Device &&
312 onlineTargetsIds.find(id => id === this.runOptions.target)
313 ? this.runOptions.target
314 : (await this.getTarget()).id;
315 }
316
317 private getAppIdSuffixFromRunArgumentsIfExists(): string | undefined {
318 const appIdSuffixIndex = this.runArguments.indexOf("--appIdSuffix");
319 if (appIdSuffixIndex > -1) {
320 return this.runArguments[appIdSuffixIndex + 1];
321 }
322 return undefined;
323 }
324
325 private async launchAppWithADBReverseAndLogCat(deviceId: string): Promise<void> {
326 await this.configureADBReverseWhenApplicable(deviceId);
327 if (this.needsToLaunchApps) {
328 await this.adbHelper.launchApp(this.runOptions.projectRoot, this.packageName, deviceId);
329 }
330 return this.startMonitoringLogCat(deviceId, this.runOptions.logCatArguments);
331 }
332
333 private async configureADBReverseWhenApplicable(deviceId: string): Promise<void> {
334 // For other emulators and devices we try to enable adb reverse
335 const apiVersion = await this.adbHelper.apiVersion(deviceId);
336 if (apiVersion >= AndroidAPILevel.LOLLIPOP) {
337 // If we support adb reverse
338 try {
339 this.adbHelper.reverseAdb(deviceId, Number(this.runOptions.packagerPort));
340 } catch (error) {
341 // "adb reverse" command could work incorrectly with remote devices, then skip the error and try to go on
342 if (
343 this.adbHelper.isRemoteTarget(deviceId) &&
344 error.message.includes(AndroidPlatform.RUN_ANDROID_FAILURE_PATTERNS[3].pattern)
345 ) {
346 this.logger.warning(error.message);
347 } else {
348 throw error;
349 }
350 }
351 } else {
352 this.logger.warning(
353 localize(
354 "DeviceSupportsOnlyAPILevel",
355 "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",
356 deviceId,
357 apiVersion,
358 AndroidAPILevel.LOLLIPOP,
359 ),
360 );
361 }
362 }
363
364 private async getPackageName(): Promise<string> {
365 const appName = await new Package(this.runOptions.projectRoot).name();
366 return new PackageNameResolver(appName).resolvePackageName(this.runOptions.projectRoot);
367 }
368
369 private startMonitoringLogCat(deviceId: string, logCatArguments: string[]): void {
370 LogCatMonitorManager.delMonitor(deviceId); // Stop previous logcat monitor if it's running
371
372 // this.logCatMonitor can be mutated, so we store it locally too
373 this.logCatMonitor = new LogCatMonitor(deviceId, this.adbHelper, logCatArguments);
374 LogCatMonitorManager.addMonitor(this.logCatMonitor);
375 this.logCatMonitor
376 .start() // The LogCat will continue running forever, so we don't wait for it
377 .catch(error => {
378 this.logger.warning(error);
379 this.logger.warning(
380 localize("ErrorWhileMonitoringLogCat", "Error while monitoring LogCat"),
381 );
382 }); // The LogCatMonitor failing won't stop the debugging experience
383 }
384}
385