При работе над Android-проектом, представляющий собой платформу для создания приложений для просмотра видео-контента, возникла необходимость динамического конфигурирования product flavors с выносом информации о signing configs во внешний файл. Подробности под катом.
Исходные данные
Имеется Android-проект, представляющий собой платформу для создания приложений для просмотра видео-контента. Кодовая база общая для всех приложений, различия заключаются в настройках параметров REST API и настройках внешнего вида приложения (баннеры, цвета, шрифты и т.д.). В проекте использованы три flavor dimension:
- market: "google" или "amazon". Т.к. приложения распространяются как в Google Play, так и в Amazon Marketplace, имеется необходимость разделять некоторый функционал в зависимости от места распространения. Например: Amazon запрещает использование In-App Purchases механизма от Google и требует реализацию своего механизма.
- endpoint: "pro" или "staging". Специфические конфигурации для production и staging версий.
- site: собственно dimension для конкретного приложения. Задается applicationId и signingConfig.
Проблемы с которыми мы столкнулись
При создании нового приложения необходимо было добавить Product Flavor:
application1 {
dimension 'site'
applicationId 'com.damsols.application1'
signingConfig signingConfigs.application1
}
Также, необходимо было добавить соответствующий Signing Config:
application1 {
storeFile file("path_to_keystore1.jks")
storePassword "password1"
keyAlias "application1"
keyPassword "password1"
}
Проблемы:
- пять строк для добавления одного приложения, отличающегося только applicationId и signingConfig. Когда количество приложений стало больше 50, build.gradle файл стал содержать более 500 строк информации о приложениях.
- хранение в plain-text информации о keystore для подписи приложений.
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 23
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
}
}
flavorDimensions "site", "endpoint", "market"
signingConfigs {
application1 {
storeFile file("application1.jks")
storePassword "password1"
keyAlias "application1"
keyPassword "password1"
}
application2 {
storeFile file("application2.jks")
storePassword "password2"
keyAlias "application2"
keyPassword "password2"
}
application3 {
storeFile file("application3.jks")
storePassword "password3"
keyAlias "application3"
keyPassword "password3"
}
}
productFlavors {
pro {
dimension 'endpoint'
}
staging {
dimension 'endpoint'
}
google {
dimension 'market'
}
amazon {
dimension 'market'
}
application1 {
dimension 'site'
applicationId "com.damsols.application1"
signingConfig signingConfigs.application1
}
application2 {
dimension 'site'
applicationId "com.damsols.application2"
signingConfig signingConfigs.application2
}
application3 {
dimension 'site'
applicationId "com.damsols.application3"
signingConfig signingConfigs.application3
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
}
Вынос информации о сертификатах
Первым шагом был вынос информации о сертификатах в отдельный json-файл. Для примера информация так-же хранится в plain-text, но ничего не мешает хранить файл в зашифрованном виде (мы используем GPG) и расшифровывать непосредственно во время сборки приложения. JSON-файл имеет следующую структуру:
{
"signingConfigs":[
{
"configName":"application1",
"storeFile":"application1.jks",
"storePassword":"password1",
"keyAlias":"application1",
"keyPassword":"password1"
},
{
"configName":"application2",
"storeFile":"application2.jks",
"storePassword":"password2",
"keyAlias":"application2",
"keyPassword":"password2"
},
{
"configName":"application3",
"storeFile":"application3.jks",
"storePassword":"password3",
"keyAlias":"application3",
"keyPassword":"password3"
},
]
}
Секцию signingConfigs в build.gradle файле удаляем.
Упрощение Product Flavors секции
Для сокращения количества строк, необходимых для описания Product Flavor с dimension = "site", был создан массив с необходимой информацией для описания конкретного приложения, а все Product Flavors с dimension="site" были удалены.
Было:
...
productFlavors {
pro {
dimension 'endpoint'
}
staging {
dimension 'endpoint'
}
google {
dimension 'market'
}
amazon {
dimension 'market'
}
application1 {
dimension 'site'
applicationId "com.damsols.application1"
signingConfig signingConfigs.application1
}
application2 {
dimension 'site'
applicationId "com.damsols.application2"
signingConfig signingConfigs.application2
}
application3 {
dimension 'site'
applicationId "com.damsols.application3"
signingConfig signingConfigs.application3
}
}
}
...
Стало:
...
productFlavors {
pro {
dimension 'endpoint'
}
staging {
dimension 'endpoint'
}
google {
dimension 'market'
}
amazon {
dimension 'market'
}
}
def applicationDefinitions = [
['name': 'application1', 'applicationId': 'com.damsols.application1'],
['name': 'application2', 'applicationId': 'com.damsols.application2'],
['name': 'application3', 'applicationId': 'com.damsols.application3']
]
}
...
Динамическое создание Product Flavors
Последним шагом оставалось динамически создавать product flavors и signing configs используя внешний JSON-файл с информацией о сертификатах из массива applicationDefinitions.
def applicationDefinitions = [
['name': 'application1', 'applicationId': 'com.damsols.application1'],
['name': 'application2', 'applicationId': 'com.damsols.application2'],
['name': 'application3', 'applicationId': 'com.damsols.application3']
]
def signKeysFile = file('signkeys/signkeys.json')
def signKeys = new JsonSlurper().parseText(signKeysFile.text)
def configs = signKeys.signingConfigs
def signingConfigsMap = [:]
configs.each { config ->
signingConfigsMap[config.configName] = config
}
applicationDefinitions.each { applicationDefinition ->
def signingConfig = signingConfigsMap[applicationDefinition['name']]
android.productFlavors.create(applicationDefinition['name'], { flavor ->
flavor.dimension = 'site'
flavor.applicationId = applicationDefinition['applicationId']
flavor.signingConfig = android.signingConfigs.create(applicationDefinition['name'])
flavor.signingConfig.storeFile = file(signingConfig.storeFile)
flavor.signingConfig.storePassword = signingConfig.storePassword
flavor.signingConfig.keyAlias = signingConfig.keyAlias
flavor.signingConfig.keyPassword = signingConfig.keyPassword
})
}
Для добавления чтения из зашифрованного хранилища необходимо заменить секцию
def signKeysFile = file('signkeys/signkeys.json')
def signKeys = new JsonSlurper().parseText(signKeysFile.text)
def configs = signKeys.signingConfigs
на чтение из зашифрованного файла.
import groovy.json.JsonSlurper
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 23
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
}
}
flavorDimensions "site", "endpoint", "market"
signingConfigs {}
productFlavors {
pro {
dimension 'endpoint'
}
staging {
dimension 'endpoint'
}
google {
dimension 'market'
}
amazon {
dimension 'market'
}
}
}
def applicationDefinitions = [
['name': 'application1', 'applicationId': 'com.damsols.application1'],
['name': 'application2', 'applicationId': 'com.damsols.application2'],
['name': 'application3', 'applicationId': 'com.damsols.application3']
]
def signKeysFile = file('signkeys/signkeys.json')
def signKeys = new JsonSlurper().parseText(signKeysFile.text)
def configs = signKeys.signingConfigs
def signingConfigsMap = [:]
configs.each { config ->
signingConfigsMap[config.configName] = config
}
applicationDefinitions.each { applicationDefinition ->
def signingConfig = signingConfigsMap[applicationDefinition['name']]
android.productFlavors.create(applicationDefinition['name'], { flavor ->
flavor.dimension = 'site'
flavor.applicationId = applicationDefinition['applicationId']
flavor.signingConfig = android.signingConfigs.create(applicationDefinition['name'])
flavor.signingConfig.storeFile = file(signingConfig.storeFile)
flavor.signingConfig.storePassword = signingConfig.storePassword
flavor.signingConfig.keyAlias = signingConfig.keyAlias
flavor.signingConfig.keyPassword = signingConfig.keyPassword
})
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
}
Спасибо!
Автор: Сергей Тычина