App开发完成后通常会上架到应用商店,海外一般上架GooglePlay。除了直接在Google Play Console后台操作外,还可以使用Google Play Developer Publishing API上传aab。本文介绍如何使用Google Play Developer Publishing API上传aab。
前置步骤
1. 开启API权限
- 在Google Play Console后台设置中找到API权限,选择关联项目。
- 选择关联已有的Google Cloud项目或者创建新项目,选择完后保存。
- 关联完之后,可以从此页面打开对应的Google Cloud项目。
2. 创建服务账号和密钥
- 在Google Cloud项目页面选择API和服务。
- 在API和服务页面,选择凭据->创建凭据->服务账号。
- 在创建服务账号页面填写账号名称,点击创建并继续。
- 选择角色,我这边选的是Owner,点击继续。
- 可以选择对其他用户账号授予权限,也可以不填,点击完成。
- 点击已创建的服务账号,进入服务账号页面。
- 在服务账号页面,选择密钥->添加密钥->创建新密钥。
- 在创建私钥页面,选择JSON类型,点击创建,保存生成的JSON文件。
3. 在API权限中添加服务账号
- 返回Google Play Console,在API权限中,选择管理中心权限。
- 在邀请页面中,选中发布版本相关的权限,点击邀请用户。
使用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报错,超时异常。
官方文档和Demo没有相关示例。自己尝试了一番后,得出解决方案如下:
- 自定义
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;
}
}
- 自定义
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();
}
}
- 自定义
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;
}
}
- 修改获取凭据、上传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。
示例
演示代码已在示例Demo中添加。