microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
d55f3c22ee18a37c605867c8bf588451292bd24e

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/android/androidPlatform.ts

282lines · 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";
21nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
22const localize = nls.loadMessageBundle();
23
24/**
25 * Android specific platform implementation for debugging RN applications.
26 */
27export class AndroidPlatform extends GeneralMobilePlatform {
28
29 // We should add the common Android build/run errors we find to this list
30 private static RUN_ANDROID_FAILURE_PATTERNS: PatternToFailure[] = [{
31 pattern: "Failed to install on any devices",
32 errorCode: InternalErrorCode.AndroidCouldNotInstallTheAppOnAnyAvailibleDevice,
33 }, {
34 pattern: "com.android.ddmlib.ShellCommandUnresponsiveException",
35 errorCode: InternalErrorCode.AndroidShellCommandTimedOut,
36 }, {
37 pattern: "Android project not found",
38 errorCode: InternalErrorCode.AndroidProjectNotFound,
39
40 }, {
41 pattern: "error: more than one device/emulator",
42 errorCode: InternalErrorCode.AndroidMoreThanOneDeviceOrEmulator,
43 }, {
44 pattern: /^Error: Activity class \{.*\} does not exist\.$/m,
45 errorCode: InternalErrorCode.AndroidFailedToLaunchTheSpecifiedActivity,
46 }];
47
48 private static RUN_ANDROID_SUCCESS_PATTERNS: string[] = ["BUILD SUCCESSFUL", "Starting the app", "Starting: Intent"];
49
50 private debugTarget: IDevice;
51 private devices: IDevice[];
52 private packageName: string;
53 private logCatMonitor: LogCatMonitor | null = null;
54 private adbHelper: AdbHelper;
55 private emulatorManager: AndroidEmulatorManager;
56
57 private needsToLaunchApps: boolean = false;
58
59 public showDevMenu(deviceId?: string): Promise<void> {
60 return this.adbHelper.showDevMenu(deviceId);
61 }
62
63 public reloadApp(deviceId?: string): Promise<void> {
64 return this.adbHelper.reloadApp(deviceId);
65 }
66
67 // 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.
68 constructor(protected runOptions: IAndroidRunOptions, platformDeps: MobilePlatformDeps = {}) {
69 super(runOptions, platformDeps);
70 this.adbHelper = new AdbHelper(this.runOptions.projectRoot, this.logger);
71 this.emulatorManager = new AndroidEmulatorManager(this.adbHelper);
72 }
73
74 // TODO: remove this method when sinon will be updated to upper version. Now it is used for tests only.
75 public setAdbHelper(adbHelper: AdbHelper) {
76 this.adbHelper = adbHelper;
77 }
78
79 public resolveVirtualDevice(target: string): Promise<IAndroidEmulator | null> {
80 if (!target.includes("device")) {
81 return this.emulatorManager.startEmulator(target)
82 .then((emulator: IAndroidEmulator | null) => {
83 if (emulator) {
84 GeneralMobilePlatform.setRunArgument(this.runArguments, "--deviceId", emulator.id);
85 }
86 return emulator;
87 });
88 }
89 else {
90 return Promise.resolve(null);
91 }
92 }
93
94 public runApp(shouldLaunchInAllDevices: boolean = false): Promise<void> {
95 let extProps: any = {
96 platform: {
97 value: PlatformType.Android,
98 isPii: false,
99 },
100 };
101
102 if (this.runOptions.isDirect) {
103 extProps.isDirect = {
104 value: true,
105 isPii: false,
106 };
107 }
108
109 extProps = TelemetryHelper.addPropertyToTelemetryProperties(this.runOptions.reactNativeVersions.reactNativeVersion, "reactNativeVersion", extProps);
110
111 return TelemetryHelper.generate("AndroidPlatform.runApp", extProps, () => {
112 const env = GeneralMobilePlatform.getEnvArgument(process.env, this.runOptions.env, this.runOptions.envFile);
113
114 if (
115 !semver.valid(this.runOptions.reactNativeVersions.reactNativeVersion) /*Custom RN implementations should support this flag*/ ||
116 semver.gte(this.runOptions.reactNativeVersions.reactNativeVersion, AndroidPlatform.NO_PACKAGER_VERSION)
117 ) {
118 this.runArguments.push("--no-packager");
119 }
120
121 let mainActivity = GeneralMobilePlatform.getOptFromRunArgs(this.runArguments, "--main-activity");
122
123 if (mainActivity) {
124 this.adbHelper.setLaunchActivity(mainActivity);
125 } else if (!isNullOrUndefined(this.runOptions.debugLaunchActivity)) {
126 this.runArguments.push("--main-activity", this.runOptions.debugLaunchActivity);
127 this.adbHelper.setLaunchActivity(this.runOptions.debugLaunchActivity);
128 }
129
130 const runAndroidSpawn = new CommandExecutor(this.projectPath, this.logger).spawnReactCommand("run-android", this.runArguments, {env});
131 const output = new OutputVerifier(
132 () =>
133 Promise.resolve(AndroidPlatform.RUN_ANDROID_SUCCESS_PATTERNS),
134 () =>
135 Promise.resolve(AndroidPlatform.RUN_ANDROID_FAILURE_PATTERNS),
136 PlatformType.Android).process(runAndroidSpawn);
137
138 return output
139 .finally(() => {
140 return this.initializeTargetDevicesAndPackageName();
141 }).then(() => [this.debugTarget], reason => {
142 if (reason.message === ErrorHelper.getInternalError(InternalErrorCode.AndroidMoreThanOneDeviceOrEmulator).message && this.devices.length > 1 && this.debugTarget) {
143 /* If it failed due to multiple devices, we'll apply this workaround to make it work anyways */
144 this.needsToLaunchApps = true;
145 return shouldLaunchInAllDevices
146 ? this.adbHelper.getOnlineDevices()
147 : Promise.resolve([this.debugTarget]);
148 } else {
149 return Promise.reject<IDevice[]>(reason);
150 }
151 }).then(devices => {
152 return new PromiseUtil().forEach(devices, device => {
153 return this.launchAppWithADBReverseAndLogCat(device);
154 });
155 });
156 });
157 }
158
159 public enableJSDebuggingMode(): Promise<void> {
160 return this.adbHelper.switchDebugMode(this.runOptions.projectRoot, this.packageName, true, this.debugTarget.id);
161 }
162
163 public disableJSDebuggingMode(): Promise<void> {
164 return this.adbHelper.switchDebugMode(this.runOptions.projectRoot, this.packageName, false, this.debugTarget.id);
165 }
166
167 public prewarmBundleCache(): Promise<void> {
168 return this.packager.prewarmBundleCache(PlatformType.Android);
169 }
170
171 public getRunArguments(): string[] {
172 let runArguments: string[] = [];
173
174 if (this.runOptions.runArguments && this.runOptions.runArguments.length > 0) {
175 runArguments = this.runOptions.runArguments;
176 } else {
177 if (this.runOptions.variant) {
178 runArguments.push("--variant", this.runOptions.variant);
179 }
180 if (this.runOptions.target) {
181 if (this.runOptions.target === AndroidPlatform.simulatorString ||
182 this.runOptions.target === AndroidPlatform.deviceString) {
183
184 const message = localize("TargetIsNotSupportedForAndroid",
185 "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.",
186 this.runOptions.target);
187 this.logger.warning(message);
188 } else {
189 runArguments.push("--deviceId", this.runOptions.target);
190 }
191 }
192 }
193
194 return runArguments;
195 }
196
197 private initializeTargetDevicesAndPackageName(): Promise<void> {
198 return this.adbHelper.getConnectedDevices().then(devices => {
199 this.devices = devices;
200 this.debugTarget = this.getTargetEmulator(devices);
201 return this.getPackageName().then(packageName => {
202 this.packageName = packageName;
203 });
204 });
205 }
206
207 private launchAppWithADBReverseAndLogCat(device: IDevice): Promise<void> {
208 return this.configureADBReverseWhenApplicable(device)
209 .then(() => {
210 return this.needsToLaunchApps
211 ? this.adbHelper.launchApp(this.runOptions.projectRoot, this.packageName, device.id)
212 : Promise.resolve();
213 })
214 .then(() => {
215 return this.startMonitoringLogCat(device, this.runOptions.logCatArguments);
216 });
217 }
218
219 private configureADBReverseWhenApplicable(device: IDevice): Promise<void> {
220 return Promise.resolve()// For other emulators and devices we try to enable adb reverse
221 .then(() => this.adbHelper.apiVersion(device.id))
222 .then(apiVersion => {
223 if (apiVersion >= AndroidAPILevel.LOLLIPOP) { // If we support adb reverse
224 return this.adbHelper.reverseAdb(device.id, Number(this.runOptions.packagerPort));
225 } else {
226 const message = localize("DeviceSupportsOnlyAPILevel",
227 "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",
228 device.id, apiVersion, AndroidAPILevel.LOLLIPOP);
229 this.logger.warning(message);
230 return void 0;
231 }
232 });
233 }
234
235 private getPackageName(): Promise<string> {
236 return new Package(this.runOptions.projectRoot).name().then(appName =>
237 new PackageNameResolver(appName).resolvePackageName(this.runOptions.projectRoot));
238 }
239
240 /**
241 * Returns the target emulator, using the following logic:
242 * * If an emulator is specified and it is connected, use that one.
243 * * Otherwise, use the first one in the list.
244 */
245 private getTargetEmulator(devices: IDevice[]): IDevice {
246 let activeFilterFunction = (device: IDevice) => {
247 return device.isOnline;
248 };
249
250 let targetFilterFunction = (device: IDevice) => {
251 return device.id === this.runOptions.target && activeFilterFunction(device);
252 };
253
254 if (this.runOptions && this.runOptions.target && devices) {
255 /* check if the specified target is active */
256 const targetDevice = devices.find(targetFilterFunction);
257 if (targetDevice) {
258 return targetDevice;
259 }
260 }
261
262 /* return the first active device in the list */
263 let activeDevices = devices && devices.filter(activeFilterFunction);
264 return activeDevices && activeDevices[0];
265 }
266
267 private startMonitoringLogCat(device: IDevice, logCatArguments: string): void {
268 this.stopMonitoringLogCat(); // Stop previous logcat monitor if it's running
269
270 // this.logCatMonitor can be mutated, so we store it locally too
271 this.logCatMonitor = new LogCatMonitor(device.id, logCatArguments, this.adbHelper);
272 this.logCatMonitor.start() // The LogCat will continue running forever, so we don't wait for it
273 .catch(error => this.logger.warning(localize("ErrorWhileMonitoringLogCat", "Error while monitoring LogCat"), error)); // The LogCatMonitor failing won't stop the debugging experience
274 }
275
276 private stopMonitoringLogCat(): void {
277 if (this.logCatMonitor) {
278 this.logCatMonitor.dispose();
279 this.logCatMonitor = null;
280 }
281 }
282}
283