Skip to content

Commit ce0ad6a

Browse files
Merge pull request #1460 from NativeScript/vladimirov/android-manifest-in-app-resources
Use AndroidManifest from App_Resources as full manifest
2 parents 07cbdcc + 5c8200a commit ce0ad6a

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)