├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main └── java └── App.java /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 1.8 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | *.iml 4 | .gradle 5 | build 6 | bin 7 | .classpath 8 | .project 9 | .settings -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Java build 2 | script: 3 | - ./gradlew clean assemble 4 | 5 | # cache gradle deps 6 | before_cache: 7 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 8 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 9 | cache: 10 | directories: 11 | - "$HOME/.gradle/caches/" 12 | - "$HOME/.gradle/wrapper/" 13 | 14 | # Deploy on tags 15 | deploy: 16 | provider: releases 17 | api_key: 18 | secure: GhUeRWclhr3riosQexpoQwNze3Aw65rAUYvpeOJo0O5Jt4qKJKDHJSDkXOPSQfD33Cswv7JICNqfHYHQW30SfAmDHAizX934HGgk8pY/fBUGO/Q3my9edDnlMEdu05djtqIm2OwHgVeMZtwFPujlsLwxKM1XmulpcQUTo87XJMhCpK69I0eKEJI3eBFMDPoFPFQxgB611N+cGVMb53Y9o9Mr5Yzsmvi7gGY0XDHbvQP0dKom+9f2UNQTu81b9iJmgvoJ1FyRryKNPWd9GgLZ8/CpkwuwwsbUBknrinj6fsKxxL3E9khhk9OwrJJgXZK5BvarkA1hsyJOdivmzRTutvj1OA5Q7JAybvaqZog8h4x9dJLxEMNVTvPOWXkX7yQrZUleF1l/TriPp6rIubQFdTeTxfN6KRKkGpPApNrlTb0nhNUjno2Z7ncRAr+yh1rL316VW9xJiGB6OpXu0qwPqKselaGuRMO5V9jHrK/Jh4l/7oiaeNTdGvHIn7ycZCAoRACuktiLwWwFIYS0PUUBMrCdohlgaHoYpUZvh83bUeGVltXyc3zKx7hY8ULAROkjbhLOcHDy23mBymCgipzeLpC425MvK3dpxtBgZnVStdcKWsxlXt2VLZW6Pm0WDS/F4ciNV/mLHqKgox56sTGByo06JScvvKFb6EirxhsZLpk= 19 | file: "./build/distributions/*" 20 | skip_cleanup: true 21 | file_glob: true 22 | on: 23 | repo: codebysd/java-play-store-uploader 24 | tags: true 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 S.D. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/codebysd/java-play-store-uploader.svg?branch=master)](https://travis-ci.org/codebysd/java-play-store-uploader) 2 | [![Open issues](https://img.shields.io/github/issues/codebysd/java-play-store-uploader.svg)](https://github.com/codebysd/java-play-store-uploader/issues) 3 | [![Pull requests](https://img.shields.io/github/issues-pr/codebysd/java-play-store-uploader.svg)](https://github.com/codebysd/java-play-store-uploader/pulls) 4 | [![GitHub issues](https://img.shields.io/github/release/codebysd/java-play-store-uploader.svg)](https://github.com/codebysd/java-play-store-uploader/releases) 5 | 6 | 7 | # Play Store Uploader 8 | 9 | This is a simple tool to upload android apk builds to play store. 10 | Suitable for automation of play store uploads in a CI system. 11 | 12 | ## Requirements 13 | 14 | 1. Java (JRE) 8 or above 15 | 16 | ## Install 17 | 18 | 1. Download the latest zip or tar archive from [Releases Section](https://github.com/codebysd/java-play-store-uploader/releases). 19 | 2. Unpack archive contents to any location. 20 | 3. Execute the binaries in `bin` directory. On unix use `PlayStoreUploader`, while on windows use `PlayStoreUploader.bat`. 21 | 22 | ## Usage 23 | 24 | ### 1. Setup Play Store 25 | 26 | Ensure that the app is created on Play Store. Setup Play Store listing and other required information so that release management is enabled and new releases can be published. It is advised to do a manual upload and release at least once in the beginning. 27 | 28 | ### 2. Setup Release Tracks 29 | 30 | Play Store allows to upload apps on release tracks like alpha, beta and production. Enable and setup the track you want to use, on Play Store console. 31 | 32 | ### 3. Get Service Account Key 33 | 34 | To access Play Store API a JSON key file is needed. On Play Store, goto API Access section and configure a service account with Play Store API access. Download and save the JSON credentials of this account. 35 | 36 | ### 4. Build apk file 37 | 38 | Build signed production android apk file to upload. In case of a CI server, this file should be already generated. 39 | 40 | ### 4. Run Upload Command 41 | 42 | Execute the binary, passing required data in arguments. 43 | 44 | ```bash 45 | PlayStoreUploader -key "key.json" -apk "app.apk" -track "alpha" -name "myApp" -notes "new release" 46 | ``` 47 | 48 | #### CLI Options 49 | 50 | Running without any arguments will print available argument options. 51 | 52 | ```bash 53 | Options: 54 | -apk VAL : The apk file to upload 55 | -key VAL : JSON key file of authorized service account 56 | -name VAL : (optional) App name on Play Store (defaults to name in apk) 57 | -notes VAL : (optional) Release notes 58 | -notesFile VAL : (optional) Release notes from file 59 | -track VAL : Release track to use. Eg. alpha, beta, production etc 60 | ``` 61 | 62 | ## Development 63 | 64 | To build: 65 | 66 | ```bash 67 | ./gradlew clean assemble 68 | ``` 69 | 70 | To run: 71 | 72 | ```bash 73 | ./gradlew run --args "...arguments" 74 | ``` 75 | 76 | Pull requests and suggestions are welcome. 77 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * Plugins 3 | */ 4 | plugins { 5 | id 'java' 6 | id 'application' 7 | id 'com.github.onslip.gradle-one-jar' version '1.0.5' 8 | } 9 | 10 | /** 11 | * Project Info 12 | */ 13 | sourceCompatibility = 1.8 14 | targetCompatibility = 1.8 15 | version = getVersion() 16 | mainClassName = 'App' 17 | 18 | /** 19 | * Repositories 20 | */ 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | /** 26 | * Dependencies 27 | */ 28 | dependencies { 29 | compile 'com.google.apis:google-api-services-androidpublisher:v3-rev20200223-1.30.9' 30 | compile 'com.google.auth:google-auth-library-oauth2-http:0.20.0' 31 | compile 'com.google.api-client:google-api-client:1.30.5' 32 | compile 'args4j:args4j:2.33' 33 | compile 'net.dongliu:apk-parser:2.6.2' 34 | } 35 | 36 | 37 | /** 38 | * Get version from last git tag. 39 | * @returns Last git tag 40 | */ 41 | def getVersion(){ 42 | def out = new ByteArrayOutputStream(); 43 | exec { 44 | executable = 'git' 45 | args = ['describe', '--tags'] 46 | standardOutput = out 47 | } 48 | return out.toString().replace('\n','') 49 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codebysd/java-play-store-uploader/5af9a3e3354d0c6e2635cd483f04c59c36572ff9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="-Xmx128m -Dfile.encoding=UTF-8" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 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 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS=-Xmx128m -Dfile.encoding=UTF-8 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'PlayStoreUploader' -------------------------------------------------------------------------------- /src/main/java/App.java: -------------------------------------------------------------------------------- 1 | import java.io.FileInputStream; 2 | import java.io.IOException; 3 | import java.nio.file.FileSystems; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.Locale; 10 | 11 | import com.google.api.client.json.JsonFactory; 12 | import com.google.api.client.json.jackson2.JacksonFactory; 13 | 14 | 15 | import com.google.api.client.http.HttpRequestInitializer; 16 | import com.google.api.client.http.HttpTransport; 17 | import com.google.api.client.http.javanet.NetHttpTransport; 18 | import com.google.api.client.http.AbstractInputStreamContent; 19 | import com.google.api.client.http.FileContent; 20 | import com.google.api.services.androidpublisher.AndroidPublisher; 21 | import com.google.api.services.androidpublisher.AndroidPublisherScopes; 22 | import com.google.api.services.androidpublisher.model.Apk; 23 | import com.google.api.services.androidpublisher.model.AppEdit; 24 | import com.google.api.services.androidpublisher.model.LocalizedText; 25 | import com.google.api.services.androidpublisher.model.Track; 26 | import com.google.api.services.androidpublisher.model.TrackRelease; 27 | 28 | import com.google.auth.http.HttpCredentialsAdapter; 29 | import com.google.auth.oauth2.GoogleCredentials; 30 | import com.google.common.collect.ImmutableList; 31 | import org.kohsuke.args4j.CmdLineException; 32 | import org.kohsuke.args4j.CmdLineParser; 33 | import org.kohsuke.args4j.Localizable; 34 | import org.kohsuke.args4j.Option; 35 | 36 | import net.dongliu.apk.parser.ApkFile; 37 | import net.dongliu.apk.parser.bean.ApkMeta; 38 | 39 | /** 40 | * Uploads android apk files to Play Store. 41 | */ 42 | public class App { 43 | private static final String MIME_TYPE_APK = "application/vnd.android.package-archive"; 44 | 45 | @Option(name = "-key", required = true, usage = "JSON key file of authorized service account") 46 | private String jsonKeyPath; 47 | 48 | @Option(name = "-name", usage = "(optional) App name on Play Store (defaults to name in apk)") 49 | private String appName; 50 | 51 | @Option(name = "-apk", required = true, usage = "The apk file to upload") 52 | private String apkPath; 53 | 54 | @Option(name = "-track", required = true, usage = "Release track to use. Eg. alpha, beta, production etc") 55 | private String trackName; 56 | 57 | @Option(name = "-notes", forbids = "-notesFile", usage = "(optional) Release notes") 58 | private String notes; 59 | 60 | @Option(name = "-notesFile", forbids = "-notes", usage = "(optional) Release notes from file") 61 | private String notesPath; 62 | 63 | @Option(name = "-proxyHost", forbids = "", usage = "(optional) Configure the proxy address") 64 | private String proxyHost; 65 | 66 | @Option(name = "-proxyPort", forbids = "", usage = "(optional) Configure the proxy port") 67 | private String proxyPort; 68 | 69 | /** 70 | * Entry point 71 | * 72 | * @param args process arguments 73 | */ 74 | public static void main(String... args) { 75 | 76 | try { 77 | // do upload 78 | new App().parseArgs(args).upload(); 79 | } catch (Exception e) { 80 | // log message and exit with bad code 81 | System.err.println(); 82 | System.err.println("ERROR: " + e.getMessage()); 83 | System.exit(2); 84 | } 85 | } 86 | 87 | /** 88 | * Construct localized version on message 89 | * 90 | * @param message message 91 | * @return localized version 92 | */ 93 | private Localizable localize(String message) { 94 | return new Localizable() { 95 | 96 | @Override 97 | public String formatWithLocale(Locale locale, Object... args) { 98 | return String.format(locale, message, args); 99 | } 100 | 101 | @Override 102 | public String format(Object... args) { 103 | return String.format(message, args); 104 | } 105 | }; 106 | } 107 | 108 | /** 109 | * Parse process arguments. 110 | * 111 | * @param args process arguments 112 | * @throws Exception argumentss error 113 | * @return {@link App} instance 114 | */ 115 | private App parseArgs(String... args) throws CmdLineException { 116 | // init parser 117 | CmdLineParser parser = new CmdLineParser(this); 118 | 119 | try { 120 | // must have args 121 | if (args == null || args.length < 1) { 122 | String msg = "No arguments given"; 123 | throw new CmdLineException(parser, this.localize(msg), msg); 124 | } 125 | 126 | // parse args 127 | parser.parseArgument(args); 128 | } catch (CmdLineException e) { 129 | // print usage and forward error 130 | System.err.println("Invalid arguments."); 131 | System.err.println("Options:"); 132 | parser.printUsage(System.err); 133 | throw e; 134 | } 135 | 136 | // return instance 137 | return this; 138 | } 139 | 140 | /** 141 | * Perform apk upload an release on given track 142 | * 143 | * @throws Exception Upload error 144 | */ 145 | private void upload() throws Exception { 146 | // configure proxy 147 | if (this.proxyHost != null && !this.proxyHost.isEmpty()) { 148 | System.setProperty("https.proxyHost", this.proxyHost); 149 | } 150 | 151 | if (this.proxyPort != null && !this.proxyPort.isEmpty()) { 152 | System.setProperty("https.proxyPort", this.proxyPort); 153 | } 154 | 155 | // load key file credentials 156 | System.out.println("Loading account credentials..."); 157 | Path jsonKey = FileSystems.getDefault().getPath(this.jsonKeyPath).normalize(); 158 | GoogleCredentials credentials = GoogleCredentials 159 | .fromStream(new FileInputStream(jsonKey.toFile())) 160 | .createScoped(Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER)); 161 | 162 | // load apk file info 163 | System.out.println("Loading apk file information..."); 164 | Path apkFile = FileSystems.getDefault().getPath(this.apkPath).normalize(); 165 | ApkFile apkInfo = new ApkFile(apkFile.toFile()); 166 | ApkMeta apkMeta = apkInfo.getApkMeta(); 167 | final String applicationName = this.appName == null ? apkMeta.getName() : this.appName; 168 | final String packageName = apkMeta.getPackageName(); 169 | System.out.println(String.format("App Name: %s", apkMeta.getName())); 170 | System.out.println(String.format("App Id: %s", apkMeta.getPackageName())); 171 | System.out.println(String.format("App Version Code: %d", apkMeta.getVersionCode())); 172 | System.out.println(String.format("App Version Name: %s", apkMeta.getVersionName())); 173 | apkInfo.close(); 174 | 175 | // load release notes 176 | System.out.println("Loading release notes..."); 177 | List releaseNotes = new ArrayList(); 178 | if (this.notesPath != null) { 179 | Path notesFile = FileSystems.getDefault().getPath(this.notesPath).normalize(); 180 | String notesContent = new String(Files.readAllBytes(notesFile)); 181 | releaseNotes.add(new LocalizedText().setLanguage(Locale.US.toString()).setText(notesContent)); 182 | } else if (this.notes != null) { 183 | releaseNotes.add(new LocalizedText().setLanguage(Locale.US.toString()).setText(this.notes)); 184 | } 185 | 186 | // init publisher 187 | System.out.println("Initialising publisher service..."); 188 | HttpTransport transport = new NetHttpTransport(); 189 | JsonFactory jsonFactory = new JacksonFactory(); 190 | HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(credentials); 191 | AndroidPublisher.Builder ab = new AndroidPublisher.Builder(transport, jsonFactory, requestInitializer); 192 | AndroidPublisher publisher = ab.setApplicationName(applicationName).build(); 193 | 194 | // create an edit 195 | System.out.println("Initialising new edit..."); 196 | AppEdit edit = publisher.edits().insert(packageName, null).execute(); 197 | final String editId = edit.getId(); 198 | System.out.println(String.format("Edit created. Id: %s", editId)); 199 | 200 | try { 201 | // upload the apk 202 | System.out.println("Uploading apk file..."); 203 | AbstractInputStreamContent apkContent = new FileContent(MIME_TYPE_APK, apkFile.toFile()); 204 | Apk apk = publisher.edits().apks().upload(packageName, editId, apkContent).execute(); 205 | System.out.println(String.format("Apk uploaded. Version Code: %s", apk.getVersionCode())); 206 | 207 | // create a release on track 208 | System.out.println(String.format("On track:%s. Creating a release...", this.trackName)); 209 | 210 | List apkVersionCodes = ImmutableList.of(Long.valueOf(apk.getVersionCode())); 211 | 212 | Track track = new Track() 213 | .setTrack(this.trackName) 214 | .setReleases( 215 | Collections.singletonList( 216 | new TrackRelease() 217 | .setName("Automated upload") 218 | .setVersionCodes(apkVersionCodes) 219 | .setStatus("completed") 220 | .setReleaseNotes(releaseNotes))); 221 | 222 | AndroidPublisher.Edits.Tracks.Update update = publisher.edits().tracks().update(packageName, editId, this.trackName, track); 223 | update.execute(); 224 | 225 | System.out.println(String.format("Release created on track: %s", this.trackName)); 226 | 227 | // commit edit 228 | System.out.println("Commiting edit..."); 229 | publisher.edits().commit(packageName, editId).execute(); 230 | System.out.println(String.format("Success. Commited Edit id: %s", editId)); 231 | 232 | // Success 233 | } catch (Exception e) { 234 | // error message 235 | String msg = "Operation Failed: " + e.getMessage(); 236 | 237 | // abort 238 | System.err.println("Opertaion failed due to an error!, Deleting edit..."); 239 | try { 240 | publisher.edits().delete(packageName, editId).execute(); 241 | } catch (Exception e2) { 242 | // log abort error as well 243 | msg += "\nFailed to delete edit: " + e2.getMessage(); 244 | } 245 | 246 | // forward error with message 247 | throw new IOException(msg, e); 248 | } 249 | } 250 | } --------------------------------------------------------------------------------