diff --git a/bin/templates/cordova/lib/prepare.js b/bin/templates/cordova/lib/prepare.js index ac63f8a6..9dc85ff3 100644 --- a/bin/templates/cordova/lib/prepare.js +++ b/bin/templates/cordova/lib/prepare.js @@ -265,6 +265,14 @@ function getImageResourcePath (resourcesDir, type, density, name, sourceName) { return resourcePath; } +function getAdaptiveImageResourcePath (resourcesDir, type, density, name, sourceName) { + if (/\.9\.png$/.test(sourceName)) { + name = name.replace(/\.png$/, '.9.png'); + } + var resourcePath = path.join(resourcesDir, (density ? type + '-' + density + '-v26' : type), name); + return resourcePath; +} + function updateSplashes (cordovaProject, platformResourcesDir) { var resources = cordovaProject.projectConfig.getSplashScreens('android'); @@ -314,20 +322,197 @@ function cleanSplashes (projectRoot, projectConfig, platformResourcesDir) { } function updateIcons (cordovaProject, platformResourcesDir) { - var icons = cordovaProject.projectConfig.getIcons('android'); + let icons = cordovaProject.projectConfig.getIcons('android'); - // if there are icon elements in config.xml + // Skip if there are no app defined icons in config.xml if (icons.length === 0) { events.emit('verbose', 'This app does not have launcher icons defined'); return; } - var resourceMap = mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'icon.png'); + // 1. loop icons determin if there is an error in the setup. + // 2. during initial loop, also setup for legacy support. + let errorMissingAttributes = []; + let errorLegacyIconNeeded = []; + let hasAdaptive = false; + icons.forEach((icon, key) => { + if ( + (icon.background && !icon.foreground) + || (!icon.background && icon.foreground) + || (!icon.background && !icon.foreground && !icon.src) + ) { + errorMissingAttributes.push(icon.density ? icon.density : 'size=' + (icon.height || icon.width)); + } - var android_icons = {}; - var default_icon; + if (icon.foreground) { + hasAdaptive = true; + + if ( + !icon.src + && ( + icon.foreground.startsWith('@color') + || path.extname(path.basename(icon.foreground)) === '.xml' + ) + ) { + errorLegacyIconNeeded.push(icon.density ? icon.density : 'size=' + (icon.height || icon.width)); + } else if (!icon.src) { + icons[key].src = icon.foreground; + } + } + }); + + let errorMessage = []; + if (errorMissingAttributes.length > 0) { + errorMessage.push('One of the following attributes are set but missing the other for the density type: ' + errorMissingAttributes.join(', ') + '. Please ensure that all require attributes are defined.'); + } + + if (errorLegacyIconNeeded.length > 0) { + errorMessage.push('For the following icons with the density of: ' + errorLegacyIconNeeded.join(', ') + ', adaptive foreground with a defined color or vector can not be used as a standard fallback icon for older Android devices. To support older Android environments, please provide a value for the src attribute.'); + } + + if (errorMessage.length > 0) { + throw new CordovaError(errorMessage.join(' ')); + } + + let resourceMap = Object.assign( + {}, + mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher.png'), + mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.png'), + mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_background.png'), + mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.xml'), + mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_background.xml'), + mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher.xml') + ); + + let preparedIcons = prepareIcons(icons); + + if (hasAdaptive) { + resourceMap = updateIconResourceForAdaptive(preparedIcons, resourceMap, platformResourcesDir); + } + + resourceMap = updateIconResourceForLegacy(preparedIcons, resourceMap, platformResourcesDir); + + events.emit('verbose', 'Updating icons at ' + platformResourcesDir); + FileUpdater.updatePaths(resourceMap, { rootDir: cordovaProject.root }, logFileOp); +} + +function updateIconResourceForAdaptive (preparedIcons, resourceMap, platformResourcesDir) { + let android_icons = preparedIcons.android_icons; + let default_icon = preparedIcons.default_icon; + + // The source paths for icons and splashes are relative to + // project's config.xml location, so we use it as base path. + let background; + let foreground; + let targetPathBackground; + let targetPathForeground; + + for (let density in android_icons) { + let backgroundVal = '@mipmap/ic_launcher_background'; + let foregroundVal = '@mipmap/ic_launcher_foreground'; + + background = android_icons[density].background; + foreground = android_icons[density].foreground; + + if (background.startsWith('@color')) { + // Colors Use Case + backgroundVal = background; // Example: @color/background_foobar_1 + } else if (path.extname(path.basename(background)) === '.xml') { + // Vector Use Case + targetPathBackground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_background.xml', path.basename(android_icons[density].background)); + resourceMap[targetPathBackground] = android_icons[density].background; + } else if (path.extname(path.basename(background)) === '.png') { + // Images Use Case + targetPathBackground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_background.png', path.basename(android_icons[density].background)); + resourceMap[targetPathBackground] = android_icons[density].background; + } + + if (foreground.startsWith('@color')) { + // Colors Use Case + foregroundVal = foreground; + } else if (path.extname(path.basename(foreground)) === '.xml') { + // Vector Use Case + targetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_foreground.xml', path.basename(android_icons[density].foreground)); + resourceMap[targetPathForeground] = android_icons[density].foreground; + } else if (path.extname(path.basename(foreground)) === '.png') { + // Images Use Case + targetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_foreground.png', path.basename(android_icons[density].foreground)); + resourceMap[targetPathForeground] = android_icons[density].foreground; + } + + // create an XML for DPI and set color + const icLauncherTemplate = ` + + + +`; + + let launcherXmlPath = path.join(platformResourcesDir, 'mipmap-' + density + '-v26', 'ic_launcher.xml'); + + // Remove the XML from the resourceMap so the file does not get removed. + delete resourceMap[launcherXmlPath]; + + fs.writeFileSync(path.resolve(launcherXmlPath), icLauncherTemplate); + } + + // There's no "default" drawable, so assume default == mdpi. + if (default_icon && !android_icons.mdpi) { + let defaultTargetPathBackground; + let defaultTargetPathForeground; + + if (background.startsWith('@color')) { + // Colors Use Case + targetPathBackground = default_icon.background; + } else if (path.extname(path.basename(background)) === '.xml') { + // Vector Use Case + defaultTargetPathBackground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_background.xml', path.basename(default_icon.background)); + resourceMap[defaultTargetPathBackground] = default_icon.background; + } else if (path.extname(path.basename(background)) === '.png') { + // Images Use Case + defaultTargetPathBackground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_background.png', path.basename(default_icon.background)); + resourceMap[defaultTargetPathBackground] = default_icon.background; + } + + if (foreground.startsWith('@color')) { + // Colors Use Case + targetPathForeground = default_icon.foreground; + } else if (path.extname(path.basename(foreground)) === '.xml') { + // Vector Use Case + defaultTargetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_foreground.xml', path.basename(default_icon.foreground)); + resourceMap[defaultTargetPathForeground] = default_icon.foreground; + } else if (path.extname(path.basename(foreground)) === '.png') { + // Images Use Case + defaultTargetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_foreground.png', path.basename(default_icon.foreground)); + resourceMap[defaultTargetPathForeground] = default_icon.foreground; + } + } + + return resourceMap; +} + +function updateIconResourceForLegacy (preparedIcons, resourceMap, platformResourcesDir) { + let android_icons = preparedIcons.android_icons; + let default_icon = preparedIcons.default_icon; + + // The source paths for icons and splashes are relative to + // project's config.xml location, so we use it as base path. + for (var density in android_icons) { + var targetPath = getImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher.png', path.basename(android_icons[density].src)); + resourceMap[targetPath] = android_icons[density].src; + } + + // There's no "default" drawable, so assume default == mdpi. + if (default_icon && !android_icons.mdpi) { + var defaultTargetPath = getImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher.png', path.basename(default_icon.src)); + resourceMap[defaultTargetPath] = default_icon.src; + } + + return resourceMap; +} + +function prepareIcons (icons) { // http://developer.android.com/design/style/iconography.html - var sizeToDensityMap = { + const SIZE_TO_DENSITY_MAP = { 36: 'ldpi', 48: 'mdpi', 72: 'hdpi', @@ -335,11 +520,15 @@ function updateIcons (cordovaProject, platformResourcesDir) { 144: 'xxhdpi', 192: 'xxxhdpi' }; + + let android_icons = {}; + let default_icon; + // find the best matching icon for a given density or size // @output android_icons var parseIcon = function (icon, icon_size) { // do I have a platform icon for that density already - var density = icon.density || sizeToDensityMap[icon_size]; + var density = icon.density || SIZE_TO_DENSITY_MAP[icon_size]; if (!density) { // invalid icon defition ( or unsupported size) return; @@ -355,12 +544,34 @@ function updateIcons (cordovaProject, platformResourcesDir) { for (var i = 0; i < icons.length; i++) { var icon = icons[i]; var size = icon.width; + if (!size) { size = icon.height; } + if (!size && !icon.density) { if (default_icon) { - events.emit('verbose', 'Found extra default icon: ' + icon.src + ' (ignoring in favor of ' + default_icon.src + ')'); + let found = {}; + let favor = {}; + + // populating found icon. + if (icon.background && icon.foreground) { + found.background = icon.background; + found.foreground = icon.foreground; + } + if (icon.src) { + found.src = icon.src; + } + + if (default_icon.background && default_icon.foreground) { + favor.background = default_icon.background; + favor.foreground = default_icon.foreground; + } + if (default_icon.src) { + favor.src = default_icon.src; + } + + events.emit('verbose', 'Found extra default icon: ' + JSON.stringify(found) + ' and ignoring in favor of ' + JSON.stringify(favor) + '.'); } else { default_icon = icon; } @@ -369,36 +580,35 @@ function updateIcons (cordovaProject, platformResourcesDir) { } } - // The source paths for icons and splashes are relative to - // project's config.xml location, so we use it as base path. - for (var density in android_icons) { - var targetPath = getImageResourcePath( - platformResourcesDir, 'mipmap', density, 'icon.png', path.basename(android_icons[density].src)); - resourceMap[targetPath] = android_icons[density].src; - } - - // There's no "default" drawable, so assume default == mdpi. - if (default_icon && !android_icons.mdpi) { - var defaultTargetPath = getImageResourcePath( - platformResourcesDir, 'mipmap', 'mdpi', 'icon.png', path.basename(default_icon.src)); - resourceMap[defaultTargetPath] = default_icon.src; - } - - events.emit('verbose', 'Updating icons at ' + platformResourcesDir); - FileUpdater.updatePaths( - resourceMap, { rootDir: cordovaProject.root }, logFileOp); + return { + android_icons: android_icons, + default_icon: default_icon + }; } function cleanIcons (projectRoot, projectConfig, platformResourcesDir) { var icons = projectConfig.getIcons('android'); - if (icons.length > 0) { - var resourceMap = mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'icon.png'); - events.emit('verbose', 'Cleaning icons at ' + platformResourcesDir); - // No source paths are specified in the map, so updatePaths() will delete the target files. - FileUpdater.updatePaths( - resourceMap, { rootDir: projectRoot, all: true }, logFileOp); + // Skip if there are no app defined icons in config.xml + if (icons.length === 0) { + events.emit('verbose', 'This app does not have launcher icons defined'); + return; } + + let resourceMap = Object.assign( + {}, + mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher.png'), + mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.png'), + mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_background.png'), + mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.xml'), + mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_background.xml'), + mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher.xml') + ); + + events.emit('verbose', 'Cleaning icons at ' + platformResourcesDir); + + // No source paths are specified in the map, so updatePaths() will delete the target files. + FileUpdater.updatePaths(resourceMap, { rootDir: projectRoot, all: true }, logFileOp); } /** diff --git a/bin/templates/project/AndroidManifest.xml b/bin/templates/project/AndroidManifest.xml index e0c53aaa..9c36963a 100644 --- a/bin/templates/project/AndroidManifest.xml +++ b/bin/templates/project/AndroidManifest.xml @@ -30,7 +30,7 @@ - + + + + \ No newline at end of file diff --git a/bin/templates/project/res/mipmap-hdpi-v26/ic_launcher_background.png b/bin/templates/project/res/mipmap-hdpi-v26/ic_launcher_background.png new file mode 100644 index 00000000..021da864 Binary files /dev/null and b/bin/templates/project/res/mipmap-hdpi-v26/ic_launcher_background.png differ diff --git a/bin/templates/project/res/mipmap-hdpi/icon.png b/bin/templates/project/res/mipmap-hdpi-v26/ic_launcher_foreground.png similarity index 100% rename from bin/templates/project/res/mipmap-hdpi/icon.png rename to bin/templates/project/res/mipmap-hdpi-v26/ic_launcher_foreground.png diff --git a/bin/templates/project/res/mipmap-hdpi/ic_launcher.png b/bin/templates/project/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..53842612 Binary files /dev/null and b/bin/templates/project/res/mipmap-hdpi/ic_launcher.png differ diff --git a/bin/templates/project/res/mipmap-ldpi-v26/ic_launcher.xml b/bin/templates/project/res/mipmap-ldpi-v26/ic_launcher.xml new file mode 100644 index 00000000..be316184 --- /dev/null +++ b/bin/templates/project/res/mipmap-ldpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/bin/templates/project/res/mipmap-ldpi-v26/ic_launcher_background.png b/bin/templates/project/res/mipmap-ldpi-v26/ic_launcher_background.png new file mode 100644 index 00000000..da47a979 Binary files /dev/null and b/bin/templates/project/res/mipmap-ldpi-v26/ic_launcher_background.png differ diff --git a/bin/templates/project/res/mipmap-ldpi-v26/ic_launcher_foreground.png b/bin/templates/project/res/mipmap-ldpi-v26/ic_launcher_foreground.png new file mode 100644 index 00000000..203223af Binary files /dev/null and b/bin/templates/project/res/mipmap-ldpi-v26/ic_launcher_foreground.png differ diff --git a/bin/templates/project/res/mipmap-ldpi/icon.png b/bin/templates/project/res/mipmap-ldpi/ic_launcher.png similarity index 100% rename from bin/templates/project/res/mipmap-ldpi/icon.png rename to bin/templates/project/res/mipmap-ldpi/ic_launcher.png diff --git a/bin/templates/project/res/mipmap-mdpi-v26/ic_launcher.xml b/bin/templates/project/res/mipmap-mdpi-v26/ic_launcher.xml new file mode 100644 index 00000000..be316184 --- /dev/null +++ b/bin/templates/project/res/mipmap-mdpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/bin/templates/project/res/mipmap-mdpi-v26/ic_launcher_background.png b/bin/templates/project/res/mipmap-mdpi-v26/ic_launcher_background.png new file mode 100644 index 00000000..74e56e5b Binary files /dev/null and b/bin/templates/project/res/mipmap-mdpi-v26/ic_launcher_background.png differ diff --git a/bin/templates/project/res/mipmap-mdpi-v26/ic_launcher_foreground.png b/bin/templates/project/res/mipmap-mdpi-v26/ic_launcher_foreground.png new file mode 100644 index 00000000..8ca658e7 Binary files /dev/null and b/bin/templates/project/res/mipmap-mdpi-v26/ic_launcher_foreground.png differ diff --git a/bin/templates/project/res/mipmap-mdpi/icon.png b/bin/templates/project/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from bin/templates/project/res/mipmap-mdpi/icon.png rename to bin/templates/project/res/mipmap-mdpi/ic_launcher.png diff --git a/bin/templates/project/res/mipmap-xhdpi-v26/ic_launcher.xml b/bin/templates/project/res/mipmap-xhdpi-v26/ic_launcher.xml new file mode 100644 index 00000000..be316184 --- /dev/null +++ b/bin/templates/project/res/mipmap-xhdpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/bin/templates/project/res/mipmap-xhdpi-v26/ic_launcher_background.png b/bin/templates/project/res/mipmap-xhdpi-v26/ic_launcher_background.png new file mode 100644 index 00000000..d0cb62dd Binary files /dev/null and b/bin/templates/project/res/mipmap-xhdpi-v26/ic_launcher_background.png differ diff --git a/bin/templates/project/res/mipmap-xhdpi-v26/ic_launcher_foreground.png b/bin/templates/project/res/mipmap-xhdpi-v26/ic_launcher_foreground.png new file mode 100644 index 00000000..ae324f2d Binary files /dev/null and b/bin/templates/project/res/mipmap-xhdpi-v26/ic_launcher_foreground.png differ diff --git a/bin/templates/project/res/mipmap-xhdpi/icon.png b/bin/templates/project/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from bin/templates/project/res/mipmap-xhdpi/icon.png rename to bin/templates/project/res/mipmap-xhdpi/ic_launcher.png diff --git a/bin/templates/project/res/mipmap-xxhdpi-v26/ic_launcher.xml b/bin/templates/project/res/mipmap-xxhdpi-v26/ic_launcher.xml new file mode 100644 index 00000000..be316184 --- /dev/null +++ b/bin/templates/project/res/mipmap-xxhdpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/bin/templates/project/res/mipmap-xxhdpi-v26/ic_launcher_background.png b/bin/templates/project/res/mipmap-xxhdpi-v26/ic_launcher_background.png new file mode 100644 index 00000000..ec38d403 Binary files /dev/null and b/bin/templates/project/res/mipmap-xxhdpi-v26/ic_launcher_background.png differ diff --git a/bin/templates/project/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png b/bin/templates/project/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png new file mode 100644 index 00000000..7983ef45 Binary files /dev/null and b/bin/templates/project/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png differ diff --git a/bin/templates/project/res/mipmap-xxhdpi/icon.png b/bin/templates/project/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from bin/templates/project/res/mipmap-xxhdpi/icon.png rename to bin/templates/project/res/mipmap-xxhdpi/ic_launcher.png diff --git a/bin/templates/project/res/mipmap-xxxhdpi-v26/ic_launcher.xml b/bin/templates/project/res/mipmap-xxxhdpi-v26/ic_launcher.xml new file mode 100644 index 00000000..be316184 --- /dev/null +++ b/bin/templates/project/res/mipmap-xxxhdpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/bin/templates/project/res/mipmap-xxxhdpi-v26/ic_launcher_background.png b/bin/templates/project/res/mipmap-xxxhdpi-v26/ic_launcher_background.png new file mode 100644 index 00000000..d652c084 Binary files /dev/null and b/bin/templates/project/res/mipmap-xxxhdpi-v26/ic_launcher_background.png differ diff --git a/bin/templates/project/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png b/bin/templates/project/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png new file mode 100644 index 00000000..6857bef7 Binary files /dev/null and b/bin/templates/project/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png differ diff --git a/bin/templates/project/res/mipmap-xxxhdpi/icon.png b/bin/templates/project/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from bin/templates/project/res/mipmap-xxxhdpi/icon.png rename to bin/templates/project/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/spec/unit/prepare.spec.js b/spec/unit/prepare.spec.js new file mode 100644 index 00000000..3b133c15 --- /dev/null +++ b/spec/unit/prepare.spec.js @@ -0,0 +1,757 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + 'License'); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var rewire = require('rewire'); +var path = require('path'); +var CordovaError = require('cordova-common').CordovaError; + +const PATH_RESOURCE = path.join('platforms', 'android', 'app', 'src', 'main', 'res'); + +/** + * Creates blank resource map object, used for testing. + * + * @param {String} target specific resource item + */ +function createResourceMap (target) { + let resources = {}; + + [ + 'mipmap-ldpi', + 'mipmap-mdpi', + 'mipmap-hdpi', + 'mipmap-xhdpi', + 'mipmap-xxhdpi', + 'mipmap-xxxhdpi', + 'mipmap-ldpi-v26', + 'mipmap-mdpi-v26', + 'mipmap-hdpi-v26', + 'mipmap-xhdpi-v26', + 'mipmap-xxhdpi-v26', + 'mipmap-xxxhdpi-v26' + ].forEach((mipmap) => { + if (!target || target === 'ic_launcher.png') resources[path.join(PATH_RESOURCE, mipmap, 'ic_launcher.png')] = null; + if (!target || target === 'ic_launcher_foreground.png') resources[path.join(PATH_RESOURCE, mipmap, 'ic_launcher_foreground.png')] = null; + if (!target || target === 'ic_launcher_background.png') resources[path.join(PATH_RESOURCE, mipmap, 'ic_launcher_background.png')] = null; + if (!target || target === 'ic_launcher_foreground.xml') resources[path.join(PATH_RESOURCE, mipmap, 'ic_launcher_foreground.xml')] = null; + if (!target || target === 'ic_launcher_background.xml') resources[path.join(PATH_RESOURCE, mipmap, 'ic_launcher_background.xml')] = null; + + if ( + !mipmap.includes('-v26') && + (!target || target === 'ic_launcher.xml') + ) { + resources[path.join(PATH_RESOURCE, mipmap, 'ic_launcher.xml')] = null; + } + }); + + return resources; +} + +/** + * Create a mock item from the getIcon collection with the supplied updated data. + * + * @param {Object} data Changes to apply to the mock getIcon item + */ +function mockGetIconItem (data) { + return Object.assign({}, { + src: undefined, + target: undefined, + density: undefined, + platform: 'android', + width: undefined, + height: undefined, + background: undefined, + foreground: undefined + }, data); +} + +describe('updateIcons method', function () { + // Rewire + let prepare; + + // Spies + let updateIconResourceForAdaptiveSpy; + let updateIconResourceForLegacySpy; + let emitSpy; + let updatePathsSpy; + + // Mock Data + let cordovaProject; + let platformResourcesDir; + + beforeEach(function () { + prepare = rewire('../../bin/templates/cordova/lib/prepare'); + + cordovaProject = { + root: '/mock', + projectConfig: { + path: '/mock/config.xml', + cdvNamespacePrefix: 'cdv' + }, + locations: { + plugins: '/mock/plugins', + www: '/mock/www' + } + }; + platformResourcesDir = PATH_RESOURCE; + + emitSpy = jasmine.createSpy('emit'); + prepare.__set__('events', { + emit: emitSpy + }); + + updatePathsSpy = jasmine.createSpy('updatePaths'); + prepare.__set__('FileUpdater', { + updatePaths: updatePathsSpy + }); + + // mocking initial responses for mapImageResources + prepare.__set__('mapImageResources', function (rootDir, subDir, type, resourceName) { + if (resourceName.includes('ic_launcher.png')) { + return createResourceMap('ic_launcher.png'); + } else if (resourceName.includes('ic_launcher_foreground.png')) { + return createResourceMap('ic_launcher_foreground.png'); + } else if (resourceName.includes('ic_launcher_background.png')) { + return createResourceMap('ic_launcher_background.png'); + } else if (resourceName.includes('ic_launcher_foreground.xml')) { + return createResourceMap('ic_launcher_foreground.xml'); + } else if (resourceName.includes('ic_launcher_background.xml')) { + return createResourceMap('ic_launcher_background.xml'); + } else if (resourceName.includes('ic_launcher.xml')) { + return createResourceMap('ic_launcher.xml'); + } + }); + }); + + it('Test#001 : Should detect no defined icons.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return []; + }; + + updateIcons(cordovaProject, platformResourcesDir); + + // The emit was called + expect(emitSpy).toHaveBeenCalled(); + + // The emit message was. + let actual = emitSpy.calls.argsFor(0)[1]; + let expected = 'This app does not have launcher icons defined'; + expect(actual).toEqual(expected); + }); + + it('Test#002 : Should detech incorrect configrations for adaptive icon and throws error.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({ + density: 'mdpi', + background: 'res/icon/android/mdpi-background.png' + })]; + }; + + expect(function () { + updateIcons(cordovaProject, platformResourcesDir); + }).toThrow( + new CordovaError('One of the following attributes are set but missing the other for the density type: mdpi. Please ensure that all require attributes are defined.') + ); + }); + + it('Test#003 : Should detech incorrect configrations (missing foreground) for adaptive icon and throw an error.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({ + density: 'mdpi', + background: 'res/icon/android/mdpi-background.png' + })]; + }; + + expect(function () { + updateIcons(cordovaProject, platformResourcesDir); + }).toThrow( + new CordovaError('One of the following attributes are set but missing the other for the density type: mdpi. Please ensure that all require attributes are defined.') + ); + }); + + it('Test#004 : Should detech incorrect configrations (missing background) for adaptive icon and throw an error.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({ + density: 'mdpi', + foreground: 'res/icon/android/mdpi-foreground.png' + })]; + }; + + expect(function () { + updateIcons(cordovaProject, platformResourcesDir); + }).toThrow( + new CordovaError('One of the following attributes are set but missing the other for the density type: mdpi. Please ensure that all require attributes are defined.') + ); + }); + + it('Test#005 : Should detech incorrect configrations and throw an error.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({density: 'mdpi'})]; + }; + + expect(function () { + updateIcons(cordovaProject, platformResourcesDir); + }).toThrow( + new CordovaError('One of the following attributes are set but missing the other for the density type: mdpi. Please ensure that all require attributes are defined.') + ); + }); + + it('Test#006 : Should display incorrect configuration with density in message.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({density: 'mdpi'})]; + }; + + expect(function () { + updateIcons(cordovaProject, platformResourcesDir); + }).toThrow( + new CordovaError('One of the following attributes are set but missing the other for the density type: mdpi. Please ensure that all require attributes are defined.') + ); + }); + + it('Test#007 : Should display incorrect configuration with size in message from height.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({height: '192'})]; + }; + + expect(function () { + updateIcons(cordovaProject, platformResourcesDir); + }).toThrow( + new CordovaError('One of the following attributes are set but missing the other for the density type: size=192. Please ensure that all require attributes are defined.') + ); + }); + + it('Test#008 : Should display incorrect configuration with size in message from width.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({width: '192'})]; + }; + + expect(function () { + updateIcons(cordovaProject, platformResourcesDir); + }).toThrow( + new CordovaError('One of the following attributes are set but missing the other for the density type: size=192. Please ensure that all require attributes are defined.') + ); + }); + + it('Test#009 : Should detech incorrect configrations (missing background) for adaptive icon and throw an error.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({ + density: 'mdpi', + foreground: 'res/icon/android/mdpi-foreground.png' + })]; + }; + + expect(function () { + updateIcons(cordovaProject, platformResourcesDir); + }).toThrow( + new CordovaError('One of the following attributes are set but missing the other for the density type: mdpi. Please ensure that all require attributes are defined.') + ); + }); + + it('Test#010 : Should detech adaptive icon with vector foreground and throws error for missing backwards compatability settings.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({ + density: 'mdpi', + background: 'res/icon/android/mdpi-background.png', + foreground: 'res/icon/android/mdpi-foreground.xml' + })]; + }; + + expect(function () { + updateIcons(cordovaProject, platformResourcesDir); + }).toThrow( + new CordovaError('For the following icons with the density of: mdpi, adaptive foreground with a defined color or vector can not be used as a standard fallback icon for older Android devices. To support older Android environments, please provide a value for the src attribute.') + ); + }); + + it('Test#011 : Should detech adaptive icon with color foreground and throws error for missing backwards compatability settings.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({ + density: 'mdpi', + background: 'res/icon/android/mdpi-background.png', + foreground: '@color/background' + })]; + }; + + expect(function () { + updateIcons(cordovaProject, platformResourcesDir); + }).toThrow( + new CordovaError('For the following icons with the density of: mdpi, adaptive foreground with a defined color or vector can not be used as a standard fallback icon for older Android devices. To support older Android environments, please provide a value for the src attribute.') + ); + }); + + it('Test#012 : Should update paths with adaptive and standard icons. Standard icon comes from adaptive foreground', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({ + density: 'mdpi', + background: 'res/icon/android/mdpi-background.png', + foreground: 'res/icon/android/mdpi-foreground.png' + })]; + }; + + // Creating Spies + let resourceMap = createResourceMap(); + let phaseOneModification = {}; + phaseOneModification[path.join(PATH_RESOURCE, 'mipmap-mdpi-v26', 'ic_launcher_foreground.png')] = 'res/icon/android/mdpi-foreground.png'; + phaseOneModification[path.join(PATH_RESOURCE, 'mipmap-mdpi-v26', 'ic_launcher_background.png')] = 'res/icon/android/mdpi-background.png'; + let phaseOneUpdatedIconsForAdaptive = Object.assign({}, resourceMap, phaseOneModification); + + updateIconResourceForAdaptiveSpy = jasmine.createSpy('updateIconResourceForAdaptiveSpy'); + prepare.__set__('updateIconResourceForAdaptive', function (preparedIcons, resourceMap, platformResourcesDir) { + updateIconResourceForAdaptiveSpy(); + return phaseOneUpdatedIconsForAdaptive; + }); + + let phaseTwoModification = {}; + phaseTwoModification[path.join(PATH_RESOURCE, 'mipmap-mdpi', 'ic_launcher.png')] = 'res/icon/android/mdpi-foreground.png'; + phaseTwoModification[path.join(PATH_RESOURCE, 'mipmap-mdpi-v26', 'ic_launcher_background.png')] = 'res/icon/android/mdpi-background.png'; + let phaseTwoUpdatedIconsForLegacy = Object.assign({}, phaseOneUpdatedIconsForAdaptive, phaseTwoModification); + + updateIconResourceForLegacySpy = jasmine.createSpy('updateIconResourceForLegacySpy'); + prepare.__set__('updateIconResourceForLegacy', function (preparedIcons, resourceMap, platformResourcesDir) { + updateIconResourceForLegacySpy(); + return phaseTwoUpdatedIconsForLegacy; + }); + + updateIcons(cordovaProject, platformResourcesDir); + + // The emit was called + expect(emitSpy).toHaveBeenCalled(); + + // The emit message was. + let actual = emitSpy.calls.argsFor(0)[1]; + let expected = 'Updating icons at ' + PATH_RESOURCE; + expect(actual).toEqual(expected); + + // Expected to be called. + expect(updatePathsSpy).toHaveBeenCalled(); + expect(updateIconResourceForAdaptiveSpy).toHaveBeenCalled(); + expect(updateIconResourceForLegacySpy).toHaveBeenCalled(); + + let actualResourceMap = updatePathsSpy.calls.argsFor(0)[0]; + let expectedResourceMap = phaseTwoUpdatedIconsForLegacy; + expect(actualResourceMap).toEqual(expectedResourceMap); + }); + + it('Test#013 : Should update paths with adaptive and standard icons.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({ + density: 'mdpi', + src: 'res/icon/android/mdpi-icon.png', + background: 'res/icon/android/mdpi-background.png', + foreground: 'res/icon/android/mdpi-foreground.png' + })]; + }; + + // Creating Spies + let resourceMap = createResourceMap(); + let phaseOneModification = {}; + phaseOneModification[path.join(PATH_RESOURCE, 'mipmap-mdpi-v26', 'ic_launcher_foreground.png')] = 'res/icon/android/mdpi-foreground.png'; + phaseOneModification[path.join(PATH_RESOURCE, 'mipmap-mdpi-v26', 'ic_launcher_background.png')] = 'res/icon/android/mdpi-background.png'; + let phaseOneUpdatedIconsForAdaptive = Object.assign({}, resourceMap, phaseOneModification); + + updateIconResourceForAdaptiveSpy = jasmine.createSpy('updateIconResourceForAdaptiveSpy'); + prepare.__set__('updateIconResourceForAdaptive', function (preparedIcons, resourceMap, platformResourcesDir) { + updateIconResourceForAdaptiveSpy(); + return phaseOneUpdatedIconsForAdaptive; + }); + + let phaseTwoModification = {}; + phaseTwoModification[path.join(PATH_RESOURCE, 'mipmap-mdpi', 'ic_launcher.png')] = 'res/icon/android/mdpi-foreground.png'; + phaseTwoModification[path.join(PATH_RESOURCE, 'mipmap-mdpi-v26', 'ic_launcher_background.png')] = 'res/icon/android/mdpi-background.png'; + let phaseTwoUpdatedIconsForLegacy = Object.assign({}, phaseOneUpdatedIconsForAdaptive, phaseTwoModification); + + updateIconResourceForLegacySpy = jasmine.createSpy('updateIconResourceForLegacySpy'); + prepare.__set__('updateIconResourceForLegacy', function (preparedIcons, resourceMap, platformResourcesDir) { + updateIconResourceForLegacySpy(); + return phaseTwoUpdatedIconsForLegacy; + }); + + updateIcons(cordovaProject, platformResourcesDir); + + // The emit was called + expect(emitSpy).toHaveBeenCalled(); + + // The emit message was. + let actual = emitSpy.calls.argsFor(0)[1]; + let expected = 'Updating icons at ' + PATH_RESOURCE; + expect(actual).toEqual(expected); + + // Expected to be called. + expect(updatePathsSpy).toHaveBeenCalled(); + expect(updateIconResourceForAdaptiveSpy).toHaveBeenCalled(); + expect(updateIconResourceForLegacySpy).toHaveBeenCalled(); + + let actualResourceMap = updatePathsSpy.calls.argsFor(0)[0]; + let expectedResourceMap = phaseTwoUpdatedIconsForLegacy; + expect(actualResourceMap).toEqual(expectedResourceMap); + }); + + it('Test#014 : Should update paths with standard icons.', function () { + const updateIcons = prepare.__get__('updateIcons'); + + // mock data. + cordovaProject.projectConfig.getIcons = function () { + return [mockGetIconItem({ + density: 'mdpi', + src: 'res/icon/android/mdpi-icon.png' + })]; + }; + + // Creating Spies + let phaseOneUpdatedIconsForAdaptive = createResourceMap(); + + updateIconResourceForAdaptiveSpy = jasmine.createSpy('updateIconResourceForAdaptiveSpy'); + prepare.__set__('updateIconResourceForAdaptive', function (preparedIcons, resourceMap, platformResourcesDir) { + updateIconResourceForAdaptiveSpy(); + return phaseOneUpdatedIconsForAdaptive; + }); + + let phaseTwoModification = {}; + phaseTwoModification[path.join(PATH_RESOURCE, 'mipmap-mdpi', 'ic_launcher.png')] = 'res/icon/android/mdpi-icon.png'; + let phaseTwoUpdatedIconsForLegacy = Object.assign({}, phaseOneUpdatedIconsForAdaptive, phaseTwoModification); + + updateIconResourceForLegacySpy = jasmine.createSpy('updateIconResourceForLegacySpy'); + prepare.__set__('updateIconResourceForLegacy', function (preparedIcons, resourceMap, platformResourcesDir) { + updateIconResourceForLegacySpy(); + return phaseTwoUpdatedIconsForLegacy; + }); + + updateIcons(cordovaProject, platformResourcesDir); + + // The emit was called + expect(emitSpy).toHaveBeenCalled(); + + // The emit message was. + let actual = emitSpy.calls.argsFor(0)[1]; + let expected = 'Updating icons at ' + PATH_RESOURCE; + expect(actual).toEqual(expected); + + // Expected to be called. + expect(updatePathsSpy).toHaveBeenCalled(); + expect(updateIconResourceForAdaptiveSpy).not.toHaveBeenCalled(); + expect(updateIconResourceForLegacySpy).toHaveBeenCalled(); + + let actualResourceMap = updatePathsSpy.calls.argsFor(0)[0]; + let expectedResourceMap = phaseTwoUpdatedIconsForLegacy; + expect(actualResourceMap).toEqual(expectedResourceMap); + }); +}); + +describe('prepareIcons method', function () { + let prepare; + let emitSpy; + let prepareIcons; + + beforeEach(function () { + prepare = rewire('../../bin/templates/cordova/lib/prepare'); + + prepareIcons = prepare.__get__('prepareIcons'); + + // Creating Spies + emitSpy = jasmine.createSpy('emit'); + prepare.__set__('events', { + emit: emitSpy + }); + }); + + it('Test#001 : should emit extra default icon found for adaptive use case.', function () { + // mock data. + let ldpi = mockGetIconItem({ + density: 'ldpi', + background: 'res/icon/android/ldpi-background.png', + foreground: 'res/icon/android/ldpi-foreground.png' + }); + + let mdpi = mockGetIconItem({ + density: 'mdpi', + background: 'res/icon/android/mdpi-background.png', + foreground: 'res/icon/android/mdpi-foreground.png' + }); + + let icons = [ldpi, mdpi]; + let actual = prepareIcons(icons); + let expected = { + android_icons: {ldpi, mdpi}, + default_icon: undefined + }; + + expect(expected).toEqual(actual); + + }); + + it('Test#002 : should emit extra default icon found for legacy use case.', function () { + // mock data. + let ldpi = mockGetIconItem({ + src: 'res/icon/android/ldpi-icon.png', + density: 'ldpi' + }); + + let mdpi = mockGetIconItem({ + src: 'res/icon/android/mdpi-icon.png', + density: 'mdpi' + }); + + let icons = [ldpi, mdpi]; + let actual = prepareIcons(icons); + let expected = { + android_icons: {ldpi, mdpi}, + default_icon: undefined + }; + + expect(expected).toEqual(actual); + + }); +}); + +describe('updateIconResourceForLegacy method', function () { + let prepare; + + // Spies + let fsWriteFileSyncSpy; + + // Mock Data + let platformResourcesDir; + let preparedIcons; + let resourceMap; + + beforeEach(function () { + prepare = rewire('../../bin/templates/cordova/lib/prepare'); + + // Mocked Data + platformResourcesDir = PATH_RESOURCE; + preparedIcons = { + android_icons: { + mdpi: mockGetIconItem({ + src: 'res/icon/android/mdpi-icon.png', + density: 'mdpi' + }) + }, + default_icon: undefined + }; + + resourceMap = createResourceMap(); + + fsWriteFileSyncSpy = jasmine.createSpy('writeFileSync'); + prepare.__set__('fs', { + writeFileSync: fsWriteFileSyncSpy + }); + }); + + it('Test#001 : Should update resource map with prepared icons.', function () { + // Get method for testing + const updateIconResourceForLegacy = prepare.__get__('updateIconResourceForLegacy'); + + // Run Test + let expectedModification = {}; + expectedModification[path.join(PATH_RESOURCE, 'mipmap-mdpi', 'ic_launcher.png')] = 'res/icon/android/mdpi-icon.png'; + let expected = Object.assign({}, resourceMap, expectedModification); + let actual = updateIconResourceForLegacy(preparedIcons, resourceMap, platformResourcesDir); + + expect(actual).toEqual(expected); + + }); +}); + +describe('updateIconResourceForAdaptive method', function () { + let prepare; + + // Spies + let fsWriteFileSyncSpy; + + // Mock Data + let platformResourcesDir; + let preparedIcons; + let resourceMap; + + beforeEach(function () { + prepare = rewire('../../bin/templates/cordova/lib/prepare'); + + // Mocked Data + platformResourcesDir = PATH_RESOURCE; + preparedIcons = { + android_icons: { + mdpi: mockGetIconItem({ + density: 'mdpi', + background: 'res/icon/android/mdpi-background.png', + foreground: 'res/icon/android/mdpi-foreground.png' + }) + }, + default_icon: undefined + }; + + resourceMap = createResourceMap(); + + fsWriteFileSyncSpy = jasmine.createSpy('writeFileSync'); + prepare.__set__('fs', { + writeFileSync: fsWriteFileSyncSpy + }); + }); + + it('Test#001 : Should update resource map with prepared icons.', function () { + // Get method for testing + const updateIconResourceForAdaptive = prepare.__get__('updateIconResourceForAdaptive'); + + // Run Test + let expectedModification = {}; + expectedModification[path.join(PATH_RESOURCE, 'mipmap-mdpi-v26', 'ic_launcher_background.png')] = 'res/icon/android/mdpi-background.png'; + expectedModification[path.join(PATH_RESOURCE, 'mipmap-mdpi-v26', 'ic_launcher_foreground.png')] = 'res/icon/android/mdpi-foreground.png'; + + let expected = Object.assign({}, resourceMap, expectedModification); + let actual = updateIconResourceForAdaptive(preparedIcons, resourceMap, platformResourcesDir); + + expect(actual).toEqual(expected); + + }); +}); + +describe('cleanIcons method', function () { + let prepare; + let emitSpy; + let updatePathsSpy; + + beforeEach(function () { + prepare = rewire('../../bin/templates/cordova/lib/prepare'); + + emitSpy = jasmine.createSpy('emit'); + prepare.__set__('events', { + emit: emitSpy + }); + + updatePathsSpy = jasmine.createSpy('updatePaths'); + prepare.__set__('FileUpdater', { + updatePaths: updatePathsSpy + }); + }); + + it('Test#001 : should detect that the app does not have defined icons.', function () { + // Mock + let icons = []; + let projectRoot = '/mock'; + let projectConfig = { + getIcons: function () { return icons; }, + path: '/mock/config.xml', + cdvNamespacePrefix: 'cdv' + }; + let platformResourcesDir = PATH_RESOURCE; + + const cleanIcons = prepare.__get__('cleanIcons'); + cleanIcons(projectRoot, projectConfig, platformResourcesDir); + + let actualEmitMessage = emitSpy.calls.argsFor(0)[1]; + expect(actualEmitMessage).toContain('This app does not have launcher icons defined'); + }); + + it('Test#002 : Should clean paths for adaptive icons.', function () { + // Mock + let icons = [mockGetIconItem({ + density: 'mdpi', + background: 'res/icon/android/mdpi-background.png', + foreground: 'res/icon/android/mdpi-foreground.png' + })]; + let projectRoot = '/mock'; + let projectConfig = { + getIcons: function () { return icons; }, + path: '/mock/config.xml', + cdvNamespacePrefix: 'cdv' + }; + let platformResourcesDir = PATH_RESOURCE; + + var expectedResourceMapBackground = createResourceMap('ic_launcher_background.png'); + + // mocking initial responses for mapImageResources + prepare.__set__('mapImageResources', function (rootDir, subDir, type, resourceName) { + if (resourceName.includes('ic_launcher_background.png')) { + return expectedResourceMapBackground; + } + }); + + const cleanIcons = prepare.__get__('cleanIcons'); + cleanIcons(projectRoot, projectConfig, platformResourcesDir); + + let actualResourceMapBackground = updatePathsSpy.calls.argsFor(0)[0]; + expect(actualResourceMapBackground).toEqual(expectedResourceMapBackground); + }); + + it('Test#003 : Should clean paths for legacy icons.', function () { + // Mock + let icons = [mockGetIconItem({ + src: 'res/icon/android/mdpi.png', + density: 'mdpi' + })]; + + let projectRoot = '/mock'; + let projectConfig = { + getIcons: function () { return icons; }, + path: '/mock/config.xml', + cdvNamespacePrefix: 'cdv' + }; + let platformResourcesDir = PATH_RESOURCE; + + var expectedResourceMap = createResourceMap(); + + // mocking initial responses for mapImageResources + prepare.__set__('mapImageResources', function (rootDir, subDir, type, resourceName) { + return expectedResourceMap; + }); + + const cleanIcons = prepare.__get__('cleanIcons'); + cleanIcons(projectRoot, projectConfig, platformResourcesDir); + + let actualResourceMap = updatePathsSpy.calls.argsFor(0)[0]; + expect(actualResourceMap).toEqual(expectedResourceMap); + }); +});