Android: Creating Dynamic Product Flavors and Signing Configs

  • Tutorial

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


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 of the application's appearance (banners, colors, fonts, etc.). Three flavor dimension are used in the project:


  1. market : "google" or "amazon". Since Applications are distributed in both Google Play and Amazon Marketplace, there is a need to share some functionality depending on the distribution location. For example: Amazon prohibits the use of the In-App Purchases mechanism from Google and requires the implementation of its mechanism.
  2. endpoint : "pro" or "staging". Specific configurations for production and staging versions.
  3. site : dimension itself for a particular application. It is set by applicationId and signingConfig.

Problems we are facing


When creating a new application, you had to add a 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:


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

Build.gradle example
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'
}

Take out information about certificates


The first step was to take the information about the certificates into a separate json file. For example, the information is also stored in plain-text, but nothing prevents you from storing the file in an encrypted form (we use GPG) and decrypt directly during the assembly of the application. 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"
      },
   ]
}

The signingConfigs section in the build.gradle file is deleted.


Simplify Product Flavors Section


To reduce the number of lines required to describe a 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'
        }        
    }
    defapplicationDefinitions = [
        ['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 information about certificates from the applicationDefinitions array.


defapplicationDefinitions = [
        ['name': 'application1', 'applicationId': 'com.damsols.application1'],
        ['name': 'application2', 'applicationId': 'com.damsols.application2'],
        ['name': 'application3', 'applicationId': 'com.damsols.application3']
]
defsignKeysFile = file('signkeys/signkeys.json')defsignKeys = newJsonSlurper().parseText(signKeysFile.text)
defconfigs = signKeys.signingConfigsdefsigningConfigsMap = [:]
configs.each { config ->
    signingConfigsMap[config.configName] = config
}
applicationDefinitions.each { applicationDefinition ->
    defsigningConfig = 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 a read from the encrypted storage, you need to replace the section


defsignKeysFile = file('signkeys/signkeys.json')defsignKeys = newJsonSlurper().parseText(signKeysFile.text)
defconfigs = signKeys.signingConfigs

to read from an encrypted file.


build.gradle entirely
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'
        }
    }
}
defapplicationDefinitions = [
        ['name': 'application1', 'applicationId': 'com.damsols.application1'],
        ['name': 'application2', 'applicationId': 'com.damsols.application2'],
        ['name': 'application3', 'applicationId': 'com.damsols.application3']
]
defsignKeysFile = file('signkeys/signkeys.json')defsignKeys = newJsonSlurper().parseText(signKeysFile.text)
defconfigs = signKeys.signingConfigsdefsigningConfigsMap = [:]
configs.each { config ->
    signingConfigsMap[config.configName] = config
}
applicationDefinitions.each { applicationDefinition ->
    defsigningConfig = 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 link


Thank!


Also popular now: