2015-10-21 07:15:57 +08:00
/ * *
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 .
* /
2020-01-29 09:12:55 +08:00
var fs = require ( 'fs-extra' ) ;
2015-10-21 07:15:57 +08:00
var path = require ( 'path' ) ;
2020-04-16 20:39:22 +08:00
const nopt = require ( 'nopt' ) ;
2015-10-21 07:15:57 +08:00
var events = require ( 'cordova-common' ) . events ;
var AndroidManifest = require ( './AndroidManifest' ) ;
2017-07-28 19:05:53 +08:00
var checkReqs = require ( './check_reqs' ) ;
2015-10-21 07:15:57 +08:00
var xmlHelpers = require ( 'cordova-common' ) . xmlHelpers ;
var CordovaError = require ( 'cordova-common' ) . CordovaError ;
var ConfigParser = require ( 'cordova-common' ) . ConfigParser ;
2016-04-20 04:28:13 +08:00
var FileUpdater = require ( 'cordova-common' ) . FileUpdater ;
2016-04-22 21:03:02 +08:00
var PlatformJson = require ( 'cordova-common' ) . PlatformJson ;
var PlatformMunger = require ( 'cordova-common' ) . ConfigChanges . PlatformMunger ;
var PluginInfoProvider = require ( 'cordova-common' ) . PluginInfoProvider ;
2020-01-31 21:02:48 +08:00
const utils = require ( './utils' ) ;
2015-10-21 07:15:57 +08:00
2018-07-11 11:14:04 +08:00
const GradlePropertiesParser = require ( './config/GradlePropertiesParser' ) ;
2020-04-16 20:39:22 +08:00
function parseArguments ( argv ) {
return nopt ( {
// `jvmargs` is a valid option however, we don't actually want to parse it because we want the entire string as is.
// jvmargs: String
} , { } , argv || [ ] , 0 ) ;
}
2016-04-20 04:28:13 +08:00
module . exports . prepare = function ( cordovaProject , options ) {
2015-10-21 07:15:57 +08:00
var self = this ;
2020-04-16 20:39:22 +08:00
let args = { } ;
if ( options && options . options ) {
args = parseArguments ( options . options . argv ) ;
}
2016-04-22 21:03:02 +08:00
var platformJson = PlatformJson . load ( this . locations . root , this . platform ) ;
var munger = new PlatformMunger ( this . platform , this . locations . root , platformJson , new PluginInfoProvider ( ) ) ;
this . _config = updateConfigFilesFrom ( cordovaProject . projectConfig , munger , this . locations ) ;
2015-10-21 07:15:57 +08:00
2019-02-13 09:11:32 +08:00
// Get the min SDK version from config.xml
const minSdkVersion = this . _config . getPreference ( 'android-minSdkVersion' , 'android' ) ;
2019-04-06 12:28:25 +08:00
const maxSdkVersion = this . _config . getPreference ( 'android-maxSdkVersion' , 'android' ) ;
const targetSdkVersion = this . _config . getPreference ( 'android-targetSdkVersion' , 'android' ) ;
2020-01-27 14:52:08 +08:00
const androidXEnabled = this . _config . getPreference ( 'AndroidXEnabled' , 'android' ) ;
2020-01-27 15:14:58 +08:00
const isGradlePluginKotlinEnabled = this . _config . getPreference ( 'GradlePluginKotlinEnabled' , 'android' ) ;
const gradlePluginKotlinCodeStyle = this . _config . getPreference ( 'GradlePluginKotlinCodeStyle' , 'android' ) ;
2019-02-13 09:11:32 +08:00
2020-01-31 21:02:48 +08:00
const gradlePropertiesUserConfig = { } ;
2019-02-13 09:11:32 +08:00
if ( minSdkVersion ) gradlePropertiesUserConfig . cdvMinSdkVersion = minSdkVersion ;
2019-04-06 12:28:25 +08:00
if ( maxSdkVersion ) gradlePropertiesUserConfig . cdvMaxSdkVersion = maxSdkVersion ;
if ( targetSdkVersion ) gradlePropertiesUserConfig . cdvTargetSdkVersion = targetSdkVersion ;
2020-04-16 20:39:22 +08:00
if ( args . jvmargs ) gradlePropertiesUserConfig [ 'org.gradle.jvmargs' ] = args . jvmargs ;
2020-01-27 15:14:58 +08:00
if ( isGradlePluginKotlinEnabled ) {
gradlePropertiesUserConfig [ 'kotlin.code.style' ] = gradlePluginKotlinCodeStyle || 'official' ;
}
2019-02-13 09:11:32 +08:00
2020-01-27 14:52:08 +08:00
// Both 'useAndroidX' and 'enableJetifier' are linked together.
if ( androidXEnabled ) {
gradlePropertiesUserConfig [ 'android.useAndroidX' ] = androidXEnabled ;
gradlePropertiesUserConfig [ 'android.enableJetifier' ] = androidXEnabled ;
}
2020-01-31 21:02:48 +08:00
const gradlePropertiesParser = new GradlePropertiesParser ( this . locations . root ) ;
2019-02-13 09:11:32 +08:00
gradlePropertiesParser . configure ( gradlePropertiesUserConfig ) ;
2018-07-11 11:14:04 +08:00
2015-10-21 07:15:57 +08:00
// Update own www dir with project's www assets and plugins' assets and js-files
2020-01-07 20:22:59 +08:00
return Promise . resolve ( updateWww ( cordovaProject , this . locations ) ) . then ( function ( ) {
2015-10-21 07:15:57 +08:00
// update project according to config.xml changes.
return updateProjectAccordingTo ( self . _config , self . locations ) ;
2017-06-14 02:42:20 +08:00
} ) . then ( function ( ) {
2016-10-29 04:05:01 +08:00
updateIcons ( cordovaProject , path . relative ( cordovaProject . root , self . locations . res ) ) ;
updateSplashes ( cordovaProject , path . relative ( cordovaProject . root , self . locations . res ) ) ;
2016-07-29 03:44:50 +08:00
updateFileResources ( cordovaProject , path . relative ( cordovaProject . root , self . locations . root ) ) ;
2017-06-14 02:42:20 +08:00
} ) . then ( function ( ) {
2016-05-04 07:25:48 +08:00
events . emit ( 'verbose' , 'Prepared android project successfully' ) ;
2015-10-21 07:15:57 +08:00
} ) ;
} ;
2016-04-20 04:28:13 +08:00
module . exports . clean = function ( options ) {
// A cordovaProject isn't passed into the clean() function, because it might have
// been called from the platform shell script rather than the CLI. Check for the
// noPrepare option passed in by the non-CLI clean script. If that's present, or if
// there's no config.xml found at the project root, then don't clean prepared files.
var projectRoot = path . resolve ( this . root , '../..' ) ;
2016-08-29 22:26:49 +08:00
if ( ( options && options . noPrepare ) || ! fs . existsSync ( this . locations . configXml ) ||
2016-05-27 06:46:38 +08:00
! fs . existsSync ( this . locations . configXml ) ) {
2020-01-07 20:22:59 +08:00
return Promise . resolve ( ) ;
2016-04-20 04:28:13 +08:00
}
2016-05-27 06:46:38 +08:00
var projectConfig = new ConfigParser ( this . locations . configXml ) ;
2016-04-20 04:28:13 +08:00
var self = this ;
2020-01-07 20:22:59 +08:00
return Promise . resolve ( ) . then ( function ( ) {
2016-04-20 04:28:13 +08:00
cleanWww ( projectRoot , self . locations ) ;
2016-10-29 04:05:01 +08:00
cleanIcons ( projectRoot , projectConfig , path . relative ( projectRoot , self . locations . res ) ) ;
cleanSplashes ( projectRoot , projectConfig , path . relative ( projectRoot , self . locations . res ) ) ;
2016-07-29 03:44:50 +08:00
cleanFileResources ( projectRoot , projectConfig , path . relative ( projectRoot , self . locations . root ) ) ;
2016-04-20 04:28:13 +08:00
} ) ;
} ;
2015-10-21 07:15:57 +08:00
/ * *
* Updates config files in project based on app ' s config . xml and config munge ,
* generated by plugins .
*
* @ param { ConfigParser } sourceConfig A project ' s configuration that will
* be merged into platform ' s config . xml
* @ param { ConfigChanges } configMunger An initialized ConfigChanges instance
* for this platform .
* @ param { Object } locations A map of locations for this platform
*
* @ return { ConfigParser } An instance of ConfigParser , that
* represents current project ' s configuration . When returned , the
* configuration is already dumped to appropriate config . xml file .
* /
2017-06-14 02:42:20 +08:00
function updateConfigFilesFrom ( sourceConfig , configMunger , locations ) {
2016-05-04 07:25:48 +08:00
events . emit ( 'verbose' , 'Generating platform-specific config.xml from defaults for android at ' + locations . configXml ) ;
2015-10-21 07:15:57 +08:00
// First cleanup current config and merge project's one into own
// Overwrite platform config.xml with defaults.xml.
2020-01-29 09:12:55 +08:00
fs . copySync ( locations . defaultConfigXml , locations . configXml ) ;
2015-10-21 07:15:57 +08:00
// Then apply config changes from global munge to all config files
// in project (including project's config)
configMunger . reapply _global _munge ( ) . save _all ( ) ;
2016-05-04 07:25:48 +08:00
events . emit ( 'verbose' , 'Merging project\'s config.xml into platform-specific android config.xml' ) ;
2015-10-21 07:15:57 +08:00
// Merge changes from app's config.xml into platform's one
var config = new ConfigParser ( locations . configXml ) ;
xmlHelpers . mergeXml ( sourceConfig . doc . getroot ( ) ,
2017-06-14 02:42:20 +08:00
config . doc . getroot ( ) , 'android' , /* clobber= */ true ) ;
2015-10-21 07:15:57 +08:00
config . write ( ) ;
return config ;
}
2016-04-20 04:28:13 +08:00
/ * *
* Logs all file operations via the verbose event stream , indented .
* /
2017-06-14 02:42:20 +08:00
function logFileOp ( message ) {
2016-04-20 04:28:13 +08:00
events . emit ( 'verbose' , ' ' + message ) ;
}
2015-10-21 07:15:57 +08:00
/ * *
* Updates platform 'www' directory by replacing it with contents of
* 'platform_www' and app www . Also copies project 's overrides' folder into
* the platform 'www' folder
*
* @ param { Object } cordovaProject An object which describes cordova project .
* @ param { Object } destinations An object that contains destination
* paths for www files .
* /
2017-06-14 02:42:20 +08:00
function updateWww ( cordovaProject , destinations ) {
2016-04-20 04:28:13 +08:00
var sourceDirs = [
path . relative ( cordovaProject . root , cordovaProject . locations . www ) ,
path . relative ( cordovaProject . root , destinations . platformWww )
] ;
2015-10-21 07:15:57 +08:00
// If project contains 'merges' for our platform, use them as another overrides
var merges _path = path . join ( cordovaProject . root , 'merges' , 'android' ) ;
if ( fs . existsSync ( merges _path ) ) {
2016-05-04 07:25:48 +08:00
events . emit ( 'verbose' , 'Found "merges/android" folder. Copying its contents into the android project.' ) ;
2016-04-20 04:28:13 +08:00
sourceDirs . push ( path . join ( 'merges' , 'android' ) ) ;
2015-10-21 07:15:57 +08:00
}
2016-04-20 04:28:13 +08:00
var targetDir = path . relative ( cordovaProject . root , destinations . www ) ;
events . emit (
'verbose' , 'Merging and updating files from [' + sourceDirs . join ( ', ' ) + '] to ' + targetDir ) ;
FileUpdater . mergeAndUpdateDir (
sourceDirs , targetDir , { rootDir : cordovaProject . root } , logFileOp ) ;
}
/ * *
* Cleans all files from the platform 'www' directory .
* /
2017-06-14 02:42:20 +08:00
function cleanWww ( projectRoot , locations ) {
2016-04-20 04:28:13 +08:00
var targetDir = path . relative ( projectRoot , locations . www ) ;
events . emit ( 'verbose' , 'Cleaning ' + targetDir ) ;
// No source paths are specified, so mergeAndUpdateDir() will clear the target directory.
FileUpdater . mergeAndUpdateDir (
[ ] , targetDir , { rootDir : projectRoot , all : true } , logFileOp ) ;
2015-10-21 07:15:57 +08:00
}
/ * *
* 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
* /
2017-06-14 02:42:20 +08:00
function updateProjectAccordingTo ( platformConfig , locations ) {
2015-10-21 07:15:57 +08:00
// Update app name by editing res/values/strings.xml
var strings = xmlHelpers . parseElementtreeSync ( locations . strings ) ;
2016-06-08 04:02:18 +08:00
var name = platformConfig . name ( ) ;
2019-10-22 00:26:17 +08:00
strings . find ( 'string[@name="app_name"]' ) . text = name . replace ( /'/g , '\\\'' ) ;
2016-06-08 04:02:18 +08:00
var shortName = platformConfig . shortName && platformConfig . shortName ( ) ;
2017-07-19 05:13:53 +08:00
if ( shortName && shortName !== name ) {
2019-10-22 00:26:17 +08:00
strings . find ( 'string[@name="launcher_name"]' ) . text = shortName . replace ( /'/g , '\\\'' ) ;
2016-06-08 04:02:18 +08:00
}
2019-01-08 13:31:14 +08:00
fs . writeFileSync ( locations . strings , strings . write ( { indent : 4 } ) , 'utf-8' ) ;
2016-05-04 07:25:48 +08:00
events . emit ( 'verbose' , 'Wrote out android application name "' + name + '" to ' + locations . strings ) ;
2015-10-21 07:15:57 +08:00
// Java packages cannot support dashes
2017-07-28 19:05:53 +08:00
var androidPkgName = ( platformConfig . android _packageName ( ) || platformConfig . packageName ( ) ) . replace ( /-/g , '_' ) ;
2015-10-21 07:15:57 +08:00
var manifest = new AndroidManifest ( locations . manifest ) ;
2017-07-28 19:05:53 +08:00
var manifestId = manifest . getPackageId ( ) ;
2015-10-21 07:15:57 +08:00
manifest . getActivity ( )
2016-03-18 20:07:16 +08:00
. setOrientation ( platformConfig . getPreference ( 'orientation' ) )
2015-10-21 07:15:57 +08:00
. setLaunchMode ( findAndroidLaunchModePreference ( platformConfig ) ) ;
manifest . setVersionName ( platformConfig . version ( ) )
. setVersionCode ( platformConfig . android _versionCode ( ) || default _versionCode ( platformConfig . version ( ) ) )
2017-07-28 19:05:53 +08:00
. setPackageId ( androidPkgName )
2015-10-21 07:15:57 +08:00
. write ( ) ;
2017-06-28 04:15:04 +08:00
// Java file paths shouldn't be hard coded
2020-01-31 21:02:48 +08:00
const javaDirectory = path . join ( locations . javaSrc , manifestId . replace ( /\./g , '/' ) ) ;
const javaPattern = /\.java$/ ;
const java _files = utils . scanDirectory ( javaDirectory , javaPattern , true ) . filter ( function ( f ) {
2020-01-29 09:12:55 +08:00
return utils . grep ( f , /extends\s+CordovaActivity/g ) !== null ;
2015-10-21 07:15:57 +08:00
} ) ;
if ( java _files . length === 0 ) {
2016-05-04 07:25:48 +08:00
throw new CordovaError ( 'No Java files found that extend CordovaActivity.' ) ;
2017-06-14 02:42:20 +08:00
} else if ( java _files . length > 1 ) {
2016-05-04 07:25:48 +08:00
events . emit ( 'log' , 'Multiple candidate Java files that extend CordovaActivity found. Guessing at the first one, ' + java _files [ 0 ] ) ;
2015-10-21 07:15:57 +08:00
}
2020-01-31 21:02:48 +08:00
const destFile = java _files [ 0 ] ;
2020-01-29 09:12:55 +08:00
// var destFile = path.join(locations.root, 'app', 'src', 'main', 'java', androidPkgName.replace(/\./g, '/'), path.basename(java_files[0]));
// fs.ensureDirSync(path.dirname(destFile));
// events.emit('verbose', java_files[0]);
// events.emit('verbose', destFile);
// console.log(locations);
// fs.copySync(java_files[0], destFile);
utils . replaceFileContents ( destFile , /package [\w.]*;/ , 'package ' + androidPkgName + ';' ) ;
2017-07-28 19:05:53 +08:00
events . emit ( 'verbose' , 'Wrote out Android package name "' + androidPkgName + '" to ' + destFile ) ;
2015-10-21 07:15:57 +08:00
2020-01-31 21:02:48 +08:00
var removeOrigPkg = checkReqs . isWindows ( ) || checkReqs . isDarwin ( )
? manifestId . toUpperCase ( ) !== androidPkgName . toUpperCase ( )
: manifestId !== androidPkgName ;
2015-10-21 07:15:57 +08:00
2017-07-28 19:05:53 +08:00
if ( removeOrigPkg ) {
2015-10-27 14:12:47 +08:00
// If package was name changed we need to remove old java with main activity
2020-01-29 09:12:55 +08:00
fs . removeSync ( java _files [ 0 ] ) ;
2015-10-27 14:12:47 +08:00
// remove any empty directories
var currentDir = path . dirname ( java _files [ 0 ] ) ;
var sourcesRoot = path . resolve ( locations . root , 'src' ) ;
2017-06-14 02:42:20 +08:00
while ( currentDir !== sourcesRoot ) {
if ( fs . existsSync ( currentDir ) && fs . readdirSync ( currentDir ) . length === 0 ) {
2015-10-27 14:12:47 +08:00
fs . rmdirSync ( currentDir ) ;
currentDir = path . resolve ( currentDir , '..' ) ;
} else {
break ;
}
}
}
}
2015-10-21 07:15:57 +08:00
// Consturct the default value for versionCode as
// PATCH + MINOR * 100 + MAJOR * 10000
// see http://developer.android.com/tools/publishing/versioning.html
2017-06-14 02:42:20 +08:00
function default _versionCode ( version ) {
2015-10-21 07:15:57 +08:00
var nums = version . split ( '-' ) [ 0 ] . split ( '.' ) ;
var versionCode = 0 ;
if ( + nums [ 0 ] ) {
versionCode += + nums [ 0 ] * 10000 ;
}
if ( + nums [ 1 ] ) {
versionCode += + nums [ 1 ] * 100 ;
}
if ( + nums [ 2 ] ) {
versionCode += + nums [ 2 ] ;
}
2016-05-04 07:25:48 +08:00
events . emit ( 'verbose' , 'android-versionCode not found in config.xml. Generating a code based on version in config.xml (' + version + '): ' + versionCode ) ;
2015-10-21 07:15:57 +08:00
return versionCode ;
}
2017-06-14 02:42:20 +08:00
function getImageResourcePath ( resourcesDir , type , density , name , sourceName ) {
2016-04-20 04:28:13 +08:00
if ( /\.9\.png$/ . test ( sourceName ) ) {
name = name . replace ( /\.png$/ , '.9.png' ) ;
}
2016-10-07 03:55:26 +08:00
var resourcePath = path . join ( resourcesDir , ( density ? type + '-' + density : type ) , name ) ;
2016-04-20 04:28:13 +08:00
return resourcePath ;
}
2018-06-05 12:43:05 +08:00
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 ;
}
2017-06-14 02:42:20 +08:00
function updateSplashes ( cordovaProject , platformResourcesDir ) {
2016-04-20 04:28:13 +08:00
var resources = cordovaProject . projectConfig . getSplashScreens ( 'android' ) ;
2015-10-21 07:15:57 +08:00
2016-04-20 04:28:13 +08:00
// if there are "splash" elements in config.xml
if ( resources . length === 0 ) {
events . emit ( 'verbose' , 'This app does not have splash screens defined' ) ;
return ;
2015-10-21 07:15:57 +08:00
}
2016-10-07 03:55:26 +08:00
var resourceMap = mapImageResources ( cordovaProject . root , platformResourcesDir , 'drawable' , 'screen.png' ) ;
2016-04-20 04:28:13 +08:00
var hadMdpi = false ;
resources . forEach ( function ( resource ) {
if ( ! resource . density ) {
return ;
}
2017-06-14 02:42:20 +08:00
if ( resource . density === 'mdpi' ) {
2016-04-20 04:28:13 +08:00
hadMdpi = true ;
}
var targetPath = getImageResourcePath (
2016-10-07 03:55:26 +08:00
platformResourcesDir , 'drawable' , resource . density , 'screen.png' , path . basename ( resource . src ) ) ;
2016-04-20 04:28:13 +08:00
resourceMap [ targetPath ] = resource . src ;
} ) ;
// There's no "default" drawable, so assume default == mdpi.
if ( ! hadMdpi && resources . defaultResource ) {
var targetPath = getImageResourcePath (
2016-10-07 03:55:26 +08:00
platformResourcesDir , 'drawable' , 'mdpi' , 'screen.png' , path . basename ( resources . defaultResource . src ) ) ;
2016-04-20 04:28:13 +08:00
resourceMap [ targetPath ] = resources . defaultResource . src ;
}
events . emit ( 'verbose' , 'Updating splash screens at ' + platformResourcesDir ) ;
FileUpdater . updatePaths (
resourceMap , { rootDir : cordovaProject . root } , logFileOp ) ;
2015-10-21 07:15:57 +08:00
}
2017-06-14 02:42:20 +08:00
function cleanSplashes ( projectRoot , projectConfig , platformResourcesDir ) {
2015-10-21 07:15:57 +08:00
var resources = projectConfig . getSplashScreens ( 'android' ) ;
if ( resources . length > 0 ) {
2016-10-07 03:55:26 +08:00
var resourceMap = mapImageResources ( projectRoot , platformResourcesDir , 'drawable' , 'screen.png' ) ;
2016-04-20 04:28:13 +08:00
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 ) ;
2015-10-21 07:15:57 +08:00
}
}
2017-06-14 02:42:20 +08:00
function updateIcons ( cordovaProject , platformResourcesDir ) {
2020-01-31 21:02:48 +08:00
const icons = cordovaProject . projectConfig . getIcons ( 'android' ) ;
2015-10-21 07:15:57 +08:00
2018-06-05 12:43:05 +08:00
// Skip if there are no app defined icons in config.xml
2015-10-21 07:15:57 +08:00
if ( icons . length === 0 ) {
events . emit ( 'verbose' , 'This app does not have launcher icons defined' ) ;
return ;
}
2018-06-05 12:43:05 +08:00
// 1. loop icons determin if there is an error in the setup.
// 2. during initial loop, also setup for legacy support.
2020-01-31 21:02:48 +08:00
const errorMissingAttributes = [ ] ;
const errorLegacyIconNeeded = [ ] ;
2018-06-05 12:43:05 +08:00
let hasAdaptive = false ;
icons . forEach ( ( icon , key ) => {
if (
2020-01-31 21:02:48 +08:00
( icon . background && ! icon . foreground ) ||
( ! icon . background && icon . foreground ) ||
( ! icon . background && ! icon . foreground && ! icon . src )
2018-06-05 12:43:05 +08:00
) {
errorMissingAttributes . push ( icon . density ? icon . density : 'size=' + ( icon . height || icon . width ) ) ;
}
if ( icon . foreground ) {
hasAdaptive = true ;
if (
2020-01-31 21:02:48 +08:00
! icon . src &&
(
icon . foreground . startsWith ( '@color' ) ||
path . extname ( path . basename ( icon . foreground ) ) === '.xml'
2018-06-05 12:43:05 +08:00
)
) {
errorLegacyIconNeeded . push ( icon . density ? icon . density : 'size=' + ( icon . height || icon . width ) ) ;
} else if ( ! icon . src ) {
icons [ key ] . src = icon . foreground ;
}
}
} ) ;
2015-10-21 07:15:57 +08:00
2020-01-31 21:02:48 +08:00
const errorMessage = [ ] ;
2018-06-05 12:43:05 +08:00
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' )
) ;
2020-01-31 21:02:48 +08:00
const preparedIcons = prepareIcons ( icons ) ;
2018-06-05 12:43:05 +08:00
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 ) {
2020-01-31 21:02:48 +08:00
const android _icons = preparedIcons . android _icons ;
const default _icon = preparedIcons . default _icon ;
2018-06-05 12:43:05 +08:00
// 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 ;
2020-01-31 21:02:48 +08:00
for ( const density in android _icons ) {
2018-06-05 12:43:05 +08:00
let backgroundVal = '@mipmap/ic_launcher_background' ;
let foregroundVal = '@mipmap/ic_launcher_foreground' ;
background = android _icons [ density ] . background ;
foreground = android _icons [ density ] . foreground ;
2020-06-18 20:58:42 +08:00
if ( ! background || ! foreground ) {
// This icon isn't an adaptive icon, so skip it
continue ;
}
2018-06-05 12:43:05 +08:00
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 = ` <?xml version="1.0" encoding="utf-8"?>
< adaptive - icon xmlns : android = "http://schemas.android.com/apk/res/android" >
< background android : drawable = "` + backgroundVal + `" / >
< foreground android : drawable = "` + foregroundVal + `" / >
< / a d a p t i v e - i c o n > ` ;
2020-01-31 21:02:48 +08:00
const launcherXmlPath = path . join ( platformResourcesDir , 'mipmap-' + density + '-v26' , 'ic_launcher.xml' ) ;
2018-06-05 12:43:05 +08:00
// 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 ) {
2020-01-31 21:02:48 +08:00
const android _icons = preparedIcons . android _icons ;
const default _icon = preparedIcons . default _icon ;
2018-06-05 12:43:05 +08:00
// 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 ) {
2015-10-21 07:15:57 +08:00
// http://developer.android.com/design/style/iconography.html
2018-06-05 12:43:05 +08:00
const SIZE _TO _DENSITY _MAP = {
2015-10-21 07:15:57 +08:00
36 : 'ldpi' ,
48 : 'mdpi' ,
72 : 'hdpi' ,
96 : 'xhdpi' ,
144 : 'xxhdpi' ,
192 : 'xxxhdpi'
} ;
2018-06-05 12:43:05 +08:00
2020-01-31 21:02:48 +08:00
const android _icons = { } ;
2018-06-05 12:43:05 +08:00
let default _icon ;
2015-10-21 07:15:57 +08:00
// find the best matching icon for a given density or size
// @output android_icons
2017-06-14 02:42:20 +08:00
var parseIcon = function ( icon , icon _size ) {
2015-10-21 07:15:57 +08:00
// do I have a platform icon for that density already
2018-06-05 12:43:05 +08:00
var density = icon . density || SIZE _TO _DENSITY _MAP [ icon _size ] ;
2015-10-21 07:15:57 +08:00
if ( ! density ) {
// invalid icon defition ( or unsupported size)
return ;
}
var previous = android _icons [ density ] ;
if ( previous && previous . platform ) {
return ;
}
android _icons [ density ] = icon ;
} ;
// iterate over all icon elements to find the default icon and call parseIcon
2017-06-14 02:42:20 +08:00
for ( var i = 0 ; i < icons . length ; i ++ ) {
2015-10-21 07:15:57 +08:00
var icon = icons [ i ] ;
var size = icon . width ;
2018-06-05 12:43:05 +08:00
2015-10-21 07:15:57 +08:00
if ( ! size ) {
size = icon . height ;
}
2018-06-05 12:43:05 +08:00
2015-10-21 07:15:57 +08:00
if ( ! size && ! icon . density ) {
if ( default _icon ) {
2020-01-31 21:02:48 +08:00
const found = { } ;
const favor = { } ;
2018-06-05 12:43:05 +08:00
// 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 ) + '.' ) ;
2015-10-21 07:15:57 +08:00
} else {
default _icon = icon ;
}
} else {
parseIcon ( icon , size ) ;
}
}
2018-06-05 12:43:05 +08:00
return {
android _icons : android _icons ,
default _icon : default _icon
} ;
2015-10-21 07:15:57 +08:00
}
2017-06-14 02:42:20 +08:00
function cleanIcons ( projectRoot , projectConfig , platformResourcesDir ) {
2016-04-20 04:28:13 +08:00
var icons = projectConfig . getIcons ( 'android' ) ;
2018-06-05 12:43:05 +08:00
// 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 ;
2016-04-20 04:28:13 +08:00
}
2018-06-05 12:43:05 +08:00
2020-01-31 21:02:48 +08:00
const resourceMap = Object . assign (
2018-06-05 12:43:05 +08:00
{ } ,
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 ) ;
2016-04-20 04:28:13 +08:00
}
/ * *
* Gets a map containing resources of a specified name from all drawable folders in a directory .
* /
2017-06-14 02:42:20 +08:00
function mapImageResources ( rootDir , subDir , type , resourceName ) {
2020-01-31 21:02:48 +08:00
const pathMap = { } ;
2020-10-06 14:38:09 +08:00
const pattern = new RegExp ( type + '-.+' ) ;
2020-01-29 09:12:55 +08:00
utils . scanDirectory ( path . join ( rootDir , subDir ) , pattern ) . forEach ( function ( drawableFolder ) {
2020-01-31 21:02:48 +08:00
const imagePath = path . join ( subDir , path . basename ( drawableFolder ) , resourceName ) ;
2016-04-20 04:28:13 +08:00
pathMap [ imagePath ] = null ;
2015-10-21 07:15:57 +08:00
} ) ;
2016-04-20 04:28:13 +08:00
return pathMap ;
2015-10-21 07:15:57 +08:00
}
2017-06-14 02:42:20 +08:00
function updateFileResources ( cordovaProject , platformDir ) {
2016-07-29 03:44:50 +08:00
var files = cordovaProject . projectConfig . getFileResources ( 'android' ) ;
// if there are resource-file elements in config.xml
if ( files . length === 0 ) {
events . emit ( 'verbose' , 'This app does not have additional resource files defined' ) ;
return ;
}
var resourceMap = { } ;
2017-06-14 02:42:20 +08:00
files . forEach ( function ( res ) {
2016-07-29 03:44:50 +08:00
var targetPath = path . join ( platformDir , res . target ) ;
resourceMap [ targetPath ] = res . src ;
} ) ;
events . emit ( 'verbose' , 'Updating resource files at ' + platformDir ) ;
FileUpdater . updatePaths (
resourceMap , { rootDir : cordovaProject . root } , logFileOp ) ;
}
2017-06-14 02:42:20 +08:00
function cleanFileResources ( projectRoot , projectConfig , platformDir ) {
2017-04-21 15:18:16 +08:00
var files = projectConfig . getFileResources ( 'android' , true ) ;
2016-07-29 03:44:50 +08:00
if ( files . length > 0 ) {
events . emit ( 'verbose' , 'Cleaning resource files at ' + platformDir ) ;
var resourceMap = { } ;
2017-06-14 02:42:20 +08:00
files . forEach ( function ( res ) {
2016-07-29 03:44:50 +08:00
var filePath = path . join ( platformDir , res . target ) ;
resourceMap [ filePath ] = null ;
} ) ;
FileUpdater . updatePaths (
2020-04-15 11:36:40 +08:00
resourceMap , { rootDir : projectRoot , all : true } , logFileOp ) ;
2016-07-29 03:44:50 +08:00
}
}
2015-10-21 07:15:57 +08:00
/ * *
* Gets and validates 'AndroidLaunchMode' prepference from config . xml . Returns
* preference value and warns if it doesn ' t seems to be valid
*
* @ param { ConfigParser } platformConfig A configParser instance for
* platform .
*
* @ return { String } Preference ' s value from config . xml or
* default value , if there is no such preference . The default value is
* 'singleTop'
* /
2017-06-14 02:42:20 +08:00
function findAndroidLaunchModePreference ( platformConfig ) {
2015-10-21 07:15:57 +08:00
var launchMode = platformConfig . getPreference ( 'AndroidLaunchMode' ) ;
if ( ! launchMode ) {
// Return a default value
return 'singleTop' ;
}
var expectedValues = [ 'standard' , 'singleTop' , 'singleTask' , 'singleInstance' ] ;
var valid = expectedValues . indexOf ( launchMode ) >= 0 ;
if ( ! valid ) {
// Note: warn, but leave the launch mode as developer wanted, in case the list of options changes in the future
events . emit ( 'warn' , 'Unrecognized value for AndroidLaunchMode preference: ' +
launchMode + '. Expected values are: ' + expectedValues . join ( ', ' ) ) ;
}
return launchMode ;
}