├── .gitignore ├── build.gradle ├── deploy-local.sh ├── deploy-remote.sh ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── plugin ├── build.gradle ├── gradle.properties └── src │ ├── main │ ├── groovy │ │ └── com │ │ │ └── mcxiaoke │ │ │ └── packer │ │ │ ├── AndroidPackerExtension.groovy │ │ │ ├── AndroidPackerPlugin.groovy │ │ │ ├── ArchiveApkBuildTypeTask.groovy │ │ │ ├── ArchiveApkVariantTask.groovy │ │ │ ├── CleanArchivesTask.groovy │ │ │ └── ModifyManifestTask.groovy │ └── resources │ │ └── META-INF │ │ └── gradle-plugins │ │ └── packer.properties │ └── test │ └── groovy │ └── com │ └── mcxiaoke │ └── packer │ └── AndroidPackerPluginTest.groovy ├── readme.md ├── sample ├── .gitignore ├── android.keystore ├── build.gradle ├── custom.txt ├── local.properties ├── markets.txt └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── mcxiaoke │ │ └── packer │ │ └── samples │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── gen │ └── com │ │ └── mcxiaoke │ │ └── mpp │ │ └── sample │ │ ├── BuildConfig.java │ │ ├── Manifest.java │ │ └── R.java │ ├── java │ └── com │ │ └── mcxiaoke │ │ └── packer │ │ └── samples │ │ ├── MainActivity.java │ │ └── ViewUtils.java │ └── res │ ├── drawable-hdpi │ ├── drawer_shadow.9.png │ ├── ic_drawer.png │ └── ic_launcher.png │ ├── drawable-mdpi │ ├── drawer_shadow.9.png │ ├── ic_drawer.png │ └── ic_launcher.png │ ├── drawable-xhdpi │ ├── drawer_shadow.9.png │ ├── ic_drawer.png │ └── ic_launcher.png │ ├── drawable-xxhdpi │ ├── drawer_shadow.9.png │ ├── ic_drawer.png │ └── ic_launcher.png │ ├── layout │ └── act_main.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── settings.gradle ├── test-build.sh └── test-market.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .gradle/ 3 | build/ 4 | repo/ 5 | *.iml 6 | .DS_Store 7 | packer.properties 8 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/build.gradle -------------------------------------------------------------------------------- /deploy-local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "deploy plugin artifacts to local repo" 3 | rm -rf /tmp/repo/ 4 | ./gradlew -PbuildNum=2013 -PRELEASE_REPOSITORY_URL=file:///tmp/repo -PSNAPSHOT_REPOSITORY_URL=file:///tmp/repo/ :plugin:clean :plugin:build :plugin:uploadArchives --stacktrace $1 5 | -------------------------------------------------------------------------------- /deploy-remote.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "build and deploy plugin artifacts to remote repo..." 3 | ./gradlew :plugin:clean :plugin:build :plugin:uploadArchives --stacktrace $1 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 16 11:37:35 CST 2014 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /plugin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | 3 | sourceCompatibility = 1.6 4 | targetCompatibility = 1.6 5 | 6 | buildscript { 7 | repositories { 8 | mavenCentral() 9 | } 10 | } 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | dependencies { 17 | compile localGroovy() 18 | compile gradleApi() 19 | compile 'com.android.tools.build:gradle:1.3.1' 20 | } 21 | 22 | apply from: 'https://github.com/mcxiaoke/gradle-mvn-push/raw/master/gradle-mvn-push.gradle' 23 | 24 | -------------------------------------------------------------------------------- /plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=1.0.4 2 | 3 | POM_ARTIFACT_ID=packer 4 | POM_PACKAGING=jar 5 | POM_NAME=Android Market Packer 6 | 7 | GROUP=com.mcxiaoke.gradle 8 | 9 | POM_DESCRIPTION=Android Multi Market Packer Gradle Plugin 10 | POM_URL=https://github.com/mcxiaoke/gradle-packer-plugin 11 | POM_SCM_URL=https://github.com/mcxiaoke/gradle-packer-plugin.git 12 | POM_SCM_CONNECTION=scm:git:https://github.com/mcxiaoke/gradle-packer-plugin.git 13 | POM_SCM_DEV_CONNECTION=scm:git:https://github.com/mcxiaoke/gradle-packer-plugin.git 14 | POM_LICENCE_NAME=Apache License, Version 2.0 15 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0 16 | POM_LICENCE_DIST=repo 17 | POM_DEVELOPER_ID=mcxiaoke 18 | POM_DEVELOPER_NAME=Xiaoke Zhang 19 | POM_DEVELOPER_EMAIL=mail@mcxiaoke.com 20 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/mcxiaoke/packer/AndroidPackerExtension.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer 2 | 3 | import org.gradle.api.Project 4 | 5 | // Android Packer Plugin Extension 6 | class AndroidPackerExtension { 7 | 8 | /** 9 | * archive task output dir 10 | */ 11 | File archiveOutput 12 | 13 | /** 14 | * file name template string 15 | * 16 | * Available vars: 17 | * 1. projectName 18 | * 2. appName 19 | * 3. appPkg 20 | * 4. buildType 21 | * 5. flavorName 22 | * 6. versionName 23 | * 7. versionCode 24 | * 8. buildTime 25 | * 26 | * default value: '${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}' 27 | */ 28 | String archiveNameFormat 29 | 30 | /** 31 | * manifest meta-data key list 32 | */ 33 | List manifestMatcher 34 | 35 | /** 36 | * support build number auto increment 37 | * 38 | * store in file: packer.properties 39 | */ 40 | boolean buildNumberAuto 41 | /** 42 | * auto build number build type list 43 | */ 44 | List buildNumberTypeMatcher 45 | 46 | 47 | AndroidPackerExtension(Project project) { 48 | archiveOutput = new File(project.rootProject.buildDir, "archives") 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/mcxiaoke/packer/AndroidPackerPlugin.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer 2 | 3 | import groovy.text.SimpleTemplateEngine 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | import org.gradle.api.ProjectConfigurationException 7 | import org.gradle.api.Task 8 | import org.gradle.api.tasks.StopExecutionException 9 | 10 | import java.text.SimpleDateFormat 11 | 12 | // Android Multi Packer Plugin Source 13 | class AndroidPackerPlugin implements Plugin { 14 | static final String PLUGIN_NAME = "packer" 15 | static final String P_MARKET = "market" 16 | static final String P_BUILD_NUM = "buildNum" 17 | 18 | static final String PROP_FILE = "packer.properties" 19 | 20 | Project project 21 | Properties props 22 | AndroidPackerExtension packerExt 23 | 24 | @Override 25 | void apply(Project project) { 26 | if (!hasAndroidPlugin(project)) { 27 | throw new ProjectConfigurationException("the android plugin must be applied", null) 28 | } 29 | 30 | this.project = project 31 | this.props = loadProperties(project, PROP_FILE) 32 | applyExtension() 33 | // checkBuildType() 34 | // add markets must before evaluate 35 | def hasMarkets = applyMarkets() 36 | applyPluginTasks(hasMarkets) 37 | } 38 | 39 | void applyExtension() { 40 | // setup plugin and extension 41 | project.configurations.create(PLUGIN_NAME).extendsFrom(project.configurations.compile) 42 | this.packerExt = project.extensions.create(PLUGIN_NAME, AndroidPackerExtension, project) 43 | } 44 | 45 | void applyPluginTasks(hasMarkets) { 46 | project.afterEvaluate { 47 | def buildTypes = project.android.buildTypes 48 | debug("applyPluginTasks() build types: ${buildTypes.collect { it.name }}") 49 | checkProperties() 50 | checkCleanTask() 51 | //applySigningConfigs() 52 | project.android.applicationVariants.all { variant -> 53 | checkSigningConfig(variant) 54 | if (variant.buildType.name != "debug") { 55 | if (hasMarkets) { 56 | // modify manifest and add archive apk 57 | // only when markets found and not debug 58 | debug("applyPluginTasks() markets found, add manifest and archive task.") 59 | checkArchiveTask(variant) 60 | checkManifest(variant) 61 | } else { 62 | debug("applyPluginTasks() markets not found, check version name.") 63 | checkVersionName(variant) 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * add beta build type for debug 72 | * @param project 73 | * @return beta added 74 | */ 75 | boolean checkBuildType() { 76 | def types = new HashSet(); 77 | types.addAll(project.android.buildTypes.collect { it.name }) 78 | debug('checkBuildType() default build types:' + types) 79 | if (types.contains("beta")) { 80 | debug('checkBuildType() beta found, ignore') 81 | return true 82 | } 83 | debug('checkBuildType() create beta build type') 84 | def betaType = project.android.buildTypes.create("beta", { 85 | debuggable true 86 | }) 87 | def configs = project.android.signingConfigs 88 | if (configs.findByName("release")) { 89 | betaType.signingConfig = configs.release 90 | } 91 | 92 | return true 93 | } 94 | 95 | /** 96 | * apply signing config for all build types 97 | * @param project Project 98 | */ 99 | void applySigningConfigs() { 100 | def configs = project.android.signingConfigs 101 | if (configs.findByName("release")) { 102 | debug("applySigningConfigs() release signingConfig found ") 103 | project.android.buildTypes.each { 104 | if (it.name != "debug") { 105 | if (it.signingConfig == null) { 106 | debug("applySigningConfigs() add signingConfig for type:" + it.name) 107 | it.signingConfig = configs.release 108 | } else { 109 | // debug("applySigningConfigs() already defined, ignore type:" + it.name) 110 | } 111 | } 112 | } 113 | } else { 114 | warn("signingConfig.release not found ") 115 | } 116 | } 117 | 118 | /** 119 | * parse markets file and apply to flavors 120 | * @param project Project 121 | * @return found markets file 122 | */ 123 | boolean applyMarkets() { 124 | if (!project.hasProperty(P_MARKET)) { 125 | debug("applyMarkets() market property not found, ignore") 126 | return false 127 | } 128 | 129 | // check markets file exists 130 | def marketsFilePath = project.property(P_MARKET).toString() 131 | if (!marketsFilePath) { 132 | debug("applyMarkets() invalid market file path, ignore") 133 | throw new StopExecutionException("invalid market file path : '${marketsFilePath}'") 134 | } 135 | 136 | File markets = project.rootProject.file(marketsFilePath) 137 | if (!markets.exists()) { 138 | debug("applyMarkets() market file not found, ignore") 139 | throw new StopExecutionException("market file not found: '${markets.absolutePath}'") 140 | } 141 | 142 | if (!markets.isFile()) { 143 | debug("applyMarkets() market file is not a file, ignore") 144 | throw new StopExecutionException("market file is not a file: '${markets.absolutePath}'") 145 | } 146 | 147 | if (!markets.canRead()) { 148 | debug("applyMarkets() market file not readable, ignore") 149 | throw new StopExecutionException("market file not readable: '${markets.absolutePath}'") 150 | } 151 | 152 | debug("applyMarkets() file: ${marketsFilePath}") 153 | def flavors = new HashSet(); 154 | flavors.addAll(project.android.productFlavors.collect { it.name }) 155 | debug("applyMarkets() default flavors:" + flavors) 156 | 157 | // add all markets 158 | markets.eachLine { line, number -> 159 | debug("applyMarkets() ${number}:'${line}'") 160 | def parts = line.split('#') 161 | if (parts && parts[0]) { 162 | def market = parts[0].trim() 163 | if (market && !flavors.contains(market)) { 164 | debug("apply new market: " + market) 165 | project.android.productFlavors.create(market, {}) 166 | } 167 | } else { 168 | warn("invalid line found in market file --> ${number}:'${line}'") 169 | // throw new IllegalArgumentException("invalid market: ${line} at line:${number} in your market file") 170 | } 171 | 172 | } 173 | return true 174 | } 175 | 176 | /** 177 | * check project properties and apply to extension 178 | */ 179 | void checkProperties() { 180 | debug("checkProperties() output:" + packerExt.archiveOutput) 181 | debug("checkProperties() manifest:" + packerExt.manifestMatcher) 182 | } 183 | 184 | void checkSigningConfig(variant) { 185 | // debug("checkSigningConfig() for ${variant.name}") 186 | if (variant.buildType.signingConfig == null) { 187 | debug("checkSigningConfig() signingConfig for ${variant.buildType.name} is null.") 188 | } 189 | } 190 | 191 | /** 192 | * check and apply build number 193 | * @param variant Variant 194 | */ 195 | void checkVersionName(variant) { 196 | debug("checkVersionName() for variant:" + variant.name) 197 | // check buildNum property first 198 | if (project.hasProperty(P_BUILD_NUM)) { 199 | variant.mergedFlavor.versionName += '.' + project.property(P_BUILD_NUM) 200 | } else { 201 | def auto = packerExt.buildNumberAuto 202 | def patterns = packerExt.buildNumberTypeMatcher 203 | def typeName = variant.buildType.name 204 | if (auto && (patterns == null || patterns.contains(typeName))) { 205 | // or apply auto increment build number 206 | def newBuildNo = checkBuildNumber() 207 | variant.mergedFlavor.versionName += "." + newBuildNo.toString(); 208 | } 209 | } 210 | 211 | debug("checkVersionName() versionName:${variant.mergedFlavor.versionName}") 212 | } 213 | 214 | int checkBuildNumber() { 215 | def buildNo = props.getProperty("version", "0").toInteger() + 1 216 | //put new build number to props 217 | props["version"] = buildNo.toString() 218 | //store property file 219 | saveProperties(project, props, PROP_FILE) 220 | return buildNo 221 | } 222 | 223 | /** 224 | * add archiveApk tasks 225 | * @param variant current Variant 226 | */ 227 | void checkArchiveTask(variant) { 228 | 229 | if (variant.buildType.signingConfig == null) { 230 | warn("${variant.name}: signingConfig is null, ignore archive task.") 231 | return 232 | } 233 | if (!variant.buildType.zipAlignEnabled) { 234 | warn("${variant.name}: zipAlignEnabled==false, ignore archive task.") 235 | return 236 | } 237 | debug("checkArchiveTask() for ${variant.name}") 238 | def String apkName = buildApkName(variant) 239 | def File inputFile = variant.outputs[0].outputFile 240 | def File outputDir = packerExt.archiveOutput 241 | debug("checkArchiveTask() input: ${inputFile}") 242 | debug("checkArchiveTask() output: ${outputDir}") 243 | def archiveTask = project.task("archiveApk${variant.name.capitalize()}", 244 | type: ArchiveApkVariantTask) { 245 | 246 | variantName = variant.name 247 | from inputFile.absolutePath 248 | into outputDir.absolutePath 249 | rename { filename -> 250 | filename.replace inputFile.name, apkName 251 | } 252 | dependsOn variant.assemble 253 | } 254 | 255 | debug("checkArchiveTask() new task:${archiveTask.name}") 256 | 257 | def buildTypeName = variant.buildType.name 258 | if (variant.name != buildTypeName) { 259 | def Task task = checkArchiveAllTask(buildTypeName) 260 | task.dependsOn archiveTask 261 | } 262 | } 263 | 264 | /** 265 | * add archiveApkType task if not added 266 | * @param buildTypeName buildTypeName 267 | * @return task 268 | */ 269 | Task checkArchiveAllTask(buildTypeName) { 270 | def taskName = "archiveApk${buildTypeName.capitalize()}" 271 | def task = project.tasks.findByName(taskName) 272 | if (task == null) { 273 | task = project.task(taskName, type: ArchiveApkBuildTypeTask) { 274 | typeName = buildTypeName 275 | } 276 | } 277 | return task 278 | } 279 | 280 | /** 281 | * add cleanArchives task if not added 282 | * @return task 283 | */ 284 | void checkCleanTask() { 285 | def output = packerExt.archiveOutput 286 | debug("checkCleanTask() create clean archives task, path:${output}") 287 | def task = project.task("cleanArchives", 288 | type: CleanArchivesTask) { 289 | target = output 290 | } 291 | 292 | project.getTasksByName("clean", true)?.each { 293 | it.dependsOn task 294 | } 295 | } 296 | 297 | /** 298 | * build human readable apk name 299 | * @param variant Variant 300 | * @return final apk name 301 | */ 302 | String buildApkName(variant) { 303 | def dateFormat = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss') 304 | def buildTime = dateFormat.format(new Date()) 305 | .replaceAll('\\.', '-') 306 | .replaceAll(':', '-') 307 | .replaceAll(' ', '-') 308 | def nameMap = [ 309 | 'appName' : project.name, 310 | 'projectName': project.rootProject.name, 311 | 'flavorName' : variant.flavorName, 312 | 'buildType' : variant.buildType.name, 313 | 'versionName': variant.versionName, 314 | 'versionCode': variant.versionCode, 315 | 'appPkg' : variant.applicationId, 316 | 'buildTime' : buildTime 317 | ] 318 | 319 | def defaultTemplate = '${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}' 320 | def engine = new SimpleTemplateEngine() 321 | def template = packerExt.archiveNameFormat == null ? defaultTemplate : packerExt.archiveNameFormat 322 | def fileName = engine.createTemplate(template).make(nameMap).toString(); 323 | def apkName = fileName + '.apk' 324 | debug("buildApkName() final apkName: " + apkName) 325 | return apkName 326 | } 327 | 328 | /** 329 | * check and add process manifest actions 330 | * @param variant Variant 331 | */ 332 | void checkManifest(variant) { 333 | if (!variant.productFlavors) { 334 | warn("${variant.name}: check manifest, no flavors found, ignore.") 335 | return 336 | } 337 | if (!variant.outputs) { 338 | warn("${variant.name}: check manifest, no outputs found, ignore.") 339 | return 340 | } 341 | if (!packerExt.manifestMatcher) { 342 | error("${variant.name}: check manifest, no manifest matcher found, quit.") 343 | return 344 | } 345 | def Task processManifestTask = variant.outputs[0].processManifest 346 | def Task processResourcesTask = variant.outputs[0].processResources 347 | def processMetaTask = project.task("modify${variant.name.capitalize()}MetaData", 348 | type: ModifyManifestTask) { 349 | try { 350 | // Android Gradle Plugin < 3.0.0 351 | manifestFile = output.processManifest.manifestOutputFile 352 | } catch (Exception ignored) { 353 | // Android Gradle Plugin >= 3.0.0 354 | manifestFile = new File(output.processManifest.manifestOutputDirectory, "AndroidManifest.xml") 355 | if (!manifestFile.isFile()) { 356 | manifestFile = new File(new File(output.processManifest.manifestOutputDirectory, output.dirName),"AndroidManifest.xml") 357 | } 358 | } 359 | manifestMatcher = packerExt.manifestMatcher 360 | flavorName = variant.productFlavors[0].name 361 | dependsOn processManifestTask 362 | } 363 | processResourcesTask.dependsOn processMetaTask 364 | } 365 | 366 | /** 367 | * check android plugin applied 368 | * @param project Project 369 | * @return plugin applied 370 | */ 371 | static boolean hasAndroidPlugin(Project project) { 372 | return project.plugins.hasPlugin("com.android.application") 373 | } 374 | 375 | /** 376 | * print debug messages 377 | * @param msg msg 378 | * @param vars vars 379 | */ 380 | void debug(String msg, Object... vars) { 381 | project.logger.debug(msg, vars) 382 | } 383 | 384 | void warn(String msg, Object... vars) { 385 | project.logger.warn(msg, vars) 386 | } 387 | 388 | void error(String msg) { 389 | project.logger.warn(msg) 390 | throw new MissingPropertyException(msg) 391 | } 392 | 393 | static void saveProperties(Project project, Properties props, String fileName) { 394 | props.store(new FileWriter(project.file(fileName)), null) 395 | } 396 | 397 | static Properties loadProperties(Project project, String fileName) { 398 | def props = new Properties() 399 | def file = project.file(fileName) 400 | if (!file.exists()) { 401 | file.createNewFile() 402 | } else { 403 | props.load(new FileReader(file)) 404 | } 405 | return props 406 | } 407 | 408 | static boolean getGitSha() { 409 | return 'git rev-parse --short HEAD'.execute().text.trim() 410 | } 411 | 412 | 413 | } 414 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/mcxiaoke/packer/ArchiveApkBuildTypeTask.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer 2 | import org.gradle.api.DefaultTask 3 | import org.gradle.api.tasks.TaskAction 4 | /** 5 | * User: mcxiaoke 6 | * Date: 14/12/19 7 | * Time: 12:08 8 | */ 9 | class ArchiveApkBuildTypeTask extends DefaultTask { 10 | 11 | String typeName 12 | 13 | ArchiveApkBuildTypeTask() { 14 | setDescription('copy archives of this build type to output dir') 15 | } 16 | 17 | @TaskAction 18 | void showMessage() { 19 | project.logger.info("${name}: copy archives of this build type to output dir for ${typeName}") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/mcxiaoke/packer/ArchiveApkVariantTask.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer 2 | 3 | import org.gradle.api.tasks.Copy 4 | import org.gradle.api.tasks.TaskAction 5 | 6 | /** 7 | * User: mcxiaoke 8 | * Date: 14/12/19 9 | * Time: 11:42 10 | */ 11 | class ArchiveApkVariantTask extends Copy { 12 | 13 | String variantName 14 | 15 | ArchiveApkVariantTask() { 16 | setDescription('copy variant apk to output and rename apk') 17 | } 18 | 19 | @TaskAction 20 | void showMessage() { 21 | project.logger.info("${name}: copy archives of ${variantName} to output dir") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/mcxiaoke/packer/CleanArchivesTask.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer 2 | import org.gradle.api.DefaultTask 3 | import org.gradle.api.tasks.Input 4 | import org.gradle.api.tasks.TaskAction 5 | /** 6 | * User: mcxiaoke 7 | * Date: 14/12/19 8 | * Time: 11:29 9 | */ 10 | class CleanArchivesTask extends DefaultTask { 11 | 12 | @Input 13 | File target 14 | 15 | CleanArchivesTask() { 16 | setDescription('clean all apk archives in output dir') 17 | } 18 | 19 | @TaskAction 20 | void showMessage() { 21 | project.logger.info("${name}: ${description}") 22 | } 23 | 24 | @TaskAction 25 | void deleteAll() { 26 | project.logger.info("${name}: delete all files in ${target.absolutePath}") 27 | deleteDir(target) 28 | } 29 | 30 | static void deleteDir(File dir) { 31 | if (dir && dir.listFiles()) { 32 | dir.listFiles().sort().each { File file -> 33 | if (file.isFile()) { 34 | file.delete() 35 | } else { 36 | file.deleteDir() 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/mcxiaoke/packer/ModifyManifestTask.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer 2 | 3 | import groovy.xml.StreamingMarkupBuilder 4 | import groovy.xml.XmlUtil 5 | import org.gradle.api.DefaultTask 6 | import org.gradle.api.tasks.Input 7 | import org.gradle.api.tasks.InputFile 8 | import org.gradle.api.tasks.TaskAction 9 | 10 | /** 11 | * User: mcxiaoke 12 | * Date: 14/12/17 13 | * Time: 16:42 14 | */ 15 | /** 16 | * parse and modify manifest file 17 | * apply market value to meta-data 18 | */ 19 | class ModifyManifestTask extends DefaultTask { 20 | @InputFile 21 | @Input 22 | def File manifestFile 23 | @Input 24 | def manifestMatcher 25 | @Input 26 | def flavorName 27 | 28 | ModifyManifestTask() { 29 | setDescription("modify manifest meta-data to apply market value") 30 | } 31 | 32 | @TaskAction 33 | void showMessage() { 34 | project.logger.info("${name}: ${description}") 35 | } 36 | 37 | @TaskAction 38 | void processMeta() { 39 | project.logger.info("${name}: manifestFile:${manifestFile.absolutePath}") 40 | def root = new XmlSlurper().parse(manifestFile) 41 | .declareNamespace(android: "http://schemas.android.com/apk/res/android") 42 | project.logger.info("${name}: matcher:${manifestMatcher}") 43 | manifestMatcher?.each { String pattern -> 44 | def metadata = root.application.'meta-data' 45 | def found = metadata.find { mt -> pattern == mt.'@android:name'.toString() } 46 | if (found.size() > 0) { 47 | project.logger.info("${name}: ${pattern} found, modify it") 48 | found.replaceNode { 49 | 'meta-data'('android:name': found."@android:name", 'android:value': flavorName) {} 50 | } 51 | } else { 52 | project.logger.info("${name}: ${pattern} not found, add it.") 53 | root.application.appendNode { 54 | 'meta-data'('android:name': pattern, 'android:value': flavorName) {} 55 | } 56 | } 57 | } 58 | 59 | serializeXml(root, manifestFile) 60 | } 61 | 62 | /** 63 | * write xml to file 64 | * @param xml xml 65 | * @param file file 66 | */ 67 | static void serializeXml(xml, file) { 68 | XmlUtil.serialize(new StreamingMarkupBuilder().bind { mkp.yield xml }, 69 | new FileWriter(file)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/gradle-plugins/packer.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.mcxiaoke.packer.AndroidPackerPlugin 2 | -------------------------------------------------------------------------------- /plugin/src/test/groovy/com/mcxiaoke/packer/AndroidPackerPluginTest.groovy: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer 2 | 3 | import org.junit.Assert 4 | import org.gradle.api.Project 5 | import org.gradle.api.ProjectConfigurationException 6 | import org.gradle.testfixtures.ProjectBuilder 7 | import org.junit.Before 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.junit.rules.ExpectedException 11 | 12 | class AndroidPackerPluginTest { 13 | @Rule 14 | public ExpectedException thrown = ExpectedException.none() 15 | 16 | @Before 17 | public void setUp() { 18 | } 19 | 20 | @Test(expected = ProjectConfigurationException.class) 21 | public void testWithoutAndroidPlugin() { 22 | Project project = ProjectBuilder.builder().build() 23 | configBuildScript(project, 'com.android.tools.build:gradle:1.0.0') 24 | new AndroidPackerPlugin().apply(project) 25 | } 26 | 27 | @Test 28 | public void testWithAndroidPlugin() { 29 | Project project = ProjectBuilder.builder().build() 30 | configBuildScript(project, 'com.android.tools.build:gradle:1.0.0') 31 | project.apply plugin: 'com.android.application' 32 | new AndroidPackerPlugin().apply(project) 33 | } 34 | 35 | 36 | // @Test 37 | // public void testVariantsAndBuildTypes() { 38 | // Project project = createProject() 39 | // project.evaluate() 40 | // def variants = project.android.applicationVariants 41 | // def variantNames = variants.collect { it.name }.join(' ') 42 | // Assert.assertEquals(variants.size(), 3) 43 | // Assert.assertTrue(variantNames.contains('beta')) 44 | // 45 | // def variantBeta = variants.find { it.buildType.name == 'beta' } 46 | // Assert.assertEquals(variantBeta.buildType.name, 'beta') 47 | // } 48 | 49 | private Project createProject() { 50 | Project project = ProjectBuilder.builder().withName('plugin-test').build() 51 | // configBuildScript(project, 'com.android.tools.build:gradle:1.0.0') 52 | project.apply plugin: 'com.android.application' 53 | project.apply plugin: 'packer' 54 | project.android { 55 | compileSdkVersion 21 56 | buildToolsVersion "21.1.1" 57 | 58 | defaultConfig { 59 | minSdkVersion 14 60 | targetSdkVersion 21 61 | versionCode 1 62 | versionName "1.0.0" 63 | applicationId 'com.mcxiaoke.packer.test' 64 | } 65 | 66 | signingConfigs { 67 | release { 68 | storeFile project.file("android.keystore") 69 | storePassword "android" 70 | keyAlias "android" 71 | keyPassword "android" 72 | } 73 | 74 | } 75 | } 76 | return project 77 | } 78 | 79 | static void configBuildScript(Project project, String androidVersion) { 80 | project.buildscript { 81 | repositories { 82 | maven { url "/tmp/repo/" } 83 | mavenCentral() 84 | } 85 | dependencies { 86 | classpath androidVersion 87 | } 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Android渠道打包工具Gradle插件 2 | ================================ 3 | 4 | ### 渠道包数量很多(如100个以上),对打包速度有要求的建议使用新版极速打包工具 [**Packer-Ng**](https://github.com/mcxiaoke/packer-ng-plugin),100个渠道包只需10秒。 5 | 6 | ## 最新版本 7 | 8 | [![Maven Central](http://img.shields.io/badge/2015.05.18-com.mcxiaoke.gradle:packer:1.0.4-brightgreen.svg)](http://search.maven.org/#artifactdetails%7Ccom.mcxiaoke.gradle%7Cpacker%7C1.0.4%7Cjar) 9 | 10 | - 2014.12.20 发布1.0.0版,支持渠道打包和版本号自增等功能 11 | - 2014.12.24 发布1.0.1版,完善market文件异常处理 12 | - 2015.02.12 发布1.0.2版,检查manifestMatcher配置 13 | - 2015.03.24 发布1.0.3版,版本号自增与versionName和buildType无关 14 | - 2015.05.18 发布1.0.4版,移除Android插件版本检查,支持最新版本 15 | 16 | ## 项目介绍 17 | 18 | **gradle-packer-plugin** 是Android多渠道打包工具Gradle插件,可方便的于自动化构建系统集成,通过很少的配置可实现如下功能 : 19 | 20 | * 支持自动替换AndroidManifest文件中的meta-data字段实现多渠道打包 21 | * 支持自定义多渠道打包输出的存放目录和最终APK文件名 22 | * 支持自动修改versionName中的build版本号,实现版本号自动增长 23 | 24 | **gradle-packer-plugin** 库路径: `com.mcxiaoke.gradle:packer:1.0.+` 简短名:`packer`,可以在项目的 `build.gradle` 中指定使用 25 | 26 | 27 | ## 使用方法 28 | 29 | #### 修改项目根目录的 `build.gradle` : 30 | 31 | ```groovy 32 | 33 | buildscript { 34 | repositories { 35 | mavenCentral() 36 | } 37 | 38 | dependencies{ 39 | classpath 'com.mcxiaoke.gradle:packer:1.0.+' 40 | } 41 | } 42 | ``` 43 | 44 | #### 修改Android项目的 `build.gradle` : 45 | 46 | ```groovy 47 | 48 | apply plugin: 'packer' 49 | ``` 50 | 51 | ### 多渠道打包 52 | 53 | 需要在命令行指定 -Pmarket=yourMarketFileName属性,market是你的渠道名列表文件名,market文件是基于**项目根目录**的 `相对路径` ,假设你的项目位于 `~/github/myapp` 你的market文件位于 `~/github/myapp/config/markets.txt` 那么参数应该是 `-Pmarket=config/markets.txt`,一般建议直接放在项目根目录,如果market文件参数错误或者文件不存在会抛出异常 54 | 55 | 渠道名列表文件是纯文本文件,每行一个渠道号,列表解析的时候会自动忽略空白行,但是格式不规范会报错,渠道名和注释之间用 `#` 号分割开,行示例: 56 | 57 | ``` 58 | Google_Play#play store market 59 | Gradle_Test#test 60 | SomeMarket#some market 61 | ``` 62 | 63 | 渠道打包的命令行参数格式示例(在项目根目录执行): 64 | 65 | ```shell 66 | ./gradlew -Pmarket=markets.txt clean archiveApkRelease 67 | ``` 68 | 69 | ### Windows系统 70 | 71 | * 如果你是在windows系统下使用,需要下载 [Gradle](http://www.gradle.org/docs/current/userguide/gradle_wrapper.html),设置 **GRADLE_HOME** 环境变量,并且将Gradle的 **bin** 目录添加到环境变量PATH,然后将命令行中的 `./gradlew` 替换为 `gradle.bat` 72 | * 如果同时还需要使用gradlew,你需要给你的项目配置使用gradle wrapper,在设置好了gradle之后,在你的项目根目录命令行输入 `gradle.bat wrapper` 然后就可以使用 `gradlew.bat` 了 73 | * Windows系统下的命令行参考: 74 | * 使用gradle: `gradle.bat clean assembleRelease` 75 | * 使用gradle wrapper: `gradlew.bat clean assembleRelease` 76 | 77 | 78 | ### 文件名格式 79 | 80 | 可以使用 `archiveNameFormat` 自定义渠道打包输出的APK文件名格式,默认格式是 81 | 82 | `${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}` 83 | 84 | 举例:假如你的App包名是 `com.your.company` ,渠道名是 `Google_Play` ,`buildType` 是 `release` ,`versionName` 是 `2.1.15` ,`versionCode` 是 `200115` ,那么生成的APK的文件名是 85 | 86 | `com.your.company-Google_Player-release-2.1.15-20015.apk` 87 | 88 | ### 版本号自增 89 | 90 | 版本号自动会自动在在 `vesionName` 尾部增加 `.buildNumer` 该字段会自动增长,举例:如果App本来的版本号是 1.2.3,那么使用版本号自动后会是 `1.2.3.1` `1.2.3.2` ... `1.2.3.25` 末尾的build版本号会随构建次数自动增长。注意:如果在命令行使用 `-PbuildNum=123` 这种形式指定了build版本号,那么自增版本号不会生效 91 | 92 | 93 | ## 配置选项 94 | 95 | * **archiveOutput** 指定渠道打包输出的APK存放目录,默认位于`${项目根目录}/build/archives` 96 | 97 | * **archiveNameFormat** - `Groovy格式字符串`, 指定渠道打包输出的APK文件名格式,默认文件名格式是: `${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}`,可使用以下变量: 98 | 99 | * *projectName* - 项目名字 100 | * *appName* - App模块名字 101 | * *appPkg* - `applicationId` (App包名packageName) 102 | * *buildType* - `buildType` (release/debug/beta等) 103 | * *flavorName* - `flavorName` (对应渠道打包中的渠道名字) 104 | * *versionName* - `versionName` (显示用的版本号) 105 | * *versionCode* - `versionCode` (内部版本号) 106 | * *buildTime* - `buildTime` (编译构建日期时间) 107 | 108 | * **manifestMatcher** 指定渠道打包需要修改的AndroidManifest.xml的meta-data的项名称,列表类型,举例: `['UMENG_CHANNEL', 'Promotion_Market']`,注意:需要同时在命令行使用 `-Pmarket=yourMarketFileName` 指定market属性多渠道打包才会生效,如果没有配置就使用多渠道打包,将会抛出异常 109 | 110 | * **buildNumberAuto** - 布尔值,是否使用自增版本号功能 设为 `true` 为使用插件提供的自增build版本号功能,该功能会在项目目录生成一个 `packer.properties` 文件,建议加入到 `.gitignore` 中,注意:该功能不会应用于多渠道打包生成的APK,不会影响渠道打包 111 | 112 | * **buildNumberTypeMatcher** - 指定需要使用自增版本号的buildType,列表类型,举例: `['release', 'beta']` 默认是全部 113 | 114 | 115 | ## 使用示例: 116 | 117 | ### 多渠道打包 118 | * 修改项目根目录的 `build.gradle` 在 `buildscript.dependencies` 部分加入 `classpath 'com.mcxiaoke.gradle:packer:1.0.0'` 119 | * 修改Android项目的 `build.gradle` 在 `apply plugin: 'com.android.application'` 下面加入 `apply plugin: 'packer'` 120 | * 修改Android项目的 `build.gradle` 加入如下配置项,`manifestMatcher` 是必须指定的,其它几项可以使用默认值: 121 | 122 | ```groovy 123 | 124 | packer { 125 | // 指定渠道打包输出目录 126 | // archiveOutput = file(new File(project.rootProject.buildDir.path, "archives")) 127 | // 指定渠道打包输出文件名格式 128 | // archiveNameFormat = '' 129 | // 指定渠道打包需要修改的AndroidManifest文件项 130 | manifestMatcher = ['UMENG_CHANNEL','Promotion_Market'] 131 | 132 | } 133 | 134 | ``` 135 | 136 | * 假设渠道列表文件位于项目根目录,文件名为 `markets.txt` ,在项目根目录打开shell运行命令: 137 | 138 | ```shell 139 | ./gradlew -Pmarket=markets.txt clean archiveApkRelease 140 | // Windows系统下替换为: 141 | gradle.bat -Pmarket=markets.txt clean archiveApkRelease 142 | // 或 143 | gradlew.bat -Pmarket=markets.txt clean archiveApkRelease 144 | ``` 145 | 146 | 如果没有错误,打包完成后你可以在 `${项目根目录}/build/archives/` 目录找到最终的渠道包。说明:渠道打包的Gradle Task名字是 `archiveApk${buildType}` buildType一般是release,也可以是你自己指定的beta或者someOtherType,使用时首字母需要大写,例如release的渠道包任务名是 `archiveApkRelease`,beta的渠道包任务名是 `archiveApkBeta`,其它的以此类推 147 | 148 | ### 版本号自增 149 | 150 | * 修改项目根目录的 `build.gradle` 在 `buildscript.dependencies` 部分加入 `classpath 'com.mcxiaoke.gradle:packer:1.0.0'` 151 | * 修改Android项目的 `build.gradle` 在 `apply plugin: 'com.android.application'` 下面加入 `apply plugin: 'packer'` 152 | * 修改Android项目的 `build.gradle` 加入如下配置项,buildNumberAuto是开关 153 | 154 | ```groovy 155 | 156 | packer { 157 | // 指定是否使用build版本号自增 158 | buildNumberAuto = true 159 | // 指定使用版本号自增的buildType,默认是全部 160 | buildNumberTypeMatcher = ['release', 'beta'] 161 | 162 | } 163 | 164 | ``` 165 | * 在项目根目录打开shell运行命令: `./gradlew clean assembleRelease` 如果没有错误,你可以安装apk查看versionName自增是否生效, 也可以运行 `./gradlew -PbuildNum=123 clean assembleRelease` 从命令行指定build版本号,该方法多用于自动化构建系统 166 | 167 | 168 | ## 完整示例: 169 | 170 | 项目的 `samples` 目录包含一个完整的项目示例,可以查看其中的 `build.gradle` 171 | 172 | ```groovy 173 | 174 | buildscript { 175 | repositories { 176 | mavenCentral() 177 | } 178 | dependencies { 179 | classpath 'com.android.tools.build:gradle:1.0.0' 180 | // `添加packer插件依赖` 181 | classpath 'com.mcxiaoke.gradle:packer:1.0.0' 182 | } 183 | } 184 | 185 | repositories { 186 | mavenCentral() 187 | } 188 | 189 | apply plugin: 'com.android.application' 190 | // 建议放在 `com.android.application` 下面 191 | // `使用 apply plugin使用packer插件` 192 | apply plugin: 'packer' 193 | 194 | packer { 195 | // 指定渠道打包输出目录 196 | archiveOutput = file(new File(project.rootProject.buildDir.path, "apks")) 197 | // 指定渠道打包输出文件名格式 198 | archiveNameFormat = '' 199 | // 指定渠道打包需要修改的AndroidManifest文件项 200 | manifestMatcher = ['UMENG_CHANNEL','Promotion_Market'] 201 | // 指定是否使用build版本号自增 202 | buildNumberAuto = true 203 | // 指定使用版本号自增的buildType,默认是全部 204 | buildNumberTypeMatcher = ['release', 'beta'] 205 | 206 | } 207 | 208 | android { 209 | compileSdkVersion 21 210 | buildToolsVersion "21.1.1" 211 | 212 | defaultConfig { 213 | applicationId "com.mcxiaoke.packer.sample" 214 | minSdkVersion 15 215 | targetSdkVersion 21 216 | versionCode 12345 217 | versionName "1.2.3" 218 | } 219 | 220 | signingConfigs { 221 | release { 222 | storeFile file("android.keystore") 223 | storePassword "android" 224 | keyAlias "android" 225 | keyPassword "android" 226 | } 227 | } 228 | 229 | buildTypes { 230 | release { 231 | signingConfig signingConfigs.release 232 | minifyEnabled false 233 | } 234 | 235 | beta { 236 | signingConfig signingConfigs.release 237 | minifyEnabled false 238 | debuggable true 239 | } 240 | 241 | } 242 | 243 | } 244 | 245 | dependencies { 246 | compile fileTree(dir: 'libs', include: ['*.jar']) 247 | compile 'com.android.support:support-v4:21.0.2' 248 | } 249 | 250 | 251 | ``` 252 | 253 | 254 | 255 | ## 参与开发 256 | 257 | `plugin` 目录是插件的源代码,用 `Groovy` 语言编写,项目 `sample` 目录是一个完整的Andoid项目示例,在项目根目录有几个脚本可以用于测试: 258 | 259 | * **deploy-local.sh** 部署插件到本地的 `/tmp/repo/` 目录,方便即时测试 260 | * **test-build.sh** 部署并测试插件是否有错误,测试build版本号自增功能 261 | * **test-market.sh** 部署并测试插件是否有错误,测试多渠道打包功能 262 | 263 | 264 | 265 | ## 感谢 266 | 267 | 本项目参考了公司内部Android项目使用的多渠道打包工具,最初作者是 [googolmo](https://github.com/googolmo),文件名模板自定义部分的代码修改自此项目 [android-appversion-gradle-plugin](https://github.com/hamsterksu/android-appversion-gradle-plugin) 268 | 269 | ------ 270 | 271 | ## 关于作者 272 | 273 | #### 联系方式 274 | 275 | * Blog: 276 | * Github: 277 | * Email: [github@mcxiaoke.com](mailto:github@mcxiaoke.com) 278 | 279 | #### 开源项目 280 | 281 | * Awesome-Kotlin: 282 | * Kotlin-Koi: 283 | * Next公共组件库: 284 | * PackerNg极速打包工具: 285 | * Gradle渠道打包: 286 | * EventBus实现xBus: 287 | * Rx文档中文翻译: 288 | * MQTT协议中文版: 289 | * 蘑菇饭App: 290 | * 饭否客户端: 291 | * Volley镜像: 292 | 293 | ------ 294 | 295 | ## License 296 | 297 | Copyright 2014 - 2016 Xiaoke Zhang 298 | 299 | Licensed under the Apache License, Version 2.0 (the "License"); 300 | you may not use this file except in compliance with the License. 301 | You may obtain a copy of the License at 302 | 303 | http://www.apache.org/licenses/LICENSE-2.0 304 | 305 | Unless required by applicable law or agreed to in writing, software 306 | distributed under the License is distributed on an "AS IS" BASIS, 307 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 308 | See the License for the specific language governing permissions and 309 | limitations under the License. 310 | 311 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | packer.properties 3 | -------------------------------------------------------------------------------- /sample/android.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/android.keystore -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | maven { url '/tmp/repo/' } 4 | mavenCentral() 5 | maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:1.2.3' 10 | classpath 'com.mcxiaoke.gradle:packer:1.0.4-SNAPSHOT' 11 | } 12 | } 13 | 14 | repositories { 15 | mavenCentral() 16 | maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } 17 | } 18 | 19 | apply plugin: 'com.android.application' 20 | apply plugin: 'packer' 21 | 22 | packer { 23 | // type:file 24 | archiveOutput = file(new File(project.rootProject.buildDir.path, "myapks")) 25 | // type:String 26 | manifestMatcher = ['UMENG_CHANNEL','Promotion_Market', 'Test_Value'] 27 | // type:groovy String template 28 | archiveNameFormat = '${appName}-${appPkg}-v${versionName}-${versionCode}-${buildType}-${flavorName}-${buildTime}' 29 | buildNumberAuto = true 30 | buildNumberTypeMatcher = ['release'] 31 | 32 | } 33 | 34 | android { 35 | compileSdkVersion 21 36 | buildToolsVersion "22.0.1" 37 | 38 | defaultConfig { 39 | applicationId "com.mcxiaoke.packer.sample" 40 | minSdkVersion 15 41 | targetSdkVersion 21 42 | versionCode 12345 43 | versionName "1.2.3" 44 | } 45 | 46 | signingConfigs { 47 | release { 48 | storeFile file("android.keystore") 49 | storePassword "android" 50 | keyAlias "android" 51 | keyPassword "android" 52 | } 53 | } 54 | 55 | buildTypes { 56 | release { 57 | signingConfig signingConfigs.release 58 | minifyEnabled false 59 | } 60 | 61 | //someType { 62 | // minifyEnabled false 63 | // debuggable true 64 | //} 65 | 66 | beta { 67 | signingConfig signingConfigs.release 68 | minifyEnabled false 69 | debuggable true 70 | } 71 | 72 | } 73 | 74 | productFlavors { 75 | //hello { 76 | // versionName "helloworld" 77 | //} 78 | 79 | //PackerTest { 80 | // versionName "1.0-packer-test" 81 | //} 82 | } 83 | 84 | lintOptions { 85 | abortOnError false 86 | } 87 | 88 | tasks.withType(JavaCompile) { 89 | options.encoding = 'UTF-8' 90 | } 91 | } 92 | 93 | // https://code.google.com/p/android/issues/detail?id=171089 94 | dependencies { 95 | compile fileTree(dir: 'libs', include: ['*.jar']) 96 | compile 'com.android.support:support-v4:21.0.2' 97 | compile 'com.android.support:appcompat-v7:21.0.2' 98 | compile 'com.jakewharton:butterknife:6.0.0' 99 | compile('com.mcxiaoke.next:core:1.0.4@aar') { 100 | exclude group: 'com.android.support', module: 'support-v4' 101 | } 102 | compile('com.mcxiaoke.next:http:1.0.8-SNAPSHOT@aar') 103 | compile('com.mcxiaoke.next:ui:1.0.8-SNAPSHOT@aar') { 104 | exclude group: 'com.android.support', module: 'support-v4' 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /sample/custom.txt: -------------------------------------------------------------------------------- 1 | App_Market# market test 2 | # 3 | PlayStore# for google play 4 | 5 | PackerTest# just test 6 | # 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /sample/local.properties: -------------------------------------------------------------------------------- 1 | ## This file is automatically generated by Android Studio. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must *NOT* be checked into Version Control Systems, 5 | # as it contains information specific to your local configuration. 6 | # 7 | # Location of the SDK. This is only used by Gradle. 8 | # For customization when using a Version Control System, please read the 9 | # header note. 10 | #Thu Dec 11 11:24:17 CST 2014 11 | sdk.dir=/Users/mcxiaoke/develop/android-sdk-macosx 12 | -------------------------------------------------------------------------------- /sample/markets.txt: -------------------------------------------------------------------------------- 1 | Github# github 2 | # 3 | PlayStore# for google play 4 | 5 | PackerTest# just test 6 | # 7 | 8 | 9 | MarketNoComments 10 | 11 | #invalid line 12 | 13 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/com/mcxiaoke/packer/samples/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.samples; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 20 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /sample/src/main/gen/com/mcxiaoke/mpp/sample/BuildConfig.java: -------------------------------------------------------------------------------- 1 | /*___Generated_by_IDEA___*/ 2 | 3 | package com.mcxiaoke.mpp.sample; 4 | 5 | /* This stub is only used by the IDE. It is NOT the BuildConfig class actually packed into the APK */ 6 | public final class BuildConfig { 7 | public final static boolean DEBUG = Boolean.parseBoolean(null); 8 | } -------------------------------------------------------------------------------- /sample/src/main/gen/com/mcxiaoke/mpp/sample/Manifest.java: -------------------------------------------------------------------------------- 1 | /*___Generated_by_IDEA___*/ 2 | 3 | package com.mcxiaoke.mpp.sample; 4 | 5 | /* This stub is only used by the IDE. It is NOT the Manifest class actually packed into the APK */ 6 | public final class Manifest { 7 | } -------------------------------------------------------------------------------- /sample/src/main/gen/com/mcxiaoke/mpp/sample/R.java: -------------------------------------------------------------------------------- 1 | /*___Generated_by_IDEA___*/ 2 | 3 | package com.mcxiaoke.mpp.sample; 4 | 5 | /* This stub is only used by the IDE. It is NOT the R class actually packed into the APK */ 6 | public final class R { 7 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.samples; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.ActivityManager; 5 | import android.app.ActivityManager.MemoryInfo; 6 | import android.content.Context; 7 | import android.content.pm.ApplicationInfo; 8 | import android.content.pm.PackageInfo; 9 | import android.content.pm.PackageManager; 10 | import android.content.pm.PackageManager.NameNotFoundException; 11 | import android.graphics.Point; 12 | import android.net.ConnectivityManager; 13 | import android.net.NetworkInfo; 14 | import android.os.Build; 15 | import android.os.Build.VERSION_CODES; 16 | import android.os.Bundle; 17 | import android.support.v7.app.ActionBarActivity; 18 | import android.util.DisplayMetrics; 19 | import android.view.Display; 20 | import android.view.ViewGroup; 21 | import android.view.ViewGroup.LayoutParams; 22 | import android.widget.TextView; 23 | import butterknife.ButterKnife; 24 | import butterknife.InjectView; 25 | import com.mcxiaoke.next.utils.AndroidUtils; 26 | import com.mcxiaoke.next.utils.LogUtils; 27 | import com.mcxiaoke.next.utils.StringUtils; 28 | import com.mcxiaoke.packer.sample.BuildConfig; 29 | import com.mcxiaoke.packer.sample.R; 30 | 31 | import java.lang.reflect.Field; 32 | import java.lang.reflect.Method; 33 | import java.lang.reflect.Modifier; 34 | import java.util.Set; 35 | 36 | 37 | public class MainActivity extends ActionBarActivity { 38 | private static final String TAG = MainActivity.class.getSimpleName(); 39 | 40 | @InjectView(R.id.container) 41 | ViewGroup mContainer; 42 | 43 | @Override 44 | protected void onCreate(Bundle savedInstanceState) { 45 | super.onCreate(savedInstanceState); 46 | setContentView(R.layout.act_main); 47 | ButterKnife.inject(this); 48 | addBuildConfigSection(); 49 | addMetaDataSection(); 50 | addAppInfoSection(); 51 | addNetworkInfoSection(); 52 | addDeviceInfoSection(); 53 | addBuildPropsSection(); 54 | 55 | } 56 | 57 | private void addAppInfoSection() { 58 | try { 59 | final PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0); 60 | final ApplicationInfo info = pi.applicationInfo; 61 | StringBuilder builder = new StringBuilder(); 62 | builder.append("[AppInfo]\n"); 63 | builder.append("Name: ").append(getString(info.labelRes)).append("\n"); 64 | builder.append("Package: ").append(BuildConfig.APPLICATION_ID).append("\n"); 65 | builder.append("VersionCode: ").append(BuildConfig.VERSION_CODE).append("\n"); 66 | builder.append("VersionName: ").append(BuildConfig.VERSION_NAME).append("\n"); 67 | builder.append("ProcessName: ").append(info.processName).append("\n"); 68 | builder.append("SourceDir: ").append(info.sourceDir).append("\n"); 69 | builder.append("DataDir: ").append(info.dataDir).append("\n"); 70 | builder.append("Signature:\n"); 71 | builder.append(AndroidUtils.getSignatureInfo(this)).append("\n"); 72 | builder.append("\n"); 73 | addSection(builder.toString()); 74 | } catch (Exception e) { 75 | } 76 | 77 | 78 | } 79 | 80 | private void addMetaDataSection() { 81 | final PackageManager pm = getPackageManager(); 82 | final String packageName = getPackageName(); 83 | try { 84 | final ApplicationInfo info = pm.getApplicationInfo(packageName, 85 | PackageManager.GET_SIGNATURES | PackageManager.GET_META_DATA); 86 | final Bundle bundle = info.metaData; 87 | final StringBuilder builder = new StringBuilder(); 88 | builder.append("[MetaData]\n"); 89 | if (bundle != null) { 90 | final Set keySet = bundle.keySet(); 91 | for (final String key : keySet) { 92 | builder.append(key).append("=").append(bundle.get(key)).append("\n"); 93 | } 94 | } 95 | addSection(builder.toString()); 96 | } catch (NameNotFoundException e) { 97 | e.printStackTrace(); 98 | } 99 | } 100 | 101 | private void addNetworkInfoSection() { 102 | StringBuilder builder = new StringBuilder(); 103 | builder.append("[Network]\n"); 104 | ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); 105 | NetworkInfo info = cm.getActiveNetworkInfo(); 106 | if (info != null) { 107 | builder.append(info); 108 | } 109 | builder.append("\n\n"); 110 | addSection(builder.toString()); 111 | } 112 | 113 | @SuppressLint("NewApi") 114 | private void addDeviceInfoSection() { 115 | StringBuilder builder = new StringBuilder(); 116 | builder.append("[Device]\n"); 117 | 118 | ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); 119 | final MemoryInfo memoryInfo = new MemoryInfo(); 120 | am.getMemoryInfo(memoryInfo); 121 | if (AndroidUtils.hasJellyBean()) { 122 | builder.append("Mem Total: ").append(StringUtils.getHumanReadableByteCount(memoryInfo.totalMem)).append("\n"); 123 | } 124 | builder.append("Mem Free: ").append(StringUtils.getHumanReadableByteCount(memoryInfo.availMem)).append("\n"); 125 | builder.append("Mem Heap: ").append(am.getMemoryClass()).append("M\n"); 126 | builder.append("Mem Low: ").append(memoryInfo.lowMemory).append("\n"); 127 | Display display = getWindowManager().getDefaultDisplay(); 128 | DisplayMetrics dm = new DisplayMetrics(); 129 | //DisplayMetrics dm = getResources().getDisplayMetrics(); 130 | display.getMetrics(dm); 131 | 132 | int statusBarHeightDp = ViewUtils.getStatusBarHeightInDp(this); 133 | int systemBarHeightDp = ViewUtils.getSystemBarHeightInDp(this); 134 | int statusBarHeight = ViewUtils.getStatusBarHeight(this); 135 | int systemBarHeight = ViewUtils.getSystemBarHeight(this); 136 | Point point = getScreenRawSize(display); 137 | builder.append("statusBarHeightDp: ").append(statusBarHeightDp).append("\n"); 138 | builder.append("systemBarHeightDp: ").append(systemBarHeightDp).append("\n"); 139 | builder.append("statusBarHeightPx: ").append(statusBarHeight).append("\n"); 140 | builder.append("systemBarHeightPx: ").append(systemBarHeight).append("\n"); 141 | builder.append("screenWidth: ").append(point.x).append("\n"); 142 | builder.append("screenHeight: ").append(point.y).append("\n"); 143 | builder.append("WindowWidth: ").append(dm.widthPixels).append("\n"); 144 | builder.append("WindowHeight: ").append(dm.heightPixels).append("\n"); 145 | builder.append(toString2(dm)); 146 | builder.append("\n"); 147 | addSection(builder.toString()); 148 | } 149 | 150 | private void addBuildConfigSection() { 151 | StringBuilder builder = new StringBuilder(); 152 | builder.append("[BuildConfig]\n"); 153 | builder.append(toString(BuildConfig.class)); 154 | builder.append("\n"); 155 | addSection(builder.toString()); 156 | } 157 | 158 | private void addBuildPropsSection() { 159 | StringBuilder builder = new StringBuilder(); 160 | builder.append("[System]\n"); 161 | builder.append(toString(Build.VERSION.class)); 162 | builder.append(toString(Build.class)); 163 | builder.append("\n"); 164 | addSection(builder.toString()); 165 | } 166 | 167 | private LayoutParams TEXT_VIEW_LP = new LayoutParams(LayoutParams.MATCH_PARENT, 168 | LayoutParams.WRAP_CONTENT); 169 | 170 | private void addSection(CharSequence text) { 171 | TextView tv = new TextView(this); 172 | tv.setLayoutParams(TEXT_VIEW_LP); 173 | tv.setText(text); 174 | tv.setTextIsSelectable(true); 175 | mContainer.addView(tv); 176 | } 177 | 178 | 179 | @SuppressLint("NewApi") 180 | public static Point getScreenRawSize(Display display) { 181 | if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { 182 | Point outPoint = new Point(); 183 | DisplayMetrics metrics = new DisplayMetrics(); 184 | display.getRealMetrics(metrics); 185 | outPoint.x = metrics.widthPixels; 186 | outPoint.y = metrics.heightPixels; 187 | return outPoint; 188 | } else { 189 | Point outPoint = new Point(); 190 | Method mGetRawH; 191 | try { 192 | mGetRawH = Display.class.getMethod("getRawHeight"); 193 | Method mGetRawW = Display.class.getMethod("getRawWidth"); 194 | outPoint.x = (Integer) mGetRawW.invoke(display); 195 | outPoint.y = (Integer) mGetRawH.invoke(display); 196 | return outPoint; 197 | } catch (Throwable e) { 198 | return new Point(0, 0); 199 | } 200 | } 201 | } 202 | 203 | public static String toString(Class clazz) { 204 | StringBuilder builder = new StringBuilder(); 205 | final String newLine = System.getProperty("line.separator"); 206 | Field[] fields = clazz.getDeclaredFields(); 207 | for (Field field : fields) { 208 | field.setAccessible(true); 209 | String fieldName = field.getName(); 210 | if (Modifier.isStatic(field.getModifiers())) { 211 | LogUtils.v(TAG, "filed:" + fieldName); 212 | try { 213 | Object fieldValue = field.get(null); 214 | builder.append(fieldName).append(": ").append(fieldValue).append(newLine); 215 | } catch (Exception ex) { 216 | ex.printStackTrace(); 217 | } 218 | } 219 | } 220 | return builder.toString(); 221 | } 222 | 223 | public static String toString2(Object object) { 224 | Class clazz = object.getClass(); 225 | StringBuilder builder = new StringBuilder(); 226 | final String newLine = System.getProperty("line.separator"); 227 | Field[] fields = clazz.getDeclaredFields(); 228 | for (Field field : fields) { 229 | field.setAccessible(true); 230 | String fieldName = field.getName(); 231 | if (!Modifier.isStatic(field.getModifiers())) { 232 | LogUtils.v(TAG, "filed:" + fieldName); 233 | try { 234 | Object fieldValue = field.get(object); 235 | builder.append(fieldName).append(": ").append(fieldValue).append(newLine); 236 | } catch (Exception ex) { 237 | ex.printStackTrace(); 238 | } 239 | } 240 | } 241 | return builder.toString(); 242 | } 243 | 244 | } 245 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mcxiaoke/packer/samples/ViewUtils.java: -------------------------------------------------------------------------------- 1 | package com.mcxiaoke.packer.samples; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.annotation.TargetApi; 5 | import android.app.Activity; 6 | import android.content.Context; 7 | import android.graphics.Point; 8 | import android.os.Build; 9 | import android.os.Build.VERSION_CODES; 10 | import android.util.DisplayMetrics; 11 | import android.util.TypedValue; 12 | import android.view.Display; 13 | import android.view.View; 14 | import android.widget.ProgressBar; 15 | import android.widget.RelativeLayout; 16 | 17 | import java.lang.reflect.Method; 18 | 19 | /** 20 | * User: mcxiaoke 21 | * Date: 14-3-26 22 | * Time: 16:08 23 | */ 24 | public class ViewUtils { 25 | 26 | public static ProgressBar createProgress(Context context) { 27 | ProgressBar p = new ProgressBar(context); 28 | p.setIndeterminate(true); 29 | RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(40, 40); 30 | lp.addRule(RelativeLayout.CENTER_IN_PARENT); 31 | p.setLayoutParams(lp); 32 | return p; 33 | } 34 | 35 | // This intro hides the system bars. 36 | @TargetApi(VERSION_CODES.KITKAT) 37 | public static void hideSystemUI(Activity activity) { 38 | // Set the IMMERSIVE flag. 39 | // Set the content to appear under the system bars so that the content 40 | // doesn't resize when the system bars hideSelf and show. 41 | View decorView = activity.getWindow().getDecorView(); 42 | decorView.setSystemUiVisibility( 43 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 44 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 45 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 46 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hideSelf nav bar 47 | | View.SYSTEM_UI_FLAG_FULLSCREEN // hideSelf status bar 48 | | View.SYSTEM_UI_FLAG_IMMERSIVE 49 | ); 50 | } 51 | 52 | // This intro shows the system bars. It does this by removing all the flags 53 | // except for the ones that make the content appear under the system bars. 54 | @TargetApi(VERSION_CODES.KITKAT) 55 | public static void showSystemUI(Activity activity) { 56 | View decorView = activity.getWindow().getDecorView(); 57 | decorView.setSystemUiVisibility( 58 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 59 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 60 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 61 | ); 62 | } 63 | 64 | /** 65 | * 23 * Returns true if view's layout direction is right-to-left. 66 | * 24 * 67 | * 25 * @param view the View whose layout is being considered 68 | * 26 69 | */ 70 | @SuppressLint("NewApi") 71 | public static boolean isLayoutRtl(View view) { 72 | if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { 73 | return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 74 | } else { 75 | // All layouts are LTR before JB MR1. 76 | return false; 77 | } 78 | } 79 | 80 | @SuppressLint("NewApi") 81 | public static Point getScreenRawSize(Display display) { 82 | if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { 83 | Point outPoint = new Point(); 84 | DisplayMetrics metrics = new DisplayMetrics(); 85 | display.getRealMetrics(metrics); 86 | outPoint.x = metrics.widthPixels; 87 | outPoint.y = metrics.heightPixels; 88 | return outPoint; 89 | } else { 90 | Point outPoint = new Point(); 91 | Method mGetRawH; 92 | try { 93 | mGetRawH = Display.class.getMethod("getRawHeight"); 94 | Method mGetRawW = Display.class.getMethod("getRawWidth"); 95 | outPoint.x = (Integer) mGetRawW.invoke(display); 96 | outPoint.y = (Integer) mGetRawH.invoke(display); 97 | return outPoint; 98 | } catch (Throwable e) { 99 | return new Point(0, 0); 100 | } 101 | } 102 | } 103 | 104 | public static int getActionBarHeightInDp(Context context) { 105 | int actionBarHeight = 0; 106 | TypedValue tv = new TypedValue(); 107 | final DisplayMetrics dm = context.getResources().getDisplayMetrics(); 108 | if (Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { 109 | if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, 110 | true)) 111 | actionBarHeight = (int) TypedValue.complexToFloat(tv.data); 112 | } else { 113 | tv.data = 48; 114 | actionBarHeight = (int) TypedValue.complexToFloat(tv.data); 115 | } 116 | return actionBarHeight; 117 | } 118 | 119 | public static int getActionBarHeight(Context context) { 120 | int actionBarHeight = 0; 121 | TypedValue tv = new TypedValue(); 122 | final DisplayMetrics dm = context.getResources().getDisplayMetrics(); 123 | if (Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { 124 | if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, 125 | true)) 126 | actionBarHeight = TypedValue.complexToDimensionPixelSize( 127 | tv.data, dm); 128 | } else { 129 | tv.data = 48; 130 | actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, 131 | dm); 132 | } 133 | return actionBarHeight; 134 | } 135 | 136 | public static int getStatusBarHeight(Context context) { 137 | int result = 0; 138 | int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); 139 | if (resourceId > 0) { 140 | 141 | result = context.getResources().getDimensionPixelSize(resourceId); 142 | } 143 | return result; 144 | } 145 | 146 | public static int getSystemBarHeight(Context context) { 147 | int result = 0; 148 | int resourceId = context.getResources().getIdentifier("system_bar_height", "dimen", "android"); 149 | 150 | if (resourceId > 0) { 151 | result = context.getResources().getDimensionPixelSize(resourceId); 152 | } 153 | return result; 154 | } 155 | 156 | public static int getStatusBarHeightInDp(Context context) { 157 | int result = 0; 158 | int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); 159 | if (resourceId > 0) { 160 | result = getResourceValue(context, resourceId); 161 | } 162 | return result; 163 | } 164 | 165 | public static int getSystemBarHeightInDp(Context context) { 166 | int result = 0; 167 | int resourceId = context.getResources().getIdentifier("system_bar_height", "dimen", "android"); 168 | if (resourceId > 0) { 169 | result = getResourceValue(context, resourceId); 170 | } 171 | return result; 172 | } 173 | 174 | // temp variable 175 | private static TypedValue mTmpValue = new TypedValue(); 176 | 177 | /** 178 | * 获取资源中的数值,没有经过转换,比如dp,sp等 179 | */ 180 | public static int getResourceValue(Context context, int resId) { 181 | TypedValue value = mTmpValue; 182 | context.getResources().getValue(resId, value, true); 183 | return (int) TypedValue.complexToFloat(value.data); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/drawer_shadow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/src/main/res/drawable-hdpi/drawer_shadow.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/src/main/res/drawable-hdpi/ic_drawer.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/drawer_shadow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/src/main/res/drawable-mdpi/drawer_shadow.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/src/main/res/drawable-mdpi/ic_drawer.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/drawer_shadow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/src/main/res/drawable-xhdpi/drawer_shadow.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/src/main/res/drawable-xhdpi/ic_drawer.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/drawer_shadow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/src/main/res/drawable-xxhdpi/drawer_shadow.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/src/main/res/drawable-xxhdpi/ic_drawer.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcxiaoke/gradle-packer-plugin/052db0fbeeb9ece40bf61effdf5dbb37655db2d8/sample/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/layout/act_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /sample/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | 8 | 240dp 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PackerSample 5 | 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':plugin' 2 | -------------------------------------------------------------------------------- /test-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./deploy-local.sh 3 | echo "test clean build" 4 | cd sample 5 | ../gradlew clean build --refresh-dependencies --stacktrace $1 $2 6 | cd .. 7 | -------------------------------------------------------------------------------- /test-market.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./deploy-local.sh 3 | echo "test market and archives apk build." 4 | cd sample 5 | ../gradlew -Pmarket=markets.txt clean assembleRelease assembleBeta archiveApkRelease archiveApkBeta --refresh-dependencies --stacktrace $1 $2 6 | cd .. 7 | --------------------------------------------------------------------------------