Автоматизация публикации приложения в Google Play — прямо из Android Studio
Но я с радостью готов сообщить, что это — не правда. Публиковать приложение прямо из Android Studio можно! Более того, можно делать это вообще без Android Studio на вашем CI — так как делаться это будет с помощью обычного Gradle task.
Мое решение похоже на то, что описано в предыдущей статье, но вместо java я использовал groovy-скрипт.
Для того, чтобы публиковать приложения из скрипта, нужно создать пользователя с доступом для публикации и получить .json-файл, который будем использовать в нашем коде для аутентификации. Как его получить и что нужно сделать для активации доступа к Google Play Developer API можно посмотреть в указанной статье, или же можете прочитать мою публикацию о работе с Google Play Billing на стороне сервера, где в части 3 описано создание Service-account-ов для доступа к Google Play.
С данного момента будем считать, что у вас уже есть заветный .json файлик с service account secret.
Для начала подготовим нам проект. Будем работать с build.gradle нашего корневого проекта, а не app. Приведем root/build.gradle к такому виду:
// активируем груви в нашем проекте
apply plugin: 'groovy'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.2'
}
}
allprojects {
repositories {
jcenter()
}
}
dependencies {
// импортируем груви в наш проект
compile 'org.codehaus.groovy:groovy-all:2.4.7'
// и библиотеку, с помощью которой будем делать publish
compile 'com.google.apis:google-api-services-androidpublisher:v2-rev38-1.22.0'
}
Что сделано:
1. apply plugin: 'groovy'
Активируем groovy-компилятор в нашем проекте.
2. dependencies — compile 'org.codehaus.groovy: groovy-all:2.4.7'
Импортируем последнюю версию groovy в наше проект
3. dependencies — compile 'com.google.apis: google-api-services-androidpublisher: v2-rev38–1.22.0'
Импортируем библиотеку от Google, которая, собственно, и предоставляет нам возможность работать с публикациями в Google play (и не только).
Теперь мы можем писать groovy-скрипты и groovy-классы и использовать их в нашем проекте.
Но для начала создадим source dir для наших groovy-классов и организуем другие файлы, которые нам будут нужны:
root/
app/
... наше приложение
.gitignore - добавим файл keystore.jks сюда, чтобы не хранить его в репозитории
keystore.jks
build.gradle
gradle/
iam/
.gitignore - добавим файл publisher.json сюда, чтобы не хранить его в репозитории
publisher.json - файл с service account secret
src/
main/
groovy/
... тут будем писать классы и скрипты
.gitignore - добавим файл signing.properties сюда, чтобы не хранить его в репозитории
build.gradle
gradle.properties
gradlew
gradlew.bat
local.properties
signing.properties - тут будем хранить пароли нашего keystore
settings.gradle
Для того, чтобы опубликовать приложение в Google Play, нужно подписать его release сертификатом. Но ведь мы не хотим хранить наш keystore, явки и пароли в репозитории? Ипользуйте .gitignore. Сами же пароли поместим в файл root/signing.properties:
keystore.file=keystore.jks
keystore.password=<пароль>
key.alias=<имя_ключа>
key.password=<пароль_ключа>
Прочитаем эти пароли из файла с создадим подходящий signing config в root/app/build.gradle
...
android {
...
Properties signingProperties = new Properties()
def file = project.rootProject.file('signing.properties')
if (fixe.exists()) {
signingProperties.load(file.newDataInputStream())
}
def prodSigning_keystoreFile = properties.getProperty('keystore.file')
def prodSigning_keystorePassword = properties.getProperty('keystore.password')
def prodSigning_keyAlias = properties.getProperty('key.alias')
def prodSigning_keyPassword = properties.getProperty('key.password')
...
signingConfigs {
...
production {
storeFile file(prodSigning_keystoreFile )
storePassword prodSigning_keystorePassword
keyAlias prodSigning_keyAlias
keyPassword prodSigning_keyPassword
}
}
productFlavors {
...
prod {
...
}
}
buildTypes {
...
release {
signingConfig production
}
}
}
Теперь мы можем использовать gradle assembleProdRelease чтобы получить apk-файл, который к загрузке в Google Play.
Приступим к созданию самого скрипта, который опубликует наш apk. Создадим файл root/src/main/groovy/ApkPublisher.groovy:
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
import com.google.api.client.http.FileContent
import com.google.api.client.json.jackson2.JacksonFactory
import com.google.api.services.androidpublisher.AndroidPublisher
import com.google.api.services.androidpublisher.AndroidPublisherScopes
import com.google.api.services.androidpublisher.model.Track
class ApkPublisher {
// имя пакета
String packageName;
// имя приложения (в теории оно необязательно, но без него будут warnings)
String name;
// имя apk.файла
String apkName;
// имя файла proguard-mapping
String mappingName;
void publish() {
assert packageName != null
assert name != null
assert apkName != null
assert mappingName != null
println "PUBLISHING [ ${packageName} / ${name} ]"
def dir = new File("assemble")
// загрузка service account secret для аутентификации
def inputStream = new FileInputStream("iam/publisher.json");
def transport = GoogleNetHttpTransport.newTrustedTransport();
def credential = GoogleCredential.fromStream(inputStream)
.createScoped(Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER));
def builder = new AndroidPublisher.Builder(transport,
JacksonFactory.getDefaultInstance(), credential);
builder.setApplicationName(name)
def androidPublisher = builder.build();
def edits = androidPublisher.edits();
// создаем запрос на редактирование
def editRequest = edits.insert(packageName, null);
def edit = editRequest.execute();
// получаем уникальный id, который нужен нам чтобы выполнять другие действия
final String editId = edit.getId();
println " - edit_id = ${editId}"
// выполняем загрузку apk
def apkFilePath = new File(dir, apkName)
println " - apk file = ${apkFilePath}"
def apkFile = new FileContent("application/vnd.android.package-archive", apkFilePath);
def apkUploadRequest = edits.apks().upload(packageName, editId, apkFile);
def apkUploadResult = apkUploadRequest.execute();
// в ответе получаем текущий verfsionCode
int versionCode = apkUploadResult.getVersionCode()
println " - version code ${versionCode} has been uploaded"
// загружаем proguard mapping
def mappingFilePath = new File(dir, mappingName)
println " - mapping file = ${mappingFilePath}"
def mappingFile = new FileContent("application/octet-stream", mappingFilePath);
def mappingUploadRequest = edits.deobfuscationfiles()
.upload(packageName, editId, versionCode, "proguard", mappingFile);
mappingUploadRequest.execute();
println " - mapping for version ${versionCode} has been uploaded"
// теперь нужно опубликовать загруженный apk
// в данном примере мы публикуем его в альфа-тестирование
List apkVersionCodes = [versionCode]
def track = new Track().setVersionCodes(apkVersionCodes)
def updateTrackRequest = edits.tracks().update(packageName, editId, "alpha", track);
def updatedTrack = updateTrackRequest.execute();
println " - track code ${updatedTrack.getTrack()} has been updated"
// после того, как все действия выполнены
// нужно подтвердить, что запрос на редактирование завершен и "закоммитить"
// его, как транзакцию
def commitRequest = edits.commit(packageName, editId);
def appEdit = commitRequest.execute();
println " - app edit with id ${appEdit.getId()} has been comitted"
println "APP [ ${packageName} / ${name} / v${versionCode} ] SUCCESSFULLY PUBLISHED"
}
}
Второй файл root/src/main/groovy/PublishApk.groovy:
def void moveToAssemble(String folder, String name, String newName) {
def from = new File(folder, name)
def to = new File("assemble", newName)
from.renameTo(to)
println "moved ${from} to ${to}"
}
// переместим файлы в папку root/assemble
// предварительно создадим ее если ее не было
// и удалим старые файлы, если они там были
def destDir = new File("assemble")
destDir.mkdir()
for (def item : destDir.listFiles()) {
item.delete()
}
moveToAssemble("app/build/outputs/apk", "app-prod-release.apk", "myapp.apk")
moveToAssemble("app/build/outputs/mapping/prod/release", "mapping.txt", "myapp-mapping.txt")
// а теперь опубликуем приложение
new ApkPublisher(
packageName: "com.example.myapp",
name: "My app",
apkName: "myapp.apk",
mappingName: "myapp-mapping.txt"
).publish()
Скрипт для загрузки файла готов. Теперь перейдем к созданию Gradle task:
root/build.gradle
// запускаем сборку apk
task assembleApk(dependsOn: [
':app:assembleProdRelease'
]) << {
println("APK assembled")
}
// компилируем и выполняем скрипт
task publishApk(dependsOn: 'classes', type: JavaExec) {
main = 'PublishApk'
classpath = sourceSets.main.runtimeClasspath
}
task assembleAndPublishApk() {
dependsOn 'assembleApk'
dependsOn 'publishApk'
tasks.findByName('publishApk').mustRunAfter 'assembleApk'
doLast {
println("APK successfilly published, find it in /assemble dir")
}
}
Теперь достаточно выполнить команду gradle assembleAndPublishApk для публикации apk в альфа-канал. И это можно легко сделать хоть после каждого коммита в development. В дополнение мы сразу загружаем proguard-mapping файл.
P.S. Что еще почитать?
1. Мою предыдущую статью — Android In-app Billing: от мобильного приложения до серверной валидации и тестирования
2. Google Play Developer API reference
3. Пример от Google на GitHub