com.drextended.gppublisher.bamboo.util.AndroidPublisherHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.drextended.gppublisher.bamboo.util.AndroidPublisherHelper.java

Source

/*
 *  Copyright Roman Donchenko. All Rights Reserved.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package com.drextended.gppublisher.bamboo.util;

import com.atlassian.bamboo.build.logger.BuildLogger;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.AbstractInputStreamContent;
import com.google.api.client.http.FileContent;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.repackaged.com.google.common.base.Preconditions;
import com.google.api.client.repackaged.com.google.common.base.Strings;
import com.google.api.services.androidpublisher.AndroidPublisher;
import com.google.api.services.androidpublisher.AndroidPublisherScopes;
import com.google.api.services.androidpublisher.model.*;
import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Helper class to initialize the publisher APIs client library.
 * <p>
 * Before making any calls to the API through the client library you need to
 * call the {@link #init()} method.
 * This will run all precondition checks.
 * </p>
 */
public class AndroidPublisherHelper {

    static final String MIME_TYPE_APK = "application/vnd.android.package-archive";
    static final String MIME_TYPE_OCTET_STREAM = "application/octet-stream";
    public static final String TRACK_NONE = "none";
    public static final String TRACK_INTERNAL = "internal";
    public static final String TRACK_ALPHA = "alpha";
    public static final String TRACK_BETA = "beta";
    public static final String TRACK_PRODUCTION = "production";
    public static final String TRACK_ROLLOUT = "rollout";
    public static final String TRACK_CUSTOM = "custom";

    private final File mWorkingDirectory;
    private final BuildLogger mLogger;
    private final String mApplicationName;
    private final String mPackageName;
    private final boolean mFindJsonKeyInFile;
    private final String mJsonKeyPath;
    private final String mJsonKeyContent;
    private final String mApkPath;
    private final String mDeobfuscationFilePath;
    private final String mRecentChangesListings;
    private final String mTrack;
    private final String mRolloutFractionString;
    private String mTrackCustomNames;

    private AndroidPublisher mAndroidPublisher;
    private File mApkFile;
    private File mDeobfuscationFile;
    private List<LocalizedText> mReleaseNotes;
    private Double mRolloutFraction;
    private String[] mCustomTracks;

    /**
     * @param workingDirectory
     * @param buildLogger
     * @param applicationName       The name of your application. If the application name is
     *                              {@code null} or blank, the application will log a warning. Suggested
     *                              format is "MyCompany-Application/1.0".
     * @param packageName           the package name of the app
     * @param findJsonKeyInFile
     * @param jsonKeyPath           the service account secret json file path
     * @param apkPath               the apk/aab file path of the apk/aab to upload
     * @param deobfuscationFilePath the deobfuscation file of the specified APK/AAB
     * @param recentChangesListings the recent changes in format: [BCP47 Language Code]:[recent changes file path].
     *                              Multiple listing thought comma. Sample: en-US:C:\temp\listing_en.txt
     * @param track                 The track for uploading the apk, can be 'alpha', beta', 'production' or 'rollout'
     * @param rolloutFraction       The rollout fraction
     * @param trackCustomNames      Comma separated track names for `custom` track
     */
    public AndroidPublisherHelper(File workingDirectory, BuildLogger buildLogger, String applicationName,
            String packageName, boolean findJsonKeyInFile, String jsonKeyPath, String jsonKeyContent,
            String apkPath, String deobfuscationFilePath, String recentChangesListings, String track,
            String rolloutFraction, String trackCustomNames) {
        mWorkingDirectory = workingDirectory;
        mLogger = buildLogger;
        mApplicationName = applicationName;
        mPackageName = packageName;
        mFindJsonKeyInFile = findJsonKeyInFile;
        mJsonKeyPath = jsonKeyPath;
        mJsonKeyContent = jsonKeyContent;
        mApkPath = apkPath;
        mDeobfuscationFilePath = deobfuscationFilePath;
        mRecentChangesListings = recentChangesListings;
        mTrack = track;
        mRolloutFractionString = rolloutFraction;
        mTrackCustomNames = trackCustomNames;
    }

