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


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


Имеется Android-проект, представляющий собой платформу для создания приложений для просмотра видео-контента. Кодовая база общая для всех приложений, различия заключаются в настройках параметров REST API и настройках внешнего вида приложения (баннеры, цвета, шрифты и т.д.). В проекте использованы три 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 {
    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

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


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


Спасибо!

Комментарии (4)


  1. petrovichtim
    03.01.2019 05:04

    Спасибо, всё не доберусь перебрать градл ) Может в этом году руки дойдут.


  1. punksta
    03.01.2019 11:18

    Это ведь не единтсвенные проблемы в подобных платформах. Бывает, что нужно еще менять название приложений, иконки лаунчеров, ресурсы самих приложений. И мастабировать не на 50, а на 5000 приложений. Вот тогда начинается хардкор ввиде билд серверов, серверов с базами для этих данных и прочее


    1. tychinasg Автор
      03.01.2019 11:21

      Flavor как раз и необходим для изменения названия, иконок, ресурсов и подобного. С масштабированием согласен, но мы пока что до такой задачи не дошли.


      1. punksta
        03.01.2019 12:15

        Я не говорил, что это плохое решение, просто рефлексия.