microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8152571adb22d7628c545490a38bc85153249841

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/android/androidPlatform.ts

267lines · 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 Q from "q";
5import * as semver from "semver";
6
7import {GeneralMobilePlatform, MobilePlatformDeps } from "../generalMobilePlatform";
8import {IAndroidRunOptions} from "../launchArgs";
9import {AdbHelper, AndroidAPILevel, IDevice} from "./adb";
10import {Package} from "../../common/node/package";
11import {PromiseUtil} from "../../common/node/promise";
12import {PackageNameResolver} from "./packageNameResolver";
13import {OutputVerifier, PatternToFailure} from "../../common/outputVerifier";
14import {TelemetryHelper} from "../../common/telemetryHelper";
15import {CommandExecutor} from "../../common/commandExecutor";
16import {LogCatMonitor} from "./logCatMonitor";
17import * as nls from "vscode-nls";
18import { InternalErrorCode } from "../../common/error/internalErrorCode";
19import { ErrorHelper } from "../../common/error/errorHelper";
20import { isNullOrUndefined } from "util";
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
56 private needsToLaunchApps: boolean = false;
57
58 public showDevMenu(deviceId?: string): Q.Promise<void> {
59 return this.adbHelper.showDevMenu(deviceId);
60 }
61
62 public reloadApp(deviceId?: string): Q.Promise<void> {
63 return this.adbHelper.reloadApp(deviceId);
64 }
65
66 // 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.
67 constructor(protected runOptions: IAndroidRunOptions, platformDeps: MobilePlatformDeps = {}) {
68 super(runOptions, platformDeps);
69 this.adbHelper = new AdbHelper(this.runOptions.projectRoot, this.logger);
70 }
71
72 // TODO: remove this method when sinon will be updated to upper version. Now it is used for tests only.
73 public setAdbHelper(adbHelper: AdbHelper) {
74 this.adbHelper = adbHelper;
75 }
76
77 public runApp(shouldLaunchInAllDevices: boolean = false): Q.Promise<void> {
78 let extProps: any = {
79 platform: {
80 value: "android",
81 isPii: false,
82 },
83 };
84
85 if (this.runOptions.isDirect) {
86 extProps.isDirect = {
87 value: true,
88 isPii: false,
89 };
90 }
91
92 extProps = TelemetryHelper.addPropertyToTelemetryProperties(this.runOptions.reactNativeVersions.reactNativeVersion, "reactNativeVersion", extProps);
93
94 return TelemetryHelper.generate("AndroidPlatform.runApp", extProps, () => {
95 const env = GeneralMobilePlatform.getEnvArgument(process.env, this.runOptions.env, this.runOptions.envFile);
96
97 if (
98 !semver.valid(this.runOptions.reactNativeVersions.reactNativeVersion) /*Custom RN implementations should support this flag*/ ||
99 semver.gte(this.runOptions.reactNativeVersions.reactNativeVersion, AndroidPlatform.NO_PACKAGER_VERSION)
100 ) {
101 this.runArguments.push("--no-packager");
102 }
103
104 let mainActivity = GeneralMobilePlatform.getOptFromRunArgs(this.runArguments, "--main-activity");
105
106 if (mainActivity) {
107 this.adbHelper.setLaunchActivity(mainActivity);
108 } else if (!isNullOrUndefined(this.runOptions.debugLaunchActivity)) {
109 this.runArguments.push("--main-activity", this.runOptions.debugLaunchActivity);
110 this.adbHelper.setLaunchActivity(this.runOptions.debugLaunchActivity);
111 }
112
113 const runAndroidSpawn = new CommandExecutor(this.projectPath, this.logger).spawnReactCommand("run-android", this.runArguments, {env});
114 const output = new OutputVerifier(
115 () =>
116 Q(AndroidPlatform.RUN_ANDROID_SUCCESS_PATTERNS),
117 () =>
118 Q(AndroidPlatform.RUN_ANDROID_FAILURE_PATTERNS),
119 "android").process(runAndroidSpawn);
120
121 return output
122 .finally(() => {
123 return this.initializeTargetDevicesAndPackageName();
124 }).then(() => [this.debugTarget], reason => {
125 if (reason.message === ErrorHelper.getInternalError(InternalErrorCode.AndroidMoreThanOneDeviceOrEmulator).message && this.devices.length > 1 && this.debugTarget) {
126 /* If it failed due to multiple devices, we'll apply this workaround to make it work anyways */
127 this.needsToLaunchApps = true;
128 return shouldLaunchInAllDevices
129 ? this.adbHelper.getOnlineDevices()
130 : Q([this.debugTarget]);
131 } else {
132 return Q.reject<IDevice[]>(reason);
133 }
134 }).then(devices => {
135 return new PromiseUtil().forEach(devices, device => {
136 return this.launchAppWithADBReverseAndLogCat(device);
137 });
138 });
139 });
140 }
141
142 public enableJSDebuggingMode(): Q.Promise<void> {
143 return this.adbHelper.switchDebugMode(this.runOptions.projectRoot, this.packageName, true, this.debugTarget.id);
144 }
145
146 public disableJSDebuggingMode(): Q.Promise<void> {
147 return this.adbHelper.switchDebugMode(this.runOptions.projectRoot, this.packageName, false, this.debugTarget.id);
148 }
149
150 public prewarmBundleCache(): Q.Promise<void> {
151 return this.packager.prewarmBundleCache("android");
152 }
153
154 public getRunArguments(): string[] {
155 let runArguments: string[] = [];
156
157 if (this.runOptions.runArguments && this.runOptions.runArguments.length > 0) {
158 runArguments = this.runOptions.runArguments;
159 } else {
160 if (this.runOptions.variant) {
161 runArguments.push("--variant", this.runOptions.variant);
162 }
163 if (this.runOptions.target) {
164 if (this.runOptions.target === AndroidPlatform.simulatorString ||
165 this.runOptions.target === AndroidPlatform.deviceString) {
166
167 const message = localize("TargetIsNotSupportedForAndroid",
168 "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.",
169 this.runOptions.target);
170 this.logger.warning(message);
171 } else {
172 runArguments.push("--deviceId", this.runOptions.target);
173 }
174 }
175 }
176
177 return runArguments;
178 }
179
180 private initializeTargetDevicesAndPackageName(): Q.Promise<void> {
181 return this.adbHelper.getConnectedDevices().then(devices => {
182 this.devices = devices;
183 this.debugTarget = this.getTargetEmulator(devices);
184 return this.getPackageName().then(packageName => {
185 this.packageName = packageName;
186 });
187 });
188 }
189
190 private launchAppWithADBReverseAndLogCat(device: IDevice): Q.Promise<void> {
191 return Q({})
192 .then(() => {
193 return this.configureADBReverseWhenApplicable(device);
194 }).then(() => {
195 return this.needsToLaunchApps
196 ? this.adbHelper.launchApp(this.runOptions.projectRoot, this.packageName, device.id)
197 : Q<void>(void 0);
198 }).then(() => {
199 return this.startMonitoringLogCat(device, this.runOptions.logCatArguments);
200 });
201 }
202
203 private configureADBReverseWhenApplicable(device: IDevice): Q.Promise<void> {
204 return Q({}) // For other emulators and devices we try to enable adb reverse
205 .then(() => this.adbHelper.apiVersion(device.id))
206 .then(apiVersion => {
207 if (apiVersion >= AndroidAPILevel.LOLLIPOP) { // If we support adb reverse
208 return this.adbHelper.reverseAdb(device.id, Number(this.runOptions.packagerPort));
209 } else {
210 const message = localize("DeviceSupportsOnlyAPILevel",
211 "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",
212 device.id, apiVersion, AndroidAPILevel.LOLLIPOP);
213 this.logger.warning(message);
214 return void 0;
215 }
216 });
217 }
218
219 private getPackageName(): Q.Promise<string> {
220 return new Package(this.runOptions.projectRoot).name().then(appName =>
221 new PackageNameResolver(appName).resolvePackageName(this.runOptions.projectRoot));
222 }
223
224 /**
225 * Returns the target emulator, using the following logic:
226 * * If an emulator is specified and it is connected, use that one.
227 * * Otherwise, use the first one in the list.
228 */
229 private getTargetEmulator(devices: IDevice[]): IDevice {
230 let activeFilterFunction = (device: IDevice) => {
231 return device.isOnline;
232 };
233
234 let targetFilterFunction = (device: IDevice) => {
235 return device.id === this.runOptions.target && activeFilterFunction(device);
236 };
237
238 if (this.runOptions && this.runOptions.target && devices) {
239 /* check if the specified target is active */
240 const targetDevice = devices.find(targetFilterFunction);
241 if (targetDevice) {
242 return targetDevice;
243 }
244 }
245
246 /* return the first active device in the list */
247 let activeDevices = devices && devices.filter(activeFilterFunction);
248 return activeDevices && activeDevices[0];
249 }
250
251 private startMonitoringLogCat(device: IDevice, logCatArguments: string): void {
252 this.stopMonitoringLogCat(); // Stop previous logcat monitor if it's running
253
254 // this.logCatMonitor can be mutated, so we store it locally too
255 this.logCatMonitor = new LogCatMonitor(device.id, logCatArguments, this.adbHelper);
256 this.logCatMonitor.start() // The LogCat will continue running forever, so we don't wait for it
257 .catch(error => this.logger.warning(localize("ErrorWhileMonitoringLogCat", "Error while monitoring LogCat"), error)) // The LogCatMonitor failing won't stop the debugging experience
258 .done();
259 }
260
261 private stopMonitoringLogCat(): void {
262 if (this.logCatMonitor) {
263 this.logCatMonitor.dispose();
264 this.logCatMonitor = null;
265 }
266 }
267}
268