    /**
     * Performs all necessary setup steps for running requests against the API.
     *
     * @throws GeneralSecurityException
     * @throws IOException
     * @throws IllegalArgumentException
     */
    public void init() throws IOException, GeneralSecurityException, IllegalArgumentException {
        mLogger.addBuildLogEntry("Initializing...");
        Preconditions.checkArgument(!Strings.isNullOrEmpty(mApplicationName),
                "Application name cannot be null or empty!");
        Preconditions.checkArgument(!Strings.isNullOrEmpty(mPackageName), "Package name cannot be null or empty!");
        Preconditions.checkArgument(!Strings.isNullOrEmpty(mTrack), "Track cannot be null or empty!");
        Preconditions.checkArgument(!Strings.isNullOrEmpty(mApkPath), "Apk/aab path cannot be null or empty!");

        if (TRACK_ROLLOUT.equals(mTrack)) {
            try {
                mRolloutFraction = Double.parseDouble(mRolloutFractionString);
            } catch (NumberFormatException ex) {
                throw new IllegalArgumentException(
                        "User fraction cannot be parsed as double: " + mRolloutFractionString);
            }
            if (mRolloutFraction < 0 || mRolloutFraction >= 1) {
                throw new IllegalArgumentException(
                        "User fraction must be in range (0 <= fraction < 1): " + mRolloutFractionString);
            }
        } else if (TRACK_CUSTOM.equals(mTrack)) {
            Preconditions.checkArgument(!Strings.isNullOrEmpty(mTrackCustomNames),
                    "Not specified names for custom tracks!");
            mCustomTracks = mTrackCustomNames.split(",\\s*");
        }

        String apkFullPath = relativeToFullPath(mApkPath);
        mApkFile = new File(apkFullPath);
        Preconditions.checkArgument(mApkFile.exists(), "Apk file not found in path: " + apkFullPath);
        if (!Strings.isNullOrEmpty(mDeobfuscationFilePath)) {
            String deobfuscationFullPath = relativeToFullPath(mDeobfuscationFilePath);
            mDeobfuscationFile = new File(deobfuscationFullPath);
            Preconditions.checkArgument(mDeobfuscationFile.exists(),
                    "Mapping (deobfuscation) file not found in path: " + deobfuscationFullPath);
        }

        final InputStream jsonKeyInputStream;
        if (mFindJsonKeyInFile) {
            Preconditions.checkArgument(!Strings.isNullOrEmpty(mJsonKeyPath),
                    "Secret json key path cannot be null or empty!");
            String jsonKeyFullPath = relativeToFullPath(mJsonKeyPath);
            File jsonKeyFile = new File(jsonKeyFullPath);
            Preconditions.checkArgument(jsonKeyFile.exists(),
                    "Secret json key file not found in path: " + jsonKeyFullPath);
            jsonKeyInputStream = new FileInputStream(jsonKeyFile);
        } else {
            Preconditions.checkArgument(!Strings.isNullOrEmpty(mJsonKeyContent),
                    "Secret json key content cannot be null or empty!");
            jsonKeyInputStream = IOUtils.toInputStream(mJsonKeyContent);
        }

        if (!Strings.isNullOrEmpty(mRecentChangesListings)) {
            String[] rcParts = mRecentChangesListings.trim().split("\\s*,\\s*");
            mReleaseNotes = new ArrayList<LocalizedText>(rcParts.length);
            for (String rcPart : rcParts) {
                String[] rcPieces = rcPart.split("\\s*::\\s*");

                Preconditions.checkArgument(rcPieces.length == 2, "Wrong recent changes entry: " + rcPart);

                String languageCode = rcPieces[0];
                String recentChangesFilePath = relativeToFullPath(rcPieces[1]);
                Preconditions.checkArgument(
                        !Strings.isNullOrEmpty(languageCode) && !Strings.isNullOrEmpty(recentChangesFilePath),
                        "Wrong recent changes entry: " + rcPart + ", lang = " + languageCode + ", path = "
                                + recentChangesFilePath);

                File rcFile = new File(recentChangesFilePath);
                Preconditions.checkArgument(rcFile.exists(), "Recent changes file for language \"" + languageCode
                        + "\" not found in path: " + recentChangesFilePath);

                FileInputStream inputStream = new FileInputStream(rcFile);
                String recentChanges = null;
                try {
                    recentChanges = IOUtils.toString(inputStream);
                } finally {
                    inputStream.close();
                }

                mReleaseNotes.add(new LocalizedText().setLanguage(languageCode).setText(recentChanges));
            }
        }
        mLogger.addBuildLogEntry("Initialized successfully!");

        mLogger.addBuildLogEntry("Creating AndroidPublisher Api Service...");
        JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
        HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
        Credential credential = GoogleCredential.fromStream(jsonKeyInputStream, httpTransport, jsonFactory)
                .createScoped(Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER));
        mAndroidPublisher = new AndroidPublisher.Builder(httpTransport, jsonFactory,
                new RequestInitializer(credential)).setApplicationName(mApplicationName).build();
        mLogger.addBuildLogEntry("AndroidPublisher Api Service created!");
    }

    private String relativeToFullPath(String path) {
        if (path != null && !new File(path).isAbsolute()) {
            return new File(mWorkingDirectory, path).getAbsolutePath();
        }
        return path;
    }

    /**
     * Publishes apk file on Google Play
     * @throws IOException
     * @throws GeneralSecurityException
     * @throws IllegalArgumentException
     */
    public void makeInsertRequest() throws IOException, GeneralSecurityException, IllegalArgumentException {
        Preconditions.checkArgument(mApkFile != null && mApkFile.exists(),
                "Apk file not found in path: " + mApkPath);

        mLogger.addBuildLogEntry("Creating a new edit session...");
        final AndroidPublisher.Edits edits = mAndroidPublisher.edits();
        AndroidPublisher.Edits.Insert editRequest = edits.insert(mPackageName, null);
        AppEdit edit = editRequest.execute();
        final String editId = edit.getId();
        mLogger.addBuildLogEntry(String.format("Created edit session with id: %s", editId));

        Integer apkVersionCode;

        if (mApkPath.endsWith(".apk")) {
            mLogger.addBuildLogEntry("Uploading new apk file...");
            final AbstractInputStreamContent apkFile = new FileContent(AndroidPublisherHelper.MIME_TYPE_APK,
                    mApkFile);
            Apk apk = edits.apks().upload(mPackageName, editId, apkFile).execute();
            apkVersionCode = apk.getVersionCode();
            mLogger.addBuildLogEntry(
                    String.format("Apk file with version code %s has been uploaded!", apkVersionCode));
        } else if (mApkPath.endsWith(".aab")) {
            mLogger.addBuildLogEntry("Uploading new aab file...");
            final AbstractInputStreamContent aabFile = new FileContent(
                    AndroidPublisherHelper.MIME_TYPE_OCTET_STREAM, mApkFile);
            Bundle bundle = edits.bundles().upload(mPackageName, editId, aabFile).execute();
            apkVersionCode = bundle.getVersionCode();
            mLogger.addBuildLogEntry(
                    String.format("App Bundle with version code %s has been uploaded!", apkVersionCode));
        } else {
            mLogger.addBuildLogEntry("File [" + mApkPath + "] is not apk nor aab file!");
            throw new IllegalArgumentException("File [" + mApkPath + "] is not apk nor aab file!");
        }

        if (mDeobfuscationFile != null) {
            mLogger.addBuildLogEntry("Uploading new mapping file...");
            Preconditions.checkArgument(mDeobfuscationFile.exists(),
                    "Mapping (deobfuscation) file not found in path: " + mDeobfuscationFilePath);
            final AbstractInputStreamContent deobfuscationFile = new FileContent(
                    AndroidPublisherHelper.MIME_TYPE_OCTET_STREAM, mDeobfuscationFile);
            edits.deobfuscationfiles().upload(mPackageName, editId, apkVersionCode, "proguard", deobfuscationFile)
                    .execute();
            mLogger.addBuildLogEntry("Mapping has been uploaded!");
        }

        if (TRACK_NONE.equals(mTrack)) {
            mLogger.addBuildLogEntry("Track was not set, so apk will not be assigned to any track...");
        } else if (TRACK_CUSTOM.equals(mTrack)) {
            for (String customTrack : mCustomTracks) {
                assignToTrack(edits, editId, apkVersionCode, customTrack);
            }
        } else {
            assignToTrack(edits, editId, apkVersionCode, mTrack);
        }
        mLogger.addBuildLogEntry("Committing changes for edit...");
        AppEdit appEdit = edits.commit(mPackageName, editId).execute();
        mLogger.addBuildLogEntry(String.format("App edit with id %s has been committed!", appEdit.getId()));
        mLogger.addBuildLogEntry("=\n\n==================\n\n PUBLISHED SUCCESSFUL \n\n==================\n\n");
    }

    private void assignToTrack(AndroidPublisher.Edits edits, String editId, Integer apkVersionCode,
            String trackName) throws IOException {
        mLogger.addBuildLogEntry("Assigning release to the track: " + trackName);

        TrackRelease release = new TrackRelease()
                .setVersionCodes(Collections.singletonList(Long.valueOf(apkVersionCode)))
                .setReleaseNotes(mReleaseNotes);

        if (TRACK_ROLLOUT.equals(trackName)) {
            release = release.setUserFraction(mRolloutFraction).setStatus("inProgress");
        } else {
            release = release.setStatus("completed");
        }

        Track trackContent = new Track().setTrack(trackName).setReleases(Collections.singletonList(release));

        edits.tracks().update(mPackageName, editId, trackName, trackContent).execute();

        mLogger.addBuildLogEntry("Release successfully assigning to the track: " + trackName);
    }
}