Skip to content

Commit 5c8200a

Browse files
Use AndroidManifest from App_Resources as full manifest
For iOS we'll have full Info.plist in app/App_Resources/iOS. Currently for Android we allow placing a file, that will be merged with default AndroidManifest from the runtime. Change the Android implementation, so now the AndroidManifest placed in app/App_Resources/Android will be the full one. In such file is not present in user's project, extract it from default template. In case there's already such file and it doesn't seems correct, rename it to old.AndroidManifest.xml, extract correct manifest from template and inform the user that he'll have to merge the files manually. Validate the AndroidManifest.xml Validate the AndroidManifest.xml in app/App_Resources/Android. In order to achieve this, introduce new XmlValidator class.
1 parent 07cbdcc commit 5c8200a

12 files changed

+224
-39
lines changed

lib/bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,4 @@ $injector.require("socketProxyFactory", "./device-sockets/ios/socket-proxy-facto
9393
$injector.require("iOSNotification", "./device-sockets/ios/notification");
9494
$injector.require("iOSSocketRequestExecutor", "./device-sockets/ios/socket-request-executor");
9595
$injector.require("messages", "./messages");
96+
$injector.require("xmlValidator", "./xml-validator");

lib/declarations.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,22 @@ interface IiOSSocketRequestExecutor {
184184
executeLaunchRequest(device: Mobile.IiOSDevice, timeout: number, readyForAttachTimeout: number): IFuture<void>;
185185
executeAttachRequest(device: Mobile.IiOSDevice, timeout: number): IFuture<void>;
186186
}
187+
188+
/**
189+
* Describes validation methods for XMLs.
190+
*/
191+
interface IXmlValidator {
192+
/**
193+
* Checks the passed xml files for errors and if such exists, print them on the stdout.
194+
* @param {string[]} sourceFiles Files to be checked. Only the ones that ends with .xml are filtered.
195+
* @return {IFuture<boolean>} true in case there are no errors in specified files and false in case there's at least one error.
196+
*/
197+
validateXmlFiles(sourceFiles: string[]): IFuture<boolean>;
198+
199+
/**
200+
* Checks the passed xml file for errors and returns them as a result.
201+
* @param {string} sourceFile File to be checked.
202+
* @return {IFuture<string>} The errors detected (as a single string) or null in case there are no errors.
203+
*/
204+
getXmlFileErrors(sourceFile: string): IFuture<string>;
205+
}

lib/definitions/project.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ interface IPlatformProjectService {
7676
getAppResourcesDestinationDirectoryPath(): IFuture<string>;
7777
deploy(deviceIdentifier: string): IFuture<void>;
7878
processConfigurationFilesFromAppResources(): IFuture<void>;
79+
/**
80+
* Ensures there is configuration file (AndroidManifest.xml, Info.plist) in app/App_Resources.
81+
*/
82+
ensureConfigurationFileInAppResources(): IFuture<void>;
7983
}
8084

8185
interface IAndroidProjectPropertiesManager {

lib/services/android-project-service.ts

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
3131
private $mobileHelper: Mobile.IMobileHelper,
3232
private $injector: IInjector,
3333
private $pluginVariablesService: IPluginVariablesService,
34-
private $deviceAppDataFactory: Mobile.IDeviceAppDataFactory) {
34+
private $deviceAppDataFactory: Mobile.IDeviceAppDataFactory,
35+
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
36+
private $projectTemplatesService: IProjectTemplatesService,
37+
private $xmlValidator: IXmlValidator) {
3538
super($fs, $projectData, $projectDataService);
3639
this._androidProjectPropertiesManagers = Object.create(null);
3740
}
@@ -253,7 +256,52 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
253256
}
254257

255258
public prepareProject(): IFuture<void> {
256-
return Future.fromResult();
259+
return (() => {
260+
let resDestinationDir = this.getAppResourcesDestinationDirectoryPath().wait();
261+
let androidManifestPath = path.join(resDestinationDir, this.platformData.configurationFileName);
262+
263+
// In case the file is not correct, looks like we are still using the default AndroidManifest.xml from runtime and the current file (in res dir)
264+
// should be merged with it.
265+
if(this.isAndroidManifestFileCorrect(androidManifestPath).wait()) {
266+
// Delete the AndroidManifest.xml file from res directory as the runtime will consider it as addition to the one in src/main and will try to merge them.
267+
// However now they are the same file.
268+
this.$fs.deleteFile(androidManifestPath).wait();
269+
}
270+
}).future<void>()();
271+
}
272+
273+
public ensureConfigurationFileInAppResources(): IFuture<void> {
274+
return (() => {
275+
let originalAndroidManifestFilePath = path.join(this.$projectData.appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, this.platformData.configurationFileName),
276+
hasAndroidManifestInAppResources = this.$fs.exists(originalAndroidManifestFilePath).wait(),
277+
shouldExtractDefaultManifest = !hasAndroidManifestInAppResources,
278+
isAndroidManifestBackedUp = false;
279+
280+
if(hasAndroidManifestInAppResources) {
281+
let isFileCorrect = this.isAndroidManifestFileCorrect(originalAndroidManifestFilePath).wait();
282+
if(!isFileCorrect) {
283+
shouldExtractDefaultManifest = true;
284+
isAndroidManifestBackedUp = true;
285+
this.backupOriginalAndroidManifest(originalAndroidManifestFilePath).wait();
286+
}
287+
}
288+
289+
// In case we should extract the manifest from default template, but for some reason we cannot, break the execution,
290+
// so the original file from Android runtime will be used.
291+
if(shouldExtractDefaultManifest && !this.extractAndroidManifestFromDefaultTemplate(originalAndroidManifestFilePath).wait()) {
292+
// now revert back
293+
this.revertBackupOfOriginalAndroidManifest(originalAndroidManifestFilePath).wait();
294+
return;
295+
}
296+
297+
if(isAndroidManifestBackedUp) {
298+
this.$logger.warn(`Your ${this.platformData.configurationFileName} in app/App_Resources/Android will be replaced by the default one from hello-world template.`);
299+
this.$logger.printMarkdown(`The original file will be moved to \`${this.configurationFileBackupName}\`. Merge it **manually** with the new \`${this.platformData.configurationFileName}\` in your app/App_Resources/Android.`);
300+
}
301+
302+
// Overwrite the AndroidManifest from runtime.
303+
this.$fs.copyFile(originalAndroidManifestFilePath, this.platformData.configurationFilePath).wait();
304+
}).future<void>()();
257305
}
258306

259307
public prepareAppResources(appResourcesDirectoryPath: string): IFuture<void> {
@@ -275,7 +323,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
275323
}
276324

277325
public processConfigurationFilesFromAppResources(): IFuture<void> {
278-
return Future.fromResult();
326+
return this.ensureConfigurationFileInAppResources();
279327
}
280328

281329
private processResourcesFromPlugin(pluginData: IPluginData, pluginPlatformsFolderPath: string): IFuture<void> {
@@ -462,5 +510,69 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
462510

463511
}).future<void>()();
464512
}
513+
514+
private isAndroidManifestFileCorrect(pathToAndroidManifest: string): IFuture<boolean> {
515+
return ((): boolean => {
516+
try {
517+
// Check if the AndroidManifest in app/App_Resouces is the correct one
518+
// Use a real magic to detect if this is the correct file, by checking some mandatory strings.
519+
let fileContent = this.$fs.readText(pathToAndroidManifest).wait(),
520+
isFileCorrect = !!(~fileContent.indexOf("android:minSdkVersion") && ~fileContent.indexOf("android:targetSdkVersion")
521+
&& ~fileContent.indexOf("uses-permission") && ~fileContent.indexOf("<application")
522+
&& ~fileContent.indexOf("<activity") && ~fileContent.indexOf("<intent-filter>")
523+
&& ~fileContent.indexOf("android.intent.action.MAIN") && ~fileContent.indexOf("com.tns.ErrorReportActivity")
524+
&& ~fileContent.indexOf("android:versionCode")
525+
&& !this.$xmlValidator.getXmlFileErrors(pathToAndroidManifest).wait());
526+
527+
this.$logger.trace(`Existing ${this.platformData.configurationFileName} is ${isFileCorrect ? "" : "NOT "}correct.`);
528+
return isFileCorrect;
529+
} catch(err) {
530+
this.$logger.trace(`Error while checking ${pathToAndroidManifest}: `, err);
531+
return false;
532+
}
533+
}).future<boolean>()();
534+
}
535+
536+
private get configurationFileBackupName(): string {
537+
return this.platformData.configurationFileName + ".backup";
538+
}
539+
540+
private backupOriginalAndroidManifest(originalAndroidManifestFilePath: string): IFuture<void> {
541+
return (() => {
542+
let newPathForOriginalManifest = path.join(path.dirname(originalAndroidManifestFilePath), this.configurationFileBackupName);
543+
shell.mv(originalAndroidManifestFilePath, newPathForOriginalManifest);
544+
}).future<void>()();
545+
}
546+
547+
private revertBackupOfOriginalAndroidManifest(originalAndroidManifestFilePath: string): IFuture<void> {
548+
return (() => {
549+
let pathToBackupFile = path.join(path.dirname(originalAndroidManifestFilePath), this.configurationFileBackupName);
550+
if(this.$fs.exists(pathToBackupFile).wait()) {
551+
this.$logger.trace(`Could not extract ${this.platformData.configurationFileName} from default template. Reverting the change of your app/App_Resources/${this.platformData.configurationFileName}.`);
552+
shell.mv(pathToBackupFile, originalAndroidManifestFilePath);
553+
}
554+
}).future<void>()();
555+
}
556+
557+
private extractAndroidManifestFromDefaultTemplate(originalAndroidManifestFilePath: string): IFuture<boolean> {
558+
return ((): boolean => {
559+
let defaultTemplatePath = this.$projectTemplatesService.defaultTemplatePath.wait();
560+
let templateAndroidManifest = path.join(defaultTemplatePath, constants.APP_RESOURCES_FOLDER_NAME, this.$devicePlatformsConstants.Android, this.platformData.configurationFileName);
561+
if (this.$fs.exists(templateAndroidManifest).wait()) {
562+
this.$logger.trace(`${originalAndroidManifestFilePath} is missing. Upgrading the source of the project with one from the new project template. Copy ${templateAndroidManifest} to ${originalAndroidManifestFilePath}`);
563+
try {
564+
this.$fs.copyFile(templateAndroidManifest, originalAndroidManifestFilePath).wait();
565+
} catch(e) {
566+
this.$logger.trace(`Copying template's ${this.platformData.configurationFileName} failed. `, e);
567+
return false;
568+
}
569+
} else {
570+
this.$logger.trace(`${originalAndroidManifestFilePath} is missing but the template ${templateAndroidManifest} is missing too, can not upgrade ${this.platformData.configurationFileName}.`);
571+
return false;
572+
}
573+
574+
return true;
575+
}).future<boolean>()();
576+
}
465577
}
466578
$injector.register("androidProjectService", AndroidProjectService);

lib/services/ios-project-service.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
400400
}).future<void>()();
401401
}
402402

403-
private mergeInfoPlists(): IFuture<void> {
403+
public ensureConfigurationFileInAppResources(): IFuture<void> {
404404
return (() => {
405405
let projectDir = this.$projectData.projectDir;
406406
let infoPlistPath = path.join(projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, this.platformData.normalizedPlatformName, this.platformData.configurationFileName);
@@ -422,6 +422,15 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
422422
}
423423
}
424424

425+
}).future<void>()();
426+
}
427+
428+
private mergeInfoPlists(): IFuture<void> {
429+
return (() => {
430+
let projectDir = this.$projectData.projectDir;
431+
let infoPlistPath = path.join(projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, this.platformData.normalizedPlatformName, this.platformData.configurationFileName);
432+
this.ensureConfigurationFileInAppResources().wait();
433+
425434
if (!this.$fs.exists(infoPlistPath).wait()) {
426435
this.$logger.trace("Info.plist: No app/App_Resources/iOS/Info.plist found, falling back to pre-1.6.0 Info.plist behavior.");
427436
return;

lib/services/platform-service.ts

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import * as helpers from "../common/helpers";
88
import * as semver from "semver";
99
import * as minimatch from "minimatch";
1010
import Future = require("fibers/future");
11-
import {EOL} from "os";
1211
let clui = require("clui");
1312

1413
export class PlatformService implements IPlatformService {
@@ -34,7 +33,8 @@ export class PlatformService implements IPlatformService {
3433
private $pluginsService: IPluginsService,
3534
private $projectFilesManager: IProjectFilesManager,
3635
private $mobileHelper: Mobile.IMobileHelper,
37-
private $hostInfo: IHostInfo) { }
36+
private $hostInfo: IHostInfo,
37+
private $xmlValidator: IXmlValidator) { }
3838

3939
public addPlatforms(platforms: string[]): IFuture<void> {
4040
return (() => {
@@ -127,6 +127,7 @@ export class PlatformService implements IPlatformService {
127127
this.$fs.deleteDirectory(path.join(frameworkDir, "../../")).wait();
128128
}
129129

130+
platformData.platformProjectService.ensureConfigurationFileInAppResources().wait();
130131
platformData.platformProjectService.interpolateData().wait();
131132
platformData.platformProjectService.afterCreateProject(platformData.projectRoot).wait();
132133

@@ -184,35 +185,6 @@ export class PlatformService implements IPlatformService {
184185
}).future<boolean>()();
185186
}
186187

187-
private checkXmlFiles(sourceFiles: string[]): IFuture<boolean> {
188-
return (() => {
189-
let xmlHasErrors = false;
190-
let DomParser = require("xmldom").DOMParser;
191-
sourceFiles
192-
.filter(file => _.endsWith(file, ".xml"))
193-
.forEach(file => {
194-
let fileContents = this.$fs.readText(file).wait();
195-
let hasErrors = false;
196-
let errorOutput = "";
197-
let domErrorHandler = (level:any, msg:string) => {
198-
errorOutput += level + EOL + msg + EOL;
199-
hasErrors = true;
200-
};
201-
let parser = new DomParser({
202-
locator:{},
203-
errorHandler: domErrorHandler
204-
});
205-
parser.parseFromString(fileContents, "text/xml");
206-
xmlHasErrors = xmlHasErrors || hasErrors;
207-
if (hasErrors) {
208-
this.$logger.warn(`${file} has syntax errors.`);
209-
this.$logger.out(errorOutput);
210-
}
211-
});
212-
return !xmlHasErrors;
213-
}).future<boolean>()();
214-
}
215-
216188
@helpers.hook('prepare')
217189
private preparePlatformCore(platform: string): IFuture<boolean> {
218190
return (() => {
@@ -250,7 +222,7 @@ export class PlatformService implements IPlatformService {
250222
}
251223

252224
// verify .xml files are well-formed
253-
this.checkXmlFiles(sourceFiles).wait();
225+
this.$xmlValidator.validateXmlFiles(sourceFiles).wait();
254226

255227
// Remove .ts and .js.map files
256228
PlatformService.EXCLUDE_FILES_PATTERN.forEach(pattern => sourceFiles = sourceFiles.filter(file => !minimatch(file, pattern, {nocase: true})));

lib/xml-validator.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
///<reference path=".d.ts"/>
2+
"use strict";
3+
4+
import { EOL } from "os";
5+
6+
export class XmlValidator implements IXmlValidator {
7+
constructor(private $fs: IFileSystem,
8+
private $logger: ILogger) { }
9+
10+
public validateXmlFiles(sourceFiles: string[]): IFuture<boolean> {
11+
return (() => {
12+
let xmlHasErrors = false;
13+
sourceFiles
14+
.filter(file => _.endsWith(file, ".xml"))
15+
.forEach(file => {
16+
let errorOutput = this.getXmlFileErrors(file).wait();
17+
let hasErrors = !!errorOutput;
18+
xmlHasErrors = xmlHasErrors || hasErrors;
19+
if (hasErrors) {
20+
this.$logger.warn(`${file} has syntax errors.`);
21+
this.$logger.out(errorOutput);
22+
}
23+
});
24+
return !xmlHasErrors;
25+
}).future<boolean>()();
26+
}
27+
28+
public getXmlFileErrors(sourceFile: string): IFuture<string> {
29+
return ((): string => {
30+
let errorOutput = "";
31+
let fileContents = this.$fs.readText(sourceFile).wait();
32+
let domErrorHandler = (level:any, msg:string) => {
33+
errorOutput += level + EOL + msg + EOL;
34+
};
35+
this.getDomParser(domErrorHandler).parseFromString(fileContents, "text/xml");
36+
37+
return errorOutput || null;
38+
}).future<string>()();
39+
}
40+
41+
private getDomParser(errorHandler: (level:any, msg:string) => void): any {
42+
let DomParser = require("xmldom").DOMParser;
43+
let parser = new DomParser({
44+
locator:{},
45+
errorHandler: errorHandler
46+
});
47+
48+
return parser;
49+
}
50+
}
51+
$injector.register("xmlValidator", XmlValidator);

test/npm-support.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {ProjectFilesProvider} from "../lib/providers/project-files-provider";
2727
import {DeviceAppDataProvider} from "../lib/providers/device-app-data-provider";
2828
import {MobilePlatformsCapabilities} from "../lib/mobile-platforms-capabilities";
2929
import {DevicePlatformsConstants} from "../lib/common/mobile/device-platforms-constants";
30+
import { XmlValidator } from "../lib/xml-validator";
3031
import Future = require("fibers/future");
3132

3233
import path = require("path");
@@ -75,6 +76,7 @@ function createTestInjector(): IInjector {
7576
testInjector.register("deviceAppDataProvider", DeviceAppDataProvider);
7677
testInjector.register("mobilePlatformsCapabilities", MobilePlatformsCapabilities);
7778
testInjector.register("devicePlatformsConstants", DevicePlatformsConstants);
79+
testInjector.register("xmlValidator", XmlValidator);
7880

7981
return testInjector;
8082
}

test/platform-commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {ProjectFilesProvider} from "../lib/providers/project-files-provider";
2020
import {DeviceAppDataProvider} from "../lib/providers/device-app-data-provider";
2121
import {MobilePlatformsCapabilities} from "../lib/mobile-platforms-capabilities";
2222
import {DevicePlatformsConstants} from "../lib/common/mobile/device-platforms-constants";
23+
import { XmlValidator } from "../lib/xml-validator";
2324

2425
let isCommandExecuted = true;
2526

@@ -135,6 +136,7 @@ function createTestInjector() {
135136
testInjector.register("deviceAppDataProvider", DeviceAppDataProvider);
136137
testInjector.register("mobilePlatformsCapabilities", MobilePlatformsCapabilities);
137138
testInjector.register("devicePlatformsConstants", DevicePlatformsConstants);
139+
testInjector.register("xmlValidator", XmlValidator);
138140

139141
return testInjector;
140142
}

0 commit comments

Comments
 (0)