import java.io.FileInputStream; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.http.AbstractInputStreamContent; import com.google.api.client.http.FileContent; import com.google.api.services.androidpublisher.AndroidPublisher; import com.google.api.services.androidpublisher.AndroidPublisherScopes; import com.google.api.services.androidpublisher.model.Apk; import com.google.api.services.androidpublisher.model.AppEdit; import com.google.api.services.androidpublisher.model.LocalizedText; import com.google.api.services.androidpublisher.model.Track; import com.google.api.services.androidpublisher.model.TrackRelease; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.Localizable; import org.kohsuke.args4j.Option; import net.dongliu.apk.parser.ApkFile; import net.dongliu.apk.parser.bean.ApkMeta; /** * Uploads android apk files to Play Store. */ public class App { private static final String MIME_TYPE_APK = "application/vnd.android.package-archive"; @Option(name = "-key", required = true, usage = "JSON key file of authorized service account") private String jsonKeyPath; @Option(name = "-name", usage = "(optional) App name on Play Store (defaults to name in apk)") private String appName; @Option(name = "-apk", required = true, usage = "The apk file to upload") private String apkPath; @Option(name = "-track", required = true, usage = "Release track to use. Eg. alpha, beta, production etc") private String trackName; @Option(name = "-notes", forbids = "-notesFile", usage = "(optional) Release notes") private String notes; @Option(name = "-notesFile", forbids = "-notes", usage = "(optional) Release notes from file") private String notesPath; @Option(name = "-proxyHost", forbids = "", usage = "(optional) Configure the proxy address") private String proxyHost; @Option(name = "-proxyPort", forbids = "", usage = "(optional) Configure the proxy port") private String proxyPort; /** * Entry point * * @param args process arguments */ public static void main(String... args) { try { // do upload new App().parseArgs(args).upload(); } catch (Exception e) { // log message and exit with bad code System.err.println(); System.err.println("ERROR: " + e.getMessage()); System.exit(2); } } /** * Construct localized version on message * * @param message message * @return localized version */ private Localizable localize(String message) { return new Localizable() { @Override public String formatWithLocale(Locale locale, Object... args) { return String.format(locale, message, args); } @Override public String format(Object... args) { return String.format(message, args); } }; } /** * Parse process arguments. * * @param args process arguments * @throws Exception argumentss error * @return {@link App} instance */ private App parseArgs(String... args) throws CmdLineException { // init parser CmdLineParser parser = new CmdLineParser(this); try { // must have args if (args == null || args.length < 1) { String msg = "No arguments given"; throw new CmdLineException(parser, this.localize(msg), msg); } // parse args parser.parseArgument(args); } catch (CmdLineException e) { // print usage and forward error System.err.println("Invalid arguments."); System.err.println("Options:"); parser.printUsage(System.err); throw e; } // return instance return this; } /** * Perform apk upload an release on given track * * @throws Exception Upload error */ private void upload() throws Exception { // configure proxy if (this.proxyHost != null && !this.proxyHost.isEmpty()) { System.setProperty("https.proxyHost", this.proxyHost); } if (this.proxyPort != null && !this.proxyPort.isEmpty()) { System.setProperty("https.proxyPort", this.proxyPort); } // load key file credentials System.out.println("Loading account credentials..."); Path jsonKey = FileSystems.getDefault().getPath(this.jsonKeyPath).normalize(); GoogleCredential cred = GoogleCredential.fromStream(new FileInputStream(jsonKey.toFile())); cred = cred.createScoped(Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER)); // load apk file info System.out.println("Loading apk file information..."); Path apkFile = FileSystems.getDefault().getPath(this.apkPath).normalize(); ApkFile apkInfo = new ApkFile(apkFile.toFile()); ApkMeta apkMeta = apkInfo.getApkMeta(); final String applicationName = this.appName == null ? apkMeta.getName() : this.appName; final String packageName = apkMeta.getPackageName(); System.out.println(String.format("App Name: %s", apkMeta.getName())); System.out.println(String.format("App Id: %s", apkMeta.getPackageName())); System.out.println(String.format("App Version Code: %d", apkMeta.getVersionCode())); System.out.println(String.format("App Version Name: %s", apkMeta.getVersionName())); apkInfo.close(); // load release notes System.out.println("Loading release notes..."); List<LocalizedText> releaseNotes = new ArrayList<LocalizedText>(); if (this.notesPath != null) { Path notesFile = FileSystems.getDefault().getPath(this.notesPath).normalize(); String notesContent = new String(Files.readAllBytes(notesFile)); releaseNotes.add(new LocalizedText().setLanguage(Locale.US.toString()).setText(notesContent)); } else if (this.notes != null) { releaseNotes.add(new LocalizedText().setLanguage(Locale.US.toString()).setText(this.notes)); } // init publisher System.out.println("Initialising publisher service..."); AndroidPublisher.Builder ab = new AndroidPublisher.Builder(cred.getTransport(), cred.getJsonFactory(), cred); AndroidPublisher publisher = ab.setApplicationName(applicationName).build(); // create an edit System.out.println("Initialising new edit..."); AppEdit edit = publisher.edits().insert(packageName, null).execute(); final String editId = edit.getId(); System.out.println(String.format("Edit created. Id: %s", editId)); try { // upload the apk System.out.println("Uploading apk file..."); AbstractInputStreamContent apkContent = new FileContent(MIME_TYPE_APK, apkFile.toFile()); Apk apk = publisher.edits().apks().upload(packageName, editId, apkContent).execute(); System.out.println(String.format("Apk uploaded. Version Code: %s", apk.getVersionCode())); // create a release on track System.out.println(String.format("On track:%s. Creating a release...", this.trackName)); TrackRelease release = new TrackRelease().setName("Automated upload").setStatus("completed") .setVersionCodes(Collections.singletonList((long) apk.getVersionCode())) .setReleaseNotes(releaseNotes); Track track = new Track().setReleases(Collections.singletonList(release)); track = publisher.edits().tracks().update(packageName, editId, this.trackName, track).execute(); System.out.println(String.format("Release created on track: %s", this.trackName)); // commit edit System.out.println("Commiting edit..."); edit = publisher.edits().commit(packageName, editId).execute(); System.out.println(String.format("Success. Commited Edit id: %s", editId)); // Success } catch (Exception e) { // error message String msg = "Operation Failed: " + e.getMessage(); // abort System.err.println("Opertaion failed due to an error!, Deleting edit..."); try { publisher.edits().delete(packageName, editId).execute(); } catch (Exception e2) { // log abort error as well msg += "\nFailed to delete edit: " + e2.getMessage(); } // forward error with message throw new IOException(msg, e); } } }