microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.12.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/android/androidPlatform.ts

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