使用Google Play Publishing API 上传aab

1,515 阅读5分钟

App开发完成后通常会上架到应用商店,海外一般上架GooglePlay。除了直接在Google Play Console后台操作外,还可以使用Google Play Developer Publishing API上传aab。本文介绍如何使用Google Play Developer Publishing API上传aab。

官方文档

前置步骤

1. 开启API权限

  • 在Google Play Console后台设置中找到API权限,选择关联项目。
1684548134760.png
  • 选择关联已有的Google Cloud项目或者创建新项目,选择完后保存。
1684548284314.png
  • 关联完之后,可以从此页面打开对应的Google Cloud项目。
1684548540148.png

2. 创建服务账号和密钥

  • 在Google Cloud项目页面选择API和服务。
1684548776890.png
  • 在API和服务页面,选择凭据->创建凭据->服务账号。
1684549061398.png
  • 在创建服务账号页面填写账号名称,点击创建并继续。
1684549248620.png
  • 选择角色,我这边选的是Owner,点击继续。
1684549408491.png
  • 可以选择对其他用户账号授予权限,也可以不填,点击完成。
1684549494415.png
  • 点击已创建的服务账号,进入服务账号页面。
1684550686612.png
  • 在服务账号页面,选择密钥->添加密钥->创建新密钥。
1684550877555.png
  • 在创建私钥页面,选择JSON类型,点击创建,保存生成的JSON文件。
1684550905253.png

3. 在API权限中添加服务账号

  • 返回Google Play Console,在API权限中,选择管理中心权限。
1684551382694.png
  • 在邀请页面中,选中发布版本相关的权限,点击邀请用户。
1684551599904.png

使用Publishing API

官方提供了封装了API请求的库,便于使用,无需自己创建http请求和解析响应。

添加库

在对应模块的build.gradle中添加代码,如下:

dependencies {
    implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20230511-2.0.0")
    implementation("com.google.auth:google-auth-library-oauth2-http:1.16.1")
}

上传aab

注意:在使用API上传aab之前,确保已经在Google Play Console后台任意渠道上传过同包名的aab。

可以发布的渠道:

public class Channel {

    /**
     * 正式版
     */
    public static String PRODUCT = "production";

    /**
     * 开放式测试
     */
    public static String TEST_BETA = "beta";

    /**
     * 封闭式测试
     */
    public static String TEST_ALPHA = "alpha";

    /**
     * 内部测试
     */
    public static String TEST_INTERNAL = "internal";
}

应用发布状态:

public class PublishStatus {

    /**
     * 草稿
     */
    public static final String DRAFT = "draft";

    /**
     * 按阶段发布,同时需要设置发布比例(userFraction)
     */
    public static final String IN_PROGRESS = "inProgress";

    /**
     * 停止按阶段发布
     */
    public static final String HALTED = "halted";

    /**
     * 直接发布
     */
    public static final String COMPLETE = "completed";
}

应用文件类型(现在主要使用aab):

public class FileType {

    public static final String APK = "application/vnd.android.package-archive";

    public static final String AAB = "application/octet-stream";
}

获取凭据、上传aab:

public class UploadAabNormal {

    /**
     * 应用名
     */
    public static final String APPLICATION_NAME = "PublishTest";

    /**
     * 应用包名
     */
    public static final String PACKAGE_NAME = "com.chenyihong.exampledemo";

    /**
     * aab存放路径
     */
    public static final String AAB_FILE_PATH = "ExampleDemo.aab";

    /**
     * 服务账号配置文件存放路径
     */
    public static final String SERVER_CLIENT_PATH = "server_client.json";

    /**
     * 发布的渠道
     */
    public static final String CHOSEN_CHANNEL = Channel.TEST_INTERNAL;

    /**
     * 应用内更新优先级0-5
     */
    public static final int IN_APP_UPDATE_PRIORITY = 0;

    /**
     * 发布状态
     */
    public static final String PUBLISH_STATUS = PublishStatus.DRAFT;

    /**
     * 分阶段发布比例
     */
    public static final double USER_FRACTION = 0.05;

    /**
     * 更新版本名
     */
    public static final String RELEASE_NAME = "test upload from java";

    /**
     * 更新内容文本
     */
    public static final String RELEASE_NOTE = "this is test for upload aab";

