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.