Skip to content

Cheat sheet

Boby Tanev edited this page Nov 15, 2016 · 2 revisions

Recipe:

Part 1 - GitHub and generated Android project

Result: a generated release buildable Android project pushed into GitHub

  • create GitHub repo (Android + ignore)
  • checkout repo
  • generate new Android project
  • add in build.gradle
    defaultConfig {
        ...
        minSdkVersion 23
        targetSdkVersion 23
        ...
    }

    lintOptions {
        abortOnError false
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }
  • generate signing cert
  • update build.gradle
    signingConfigs {
        release {
            storeFile file("release.jks")
            storePassword project.hasProperty("releaseStorePass") ? releaseStorePass : "n/a"
            keyAlias "j2d"
            keyPassword project.hasProperty("releaseKeyPass") ? releaseKeyPass : "n/a"
        }
    }

    buildTypes {
        debug {

        }

        release {
            signingConfig signingConfigs.release

            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
  • talk about the env variables and how we can configure them
  • option 1: gradle.properties - auto loaded
  • option 2: external git ignored properties file -> How to load it into gradle project?
  • introduce getSecret method
def getSecret(def key, def defaultValue = null) {
    return project.hasProperty(key) ? project.property(key) : defaultValue
}
  • add gradle utils file
apply from: "$rootDir/gradleHelper.gradle"
  • copy getSecret into gradleHelper, show how to "export" the method
/**
 * Get the value for the secret key
 *
 * @param key the key identifier
 * @param defaultValue default value of such key is missing
 * @return the value behind the key or the defaultValue
 */
def getSecret(def key, def defaultValue = null) {
    return project.hasProperty(key) ? project.property(key) : defaultValue
}

// Export methods by turning them into closures
ext {
    getSecret = this.&getSecret
}
  • talk about option 2 - load properties file method
/**
 * Load properties file into the project
 * @param fileName properties file name
 */
def loadProperties(def fileName) {
    def propertiesFile = file(fileName)
    if (!propertiesFile.exists()) {
        println("Properties file missing: " + fileName)
        return
    }

    Properties properties = new Properties()
    properties.load(propertiesFile.newReader("UTF-8"))
    properties.each { property ->
        // all extra properties must be set in: project.ext
        project.ext.set(property.key, property.value)
    }
}
  • execure loadProperties("localSecrets.prperties") in android block
  • update git ignore with "localSecrets.prperties"
  • build local: debug and release
  • push to GitHub

Part 2 - initial CircleCI integration

Result: a release build APK that can be downloaded from CircleCI and installed on Device

  • go to circleci.com and add the project from GitHub
  • add env variables:
  • ORG_GRADLE_PROJECT_releaseStorePass
  • ORG_GRADLE_PROJECT_releaseKeyPass
  • talk about circleci android build: https://circleci.com/docs/android/
  • java8 required for API 24+ build but missing
machine:
  environment:
    GRADLE_OPTS: -Xmx512m
  java:
    version: oraclejdk8
  • installing additional/update android sdk/libs
dependencies:
  pre:
    - echo y | android update sdk --no-ui --all --filter tools
    - echo y | android update sdk --no-ui --all --filter extra-android-m2repository
    - echo y | android update sdk --no-ui --all --filter extra-android-support
    - echo y | android update sdk --no-ui --all --filter extra-google-m2repository
    - echo y | android update sdk --no-ui --all --filter build-tools-25.0.0
    - echo y | android update sdk --no-ui --all --filter android-25
  • create circle.yml file
machine:
  environment:
    GRADLE_OPTS: -Xmx512m
  java:
    version: oraclejdk8

dependencies:
  pre:
    - echo y | android update sdk --no-ui --all --filter tools
    - echo y | android update sdk --no-ui --all --filter extra-android-m2repository
    - echo y | android update sdk --no-ui --all --filter extra-android-support
    - echo y | android update sdk --no-ui --all --filter extra-google-m2repository
    - echo y | android update sdk --no-ui --all --filter build-tools-25.0.0
    - echo y | android update sdk --no-ui --all --filter android-25

test:
  override:
    # assemble app & run unit test(s)
    - ./gradlew clean build test -PdisablePreDex -Pcom.android.build.threadPoolSize=4

    # copy the build outputs to artifacts
    - cp -r app/build/outputs/* $CIRCLE_ARTIFACTS

deployment:
  beta:
    branch: [develop]
    commands:
      - ./gradlew assembleDebug

  prod:
    branch: [master]
    commands:
      - ./gradlew assembleRelease
  • push circle.yml and trigger a develop build
  • download apk and install it manually on device
adb install app-release.apk

Part 3: initial Crashlytics / Fabric integration

Result: release APK installable via Crashlytics Beta

  • what is Fabric / Crashlytics?
  • go to fabric.io and create a team
  • copy api key and secret
  • create android Application class
  • use the fabric Android studio pluging to add the initial Fabric / Crashlytics integration
  • STOP! and remove the hardcoded key from AndroidManifest. Talk about fabric.properties file
  • https://docs.fabric.io/android/fabric/settings/working-in-teams.html#android-projects
  • fabric.properties
  • contains the key and the secret - must not be pushed into the repository(!) what about circleci?
  • generate properties file in gradle
  • afterEvaluate gradle hook
  • update localSecrets.prperties file with fabric key and secret
  • introduce buildFabricPropertiesIfNeeded() method
afterEvaluate {
    loadProperties("localSecrets.properties")
    buildFabricPropertiesIfNeeded()
}

...

/**
 *
 * build fabric properties file, if missing
 */
def buildFabricPropertiesIfNeeded() {
    def propertiesFile = file("fabric.properties")
    if (!propertiesFile.exists()) {
        def commentMessage = "This is autogenerated crashlytics property from system environment to prevent key to be committed to source control."
        ant.propertyfile(file: "fabric.properties", comment: commentMessage) {
            entry(key: "apiSecret", value: getSecret("fabricApiSecret"), operation: "=")
            entry(key: "apiKey", value: getSecret("fabricApiKey"), operation: "=")
        }
    }
}
  • update circle.yml with crashlytics distribution
deployment:
  beta:
    branch: [develop]
    commands:
      - ./gradlew crashlyticsUploadDistributionRelease

  prod:
    branch: [master]
    commands:
      - ./gradlew crashlyticsUploadDistributionRelease
  • add circleci env variables:
  • ORG_GRADLE_PROJECT_fabricApiKey
  • ORG_GRADLE_PROJECT_fabricApiSecret
  • add application id suffux for Debug builds: .debug
  • initial crashlytics gradle config
  • explicitly enable for Release builds
  • explicitly disable for Debug builds - why do we need it do be disabled for debug builds?
        debug {
            applicationIdSuffix ".debug"

            // disable crashlytics
            buildConfigField("boolean", "USE_CRASHLYTICS", "false");
            ext.enableCrashlytics = false
        }

        release {
            signingConfig signingConfigs.release

            // enable crashlytics
            buildConfigField("boolean", "USE_CRASHLYTICS", "true");
            ext.enableCrashlytics = true

            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
  • edit the Application object - only init Crashlytics when enabled
    @Override
    public void onCreate() {
        super.onCreate();

        if (BuildConfig.USE_CRASHLYTICS) {
            Fabric.with(this, new Crashlytics());
            Timber.plant(new CrashReportingTree());
        }
    }
  • push and build
  • first build will fail to publish to Beta because the application is not registered
  • download APK and run manually
  • rebuild -> all successful

Part 3.1: Versioning and changelog

Result: The binaries distributed via Crashlytics Beta will have proper build version and dev changelog

  • edit gradle.properties and add (we'll use those values to generated version code/name):
versionMajor=1
versionMinor=0
versionPatch=0
  • edit build.gradle and add in defaultConfig section:
    defaultConfig {
        ...
        versionCode getBuildNumber()
        versionName getVersionAsString()
        ...
    }
  • update gradleHelper.gradle with:
/**
 *
 * @return CircleCI build branch or "localdev"
 */
def getBranchName() {
    def branch = System.getenv("CIRCLE_BRANCH")
    if (branch?.length() > 0) {
        branch = branch.trim()
    } else {
        // fallback for local dev
        branch = "localdev"
    }

    return branch
}

/**
 *
 * @return the branch name containing only lowercase alphabetic symbols
 */
def getSafeBranchIdentifier() {
    def branch = getBranchName()
    // remove all non alphabetic symbols
    branch = branch.replaceAll("[^a-zA-Z]", "").toLowerCase()

    if ("master".equalsIgnoreCase(branch)) {
        // do not generate suffix for "master" branch
        return ""
    }

    return branch
}

/**
 *
 * @return CI build number or a generated one
 */
def getBuildNumber() {
    def circleBranch = System.getenv("CIRCLE_BUILD_NUM")
    if (circleBranch?.length() > 0) {
        // CircleCI build number
        return circleBranch as int
    }

    // fallback mechanism for non CI env
    return (versionMajor.toInteger() * 10000) + (versionMinor.toInteger() * 100) + (versionPatch.toInteger())
}

/**
 *
 * @return version string identifier (major.minor.buildNumber[-branch])
 */
def getVersionAsString() {
    def buildNumber = getBuildNumber()
    def branchSuffix = getSafeBranchIdentifier()
    if (branchSuffix?.length() > 0) {
        branchSuffix = "-$branchSuffix"
    } else {
        branchSuffix = ""
    }

    return "$versionMajor.$versionMinor.$buildNumber$branchSuffix"
}
  • generate changelog for Crashlytics Beta releases using GitHub commits
            // enable crashlytics
            buildConfigField("boolean", "USE_CRASHLYTICS", "true");
            ext.enableCrashlytics = true
            ext.betaDistributionReleaseNotes = getChangelog()

...

/**
 *
 * @return Generated changelog from the last 10 GitHub commits
 */
def getChangelog() {
    if ("localdev".equalsIgnoreCase(getBranchName())) {
        return "Local development build"
    }

    def logCmd = 'git log --oneline --no-decorate -n 10'
    def logs = logCmd.execute().text.trim()
    def items = []

    if (logs.length() > 0) {
        def lines = logs.split("\n")
        for (int i = 0; i < lines.length; i++) {
            items.add(String.format("%02d) %s", i + 1, lines[i]))
        }
    }

    return items.join("\n")
}
  • push and build

Part 4: Product flavors

Result: 3 new product flavors (with different configurations) added to the project: local, beta, prod. Resulting in 6 different builds 3 * (Debug, Release)

  • add three new product flavors in the build.gradle
    productFlavors {
        localdev {

        }
        beta {

        }
        prod {

        }
    }
  • sync and observe changes
  • do a build gradle refactoring to enable different product flavors:
  • app names (remove app_name from strings first)
  • application identifiers (packages) (dependent on branch and flavor)
  • crashlytics to be enabled only on local flavor
android {
    // explicitly load local secrets here
    loadProperties("localSecrets.properties")

    compileSdkVersion 25
    buildToolsVersion "25.0.0"

    def applicationIdentifier = "com.java2days.j2ddemo"
    def applicationName = "J2D Demo"

    defaultConfig {
        applicationId applicationIdentifier
        minSdkVersion 23
        targetSdkVersion 23
        versionCode getBuildNumber()
        versionName getVersionAsString()
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        // enable crashlytics
        buildConfigField("boolean", "USE_CRASHLYTICS", "true");
        ext.enableCrashlytics = true
        ext.betaDistributionReleaseNotes = getChangelog()

        // app name
        resValue "string", "app_name", applicationName
    }
    productFlavors {
        def branchSuffix = getSafeBranchIdentifier()
        // flavor specific identifier
        def flavorApplicationId = "${applicationIdentifier}${branchSuffix?.length() > 0 ? "." : "" }${branchSuffix}"
        // flavor specific app name
        def flavorApplicationName = "${applicationName}${branchSuffix?.length() > 0 ? "-" : "" }${branchSuffix}"

        localdev {
            applicationId "${flavorApplicationId}.local"

            // disable Crashlytics for local builds
            buildConfigField("boolean", "USE_CRASHLYTICS", "false");
            ext.enableCrashlytics = false

            // app name
            resValue "string", "app_name", flavorApplicationName
        }
        beta {
            // base package.<branch>.beta
            applicationId "${flavorApplicationId}.beta"

            // app name
            resValue "string", "app_name", "$flavorApplicationName Beta"
        }
        prod {
            applicationId flavorApplicationId
        }
    }
    signingConfigs {
        release {
            storeFile file("keystore.jks")
            storePassword getSecret("releaseStorePass", "n/a")
            keyAlias "j2d"
            keyPassword getSecret("releaseKeyPass", "n/a")
        }
    }
    buildTypes {
        debug {
            // explicitly change the package of the debug builds
            applicationIdSuffix ".debug"
        }

        release {
            signingConfig signingConfigs.release

            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    lintOptions {
        abortOnError false
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }
}
  • update circle.yml
deployment:
  beta:
    branch: [develop]
    commands:
      - ./gradlew crashlyticsUploadDistributionBetaRelease

  prod:
    branch: [master]
    commands:
      - ./gradlew crashlyticsUploadDistributionProdRelease
  • push and build
  • first build will fail to publish to Beta because the application is not registered
  • download APK and run manually
  • rebuild -> all successful

Part 5: Debug only libraries

Result: debug builds only dev helpful libraries added. Stetho. Also non fatal exceptions and logs will be sync'd in Crashlytics

  • create debug variant directory under src and add:
  • debug Application class
  • debug AndroidManifest
public class J2DDemoDebugApplication extends J2DDemoApplication {
    @Override
    public void onCreate() {
        super.onCreate();
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          package="com.java2days.j2ddemo">

  <application
      tools:replace="android:name"
      android:name=".J2DDemoDebugApplication" />

</manifest>
  • in build.grade add the following dependencies (Timber (logging) and Stetho as debug only)
dependencies {
    compile 'com.jakewharton.timber:timber:4.3.1'
    // debug lib
    debugCompile 'com.facebook.stetho:stetho:1.4.1'
    debugCompile 'com.facebook.stetho:stetho-timber:1.4.1'
    debugCompile 'com.facebook.stetho:stetho-urlconnection:1.4.1'
    // debugCompile 'com.facebook.stetho:stetho-okhttp3:1.4.1'
}
  • in the debug Application class init Stetho and debug Timber tree
    @Override
    public void onCreate() {
        super.onCreate();

        // init Stetho
        Stetho.initializeWithDefaults(this);

        // init debug Timber
        Timber.plant(new Timber.DebugTree());
        Timber.plant(new StethoTree());
    }
  • edit main Application class. Add special crashlytics timber tree to allow logs and non fatals syncing
public class J2DDemoApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        if (BuildConfig.USE_CRASHLYTICS) {
            Fabric.with(this, new Crashlytics());
            Timber.plant(new CrashReportingTree());
        }
    }

    private static class CrashReportingTree extends Timber.Tree {
        @Override
        protected boolean isLoggable(String tag, int priority) {
            return priority >= Log.INFO;
        }

        @Override protected void log(int priority, String tag, String message, Throwable t) {
            Crashlytics.log(priority, tag, message);
            if (t != null) {
                // log non fatal exception
                Crashlytics.logException(t);
            }
        }
    }
}
  • demo time: show how Stetho works in chrome
  • demo time: show crashlytics exceptions and non fatals