    public static void main(String[] args) {
        InputStream serverClientStream = null;
        try {
            ClassLoader classLoader = UploadAabNormal.class.getClassLoader();
            if (classLoader == null) {
                return;
            }
            serverClientStream = classLoader.getResourceAsStream(SERVER_CLIENT_PATH);
            URL aabResource = classLoader.getResource(AAB_FILE_PATH);
            if (serverClientStream != null && aabResource != null) {
                // 获取凭据
                GoogleCredentials googleCredentials = GoogleCredentials.fromStream(serverClientStream).createScoped(Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER));
                if (googleCredentials != null) {
                    if (googleCredentials.getAccessToken() == null) {
                        googleCredentials.refresh();
                    }
                    AndroidPublisher publisher = new AndroidPublisher.Builder(GoogleNetHttpTransport.newTrustedTransport(), GsonFactory.getDefaultInstance(), new HttpCredentialsAdapter(googleCredentials))
                            .setApplicationName(APPLICATION_NAME)
                            .build();
                    // 创建一个新的更改
                    AndroidPublisher.Edits edits = publisher.edits();
                    AndroidPublisher.Edits.Insert editRequest = edits.insert(PACKAGE_NAME, null);
                    String editId = editRequest.execute().getId();
                    // 上传aab
                    Bundle uploadBundle = edits.bundles().upload(PACKAGE_NAME, editId, new FileContent(FileType.AAB, new File(aabResource.toURI().getPath()))).execute();
                    Long uploadBundleVersionCode = (long) uploadBundle.getVersionCode();
                    // 设置发布渠道
                    Track track = new Track();
                    track.setTrack(CHOSEN_CHANNEL);
                    // 设置更新相关信息
                    TrackRelease trackRelease = new TrackRelease();
                    // 设置更新版本号
                    trackRelease.setVersionCodes(Collections.singletonList(uploadBundleVersionCode));
                    // 设置更新版本名
                    trackRelease.setName(RELEASE_NAME);
                    // 设置更新内容
                    trackRelease.setReleaseNotes(Collections.singletonList(new LocalizedText().setText(RELEASE_NOTE)));
                    // 设置应用内更新优先级(0-5,等级4、5为强制更新)
                    trackRelease.setInAppUpdatePriority(IN_APP_UPDATE_PRIORITY);
                    // 设置发布状态
                    trackRelease.setStatus(PUBLISH_STATUS);
                    if (PublishStatus.IN_PROGRESS.equals(trackRelease.getStatus()) || PublishStatus.HALTED.equals(trackRelease.getStatus())) {
                        // 分阶段发布时,设置发布比例
                        trackRelease.setUserFraction(USER_FRACTION);
                    }
                    track.setReleases(Collections.singletonList(trackRelease));
                    edits.tracks().update(PACKAGE_NAME, editId, track.getTrack(), track).execute();
                    // 提交此次更改
                    edits.commit(PACKAGE_NAME, editId).execute();
                }
            }
        } catch (GeneralSecurityException | IOException | URISyntaxException e) {
            e.printStackTrace();
        } finally {
            try {
                if (serverClientStream != null) {
                    serverClientStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

上传超时

执行了上面的代码后,发现上传aab报错,超时异常。

1684561117310.png

官方文档和Demo没有相关示例。自己尝试了一番后,得出解决方案如下:

  1. 自定义ConnectionFactory,配置超时时间。
public class TimeoutConnectionFactory implements ConnectionFactory {

    private final Proxy proxy;

    private final int timeout;

    public TimeoutConnectionFactory() {
        this(null, 0);
    }

    public TimeoutConnectionFactory(int timeout) {
        this(null, timeout);
    }

    public TimeoutConnectionFactory(Proxy proxy, int timeout) {
        this.proxy = proxy;
        this.timeout = timeout;
    }

    @Override
    public HttpURLConnection openConnection(URL url) throws IOException, ClassCastException {
        HttpURLConnection urlConnection = (HttpURLConnection) (proxy == null ? url.openConnection() : url.openConnection(proxy));
        if (timeout != 0) {
            // 设置超时时间
            urlConnection.setConnectTimeout(timeout);
            urlConnection.setReadTimeout(timeout);
        }
        return urlConnection;
    }
}
  1. 自定义TimeoutNetHttpTransport,保留GoogleNetHttpTransport配置并配置自定义的ConnectionFactory
public class TimeoutNetHttpTransport {

    public static NetHttpTransport newTimeoutTransport(int timeout) throws GeneralSecurityException, IOException {
        MtlsProvider mtlsProvider = MtlsUtils.getDefaultMtlsProvider();
        KeyStore mtlsKeyStore = null;
        String mtlsKeyStorePassword = null;
        if (mtlsProvider.useMtlsClientCertificate()) {
            mtlsKeyStore = mtlsProvider.getKeyStore();
            mtlsKeyStorePassword = mtlsProvider.getKeyStorePassword();
        }
        if (mtlsKeyStore != null && mtlsKeyStorePassword != null) {
            return new NetHttpTransport.Builder()
                    .trustCertificates(GoogleUtils.getCertificateTrustStore(), mtlsKeyStore, mtlsKeyStorePassword)
                    .setConnectionFactory(new TimeoutConnectionFactory(timeout))
                    .build();
        }
        return new NetHttpTransport.Builder()
                .trustCertificates(GoogleUtils.getCertificateTrustStore())
                .setConnectionFactory(new TimeoutConnectionFactory(timeout))
                .build();
    }
}
  1. 自定义TimeoutCredentialsAdapter,保留HttpCredentialsAdapter配置并配置超时时间。
public class TimeoutCredentialsAdapter implements HttpRequestInitializer, HttpUnsuccessfulResponseHandler {

    private static final Logger LOGGER = Logger.getLogger(HttpCredentialsAdapter.class.getName());

    private static final Pattern INVALID_TOKEN_ERROR = Pattern.compile("\s*error\s*=\s*"?invalid_token"?");

    static final String BEARER_PREFIX = AuthHttpConstants.BEARER + " ";

    private final Credentials credentials;

    private final int timeout;

    public TimeoutCredentialsAdapter(Credentials credentials, int timeout) {
        Preconditions.checkNotNull(credentials);
        this.credentials = credentials;
        this.timeout = timeout;
    }

    public Credentials getCredentials() {
        return credentials;
    }

    @Override
    public void initialize(HttpRequest request) throws IOException {
        request.setUnsuccessfulResponseHandler(this);
        if (timeout != 0) {
            request.setConnectTimeout(timeout);
            request.setReadTimeout(timeout);
            request.setWriteTimeout(timeout);
        }
        if (!credentials.hasRequestMetadata()) {
            return;
        }
        HttpHeaders requestHeaders = request.getHeaders();
        URI uri = null;
        if (request.getUrl() != null) {
            uri = request.getUrl().toURI();
        }
        Map<String, List<String>> credentialHeaders = credentials.getRequestMetadata(uri);
        if (credentialHeaders == null) {
            return;
        }
        for (Map.Entry<String, List<String>> entry : credentialHeaders.entrySet()) {
            String headerName = entry.getKey();
            List<String> requestValues = new ArrayList<>(entry.getValue());
            requestHeaders.put(headerName, requestValues);
        }
    }

    @Override
    public boolean handleResponse(HttpRequest request, HttpResponse response, boolean supportsRetry) throws IOException {
        boolean refreshToken = false;
        boolean bearer = false;

        List<String> authenticateList = response.getHeaders().getAuthenticateAsList();

        // if authenticate list is not null we will check if one of the entries contains "Bearer"
        if (authenticateList != null) {
            for (String authenticate : authenticateList) {
                if (authenticate.startsWith(BEARER_PREFIX)) {
                    // mark that we found a "Bearer" value, and check if there is a invalid_token error
                    bearer = true;
                    refreshToken = INVALID_TOKEN_ERROR.matcher(authenticate).find();
                    break;
                }
            }
        }

        // if "Bearer" wasn't found, we will refresh the token, if we got 401
        if (!bearer) {
            refreshToken = response.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED;
        }

        if (refreshToken) {
            try {
                credentials.refresh();
                initialize(request);
                return true;
            } catch (IOException exception) {
                LOGGER.log(Level.SEVERE, "unable to refresh token", exception);
            }
        }
        return false;
    }
}
  1. 修改获取凭据、上传aab代码(这里只列出需要调整的部分,其余与之前一致)。
public class UploadAab {

    /**
     * 超时时间
     */
    private static final int TIMEOUT = 2 * 60 * 1000;

    public static void main(String[] args) {
        ...
        NetHttpTransport httpTransport = TimeoutNetHttpTransport.newTimeoutTransport(TIMEOUT);
        TimeoutCredentialsAdapter httpCredentialsAdapter = new TimeoutCredentialsAdapter(googleCredentials,TIMEOUT);
        AndroidPublisher publisher = new AndroidPublisher.Builder(httpTransport, GsonFactory.getDefaultInstance(), httpCredentialsAdapter)
                .setApplicationName(APPLICATION_NAME)
                .build();
    }
}

修改完后就可以正常上传了。上传完成后在Google Play Console后台就可以看到刚刚上传的aab。

1684562577258.png

示例

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee