When working on an Android project, which is a
platform for creating applications for viewing video content, it is became
necessary to dynamically configure product flavors with the transfer of
information about signing configs to an external file.

 Initial data

There is an
Android project, which is a platform for creating applications for viewing
video content. The code base is common for all applications, the differences
are in the settings of the REST API parameters and the settings for the
appearance of the application (banners, colors, fonts, etc.). Three flavor
dimension were used in the project:

  • market: google or amazon. Because
    applications are distributed both on Google Play and in the Amazon Marketplace,
    there is a need to share some functionality depending on the distribution
    place. For example: Amazon prohibits the use of In-App Purchases mechanism from
    Google and requires the implementation of its mechanism.
  • endpoint: “pro” or
    “staging”. Specific configurations for production and staging
    versions.
  • site: the actual dimension for a particular
    application. Set applicationId and signingConfig.

 The problems we encountered

When
creating a new application, it was necessary to add Product Flavor:

application1 { dimension 'site' applicationId 'com.damsols.application1' signingConfig signingConfigs.application1 }

Also, it was
necessary to add the appropriate Signing Config:

application1 { storeFile file("path_to_keystore1.jks") storePassword "password1" keyAlias "application1" keyPassword "password1" }

Problems:

  • five lines to add one application, differing only in
    applicationId and signingConfig. When the number of applications became more
    than 50, the build.gradle file began to contain more than 500 lines of
    application information.
  • storage in plain-text information about keystore for
    signing applicat

Removing Certificate
Information

 The first step was to transfer the certificate
information to a separate json file. For example, information is also stored in
plain-text, but nothing prevents you from storing the file in encrypted form
(we use GPG) and decrypting it directly during application build. The JSON file
has the following structure:

 { "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" }, ] }

Simplify Product Flavors
Sections

 To reduce the number of lines required to
describe Product Flavor with dimension = “site”, an array was created
with the necessary information to describe a specific application, and all
Product Flavors with dimension = “site” were deleted.

It was

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

It became

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

Dynamic creation of
Product Flavors

 The last step was to dynamically create product
flavors and signing configs using an external JSON file with certificate
information from the applicationDefinitions array.

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

To add
reading from encrypted storage, you need to replace the section

def signKeysFile = file('signkeys/signkeys.json') def signKeys = new JsonSlurper().parseText(signKeysFile.text) def configs = signKeys.signingConfigs

to read from an encrypted file.