microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.9.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/android/androidPlatform.ts

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