diff --git a/cordova-js-src/platform.js b/cordova-js-src/platform.js index 14eddd7f..b9921188 100644 --- a/cordova-js-src/platform.js +++ b/cordova-js-src/platform.js @@ -36,6 +36,9 @@ module.exports = { // TODO: Extract this as a proper plugin. modulemapper.clobbers('cordova/plugin/android/app', 'navigator.app'); + // Core Splash Screen + modulemapper.clobbers('cordova/plugin/android/splashscreen', 'navigator.splashscreen'); + var APP_PLUGIN_NAME = Number(cordova.platformVersion.split('.')[0]) >= 4 ? 'CoreAndroid' : 'App'; // Inject a listener for the backbutton on the document. diff --git a/cordova-js-src/plugin/android/splashscreen.js b/cordova-js-src/plugin/android/splashscreen.js new file mode 100644 index 00000000..63e77854 --- /dev/null +++ b/cordova-js-src/plugin/android/splashscreen.js @@ -0,0 +1,33 @@ +/* + * + * 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 exec = require('cordova/exec'); + +var splashscreen = { + show: function () { + console.log('"navigator.splashscreen.show()" is unsupported on Android.'); + }, + hide: function () { + exec(null, null, 'CordovaSplashScreenPlugin', 'hide', []); + } +}; + +module.exports = splashscreen; diff --git a/framework/build.gradle b/framework/build.gradle index 0f192d15..8fcf4b59 100644 --- a/framework/build.gradle +++ b/framework/build.gradle @@ -80,6 +80,7 @@ android { dependencies { implementation "androidx.appcompat:appcompat:${cordovaConfig.ANDROIDX_APP_COMPAT_VERSION}" implementation "androidx.webkit:webkit:${cordovaConfig.ANDROIDX_WEBKIT_VERSION}" + implementation "androidx.core:core-splashscreen:${cordovaConfig.ANDROIDX_CORE_SPLASHSCREEN_VERSION}" } /** diff --git a/framework/cdv-gradle-config-defaults.json b/framework/cdv-gradle-config-defaults.json index 1cf08f77..5fcd5be6 100644 --- a/framework/cdv-gradle-config-defaults.json +++ b/framework/cdv-gradle-config-defaults.json @@ -8,6 +8,7 @@ "KOTLIN_VERSION": "1.5.21", "ANDROIDX_APP_COMPAT_VERSION": "1.4.2", "ANDROIDX_WEBKIT_VERSION": "1.4.0", + "ANDROIDX_CORE_SPLASHSCREEN_VERSION": "1.0.0-rc01", "GRADLE_PLUGIN_GOOGLE_SERVICES_VERSION": "4.3.10", "IS_GRADLE_PLUGIN_GOOGLE_SERVICES_ENABLED": false, "IS_GRADLE_PLUGIN_KOTLIN_ENABLED": false diff --git a/framework/src/org/apache/cordova/ConfigXmlParser.java b/framework/src/org/apache/cordova/ConfigXmlParser.java index 69d02ee6..ca3cbdaa 100644 --- a/framework/src/org/apache/cordova/ConfigXmlParser.java +++ b/framework/src/org/apache/cordova/ConfigXmlParser.java @@ -76,6 +76,14 @@ public class ConfigXmlParser { ) ); + pluginEntries.add( + new PluginEntry( + SplashScreenPlugin.PLUGIN_NAME, + "org.apache.cordova.SplashScreenPlugin", + true + ) + ); + parse(action.getResources().getXml(id)); } diff --git a/framework/src/org/apache/cordova/CordovaActivity.java b/framework/src/org/apache/cordova/CordovaActivity.java index 5cdded66..ef6ae5e5 100755 --- a/framework/src/org/apache/cordova/CordovaActivity.java +++ b/framework/src/org/apache/cordova/CordovaActivity.java @@ -42,6 +42,7 @@ import android.webkit.WebViewClient; import android.widget.FrameLayout; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.splashscreen.SplashScreen; /** * This class is the main Android activity that represents the Cordova @@ -98,11 +99,16 @@ public class CordovaActivity extends AppCompatActivity { protected ArrayList pluginEntries; protected CordovaInterfaceImpl cordovaInterface; + private SplashScreen splashScreen; + /** * Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { + // Handle the splash screen transition. + splashScreen = SplashScreen.installSplashScreen(this); + // need to activate preferences before super.onCreate to avoid "requestFeature() must be called before adding content" exception loadConfig(); @@ -125,8 +131,6 @@ public class CordovaActivity extends AppCompatActivity { // (as was the case in previous cordova versions) if (!preferences.getBoolean("FullscreenNotImmersive", false)) { immersiveMode = true; - // The splashscreen plugin needs the flags set before we're focused to prevent - // the nav and title bars from flashing in. setImmersiveUiVisibility(); } else { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, @@ -153,6 +157,9 @@ public class CordovaActivity extends AppCompatActivity { } cordovaInterface.onCordovaInit(appView.getPluginManager()); + // Setup the splash screen based on preference settings + cordovaInterface.pluginManager.postMessage("setupSplashScreen", splashScreen); + // Wire the hardware volume controls to control media if desired. String volumePref = preferences.getString("DefaultVolumeStream", ""); if ("media".equals(volumePref.toLowerCase(Locale.ENGLISH))) { @@ -526,5 +533,4 @@ public class CordovaActivity extends AppCompatActivity { } } - } diff --git a/framework/src/org/apache/cordova/SplashScreenPlugin.java b/framework/src/org/apache/cordova/SplashScreenPlugin.java new file mode 100644 index 00000000..79b2bc2a --- /dev/null +++ b/framework/src/org/apache/cordova/SplashScreenPlugin.java @@ -0,0 +1,168 @@ +/* + 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. +*/ + +package org.apache.cordova; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.SuppressLint; +import android.os.Handler; +import android.view.View; +import android.view.animation.AccelerateInterpolator; + +import androidx.annotation.NonNull; +import androidx.core.splashscreen.SplashScreen; +import androidx.core.splashscreen.SplashScreenViewProvider; + +import org.json.JSONArray; +import org.json.JSONException; + +@SuppressLint("LongLogTag") +public class SplashScreenPlugin extends CordovaPlugin { + static final String PLUGIN_NAME = "CordovaSplashScreenPlugin"; + + // Default config preference values + private static final boolean DEFAULT_AUTO_HIDE = true; + private static final int DEFAULT_DELAY_TIME = -1; + private static final boolean DEFAULT_FADE = true; + private static final int DEFAULT_FADE_TIME = 500; + + // Config preference values + /** + * @param boolean autoHide to auto splash screen (default=true) + */ + private boolean autoHide; + /** + * @param int delayTime in milliseconds (default=-1) + */ + private int delayTime; + /** + * @param int fade to fade out splash screen (default=true) + */ + private boolean isFadeEnabled; + /** + * @param int fadeDuration fade out duration in milliseconds (default=500) + */ + private int fadeDuration; + + // Internal variables + /** + * @param boolean keepOnScreen flag to determine if the splash screen remains visible. + */ + private boolean keepOnScreen = true; + + @Override + protected void pluginInitialize() { + // Auto Hide & Delay Settings + autoHide = preferences.getBoolean("AutoHideSplashScreen", DEFAULT_AUTO_HIDE); + delayTime = preferences.getInteger("SplashScreenDelay", DEFAULT_DELAY_TIME); + LOG.d(PLUGIN_NAME, "Auto Hide: " + autoHide); + if (delayTime != DEFAULT_DELAY_TIME) { + LOG.d(PLUGIN_NAME, "Delay: " + delayTime + "ms"); + } + + // Fade & Fade Duration + isFadeEnabled = preferences.getBoolean("FadeSplashScreen", DEFAULT_FADE); + fadeDuration = preferences.getInteger("FadeSplashScreenDuration", DEFAULT_FADE_TIME); + LOG.d(PLUGIN_NAME, "Fade: " + isFadeEnabled); + if (isFadeEnabled) { + LOG.d(PLUGIN_NAME, "Fade Duration: " + fadeDuration + "ms"); + } + } + + @Override + public boolean execute( + String action, + JSONArray args, + CallbackContext callbackContext + ) throws JSONException { + if (action.equals("hide") && autoHide == false) { + /* + * The `.hide()` method can only be triggered if the `splashScreenAutoHide` + * is set to `false`. + */ + keepOnScreen = false; + } else { + return false; + } + + callbackContext.success(); + return true; + } + + @Override + public Object onMessage(String id, Object data) { + switch (id) { + case "setupSplashScreen": + setupSplashScreen((SplashScreen) data); + break; + + case "onPageFinished": + attemptCloseOnPageFinished(); + break; + } + + return null; + } + + private void setupSplashScreen(SplashScreen splashScreen) { + // Setup Splash Screen Delay + splashScreen.setKeepOnScreenCondition(() -> keepOnScreen); + + // auto hide splash screen when custom delay is defined. + if (autoHide && delayTime != DEFAULT_DELAY_TIME) { + Handler splashScreenDelayHandler = new Handler(); + splashScreenDelayHandler.postDelayed(() -> keepOnScreen = false, delayTime); + } + + // auto hide splash screen with default delay (-1) delay is controlled by the + // `onPageFinished` message. + + // If auto hide is disabled (false), the hiding of the splash screen must be determined & + // triggered by the front-end code with the `navigator.splashscreen.hide()` method. + + // Setup the fade + splashScreen.setOnExitAnimationListener(new SplashScreen.OnExitAnimationListener() { + @Override + public void onSplashScreenExit(@NonNull SplashScreenViewProvider splashScreenViewProvider) { + View splashScreenView = splashScreenViewProvider.getView(); + + splashScreenView + .animate() + .alpha(0.0f) + .setDuration(isFadeEnabled ? fadeDuration : 0) + .setStartDelay(isFadeEnabled ? 0 : fadeDuration) + .setInterpolator(new AccelerateInterpolator()) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + splashScreenViewProvider.remove(); + } + }).start(); + } + }); + } + + private void attemptCloseOnPageFinished() { + if (autoHide && delayTime == DEFAULT_DELAY_TIME) { + keepOnScreen = false; + } + } +} diff --git a/lib/Api.js b/lib/Api.js index d0689a04..4f164695 100644 --- a/lib/Api.js +++ b/lib/Api.js @@ -73,6 +73,8 @@ class Api { configXml: path.join(appRes, 'xml', 'config.xml'), defaultConfigXml: path.join(this.root, 'cordova', 'defaults.xml'), strings: path.join(appRes, 'values', 'strings.xml'), + themes: path.join(appRes, 'values', 'themes.xml'), + colors: path.join(appRes, 'values', 'colors.xml'), manifest: path.join(appMain, 'AndroidManifest.xml'), build: path.join(this.root, 'build'), javaSrc: path.join(appMain, 'java') diff --git a/lib/prepare.js b/lib/prepare.js index 483880da..68ff7120 100644 --- a/lib/prepare.js +++ b/lib/prepare.js @@ -62,16 +62,14 @@ module.exports.prepare = function (cordovaProject, options) { updateUserProjectGradlePropertiesConfig(this, args); // Update own www dir with project's www assets and plugins' assets and js-files - return Promise.resolve(updateWww(cordovaProject, this.locations)).then(function () { - // update project according to config.xml changes. - return updateProjectAccordingTo(self._config, self.locations); - }).then(function () { - updateIcons(cordovaProject, path.relative(cordovaProject.root, self.locations.res)); - updateSplashes(cordovaProject, path.relative(cordovaProject.root, self.locations.res)); - updateFileResources(cordovaProject, path.relative(cordovaProject.root, self.locations.root)); - }).then(function () { - events.emit('verbose', 'Prepared android project successfully'); - }); + return Promise.resolve(updateWww(cordovaProject, this.locations)) + .then(() => updateProjectAccordingTo(self._config, self.locations)) + .then(function () { + updateIcons(cordovaProject, path.relative(cordovaProject.root, self.locations.res)); + updateFileResources(cordovaProject, path.relative(cordovaProject.root, self.locations.root)); + }).then(function () { + events.emit('verbose', 'Prepared android project successfully'); + }); }; /** @param {PlatformApi} project */ @@ -171,7 +169,6 @@ module.exports.clean = function (options) { return Promise.resolve().then(function () { cleanWww(projectRoot, self.locations); cleanIcons(projectRoot, projectConfig, path.relative(projectRoot, self.locations.res)); - cleanSplashes(projectRoot, projectConfig, path.relative(projectRoot, self.locations.res)); cleanFileResources(projectRoot, projectConfig, path.relative(projectRoot, self.locations.root)); }); }; @@ -267,19 +264,10 @@ function cleanWww (projectRoot, locations) { * @param {Object} locations A map of locations for this platform */ function updateProjectAccordingTo (platformConfig, locations) { - // Update app name by editing res/values/strings.xml - const strings = xmlHelpers.parseElementtreeSync(locations.strings); + updateProjectStrings(platformConfig, locations); + updateProjectSplashScreen(platformConfig, locations); const name = platformConfig.name(); - strings.find('string[@name="app_name"]').text = name.replace(/'/g, '\\\''); - - const shortName = platformConfig.shortName && platformConfig.shortName(); - if (shortName && shortName !== name) { - strings.find('string[@name="launcher_name"]').text = shortName.replace(/'/g, '\\\''); - } - - fs.writeFileSync(locations.strings, strings.write({ indent: 4 }), 'utf-8'); - events.emit('verbose', 'Wrote out android application name "' + name + '" to ' + locations.strings); // Update app name for gradle project fs.writeFileSync(path.join(locations.root, 'cdv-gradle-name.gradle'), @@ -342,6 +330,262 @@ function updateProjectAccordingTo (platformConfig, locations) { } } +/** + * Updates project structure and AndroidManifest according to project's configuration. + * + * @param {ConfigParser} platformConfig A project's configuration that will + * be used to update project + * @param {Object} locations A map of locations for this platform + */ +function updateProjectStrings (platformConfig, locations) { + // Update app name by editing res/values/strings.xml + const strings = xmlHelpers.parseElementtreeSync(locations.strings); + + const name = platformConfig.name(); + strings.find('string[@name="app_name"]').text = name.replace(/'/g, '\\\''); + + const shortName = platformConfig.shortName && platformConfig.shortName(); + if (shortName && shortName !== name) { + strings.find('string[@name="launcher_name"]').text = shortName.replace(/'/g, '\\\''); + } + + fs.writeFileSync(locations.strings, strings.write({ indent: 4 }), 'utf-8'); + events.emit('verbose', 'Wrote out android application name "' + name + '" to ' + locations.strings); +} + +/** + * @param {ConfigParser} platformConfig A project's configuration that will + * be used to update project + * @param {Object} locations A map of locations for this platform + */ +function updateProjectSplashScreen (platformConfig, locations) { + // res/values/themes.xml + const themes = xmlHelpers.parseElementtreeSync(locations.themes); + const splashScreenTheme = themes.find('style[@name="Theme.App.SplashScreen"]'); + + [ + 'windowSplashScreenAnimatedIcon', + 'windowSplashScreenAnimationDuration', + 'windowSplashScreenBackground', + 'windowSplashScreenBrandingImage', + 'windowSplashScreenIconBackgroundColor', + 'postSplashScreenTheme' + ].forEach(themeKey => { + const cdvConfigPrefKey = 'Android' + themeKey.charAt(0).toUpperCase() + themeKey.slice(1); + const cdvConfigPrefValue = platformConfig.getPreference(cdvConfigPrefKey, this.platform); + let themeTargetNode = splashScreenTheme.find(`item[@name="${themeKey}"]`); + + switch (themeKey) { + case 'windowSplashScreenBackground': + // use the user defined value for "colors.xml" + updateProjectSplashScreenBackgroundColor(cdvConfigPrefValue, locations); + + // force the themes value to `@color/cdv_splashscreen_background` + themeTargetNode.text = '@color/cdv_splashscreen_background'; + break; + + case 'windowSplashScreenAnimatedIcon': + // handle here the cases of "png" vs "xml" (drawable) + // If "png": + // - Clear out default or previous set "drawable/ic_cdv_splashscreen.xml" if exisiting. + // - Copy png in correct mipmap dir with name "ic_cdv_splashscreen.png" + // If "xml": + // - Clear out "{mipmap}/ic_cdv_splashscreen.png" if exisiting. + // - Copy xml into drawable dir with name "ic_cdv_splashscreen.xml" + + // updateProjectSplashScreenIcon() + // value should change depending on case: + // If "png": "@mipmap/ic_cdv_splashscreen" + // If "xml": "@drawable/ic_cdv_splashscreen" + updateProjectSplashScreenImage(locations, themeKey, cdvConfigPrefKey, cdvConfigPrefValue); + break; + + case 'windowSplashScreenBrandingImage': + // display warning only when set. + if (cdvConfigPrefValue) { + events.emit('warn', `"${themeKey}" is currently not supported by the splash screen compatibility library. https://issuetracker.google.com/issues/194301890`); + } + + updateProjectSplashScreenImage(locations, themeKey, cdvConfigPrefKey, cdvConfigPrefValue); + + // force the themes value to `@color/cdv_splashscreen_icon_background` + if (!cdvConfigPrefValue && themeTargetNode) { + splashScreenTheme.remove(themeTargetNode); + } else if (cdvConfigPrefValue) { + // if there is no current node, create a new node. + if (!themeTargetNode) { + themeTargetNode = themes.getroot().makeelement('item', { name: themeKey }); + splashScreenTheme.append(themeTargetNode); + } + + // set the user defined color. + themeTargetNode.text = '@drawable/ic_cdv_splashscreen_branding'; + } + break; + + case 'windowSplashScreenIconBackgroundColor': + // use the user defined value for "colors.xml" + updateProjectSplashScreenIconBackgroundColor(cdvConfigPrefValue, locations); + + // force the themes value to `@color/cdv_splashscreen_icon_background` + if (!cdvConfigPrefValue && themeTargetNode) { + // currentItem.remove(); + splashScreenTheme.remove(themeTargetNode); + } else if (cdvConfigPrefValue) { + // if there is no current color, create a new node. + if (!themeTargetNode) { + themeTargetNode = themes.getroot().makeelement('item', { name: themeKey }); + splashScreenTheme.append(themeTargetNode); + } + + // set the user defined color. + themeTargetNode.text = '@color/cdv_splashscreen_icon_background'; + } + break; + + case 'windowSplashScreenAnimationDuration': + themeTargetNode.text = cdvConfigPrefValue || '200'; + break; + + case 'postSplashScreenTheme': + themeTargetNode.text = cdvConfigPrefValue || '@style/Theme.AppCompat.NoActionBar'; + break; + + default: + events.emit('warn', `The theme property "${themeKey}" does not exist`); + } + }); + + fs.writeFileSync(locations.themes, themes.write({ indent: 4 }), 'utf-8'); + events.emit('verbose', 'Wrote out Android application themes to ' + locations.themes); +} + +/** + * @param {String} splashBackgroundColor SplashScreen Background Color Hex Code + * be used to update project + * @param {Object} locations A map of locations for this platform + */ +function updateProjectSplashScreenBackgroundColor (splashBackgroundColor, locations) { + if (!splashBackgroundColor) { splashBackgroundColor = '#FFFFFF'; } + + // res/values/colors.xml + const colors = xmlHelpers.parseElementtreeSync(locations.colors); + colors.find('color[@name="cdv_splashscreen_background"]').text = splashBackgroundColor.replace(/'/g, '\\\''); + + fs.writeFileSync(locations.colors, colors.write({ indent: 4 }), 'utf-8'); + events.emit('verbose', 'Wrote out Android application SplashScreen Color to ' + locations.colors); +} + +/** + * @param {String} splashIconBackgroundColor SplashScreen Icon Background Color Hex Code + * be used to update project + * @param {Object} locations A map of locations for this platform + */ +function updateProjectSplashScreenIconBackgroundColor (splashIconBackgroundColor, locations) { + // res/values/colors.xml + const colors = xmlHelpers.parseElementtreeSync(locations.colors); + // node name + const name = 'cdv_splashscreen_icon_background'; + + // get the current defined color + let currentColor = colors.find(`color[@name="${name}"]`); + + if (!splashIconBackgroundColor && currentColor) { + colors.getroot().remove(currentColor); + } else if (splashIconBackgroundColor) { + // if there is no current color, create a new node. + if (!currentColor) { + currentColor = colors.getroot().makeelement('color', { name }); + colors.getroot().append(currentColor); + } + + // set the user defined color. + currentColor.text = splashIconBackgroundColor.replace(/'/g, '\\\''); + } + + // write out the changes. + fs.writeFileSync(locations.colors, colors.write({ indent: 4 }), 'utf-8'); + events.emit('verbose', 'Wrote out Android application SplashScreen Icon Color to ' + locations.colors); +} + +function cleanupAndSetProjectSplashScreenImage (srcFile, destFilePath, possiblePreviousDestFilePath, cleanupOnly = false) { + if (fs.existsSync(possiblePreviousDestFilePath)) { + fs.removeSync(possiblePreviousDestFilePath); + } + + if (cleanupOnly && fs.existsSync(destFilePath)) { + // Also remove dest file path for cleanup even if previous was not use. + fs.removeSync(destFilePath); + } + + if (!cleanupOnly && srcFile && fs.existsSync(srcFile)) { + fs.copySync(srcFile, destFilePath); + } +} + +function updateProjectSplashScreenImage (locations, themeKey, cdvConfigPrefKey, cdvConfigPrefValue = '') { + const SPLASH_SCREEN_IMAGE_BY_THEME_KEY = { + windowSplashScreenAnimatedIcon: 'ic_cdv_splashscreen', + windowSplashScreenBrandingImage: 'ic_cdv_splashscreen_branding' + }; + + const destFileName = SPLASH_SCREEN_IMAGE_BY_THEME_KEY[themeKey] || null; + if (!destFileName) throw new CordovaError(`${themeKey} is not valid for image detection.`); + + // Default paths of where images are saved + const destPngDir = path.join(locations.res, 'drawable-nodpi'); + const destXmlDir = path.join(locations.res, 'drawable'); + + // Dest File Name and Path + const destFileNameExt = destFileName + '.xml'; + let destFilePath = path.join(destXmlDir, destFileNameExt); + let possiblePreviousDestFilePath = path.join(destPngDir, destFileName + '.png'); + + // Default Drawable Source File + const defaultSrcFilePath = themeKey !== 'windowSplashScreenBrandingImage' + ? require.resolve('cordova-android/templates/project/res/drawable/' + destFileNameExt) + : null; + + if (!cdvConfigPrefValue || !fs.existsSync(cdvConfigPrefValue)) { + let emitType = 'verbose'; + let emmitMessage = `The "${cdvConfigPrefKey}" is undefined. Cordova's default will be used.`; + + if (cdvConfigPrefValue && !fs.existsSync(cdvConfigPrefValue)) { + emitType = 'warn'; + emmitMessage = `The "${cdvConfigPrefKey}" value does not exist. Cordova's default will be used.`; + } + + events.emit(emitType, emmitMessage); + const cleanupOnly = themeKey === 'windowSplashScreenBrandingImage'; + cleanupAndSetProjectSplashScreenImage(defaultSrcFilePath, destFilePath, possiblePreviousDestFilePath, cleanupOnly); + return; + } + + const iconExtension = path.extname(cdvConfigPrefValue).toLowerCase(); + + if (iconExtension === '.png') { + // Put the image at this location. + destFilePath = path.join(destPngDir, destFileName + '.png'); + + // Check for this file and remove. + possiblePreviousDestFilePath = path.join(destXmlDir, destFileName + '.xml'); + + // copy the png to correct mipmap folder with name of ic_cdv_splashscreen.png + // delete ic_cdv_splashscreen.xml from drawable folder + // update themes.xml windowSplashScreenAnimatedIcon value to @mipmap/ic_cdv_splashscreen + cleanupAndSetProjectSplashScreenImage(cdvConfigPrefValue, destFilePath, possiblePreviousDestFilePath); + } else if (iconExtension === '.xml') { + // copy the xml to drawable folder with name of ic_cdv_splashscreen.xml + // delete ic_cdv_splashscreen.png from mipmap folder + // update themes.xml windowSplashScreenAnimatedIcon value to @drawable/ic_cdv_splashscreen + cleanupAndSetProjectSplashScreenImage(cdvConfigPrefValue, destFilePath, possiblePreviousDestFilePath); + } else { + // use the default destFilePath & possiblePreviousDestFilePath, no update require. + events.emit('warn', `The "${cdvConfigPrefKey}" had an unsupported extension. Cordova's default will be used.`); + cleanupAndSetProjectSplashScreenImage(defaultSrcFilePath, destFilePath, possiblePreviousDestFilePath); + } +} + // Consturct the default value for versionCode as // PATCH + MINOR * 100 + MAJOR * 10000 // see http://developer.android.com/tools/publishing/versioning.html @@ -380,68 +624,6 @@ function getAdaptiveImageResourcePath (resourcesDir, type, density, name, source return resourcePath; } -function makeSplashCleanupMap (projectRoot, resourcesDir) { - // Build an initial resource map that deletes all existing splash screens - const existingSplashPaths = glob.sync( - `${resourcesDir.replace(/\\/g, '/')}/drawable-*/screen.{png,9.png,webp,jpg,jpeg}`, - { cwd: projectRoot } - ); - return makeCleanResourceMap(existingSplashPaths); -} - -function updateSplashes (cordovaProject, platformResourcesDir) { - const resources = cordovaProject.projectConfig.getSplashScreens('android'); - - // if there are no "splash" elements in config.xml - if (resources.length === 0) { - events.emit('verbose', 'This app does not have splash screens defined'); - // We must not return here! - // If the user defines no splash screens, the cleanup map will cause any - // existing splash screen images (e.g. the defaults that we copy into a - // new app) to be removed from the app folder, which is what we want. - } - - // Build an initial resource map that deletes all existing splash screens - const resourceMap = makeSplashCleanupMap(cordovaProject.root, platformResourcesDir); - - let hadMdpi = false; - resources.forEach(function (resource) { - if (!resource.density) { - return; - } - if (resource.density === 'mdpi') { - hadMdpi = true; - } - const targetPath = getImageResourcePath( - platformResourcesDir, 'drawable', resource.density, 'screen', path.basename(resource.src)); - resourceMap[targetPath] = resource.src; - }); - - // There's no "default" drawable, so assume default == mdpi. - if (!hadMdpi && resources.defaultResource) { - const targetPath = getImageResourcePath( - platformResourcesDir, 'drawable', 'mdpi', 'screen', path.basename(resources.defaultResource.src)); - resourceMap[targetPath] = resources.defaultResource.src; - } - - events.emit('verbose', 'Updating splash screens at ' + platformResourcesDir); - FileUpdater.updatePaths( - resourceMap, { rootDir: cordovaProject.root }, logFileOp); -} - -function cleanSplashes (projectRoot, projectConfig, platformResourcesDir) { - const resources = projectConfig.getSplashScreens('android'); - if (resources.length > 0) { - const resourceMap = makeSplashCleanupMap(projectRoot, platformResourcesDir); - - events.emit('verbose', 'Cleaning splash screens 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); - } -} - function updateIcons (cordovaProject, platformResourcesDir) { const icons = cordovaProject.projectConfig.getIcons('android'); @@ -521,7 +703,7 @@ function updateIconResourceForAdaptive (preparedIcons, resourceMap, platformReso const android_icons = preparedIcons.android_icons; const default_icon = preparedIcons.default_icon; - // The source paths for icons and splashes are relative to + // The source paths for icons are relative to // project's config.xml location, so we use it as base path. let background; let foreground; @@ -620,7 +802,7 @@ function updateIconResourceForLegacy (preparedIcons, resourceMap, platformResour const android_icons = preparedIcons.android_icons; const default_icon = preparedIcons.default_icon; - // The source paths for icons and splashes are relative to + // The source paths for icons are relative to // project's config.xml location, so we use it as base path. for (const density in android_icons) { const targetPath = getImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher', path.basename(android_icons[density].src)); @@ -750,16 +932,6 @@ function mapImageResources (rootDir, subDir, type, resourceName) { return pathMap; } -/** Returns resource map that deletes all given paths */ -function makeCleanResourceMap (resourcePaths) { - const pathMap = {}; - resourcePaths.map(path.normalize) - .forEach(resourcePath => { - pathMap[resourcePath] = null; - }); - return pathMap; -} - function updateFileResources (cordovaProject, platformDir) { const files = cordovaProject.projectConfig.getFileResources('android'); diff --git a/spec/unit/prepare.spec.js b/spec/unit/prepare.spec.js index 763a0ae9..8dec31eb 100644 --- a/spec/unit/prepare.spec.js +++ b/spec/unit/prepare.spec.js @@ -83,18 +83,6 @@ function mockGetIconItem (data) { }, data); } -/** - * Create a mock item from the getSplashScreen collection with the supplied updated data. - * - * @param {Object} data Changes to apply to the mock getSplashScreen item - */ -function mockGetSplashScreenItem (data) { - return Object.assign({}, { - src: undefined, - density: undefined - }, data); -} - describe('prepare', () => { // Rewire let prepare; @@ -779,7 +767,6 @@ describe('prepare', () => { prepare.__set__('updateProjectAccordingTo', jasmine.createSpy('updateProjectAccordingTo') .and.returnValue(Promise.resolve())); prepare.__set__('updateIcons', jasmine.createSpy('updateIcons').and.returnValue(Promise.resolve())); - prepare.__set__('updateSplashes', jasmine.createSpy('updateSplashes').and.returnValue(Promise.resolve())); prepare.__set__('updateFileResources', jasmine.createSpy('updateFileResources').and.returnValue(Promise.resolve())); prepare.__set__('updateConfigFilesFrom', jasmine.createSpy('updateConfigFilesFrom') @@ -871,7 +858,7 @@ describe('prepare', () => { prepare.__set__('updateWww', jasmine.createSpy('updateWww')); prepare.__set__('updateIcons', jasmine.createSpy('updateIcons').and.returnValue(Promise.resolve())); - prepare.__set__('updateSplashes', jasmine.createSpy('updateSplashes').and.returnValue(Promise.resolve())); + prepare.__set__('updateProjectSplashScreen', jasmine.createSpy('updateProjectSplashScreen')); prepare.__set__('updateFileResources', jasmine.createSpy('updateFileResources').and.returnValue(Promise.resolve())); prepare.__set__('updateConfigFilesFrom', jasmine.createSpy('updateConfigFilesFrom') @@ -960,110 +947,4 @@ describe('prepare', () => { }); }); }); - - describe('updateSplashes method', function () { - // Mock Data - let cordovaProject; - let platformResourcesDir; - - beforeEach(function () { - cordovaProject = { - root: '/mock', - projectConfig: { - path: '/mock/config.xml', - cdvNamespacePrefix: 'cdv' - }, - locations: { - plugins: '/mock/plugins', - www: '/mock/www' - } - }; - platformResourcesDir = PATH_RESOURCE; - - // mocking initial responses for mapImageResources - prepare.__set__('makeSplashCleanupMap', (rootDir, resourcesDir) => ({ - [path.join(resourcesDir, 'drawable-mdpi/screen.png')]: null, - [path.join(resourcesDir, 'drawable-mdpi/screen.webp')]: null - })); - }); - - it('Test#001 : Should detect no defined splash screens.', function () { - const updateSplashes = prepare.__get__('updateSplashes'); - - // mock data. - cordovaProject.projectConfig.getSplashScreens = function (platform) { - return []; - }; - - updateSplashes(cordovaProject, platformResourcesDir); - - // The emit was called - expect(emitSpy).toHaveBeenCalled(); - - // The emit message was. - const actual = emitSpy.calls.argsFor(0)[1]; - const expected = 'This app does not have splash screens defined'; - expect(actual).toEqual(expected); - }); - - it('Test#02 : Should update splash png icon.', function () { - const updateSplashes = prepare.__get__('updateSplashes'); - - // mock data. - cordovaProject.projectConfig.getSplashScreens = function (platform) { - return [mockGetSplashScreenItem({ - density: 'mdpi', - src: 'res/splash/android/mdpi-screen.png' - })]; - }; - - updateSplashes(cordovaProject, platformResourcesDir); - - // The emit was called - expect(emitSpy).toHaveBeenCalled(); - - // The emit message was. - const actual = emitSpy.calls.argsFor(0)[1]; - const expected = 'Updating splash screens at ' + PATH_RESOURCE; - expect(actual).toEqual(expected); - - const actualResourceMap = updatePathsSpy.calls.argsFor(0)[0]; - const expectedResourceMap = {}; - expectedResourceMap[path.join(PATH_RESOURCE, 'drawable-mdpi', 'screen.png')] = 'res/splash/android/mdpi-screen.png'; - expectedResourceMap[path.join(PATH_RESOURCE, 'drawable-mdpi', 'screen.webp')] = null; - - expect(actualResourceMap).toEqual(expectedResourceMap); - }); - - it('Test#03 : Should update splash webp icon.', function () { - const updateSplashes = prepare.__get__('updateSplashes'); - - // mock data. - cordovaProject.projectConfig.getSplashScreens = function (platform) { - return [mockGetSplashScreenItem({ - density: 'mdpi', - src: 'res/splash/android/mdpi-screen.webp' - })]; - }; - - // Creating Spies - updateSplashes(cordovaProject, platformResourcesDir); - - // The emit was called - expect(emitSpy).toHaveBeenCalled(); - - // The emit message was. - const actual = emitSpy.calls.argsFor(0)[1]; - const expected = 'Updating splash screens at ' + PATH_RESOURCE; - expect(actual).toEqual(expected); - - const actualResourceMap = updatePathsSpy.calls.argsFor(0)[0]; - - const expectedResourceMap = {}; - expectedResourceMap[path.join(PATH_RESOURCE, 'drawable-mdpi', 'screen.webp')] = 'res/splash/android/mdpi-screen.webp'; - expectedResourceMap[path.join(PATH_RESOURCE, 'drawable-mdpi', 'screen.png')] = null; - - expect(actualResourceMap).toEqual(expectedResourceMap); - }); - }); }); diff --git a/templates/project/Activity.java b/templates/project/Activity.java index 567b6c72..5591d605 100644 --- a/templates/project/Activity.java +++ b/templates/project/Activity.java @@ -20,6 +20,7 @@ package __ID__; import android.os.Bundle; + import org.apache.cordova.*; public class __ACTIVITY__ extends CordovaActivity diff --git a/templates/project/AndroidManifest.xml b/templates/project/AndroidManifest.xml index 5edc2af6..bf4a9a4d 100644 --- a/templates/project/AndroidManifest.xml +++ b/templates/project/AndroidManifest.xml @@ -35,7 +35,7 @@ diff --git a/templates/project/app/build.gradle b/templates/project/app/build.gradle index cc47c1c5..35d5b9f1 100644 --- a/templates/project/app/build.gradle +++ b/templates/project/app/build.gradle @@ -287,6 +287,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: '*.jar') implementation "androidx.appcompat:appcompat:${cordovaConfig.ANDROIDX_APP_COMPAT_VERSION}" + implementation "androidx.core:core-splashscreen:${cordovaConfig.ANDROIDX_CORE_SPLASHSCREEN_VERSION}" if (cordovaConfig.IS_GRADLE_PLUGIN_KOTLIN_ENABLED) { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${cordovaConfig.KOTLIN_VERSION}" diff --git a/templates/project/res/drawable-land-hdpi/screen.png b/templates/project/res/drawable-land-hdpi/screen.png deleted file mode 100644 index 16008639..00000000 Binary files a/templates/project/res/drawable-land-hdpi/screen.png and /dev/null differ diff --git a/templates/project/res/drawable-land-ldpi/screen.png b/templates/project/res/drawable-land-ldpi/screen.png deleted file mode 100644 index f7ad3cfa..00000000 Binary files a/templates/project/res/drawable-land-ldpi/screen.png and /dev/null differ diff --git a/templates/project/res/drawable-land-mdpi/screen.png b/templates/project/res/drawable-land-mdpi/screen.png deleted file mode 100644 index b6243130..00000000 Binary files a/templates/project/res/drawable-land-mdpi/screen.png and /dev/null differ diff --git a/templates/project/res/drawable-land-xhdpi/screen.png b/templates/project/res/drawable-land-xhdpi/screen.png deleted file mode 100644 index 9720f415..00000000 Binary files a/templates/project/res/drawable-land-xhdpi/screen.png and /dev/null differ diff --git a/templates/project/res/drawable-land-xxhdpi/screen.png b/templates/project/res/drawable-land-xxhdpi/screen.png deleted file mode 100644 index 80d6b470..00000000 Binary files a/templates/project/res/drawable-land-xxhdpi/screen.png and /dev/null differ diff --git a/templates/project/res/drawable-land-xxxhdpi/screen.png b/templates/project/res/drawable-land-xxxhdpi/screen.png deleted file mode 100644 index 84c5a2ba..00000000 Binary files a/templates/project/res/drawable-land-xxxhdpi/screen.png and /dev/null differ diff --git a/templates/project/res/drawable-port-hdpi/screen.png b/templates/project/res/drawable-port-hdpi/screen.png deleted file mode 100644 index b2f60af6..00000000 Binary files a/templates/project/res/drawable-port-hdpi/screen.png and /dev/null differ diff --git a/templates/project/res/drawable-port-ldpi/screen.png b/templates/project/res/drawable-port-ldpi/screen.png deleted file mode 100644 index 4b2abbb1..00000000 Binary files a/templates/project/res/drawable-port-ldpi/screen.png and /dev/null differ diff --git a/templates/project/res/drawable-port-mdpi/screen.png b/templates/project/res/drawable-port-mdpi/screen.png deleted file mode 100644 index 1f1ae9d4..00000000 Binary files a/templates/project/res/drawable-port-mdpi/screen.png and /dev/null differ diff --git a/templates/project/res/drawable-port-xhdpi/screen.png b/templates/project/res/drawable-port-xhdpi/screen.png deleted file mode 100644 index 690c57eb..00000000 Binary files a/templates/project/res/drawable-port-xhdpi/screen.png and /dev/null differ diff --git a/templates/project/res/drawable-port-xxhdpi/screen.png b/templates/project/res/drawable-port-xxhdpi/screen.png deleted file mode 100644 index 214f0a01..00000000 Binary files a/templates/project/res/drawable-port-xxhdpi/screen.png and /dev/null differ diff --git a/templates/project/res/drawable-port-xxxhdpi/screen.png b/templates/project/res/drawable-port-xxxhdpi/screen.png deleted file mode 100644 index c4ef7723..00000000 Binary files a/templates/project/res/drawable-port-xxxhdpi/screen.png and /dev/null differ diff --git a/templates/project/res/drawable/ic_cdv_splashscreen.xml b/templates/project/res/drawable/ic_cdv_splashscreen.xml new file mode 100644 index 00000000..0033ec52 --- /dev/null +++ b/templates/project/res/drawable/ic_cdv_splashscreen.xml @@ -0,0 +1,34 @@ + + + + + + diff --git a/templates/project/res/values/colors.xml b/templates/project/res/values/colors.xml new file mode 100644 index 00000000..a207b621 --- /dev/null +++ b/templates/project/res/values/colors.xml @@ -0,0 +1,22 @@ + + + + #FFFFFFFF + diff --git a/templates/project/res/values/strings.xml b/templates/project/res/values/strings.xml index bb049db3..9216964f 100644 --- a/templates/project/res/values/strings.xml +++ b/templates/project/res/values/strings.xml @@ -1,4 +1,22 @@ + __NAME__ diff --git a/templates/project/res/values/themes.xml b/templates/project/res/values/themes.xml new file mode 100644 index 00000000..9190bbb5 --- /dev/null +++ b/templates/project/res/values/themes.xml @@ -0,0 +1,34 @@ + + + + + diff --git a/templates/project/res/xml/config.xml b/templates/project/res/xml/config.xml index 79e01a0b..0e76a732 100644 --- a/templates/project/res/xml/config.xml +++ b/templates/project/res/xml/config.xml @@ -49,7 +49,6 @@