- 
                Notifications
    
You must be signed in to change notification settings  - Fork 1
 
Cheat sheet
        Boby Tanev edited this page Nov 15, 2016 
        ·
        2 revisions
      
    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 
getSecretinto 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")inandroidblock - update git ignore with "localSecrets.prperties"
 - build local: debug and release
 - push to GitHub
 
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_releaseStorePassORG_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.ymlfile 
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.ymland trigger adevelopbuild - download apk and install it manually on device
 
adb install app-release.apk
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 
Applicationclass - 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
 - 
afterEvaluategradle hook - update 
localSecrets.prpertiesfile 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.ymlwith crashlytics distribution 
deployment:
  beta:
    branch: [develop]
    commands:
      - ./gradlew crashlyticsUploadDistributionRelease
  prod:
    branch: [master]
    commands:
      - ./gradlew crashlyticsUploadDistributionRelease
- add circleci env variables:
 ORG_GRADLE_PROJECT_fabricApiKeyORG_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
 
Result: The binaries distributed via Crashlytics Beta will have proper build version and dev changelog
- edit 
gradle.propertiesand add (we'll use those values to generated version code/name): 
versionMajor=1
versionMinor=0
versionPatch=0
- edit 
build.gradleand add indefaultConfigsection: 
    defaultConfig {
        ...
        versionCode getBuildNumber()
        versionName getVersionAsString()
        ...
    }
- update 
gradleHelper.gradlewith: 
/**
 *
 * @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
 
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_namefromstringsfirst) - 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
 
Result: debug builds only dev helpful libraries added. Stetho. Also non fatal exceptions and logs will be sync'd in Crashlytics
- create 
debugvariant directory undersrcand 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.gradeadd 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