Главная » Хабрахабр » Android: создание динамических Product Flavors и Signing Configs

Android: создание динамических Product Flavors и Signing Configs

Подробности под катом. При работе над Android-проектом, представляющий собой платформу для создания приложений для просмотра видео-контента, возникла необходимость динамического конфигурирования product flavors с выносом информации о signing configs во внешний файл.

Исходные данные

Кодовая база общая для всех приложений, различия заключаются в настройках параметров REST API и настройках внешнего вида приложения (баннеры, цвета, шрифты и т.д.). Имеется Android-проект, представляющий собой платформу для создания приложений для просмотра видео-контента. В проекте использованы три flavor dimension:

  1. market: "google" или "amazon". Т.к. приложения распространяются как в Google Play, так и в Amazon Marketplace, имеется необходимость разделять некоторый функционал в зависимости от места распространения. Например: Amazon запрещает использование In-App Purchases механизма от Google и требует реализацию своего механизма.
  2. endpoint: "pro" или "staging". Специфические конфигурации для production и staging версий.
  3. 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"
}

Проблемы:

  1. пять строк для добавления одного приложения, отличающегося только applicationId и signingConfig. Когда количество приложений стало больше 50, build.gradle файл стал содержать более 500 строк информации о приложениях.
  2. хранение в plain-text информации о keystore для подписи приложений.

Пример build.gradle

apply plugin: 'com.android.application' android 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'
}

Вынос информации о сертификатах

Для примера информация так-же хранится в plain-text, но ничего не мешает хранить файл в зашифрованном виде (мы используем GPG) и расшифровывать непосредственно во время сборки приложения. Первым шагом был вынос информации о сертификатах в отдельный json-файл. 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

на чтение из зашифрованного файла.

build.gradle целиком

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'
}

Ссылка на GitHub

Спасибо!


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Как в Сингапуре работают с инновациями: от госрегулирования до ночных клубов

Привет, Хабр! Меня зовут Сергей Лукашкин, я директор по управлению проектами в управлении цифровой трансформации ДИТ ВТБ. Много лет занимаюсь финтехом и инновациями в финансовой сфере. Конечно, уделяю много времени изучению лучших российских и зарубежных финтех-практик. В мире есть несколько ...

[Из песочницы] Ticket to Ride.Европа — скромные шаги в арифметику игры

День первый. Нам подарили игру «Ticket to ride. Европа». Это моё первое знакомство с игрой данной серии, надо обязательно попробовать и заценить. Как-то надоело регулярно проигрывать, пора бы призвать на помощь математику и попробовать таким образом одержать заслуженную победу. День ...