Всем привет! Пол года назад на хабре была статья о том, как автоматизировать загрузку обновлений приложения в Google Play. Первый комментарий к статье и ответ на него гласил одну неприятную вещь:
Но я с радостью готов сообщить, что это — не правда. Публиковать приложение прямо из 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
ссылка на оригинал статьи https://habrahabr.ru/post/316302/
Добавить комментарий