From 973fe6da6dada837db6435c09fce90ccd3113201 Mon Sep 17 00:00:00 2001 From: daserge Date: Thu, 1 Dec 2016 20:50:14 +0300 Subject: [PATCH] CB-9287 Not enough Icons and Splashscreens for Windows 8.1 and Windows Phone 8.1 Added JPG support Introduced AppxManifest access in cordova.js cordova.js - refine splashscreen extension based on config.xml Added a warning when non-MRT splash screens have mixed extensions Added a warning for size limit exceeding (applies to win10 project only) Added a warning for images with unsupported format Updated the tests --- cordova-js-src/confighelper.js | 81 ++++++--- cordova-js-src/platform.js | 10 +- cordova-js-src/splashscreen.js | 13 +- spec/unit/AppxManifest.spec.js | 20 +++ spec/unit/Prepare.Win10.spec.js | 175 +++++++++++++++++++- spec/unit/clean.spec.js | 14 +- template/CordovaApp.Phone.jsproj | 4 +- template/CordovaApp.Windows.jsproj | 4 +- template/CordovaApp.Windows10.jsproj | 4 +- template/cordova/lib/AppxManifest.js | 13 ++ template/cordova/lib/prepare.js | 235 ++++++++++++++++++++++----- template/www/cordova.js | 104 +++++++++--- 12 files changed, 578 insertions(+), 99 deletions(-) diff --git a/cordova-js-src/confighelper.js b/cordova-js-src/confighelper.js index 47971d3a..c1660520 100644 --- a/cordova-js-src/confighelper.js +++ b/cordova-js-src/confighelper.js @@ -19,26 +19,47 @@ * */ -// config.xml wrapper (non-node ConfigParser analogue) -var config; -function Config(xhr) { - function loadPreferences(xhr) { - var parser = new DOMParser(); - var doc = parser.parseFromString(xhr.responseText, "application/xml"); - - var preferences = doc.getElementsByTagName("preference"); - return Array.prototype.slice.call(preferences); +// config.xml and AppxManifest.xml wrapper (non-node ConfigParser analogue) +var configCache = {}; +var utils = require("cordova/utils"); + +var isPhone = (cordova.platformId == 'windows') && WinJS.Utilities.isPhone; +var isWin10UWP = navigator.appVersion.indexOf('MSAppHost/3.0') !== -1; +var splashScreenTagName = isWin10UWP ? "SplashScreen" : (isPhone ? "m3:SplashScreen" : "m2:SplashScreen"); + +function XmlFile(text) { + this.text = text; +} + +XmlFile.prototype.loadTags = function (tagName) { + var parser; + if (!this.doc) { + parser = new DOMParser(); + this.doc = parser.parseFromString(this.text, "application/xml"); } - this.xhr = xhr; - this.preferences = loadPreferences(this.xhr); + var tags = this.doc.getElementsByTagName(tagName); + return Array.prototype.slice.call(tags); } -function readConfig(success, error) { +function Config(text) { + XmlFile.apply(this, arguments); + this.preferences = this.loadTags("preference"); +} + +function Manifest(text) { + XmlFile.apply(this, arguments); + this.splashScreen = this.loadTags(splashScreenTagName)[0]; +} + +utils.extend(Config, XmlFile); +utils.extend(Manifest, XmlFile); + +function requestFile(filePath, success, error) { var xhr; - if (typeof config != 'undefined') { - success(config); + if (typeof configCache[filePath] != 'undefined') { + success(configCache[filePath]); } function fail(msg) { @@ -52,11 +73,11 @@ function readConfig(success, error) { var xhrStatusChangeHandler = function () { if (xhr.readyState == 4) { if (xhr.status == 200 || xhr.status == 304 || xhr.status == 0 /* file:// */) { - config = new Config(xhr); - success(config); + configCache[filePath] = xhr.responseText; + success(xhr.responseText); } else { - fail('[Windows][cordova.js][xhrStatusChangeHandler] Could not XHR config.xml: ' + xhr.statusText); + fail('[Windows][cordova.js][xhrStatusChangeHandler] Could not XHR ' + filePath + ': ' + xhr.statusText); } } }; @@ -65,18 +86,30 @@ function readConfig(success, error) { xhr.addEventListener("load", xhrStatusChangeHandler); try { - xhr.open("get", "/config.xml", true); + xhr.open("get", filePath, true); xhr.send(); } catch (e) { - fail('[Windows][cordova.js][readConfig] Could not XHR config.xml: ' + JSON.stringify(e)); + fail('[Windows][cordova.js][xhrFile] Could not XHR ' + filePath + ': ' + JSON.stringify(e)); } } +function readConfig(success, error) { + requestFile("/config.xml", function (contents) { + success(new Config(contents)); + }, error); +} + +function readManifest(success, error) { + requestFile("/AppxManifest.xml", function (contents) { + success(new Manifest(contents)); + }, error); +} + /** * Reads a preference value from config.xml. * Returns preference value or undefined if it does not exist. * @param {String} preferenceName Preference name to read */ -Config.prototype.getPreferenceValue = function getPreferenceValue(preferenceName) { +Config.prototype.getPreferenceValue = function (preferenceName) { var preferenceItem = this.preferences && this.preferences.filter(function (item) { return item.attributes['name'].value === preferenceName; }); @@ -86,4 +119,12 @@ Config.prototype.getPreferenceValue = function getPreferenceValue(preferenceName } } +/** + * Reads SplashScreen image path + */ +Manifest.prototype.getSplashScreenImagePath = function () { + return this.splashScreen.attributes['Image'].value; +} + exports.readConfig = readConfig; +exports.readManifest = readManifest; diff --git a/cordova-js-src/platform.js b/cordova-js-src/platform.js index 1f9ddbdf..c1431ebe 100644 --- a/cordova-js-src/platform.js +++ b/cordova-js-src/platform.js @@ -78,8 +78,14 @@ module.exports = { return; } - e.setPromise(makePromise(configHelper.readConfig).then(function (config) { - splashscreen.firstShow(config, e); + var manifest; + + e.setPromise(makePromise(configHelper.readManifest).then(function (manifestTmp) { + manifest = manifestTmp; + return makePromise(configHelper.readConfig); + }) + .then(function (config) { + splashscreen.firstShow(config, manifest, e); }).then(function () { // Avoids splashimage flicker on Windows Phone 8.1/10 return WinJS.Promise.timeout(); diff --git a/cordova-js-src/splashscreen.js b/cordova-js-src/splashscreen.js index 838b2c32..786305da 100644 --- a/cordova-js-src/splashscreen.js +++ b/cordova-js-src/splashscreen.js @@ -58,8 +58,11 @@ function readBoolFromCfg(preferenceName, defaultValue, cfg) { } } -function readPreferencesFromCfg(cfg) { +function readPreferencesFromCfg(cfg, manifest) { try { + // Update splashscreen image path to match application manifest + splashImageSrc = schema + ':///' + manifest.getSplashScreenImagePath().replace(/\\/g, '/'); + bgColor = cfg.getPreferenceValue('SplashScreenBackgroundColor') || bgColor; bgColor = bgColor.replace('0x', '#').replace('0X', '#'); if (bgColor.length > 7) { @@ -102,8 +105,8 @@ function centerY() { } } -function init(config) { - readPreferencesFromCfg(config); +function init(config, manifest) { + readPreferencesFromCfg(config, manifest); var splashscreenStyles = document.createElement("link"); splashscreenStyles.rel = 'stylesheet'; @@ -321,8 +324,8 @@ function onResize() { //// module.exports = { - firstShow: function (config, activatedEventArgs) { - init(config); + firstShow: function (config, manifest, activatedEventArgs) { + init(config, manifest); activated(activatedEventArgs); if (!isVisible() && (splashScreenDelay > 0 || !autoHideSplashScreen)) { diff --git a/spec/unit/AppxManifest.spec.js b/spec/unit/AppxManifest.spec.js index a2c4bb5f..65f9092b 100644 --- a/spec/unit/AppxManifest.spec.js +++ b/spec/unit/AppxManifest.spec.js @@ -212,6 +212,26 @@ describe('AppxManifest', function () { visualElementsWindows10.setForegroundText(foregroundTextLight); expect(visualElementsWindows10.getForegroundText()).toEqual(undefined); }); + + it('getSplashScreenExtension/setSplashScreenExtension', function () { + var visualElementsWindows = AppxManifest.get(WINDOWS_MANIFEST).getVisualElements(); + var visualElementsWindows10 = AppxManifest.get(WINDOWS_10_MANIFEST).getVisualElements(); + var visualElementsWindowsPhone = AppxManifest.get(WINDOWS_PHONE_MANIFEST).getVisualElements(); + var jpgExtension = '.jpg'; + + // PNG is default extension + expect(visualElementsWindows.getSplashScreenExtension()).toEqual('.png'); + expect(visualElementsWindows10.getSplashScreenExtension()).toEqual('.png'); + expect(visualElementsWindowsPhone.getSplashScreenExtension()).toEqual('.png'); + + // Set to jpg + visualElementsWindows.setSplashScreenExtension(jpgExtension); + expect(visualElementsWindows.getSplashScreenExtension()).toEqual(jpgExtension); + visualElementsWindows10.setSplashScreenExtension(jpgExtension); + expect(visualElementsWindows10.getSplashScreenExtension()).toEqual(jpgExtension); + visualElementsWindowsPhone.setSplashScreenExtension(jpgExtension); + expect(visualElementsWindowsPhone.getSplashScreenExtension()).toEqual(jpgExtension); + }); }); }); diff --git a/spec/unit/Prepare.Win10.spec.js b/spec/unit/Prepare.Win10.spec.js index 222b7b4f..f64f9c53 100644 --- a/spec/unit/Prepare.Win10.spec.js +++ b/spec/unit/Prepare.Win10.spec.js @@ -34,7 +34,12 @@ var rewire = require('rewire'), applyStartPage = prepare.__get__('applyStartPage'); var Win10ManifestPath = 'template/package.windows10.appxmanifest', - Win81ManifestPath = 'template/package.windows.appxmanifest'; + Win81ManifestPath = 'template/package.windows.appxmanifest', + WP81ManifestPath = 'template/package.phone.appxmanifest'; + +var Win10ManifestName = path.basename(Win10ManifestPath), + Win81ManifestName = path.basename(Win81ManifestPath), + WP81ManifestName = path.basename(WP81ManifestPath); /*** * Unit tests for validating default ms-appx-web:// URI scheme in Win10 @@ -461,10 +466,10 @@ describe('copyIcons method', function () { var PROJECT = '/some/path'; - function createMockConfig(images) { + function createMockConfig(images, splashScreens) { var result = jasmine.createSpyObj('config', ['getIcons', 'getSplashScreens']); result.getIcons.andReturn(images); - result.getSplashScreens.andReturn([]); + result.getSplashScreens.andReturn(splashScreens || []); return result; } @@ -541,4 +546,168 @@ describe('copyIcons method', function () { expect(FileUpdater.updatePaths).toHaveBeenCalledWith(expectedPathMap, { rootDir: PROJECT }, logFileOp); }); }); + + it('should ignore splashScreens for Windows 10 project with size >200K and emit a warning', function () { + var size300K = 300 * 1024; + var warnSpy = jasmine.createSpy('warn'); + events.on('warn', warnSpy); + + var splashScreens = [ + {src: 'res/Windows/splashscreen.png', target: 'SplashScreen' }, // targetProject: 10 + {src: 'res/Windows/splashscreen.scale-180.png', width: '1116', height: '540' }, // targetProject: 8.1 + {src: 'res/Windows/splashscreen.scale-200.png', width: '1240', height: '600' }, // targetProject: 10 + {src: 'res/Windows/splashscreen.scale-400.png', width: '2480', height: '1200' }, // targetProject: 10 + {src: 'res/Windows/splashscreenphone.scale-240.png', width: '1152', height: '1920' }, // targetProject: WP 8.1 + {src: 'res/Windows/splashscreenphone.png', target: 'SplashScreenPhone' }, // targetProject: WP 8.1 + ]; + + var splashScreensFiles = splashScreens.map(function(splash) { + return path.basename(splash.src); + }); + spyOn(fs, 'readdirSync').andReturn(splashScreensFiles); + + spyOn(fs, 'statSync').andReturn({ + size: size300K + }); + + var project = { projectConfig: createMockConfig([], splashScreens), root: PROJECT }; + var locations = { root: PROJECT }; + + copyImages(project, locations); + + var expectedPathMap = {}; + expectedPathMap['images' + path.sep + 'SplashScreen.scale-180.png'] = 'res/Windows/splashscreen.scale-180.png'; + expectedPathMap['images' + path.sep + 'SplashScreenPhone.scale-240.png'] = path.join('res', 'Windows', 'splashscreenphone.scale-240.png'); + expectedPathMap['images' + path.sep + 'SplashScreenPhone.scale-100.png'] = path.join('res', 'Windows', 'splashscreenphone.png'); + expect(FileUpdater.updatePaths).toHaveBeenCalledWith(expectedPathMap, { rootDir: PROJECT }, logFileOp); + expect(warnSpy.calls[0].args[0]).toMatch('file size exceeds the limit'); + }); + + it('should ignore splashScreens with unsupported extensions and emit a warning', function () { + var warnSpy = jasmine.createSpy('warn'); + events.on('warn', warnSpy); + + var splashScreens = [ + {src: 'res/Windows/splashscreen.gif', target: 'SplashScreen' }, // targetProject: 10 + {src: 'res/Windows/splashscreen.scale-180.bmp', width: '1116', height: '540' }, // targetProject: 8.1 + {src: 'res/Windows/splashscreenphone.tga', target: 'SplashScreenPhone' }, // targetProject: WP 8.1 + ]; + + var splashScreensFiles = splashScreens.map(function(splash) { + return path.basename(splash.src); + }); + spyOn(fs, 'readdirSync').andReturn(splashScreensFiles); + + spyOn(fs, 'statSync').andReturn({ + size: 0 + }); + + var project = { projectConfig: createMockConfig([], splashScreens), root: PROJECT }; + var locations = { root: PROJECT }; + + copyImages(project, locations); + + var extensionNotSupportedMsg = 'extension is not supported'; + var expectedPathMap = {}; + expect(FileUpdater.updatePaths).toHaveBeenCalledWith(expectedPathMap, { rootDir: PROJECT }, logFileOp); + expect(warnSpy.calls[0].args[0]).toMatch(extensionNotSupportedMsg); + expect(warnSpy.calls[1].args[0]).toMatch(extensionNotSupportedMsg); + expect(warnSpy.calls[2].args[0]).toMatch(extensionNotSupportedMsg); + }); + + it('should warn about mixed splashscreen extensions used for non-MRT syntax', function () { + var updateSplashScreenImageExtensions = prepare.__get__('updateSplashScreenImageExtensions'); + spyOn(fs, 'writeFileSync'); + spyOn(AppxManifest, 'get').andReturn({ + getVisualElements: function() { + return { + getSplashScreenExtension: function() { + return '.png'; + }, + setSplashScreenExtension: function() {} + }; + }, + write: function() {} + }); + var warnSpy = jasmine.createSpy('warn'); + events.on('warn', warnSpy); + + var splashScreens = [ + {src: 'res/Windows/splashscreen.png', width: '620', height: '300' }, // targetProject: 10 + {src: 'res/Windows/splashscreen.scale-180.jpg', width: '1116', height: '540' }, // targetProject: 8.1 + {src: 'res/Windows/splashscreen.scale-200.png', width: '1240', height: '600' }, // targetProject: 10 + {src: 'res/Windows/splashscreen.scale-400.jpg', width: '2480', height: '1200' }, // targetProject: 10 + {src: 'res/Windows/splashscreenphone.scale-240.png', width: '1152', height: '1920' }, // targetProject: WP 8.1 + {src: 'res/Windows/splashscreenphone.jpg', width: '480', height: '800' }, // targetProject: WP 8.1 + ]; + + var splashScreensFiles = splashScreens.map(function(splash) { + return path.basename(splash.src); + }); + spyOn(fs, 'readdirSync').andReturn(splashScreensFiles); + + spyOn(fs, 'statSync').andReturn({ + size: 0 + }); + + var project = { projectConfig: createMockConfig([], splashScreens), root: PROJECT }; + var locations = { root: PROJECT }; + + updateSplashScreenImageExtensions(project, locations); + + var mixedExtensionsMsg = 'splash screens have mixed file extensions'; + expect(warnSpy.calls[0].args[0]).toMatch(mixedExtensionsMsg); + expect(warnSpy.calls[1].args[0]).toMatch(mixedExtensionsMsg); + }); + + it('should update manifests with proper splashscreen image extension', function () { + // 1. Set manifest with SplashScreen.Image = "image.png" (this is default) + // 2. Set config.xml with splash src="image.jpg" + // 3. updateSplashScreenImageExtensions should call getSplashScreenExtension, setSplashScreenExtension('.jpg') + + var updateSplashScreenImageExtensions = prepare.__get__('updateSplashScreenImageExtensions'); + spyOn(fs, 'writeFileSync'); + + var win10Manifest = AppxManifest.get(Win10ManifestPath), + win81Manifest = AppxManifest.get(Win81ManifestPath), + wp81Manifest = AppxManifest.get(WP81ManifestPath); + + spyOn(AppxManifest, 'get').andCallFake(function(manifestPath) { + if (manifestPath.indexOf(Win10ManifestName) !== -1) { + return win10Manifest; + } + + if (manifestPath.indexOf(Win81ManifestName) !== -1) { + return win81Manifest; + } + + if (manifestPath.indexOf(WP81ManifestName) !== -1) { + return wp81Manifest; + } + }); + + var splashScreens = [ + {src: 'res/Windows/splashscreen.jpg', width: '620', height: '300' }, // targetProject: 10 + {src: 'res/Windows/splashscreen.scale-180.jpg', width: '1116', height: '540' }, // targetProject: 8.1 + {src: 'res/Windows/splashscreenphone.jpg', width: '480', height: '800' }, // targetProject: WP 8.1 + ]; + + var splashScreensFiles = splashScreens.map(function(splash) { + return path.basename(splash.src); + }); + spyOn(fs, 'readdirSync').andReturn(splashScreensFiles); + + spyOn(fs, 'statSync').andReturn({ + size: 0 + }); + + var project = { projectConfig: createMockConfig([], splashScreens), root: PROJECT }; + var locations = { root: PROJECT }; + + updateSplashScreenImageExtensions(project, locations); + + expect(win10Manifest.getVisualElements().getSplashScreenExtension()).toBe('.jpg'); + expect(win81Manifest.getVisualElements().getSplashScreenExtension()).toBe('.jpg'); + expect(wp81Manifest.getVisualElements().getSplashScreenExtension()).toBe('.jpg'); + }); }); diff --git a/spec/unit/clean.spec.js b/spec/unit/clean.spec.js index c969141b..3892b282 100644 --- a/spec/unit/clean.spec.js +++ b/spec/unit/clean.spec.js @@ -16,6 +16,15 @@ describe('Cordova clean command', function() { if (/config\.xml$/.test(filePath)) return true; return fsExistsSyncOrig(filePath); }); + var fsStatSyncOrig = fs.statSync; + spyOn(fs, 'statSync').andCallFake(function (filePath) { + if (/SplashScreen\.scale-100\.png$/.test(filePath)) { + // Use absolute path: + return fsStatSyncOrig(iconPath); + } + + return fsStatSyncOrig(filePath); + }); }); afterEach(function() { @@ -33,7 +42,10 @@ describe('Cordova clean command', function() { } }; - var rejected = jasmine.createSpy(); + var rejected = jasmine.createSpy().andCallFake(function(err) { + // Log error: + expect(err).not.toBeDefined(); + }); prepareModule.clean.call(config) .then(function() { expect(fs.existsSync(iconPath)).toBeFalsy(); diff --git a/template/CordovaApp.Phone.jsproj b/template/CordovaApp.Phone.jsproj index c3f740ef..4dc71e56 100644 --- a/template/CordovaApp.Phone.jsproj +++ b/template/CordovaApp.Phone.jsproj @@ -79,7 +79,9 @@ Designer - + + + diff --git a/template/CordovaApp.Windows.jsproj b/template/CordovaApp.Windows.jsproj index c0387e6f..b5f43d66 100644 --- a/template/CordovaApp.Windows.jsproj +++ b/template/CordovaApp.Windows.jsproj @@ -79,7 +79,9 @@ Designer - + + + diff --git a/template/CordovaApp.Windows10.jsproj b/template/CordovaApp.Windows10.jsproj index bd1f9df9..8a496dd3 100644 --- a/template/CordovaApp.Windows10.jsproj +++ b/template/CordovaApp.Windows10.jsproj @@ -98,7 +98,9 @@ Designer - + + + diff --git a/template/cordova/lib/AppxManifest.js b/template/cordova/lib/AppxManifest.js index 8ad70458..1875bf7a 100644 --- a/template/cordova/lib/AppxManifest.js +++ b/template/cordova/lib/AppxManifest.js @@ -20,6 +20,7 @@ var fs = require('fs'); var util = require('util'); var et = require('elementtree'); +var path = require('path'); var xml= require('cordova-common').xmlHelpers; var UAP_RESTRICTED_CAPS = ['enterpriseAuthentication', 'sharedUserCertificates', @@ -416,6 +417,18 @@ AppxManifest.prototype.getVisualElements = function () { } return this; }, + getSplashScreenExtension: function (extension) { + var splashNode = visualElements.find('./' + self.prefix + 'SplashScreen'); + return splashNode && splashNode.attrib.Image && path.extname(splashNode.attrib.Image); + }, + setSplashScreenExtension: function (extension) { + var splashNode = visualElements.find('./' + self.prefix + 'SplashScreen'); + if (splashNode) { + var oldPath = splashNode.attrib.Image; + splashNode.attrib.Image = path.dirname(oldPath) + '\\' + path.basename(oldPath, path.extname(oldPath)) + extension; + } + return this; + }, getToastCapable: function () { return visualElements.attrib.ToastCapable; }, diff --git a/template/cordova/lib/prepare.js b/template/cordova/lib/prepare.js index 31cef528..da6c8461 100644 --- a/template/cordova/lib/prepare.js +++ b/template/cordova/lib/prepare.js @@ -48,6 +48,14 @@ var TEMPLATE = '\n'; +var SUPPORTED_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg']; +var SPLASH_SCREEN_SIZE_LIMIT = 200 * 1024; // 200 KBytes +var TARGET_PROJECT_81 = 'TARGET_PROJECT_81', + TARGET_PROJECT_WP81 = 'TARGET_PROJECT_WP81', + TARGET_PROJECT_10 = 'TARGET_PROJECT_10'; +var SPLASH_SCREEN_DESKTOP_TARGET_NAME = 'SplashScreen'; +var SPLASH_SCREEN_PHONE_TARGET_NAME = 'SplashScreenPhone'; + /** Note: this is only for backward compatibility, since it is being called directly from windows_parser */ module.exports.applyPlatformConfig = function() { var projectRoot = path.join(__dirname, '../..'); @@ -297,48 +305,83 @@ function applyNavigationWhitelist(config, manifest) { manifest.getApplication().setAccessRules(whitelistRules); } -function mapImageResources(images, imagesDir) { - var pathMap = {}; +// Platform default images +var PLATFORM_IMAGES = [ + {dest: 'Square150x150Logo.scale-100', width: 150, height: 150}, + {dest: 'Square30x30Logo.scale-100', width: 30, height: 30}, + {dest: 'StoreLogo.scale-100', width: 50, height: 50}, + {dest: 'SplashScreen.scale-100', width: 620, height: 300, targetProject: TARGET_PROJECT_10}, + {dest: 'SplashScreen.scale-125', width: 775, height: 375, targetProject: TARGET_PROJECT_10}, + {dest: 'SplashScreen.scale-140', width: 868, height: 420, targetProject: TARGET_PROJECT_81}, + {dest: 'SplashScreen.scale-150', width: 930, height: 450, targetProject: TARGET_PROJECT_10}, + {dest: 'SplashScreen.scale-180', width: 1116, height: 540, targetProject: TARGET_PROJECT_81}, + {dest: 'SplashScreen.scale-200', width: 1240, height: 600, targetProject: TARGET_PROJECT_10}, + {dest: 'SplashScreen.scale-400', width: 2480, height: 1200, targetProject: TARGET_PROJECT_10}, + // scaled images are specified here for backward compatibility only so we can find them by size + {dest: 'StoreLogo.scale-240', width: 120, height: 120}, + {dest: 'Square44x44Logo.scale-100', width: 44, height: 44}, + {dest: 'Square44x44Logo.scale-240', width: 106, height: 106}, + {dest: 'Square70x70Logo.scale-100', width: 70, height: 70}, + {dest: 'Square71x71Logo.scale-100', width: 71, height: 71}, + {dest: 'Square71x71Logo.scale-240', width: 170, height: 170}, + {dest: 'Square150x150Logo.scale-240', width: 360, height: 360}, + {dest: 'Square310x310Logo.scale-100', width: 310, height: 310}, + {dest: 'Wide310x150Logo.scale-100', width: 310, height: 150}, + {dest: 'Wide310x150Logo.scale-240', width: 744, height: 360}, + {dest: 'SplashScreenPhone.scale-100', width: 480, height: 800, targetProject: TARGET_PROJECT_WP81}, + {dest: 'SplashScreenPhone.scale-140', width: 672, height: 1120, targetProject: TARGET_PROJECT_WP81}, + {dest: 'SplashScreenPhone.scale-240', width: 1152, height: 1920, targetProject: TARGET_PROJECT_WP81} +]; + +function findPlatformImage(width, height) { + if (!width && !height){ + // this could be default image, + // Windows requires specific image dimension so we can't apply it + return null; + } + for (var idx in PLATFORM_IMAGES){ + var res = PLATFORM_IMAGES[idx]; + // If only one of width or height is not specified, use another parameter for comparation + // If both specified, compare both. + if ((!width || (width == res.width)) && + (!height || (height == res.height))){ + return res; + } + } + return null; +} - // Platform default images - var platformImages = [ - {dest: 'Square150x150Logo.scale-100.png', width: 150, height: 150}, - {dest: 'Square30x30Logo.scale-100.png', width: 30, height: 30}, - {dest: 'StoreLogo.scale-100.png', width: 50, height: 50}, - {dest: 'SplashScreen.scale-100.png', width: 620, height: 300}, - // scaled images are specified here for backward compatibility only so we can find them by size - {dest: 'StoreLogo.scale-240.png', width: 120, height: 120}, - {dest: 'Square44x44Logo.scale-100.png', width: 44, height: 44}, - {dest: 'Square44x44Logo.scale-240.png', width: 106, height: 106}, - {dest: 'Square70x70Logo.scale-100.png', width: 70, height: 70}, - {dest: 'Square71x71Logo.scale-100.png', width: 71, height: 71}, - {dest: 'Square71x71Logo.scale-240.png', width: 170, height: 170}, - {dest: 'Square150x150Logo.scale-240.png', width: 360, height: 360}, - {dest: 'Square310x310Logo.scale-100.png', width: 310, height: 310}, - {dest: 'Wide310x150Logo.scale-100.png', width: 310, height: 150}, - {dest: 'Wide310x150Logo.scale-240.png', width: 744, height: 360}, - {dest: 'SplashScreenPhone.scale-240.png', width: 1152, height: 1920} - ]; +/** Maps MRT splashscreen image to its target project defined in PLATFORM_IMAGES -> 8.1|WP8.1|10 + * This assumes we have different scales used for 8.1 and 10 projects. + * The only intersection is scale-100, which is treated as Win10 project' splashscreen (because + * size limit applies to Win10 project so we'll need to check it). + * @param {MRTImage} mrtImage + * @return {String} targetProject defined in PLATFORM_IMAGES + */ +function mrtSplashScreenToTargetProject(mrtImage) { + // Gives something like -> splashscreen.scale-100 + var splashDestToFind = [mrtImage.basename, mrtImage.qualifiers].join('.').toLowerCase(); + var matchingSplashScreen = PLATFORM_IMAGES.filter(function (img) { + return img.dest.toLowerCase() === splashDestToFind; + })[0]; + + return matchingSplashScreen && matchingSplashScreen.targetProject; +} - function findPlatformImage(width, height) { - if (!width && !height){ - // this could be default image, - // Windows requires specific image dimension so we can't apply it - return null; - } - for (var idx in platformImages){ - var res = platformImages[idx]; - // If only one of width or height is not specified, use another parameter for comparation - // If both specified, compare both. - if ((!width || (width == res.width)) && - (!height || (height == res.height))){ - return res; - } - } - return null; +function mapImageResources(images, imagesDir) { + function exceedsSizeLimit(filePath) { + return fs.statSync(filePath).size > SPLASH_SCREEN_SIZE_LIMIT; } + var pathMap = {}; + images.forEach(function (img) { + var ext = path.extname(img.src).toLowerCase(); + if (SUPPORTED_IMAGE_EXTENSIONS.indexOf(ext) === -1) { + events.emit('warn', '"' + ext + '" extension is not supported, skipping. Supported image extensions are ' + JSON.stringify(SUPPORTED_IMAGE_EXTENSIONS)); + return; + } + if (img.target) { // Parse source path into new MRTImage var imageToCopy = new MRTImage(img.src); @@ -353,6 +396,13 @@ function mapImageResources(images, imagesDir) { events.emit('warn', 'No images found for target: ' + img.target); } else { candidates.forEach(function(mrtImage) { + if (img.target === SPLASH_SCREEN_DESKTOP_TARGET_NAME && + mrtSplashScreenToTargetProject(mrtImage) === TARGET_PROJECT_10 && + exceedsSizeLimit(mrtImage.path)) { + events.emit('warn', '"' + mrtImage.path + '" file size exceeds the limit of ' + SPLASH_SCREEN_SIZE_LIMIT + ' bytes, skipping.'); + return; + } + // copy images with new base name but keeping qualifier var targetPath = path.join(imagesDir, mrtImage.generateFilenameFrom(img.target)); pathMap[targetPath] = mrtImage.path; @@ -362,7 +412,13 @@ function mapImageResources(images, imagesDir) { // find target image by size var targetImg = findPlatformImage(img.width, img.height); if (targetImg) { - var targetPath = path.join(imagesDir, targetImg.dest); + if (targetImg.targetProject === TARGET_PROJECT_10 && + exceedsSizeLimit(img.src)) { + events.emit('warn', '"' + img.src + '" file size exceeds the limit of ' + SPLASH_SCREEN_SIZE_LIMIT + ' bytes, skipping.'); + return; + } + + var targetPath = path.join(imagesDir, targetImg.dest) + ext; pathMap[targetPath] = img.src; } else { events.emit('warn', 'The following image was skipped because it has an unsupported size (' + img.width + 'x' + img.height + '): ' + img.src); @@ -439,6 +495,104 @@ function getUAPVersions(config) { }; } +/** + * @param {Object} splash + * @returns {String} 'SplashScreen'|'SplashScreenPhone' + */ +function getTargetForImage(splash) { + if (splash.target) { + // MRT syntax: + return splash.target; + } + + // Fall back on find by size for old non-MRT syntax: + var targetImg = findPlatformImage(splash.width, splash.height); + if (!targetImg.targetProject) { + return; + } + + if (targetImg.targetProject === TARGET_PROJECT_81 || targetImg.targetProject === TARGET_PROJECT_10) { + return SPLASH_SCREEN_DESKTOP_TARGET_NAME; + } + + if (targetImg.targetProject === TARGET_PROJECT_WP81) { + return SPLASH_SCREEN_PHONE_TARGET_NAME; + } +} + +// Updates manifests to match the app splash screen image types (PNG/JPG/JPEG) +function updateSplashScreenImageExtensions(cordovaProject, locations) { + + // Saving all extensions used for targets to verify them later + var extensionsUsed = {}; + + function checkThatExtensionsAreNotMixed() { + for (var target in extensionsUsed) { + /*jshint loopfunc: true */ + if (extensionsUsed.hasOwnProperty(target)) { + var extensionsUsedForTarget = extensionsUsed[target]; + + // Check that extensions are not mixed: + if (extensionsUsedForTarget.length > 1 && extensionsUsedForTarget.some(function(item) { + return item !== extensionsUsedForTarget[0]; + })) { + events.emit('warn', '"' + target + '" splash screens have mixed file extensions which is not supported. Some of the images will not be used.'); + } + } + } + } + + function checkTargetMatchAndUpdateUsedExtensions(img, target) { + var matchesTarget = getTargetForImage(img) === target; + + if (matchesTarget === true) { + extensionsUsed[target] = extensionsUsed[target] || []; + extensionsUsed[target].push(path.extname(img.src.toLowerCase())); + } + + return matchesTarget; + } + + function updateSplashExtensionInManifest(manifestFileName, splashScreen) { + var manifest = AppxManifest.get(path.join(locations.root, manifestFileName)); + var newExtension = path.extname(splashScreen.src); + + if (manifest.getVisualElements().getSplashScreenExtension() !== newExtension) { + events.emit('verbose', 'Set ' + manifestFileName + ' SplashScreen image extension to "' + newExtension + '"'); + manifest.getVisualElements().setSplashScreenExtension(newExtension); + manifest.write(); + } + } + + var splashScreens = cordovaProject.projectConfig.getSplashScreens('windows'); + + var desktopSplashScreen = splashScreens.filter(function(img) { + return checkTargetMatchAndUpdateUsedExtensions(img, SPLASH_SCREEN_DESKTOP_TARGET_NAME); + })[0]; + + var phoneSplashScreen = splashScreens.filter(function(img) { + return checkTargetMatchAndUpdateUsedExtensions(img, SPLASH_SCREEN_PHONE_TARGET_NAME); + })[0]; + + checkThatExtensionsAreNotMixed(); + + var manifestSplashScreenMap = {}; + manifestSplashScreenMap[MANIFEST_WINDOWS] = desktopSplashScreen; + manifestSplashScreenMap[MANIFEST_WINDOWS10] = desktopSplashScreen; + manifestSplashScreenMap[MANIFEST_PHONE] = phoneSplashScreen; + + for (var manifest in manifestSplashScreenMap) { + if (manifestSplashScreenMap.hasOwnProperty(manifest)) { + var splashScreen = manifestSplashScreenMap[manifest]; + if (!splashScreen) { + return; + } + + updateSplashExtensionInManifest(manifest, splashScreen); + } + } +} + module.exports.prepare = function (cordovaProject, options) { var self = this; @@ -459,6 +613,11 @@ module.exports.prepare = function (cordovaProject, options) { }) .then(function () { copyImages(cordovaProject, self.locations); + + // Update SplashScreen image extensions in the manifests + // TODO: Do this only when config.xml changes + updateSplashScreenImageExtensions(cordovaProject, self.locations); + // CB-5421 Add BOM to all html, js, css files // to ensure app can pass Windows Store Certification addBOMSignature(self.locations.www); diff --git a/template/www/cordova.js b/template/www/cordova.js index ab264d02..fed86130 100644 --- a/template/www/cordova.js +++ b/template/www/cordova.js @@ -1,5 +1,5 @@ // Platform: windows -// 53ea1913735222d326e65326e03391405df3cd4e +// a3732cb71d9b1dd590338e8cf44196f366d46da3 /* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file @@ -844,26 +844,47 @@ module.exports = channel; // file: F:/coho/cordova-windows/cordova-js-src/confighelper.js define("cordova/confighelper", function(require, exports, module) { -// config.xml wrapper (non-node ConfigParser analogue) -var config; -function Config(xhr) { - function loadPreferences(xhr) { - var parser = new DOMParser(); - var doc = parser.parseFromString(xhr.responseText, "application/xml"); +// config.xml and AppxManifest.xml wrapper (non-node ConfigParser analogue) +var configCache = {}; +var utils = require("cordova/utils"); - var preferences = doc.getElementsByTagName("preference"); - return Array.prototype.slice.call(preferences); +var isPhone = (cordova.platformId == 'windows') && WinJS.Utilities.isPhone; +var isWin10UWP = navigator.appVersion.indexOf('MSAppHost/3.0') !== -1; +var splashScreenTagName = isWin10UWP ? "SplashScreen" : (isPhone ? "m3:SplashScreen" : "m2:SplashScreen"); + +function XmlFile(text) { + this.text = text; +} + +XmlFile.prototype.loadTags = function (tagName) { + var parser; + if (!this.doc) { + parser = new DOMParser(); + this.doc = parser.parseFromString(this.text, "application/xml"); } - this.xhr = xhr; - this.preferences = loadPreferences(this.xhr); + var tags = this.doc.getElementsByTagName(tagName); + return Array.prototype.slice.call(tags); } -function readConfig(success, error) { +function Config(text) { + XmlFile.apply(this, arguments); + this.preferences = this.loadTags("preference"); +} + +function Manifest(text) { + XmlFile.apply(this, arguments); + this.splashScreen = this.loadTags(splashScreenTagName)[0]; +} + +utils.extend(Config, XmlFile); +utils.extend(Manifest, XmlFile); + +function requestFile(filePath, success, error) { var xhr; - if (typeof config != 'undefined') { - success(config); + if (typeof configCache[filePath] != 'undefined') { + success(configCache[filePath]); } function fail(msg) { @@ -877,11 +898,11 @@ function readConfig(success, error) { var xhrStatusChangeHandler = function () { if (xhr.readyState == 4) { if (xhr.status == 200 || xhr.status == 304 || xhr.status == 0 /* file:// */) { - config = new Config(xhr); - success(config); + configCache[filePath] = xhr.responseText; + success(xhr.responseText); } else { - fail('[Windows][cordova.js][xhrStatusChangeHandler] Could not XHR config.xml: ' + xhr.statusText); + fail('[Windows][cordova.js][xhrStatusChangeHandler] Could not XHR ' + filePath + ': ' + xhr.statusText); } } }; @@ -890,18 +911,30 @@ function readConfig(success, error) { xhr.addEventListener("load", xhrStatusChangeHandler); try { - xhr.open("get", "/config.xml", true); + xhr.open("get", filePath, true); xhr.send(); } catch (e) { - fail('[Windows][cordova.js][readConfig] Could not XHR config.xml: ' + JSON.stringify(e)); + fail('[Windows][cordova.js][xhrFile] Could not XHR ' + filePath + ': ' + JSON.stringify(e)); } } +function readConfig(success, error) { + requestFile("/config.xml", function (contents) { + success(new Config(contents)); + }, error); +} + +function readManifest(success, error) { + requestFile("/AppxManifest.xml", function (contents) { + success(new Manifest(contents)); + }, error); +} + /** * Reads a preference value from config.xml. * Returns preference value or undefined if it does not exist. * @param {String} preferenceName Preference name to read */ -Config.prototype.getPreferenceValue = function getPreferenceValue(preferenceName) { +Config.prototype.getPreferenceValue = function (preferenceName) { var preferenceItem = this.preferences && this.preferences.filter(function (item) { return item.attributes['name'].value === preferenceName; }); @@ -911,7 +944,15 @@ Config.prototype.getPreferenceValue = function getPreferenceValue(preferenceName } } +/** + * Reads SplashScreen image path + */ +Manifest.prototype.getSplashScreenImagePath = function () { + return this.splashScreen.attributes['Image'].value; +} + exports.readConfig = readConfig; +exports.readManifest = readManifest; }); @@ -1568,8 +1609,14 @@ module.exports = { return; } - e.setPromise(makePromise(configHelper.readConfig).then(function (config) { - splashscreen.firstShow(config, e); + var manifest; + + e.setPromise(makePromise(configHelper.readManifest).then(function (manifestTmp) { + manifest = manifestTmp; + return makePromise(configHelper.readConfig); + }) + .then(function (config) { + splashscreen.firstShow(config, manifest, e); }).then(function () { // Avoids splashimage flicker on Windows Phone 8.1/10 return WinJS.Promise.timeout(); @@ -1867,8 +1914,11 @@ function readBoolFromCfg(preferenceName, defaultValue, cfg) { } } -function readPreferencesFromCfg(cfg) { +function readPreferencesFromCfg(cfg, manifest) { try { + // Update splashscreen image path to match application manifest + splashImageSrc = schema + ':///' + manifest.getSplashScreenImagePath().replace(/\\/g, '/'); + bgColor = cfg.getPreferenceValue('SplashScreenBackgroundColor') || bgColor; bgColor = bgColor.replace('0x', '#').replace('0X', '#'); if (bgColor.length > 7) { @@ -1911,8 +1961,8 @@ function centerY() { } } -function init(config) { - readPreferencesFromCfg(config); +function init(config, manifest) { + readPreferencesFromCfg(config, manifest); var splashscreenStyles = document.createElement("link"); splashscreenStyles.rel = 'stylesheet'; @@ -2130,8 +2180,8 @@ function onResize() { //// module.exports = { - firstShow: function (config, activatedEventArgs) { - init(config); + firstShow: function (config, manifest, activatedEventArgs) { + init(config, manifest); activated(activatedEventArgs); if (!isVisible() && (splashScreenDelay > 0 || !autoHideSplashScreen)) {