microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
revert-2693-dev/lucygramley/networkIsolationPolicy

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/android/androidPlatform.ts

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