Optimize the size of the coroutines library in Android projects (#1282)

* Includes additional R8 rules to disable debugging & stack-trace recovery in optimized
Android builds. Additional savings with AGP 4.0.0-alpha06 (r8-2.0.4-dev) are ~16kb
in uncompressed DEX size.
* Tests are modified to verify that the classes that are supposed to be removed are
indeed removed.
* Cleaner build logic without error-prone "return" in the middle
* Report the size of optimized Android Dex as teamcity metric
diff --git a/README.md b/README.md
index 9ff957b..42b2b3a 100644
--- a/README.md
+++ b/README.md
@@ -164,11 +164,8 @@
 
 #### R8 and ProGuard
 
-For R8 no actions required, it will take obfuscation rules from the jar.
-
-For Proguard  you need to add options from [coroutines.pro](kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro) to your rules manually.
- 
-R8 is a replacement for ProGuard in Android ecosystem, it is enabled by default since Android gradle plugin 3.4.0 (3.3.0-beta also had it enabled).
+R8 and ProGuard rules are bundled into the [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module.
+For more details see ["Optimization" section for Android](ui/kotlinx-coroutines-android/README.md#optimization). 
 
 ### JS
 
diff --git a/build.gradle b/build.gradle
index 273fd00..67e1e27 100644
--- a/build.gradle
+++ b/build.gradle
@@ -138,10 +138,9 @@
     ignoredPackages += "kotlinx.coroutines.internal"
 }
 
-
+// Configure repositories
 allprojects {
-    apply plugin: 'kotlinx-atomicfu' // it also adds all the necessary dependencies
-    def projectName = it.name
+    String projectName = it.name
     repositories {
         /*
          * google should be first in the repository list because some of the play services
@@ -159,30 +158,33 @@
         maven { url "https://kotlin.bintray.com/kotlin-eap" }
         maven { url "https://kotlin.bintray.com/kotlinx" }
     }
+}
 
-    if (projectName == rootModule || projectName == coreModule) return
-
-    // Add dependency to core source sets. Core is configured in kx-core/build.gradle
+// Add dependency to core source sets. Core is configured in kx-core/build.gradle
+configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != coreModule }) {
     evaluationDependsOn(":$coreModule")
-    if (sourceless.contains(projectName)) return
-
     def platform = platformOf(it)
     apply from: rootProject.file("gradle/compile-${platform}.gradle")
-
     dependencies {
         // See comment below for rationale, it will be replaced with "project" dependency
         compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"
-
         // the only way IDEA can resolve test classes
         testCompile project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs
     }
+}
 
+// Configure subprojects with Kotlin sources
+configure(subprojects.findAll { !sourceless.contains(it.name) }) {
+    // Use atomicfu plugin, it also adds all the necessary dependencies
+    apply plugin: 'kotlinx-atomicfu'
+
+    // Configure options for all Kotlin compilation tasks
     tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all {
         kotlinOptions.freeCompilerArgs += experimentalAnnotations.collect { "-Xuse-experimental=" + it }
         kotlinOptions.freeCompilerArgs += "-progressive"
         kotlinOptions.freeCompilerArgs += "-XXLanguage:+InlineClasses"
-        // Binary compatibility support
-        kotlinOptions.freeCompilerArgs += ["-Xdump-declarations-to=${buildDir}/visibilities.json"]
+        // Remove null assertions to get smaller bytecode on Android
+        kotlinOptions.freeCompilerArgs += ["-Xno-param-assertions", "-Xno-receiver-assertions", "-Xno-call-assertions"]
     }
 }
 
diff --git a/docs/debugging.md b/docs/debugging.md
index d3caa9a..6c846f2 100644
--- a/docs/debugging.md
+++ b/docs/debugging.md
@@ -8,10 +8,12 @@
   * [Stacktrace recovery machinery](#stacktrace-recovery-machinery)
 * [Debug agent](#debug-agent)
   * [Debug agent and Android](#debug-agent-and-android)
+* [Android optimization](#android-optimization)
 
 <!--- END -->
 
 ## Debugging coroutines
+
 Debugging asynchronous programs is challenging, because multiple concurrent coroutines are typically working at the same time.
 To help with that, `kotlinx.coroutines` comes with additional features for debugging: debug mode, stacktrace recovery 
 and debug agent.
@@ -86,6 +88,14 @@
         at kotlinx.coroutines.debug.DebugProbes.install(DebugProbes.kt:49)
 -->
 
+## Android optimization
+
+In optimized (release) builds with R8 version 1.6.0 or later both 
+[Debugging mode](../../docs/debugging.md#debug-mode) and 
+[Stacktrace recovery](../../docs/debugging.md#stacktrace-recovery) 
+are permanently turned off. 
+For more details see ["Optimization" section for Android](../ui/kotlinx-coroutines-android/README.md#optimization). 
+
 <!--- MODULE kotlinx-coroutines-core -->
 <!--- INDEX kotlinx.coroutines -->
 [DEBUG_PROPERTY_NAME]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-d-e-b-u-g_-p-r-o-p-e-r-t-y_-n-a-m-e.html
diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt
index 98ead14..b741a1a 100644
--- a/kotlinx-coroutines-core/common/src/selects/Select.kt
+++ b/kotlinx-coroutines-core/common/src/selects/Select.kt
@@ -264,7 +264,10 @@
         assert { isSelected } // "Must be selected first"
         _result.loop { result ->
             when {
-                result === UNDECIDED -> if (_result.compareAndSet(UNDECIDED, value())) return
+                result === UNDECIDED -> {
+                    val update = value()
+                    if (_result.compareAndSet(UNDECIDED, update)) return
+                }
                 result === COROUTINE_SUSPENDED -> if (_result.compareAndSet(COROUTINE_SUSPENDED, RESUMED)) {
                     block()
                     return
diff --git a/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt b/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt
index e835d34..94e3818 100644
--- a/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt
+++ b/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
 
 package kotlinx.coroutines.internal
@@ -16,7 +16,9 @@
 
 internal actual inline fun <T> ReentrantLock.withLock(action: () -> T) = this.withLockJvm(action)
 
-internal actual fun <E> identitySet(expectedSize: Int): MutableSet<E> = Collections.newSetFromMap(IdentityHashMap(expectedSize))
+@Suppress("NOTHING_TO_INLINE") // So that R8 can completely remove ConcurrentKt class
+internal actual inline fun <E> identitySet(expectedSize: Int): MutableSet<E> =
+    Collections.newSetFromMap(IdentityHashMap(expectedSize))
 
 private val REMOVE_FUTURE_ON_CANCEL: Method? = try {
     ScheduledThreadPoolExecutor::class.java.getMethod("setRemoveOnCancelPolicy", Boolean::class.java)
diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt
index 6f11cdf..fe21a25 100644
--- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt
+++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt
@@ -1,3 +1,7 @@
+/*
+ * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
 package kotlinx.coroutines.internal
 
 import kotlinx.coroutines.*
@@ -30,11 +34,12 @@
                         MainDispatcherFactory::class.java.classLoader
                 ).iterator().asSequence().toList()
             }
+            @Suppress("ConstantConditionIf")
             factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
-                ?: MissingMainCoroutineDispatcher(null)
+                ?: createMissingDispatcher()
         } catch (e: Throwable) {
             // Service loader can throw an exception as well
-            MissingMainCoroutineDispatcher(e)
+            createMissingDispatcher(e)
         }
     }
 }
@@ -51,13 +56,30 @@
     try {
         createDispatcher(factories)
     } catch (cause: Throwable) {
-        MissingMainCoroutineDispatcher(cause, hintOnError())
+        createMissingDispatcher(cause, hintOnError())
     }
 
 /** @suppress */
 @InternalCoroutinesApi
 public fun MainCoroutineDispatcher.isMissing(): Boolean = this is MissingMainCoroutineDispatcher
 
+// R8 optimization hook, not const on purpose to enable R8 optimizations via "assumenosideeffects"
+@Suppress("MayBeConstant")
+private val SUPPORT_MISSING = true
+
+@Suppress("ConstantConditionIf")
+private fun createMissingDispatcher(cause: Throwable? = null, errorHint: String? = null) =
+    if (SUPPORT_MISSING) MissingMainCoroutineDispatcher(cause, errorHint) else
+        cause?.let { throw it } ?: throwMissingMainDispatcherException()
+
+internal fun throwMissingMainDispatcherException(): Nothing {
+    throw IllegalStateException(
+        "Module with the Main dispatcher is missing. " +
+            "Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' " +
+            "and ensure it has the same version as 'kotlinx-coroutines-core'"
+    )
+}
+
 private class MissingMainCoroutineDispatcher(
     private val cause: Throwable?,
     private val errorHint: String? = null
@@ -85,11 +107,7 @@
 
     private fun missing(): Nothing {
         if  (cause == null) {
-            throw IllegalStateException(
-                "Module with the Main dispatcher is missing. " +
-                    "Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' " +
-                        "and ensure it has the same version as 'kotlinx-coroutines-core'"
-            )
+            throwMissingMainDispatcherException()
         } else {
             val message = "Module with the Main dispatcher had failed to initialize" + (errorHint?.let { ". $it" } ?: "")
             throw IllegalStateException(message, cause)
diff --git a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt
index b512815..9c9931c 100644
--- a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt
+++ b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
 
 @file:Suppress("UNCHECKED_CAST")
@@ -52,7 +52,8 @@
     return this
 }
 
-internal actual fun <E : Throwable> recoverStackTrace(exception: E, continuation: Continuation<*>): E {
+@Suppress("NOTHING_TO_INLINE") // Inline for better R8 optimization
+internal actual inline fun <E : Throwable> recoverStackTrace(exception: E, continuation: Continuation<*>): E {
     if (!RECOVER_STACK_TRACES || continuation !is CoroutineStackFrame) return exception
     return recoverFromStackFrame(exception, continuation)
 }
@@ -155,8 +156,11 @@
     }
 }
 
-internal actual fun <E : Throwable> unwrap(exception: E): E {
-    if (!RECOVER_STACK_TRACES) return exception
+@Suppress("NOTHING_TO_INLINE") // Inline for better R8 optimizations
+internal actual inline fun <E : Throwable> unwrap(exception: E): E =
+    if (!RECOVER_STACK_TRACES) exception else unwrapImpl(exception)
+
+internal fun <E : Throwable> unwrapImpl(exception: E): E {
     val cause = exception.cause
     // Fast-path to avoid array cloning
     if (cause == null || cause.javaClass != exception.javaClass) {
diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
index 7a52d34..03604ce 100644
--- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
+++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
@@ -73,7 +73,7 @@
  * Only [corePoolSize] workers can be created for regular CPU tasks)
  *
  * ### Support for blocking tasks
- * The scheduler also supports the notion of [blocking][TaskMode.PROBABLY_BLOCKING] tasks.
+ * The scheduler also supports the notion of [blocking][TASK_PROBABLY_BLOCKING] tasks.
  * When executing or enqueuing blocking tasks, the scheduler notifies or creates one more worker in
  * addition to core pool size, so at any given moment, it has [corePoolSize] threads (potentially not yet created)
  * to serve CPU-bound tasks. To properly guarantee liveness, the scheduler maintains
@@ -394,7 +394,7 @@
         }
         val skipUnpark = tailDispatch && currentWorker != null
         // Checking 'task' instead of 'notAdded' is completely okay
-        if (task.mode == TaskMode.NON_BLOCKING) {
+        if (task.mode == TASK_NON_BLOCKING) {
             if (skipUnpark) return
             signalCpuWork()
         } else {
@@ -499,7 +499,7 @@
          */
         if (state === WorkerState.TERMINATED) return task
         // Do not add CPU tasks in local queue if we are not able to execute it
-        if (task.mode === TaskMode.NON_BLOCKING && state === WorkerState.BLOCKING) {
+        if (task.mode == TASK_NON_BLOCKING && state === WorkerState.BLOCKING) {
             return task
         }
         mayHaveLocalTasks = true
@@ -739,16 +739,16 @@
             afterTask(taskMode)
         }
 
-        private fun beforeTask(taskMode: TaskMode) {
-            if (taskMode == TaskMode.NON_BLOCKING) return
+        private fun beforeTask(taskMode: Int) {
+            if (taskMode == TASK_NON_BLOCKING) return
             // Always notify about new work when releasing CPU-permit to execute some blocking task
             if (tryReleaseCpu(WorkerState.BLOCKING)) {
                 signalCpuWork()
             }
         }
 
-        private fun afterTask(taskMode: TaskMode) {
-            if (taskMode == TaskMode.NON_BLOCKING) return
+        private fun afterTask(taskMode: Int) {
+            if (taskMode == TASK_NON_BLOCKING) return
             decrementBlockingTasks()
             val currentState = state
             // Shutdown sequence of blocking dispatcher
@@ -846,10 +846,10 @@
         }
 
         // It is invoked by this worker when it finds a task
-        private fun idleReset(mode: TaskMode) {
+        private fun idleReset(mode: Int) {
             terminationDeadline = 0L // reset deadline for termination
             if (state == WorkerState.PARKING) {
-                assert { mode == TaskMode.PROBABLY_BLOCKING }
+                assert { mode == TASK_PROBABLY_BLOCKING }
                 state = WorkerState.BLOCKING
             }
         }
@@ -926,12 +926,12 @@
 
     enum class WorkerState {
         /**
-         * Has CPU token and either executes [TaskMode.NON_BLOCKING] task or tries to find one.
+         * Has CPU token and either executes [TASK_NON_BLOCKING] task or tries to find one.
          */
         CPU_ACQUIRED,
 
         /**
-         * Executing task with [TaskMode.PROBABLY_BLOCKING].
+         * Executing task with [TASK_PROBABLY_BLOCKING].
          */
         BLOCKING,
 
diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt
index bbc2b35..bbbb706 100644
--- a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt
+++ b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
 
 package kotlinx.coroutines.scheduling
@@ -85,7 +85,7 @@
      */
     public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher {
         require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
-        return LimitingDispatcher(this, parallelism, TaskMode.PROBABLY_BLOCKING)
+        return LimitingDispatcher(this, parallelism, TASK_PROBABLY_BLOCKING)
     }
 
     /**
@@ -98,7 +98,7 @@
     public fun limited(parallelism: Int): CoroutineDispatcher {
         require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
         require(parallelism <= corePoolSize) { "Expected parallelism level lesser than core pool size ($corePoolSize), but have $parallelism" }
-        return LimitingDispatcher(this, parallelism, TaskMode.NON_BLOCKING)
+        return LimitingDispatcher(this, parallelism, TASK_NON_BLOCKING)
     }
 
     internal fun dispatchWithContext(block: Runnable, context: TaskContext, tailDispatch: Boolean) {
@@ -132,7 +132,7 @@
 private class LimitingDispatcher(
     val dispatcher: ExperimentalCoroutineDispatcher,
     val parallelism: Int,
-    override val taskMode: TaskMode
+    override val taskMode: Int
 ) : ExecutorCoroutineDispatcher(), TaskContext, Executor {
 
     private val queue = ConcurrentLinkedQueue<Runnable>()
diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt
index c0a3e64..ca0b2de 100644
--- a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt
+++ b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
 
 package kotlinx.coroutines.scheduling
@@ -51,26 +51,23 @@
 @JvmField
 internal var schedulerTimeSource: TimeSource = NanoTimeSource
 
-internal enum class TaskMode {
+/**
+ * Marker indicating that task is CPU-bound and will not block
+ */
+internal const val TASK_NON_BLOCKING = 0
 
-    /**
-     * Marker indicating that task is CPU-bound and will not block
-     */
-    NON_BLOCKING,
-
-    /**
-     * Marker indicating that task may potentially block, thus giving scheduler a hint that additional thread may be required
-     */
-    PROBABLY_BLOCKING,
-}
+/**
+ * Marker indicating that task may potentially block, thus giving scheduler a hint that additional thread may be required
+ */
+internal const val TASK_PROBABLY_BLOCKING = 1
 
 internal interface TaskContext {
-    val taskMode: TaskMode
+    val taskMode: Int // TASK_XXX
     fun afterTask()
 }
 
 internal object NonBlockingContext : TaskContext {
-    override val taskMode: TaskMode = TaskMode.NON_BLOCKING
+    override val taskMode: Int = TASK_NON_BLOCKING
 
     override fun afterTask() {
        // Nothing for non-blocking context
@@ -82,10 +79,10 @@
     @JvmField var taskContext: TaskContext
 ) : Runnable {
     constructor() : this(0, NonBlockingContext)
-    inline val mode: TaskMode get() = taskContext.taskMode
+    inline val mode: Int get() = taskContext.taskMode // TASK_XXX
 }
 
-internal inline val Task.isBlocking get() = taskContext.taskMode == TaskMode.PROBABLY_BLOCKING
+internal inline val Task.isBlocking get() = taskContext.taskMode == TASK_PROBABLY_BLOCKING
 
 // Non-reusable Task implementation to wrap Runnable instances that do not otherwise implement task
 internal class TaskImpl(
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt
index 38145af..b0a5954 100644
--- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt
@@ -12,11 +12,12 @@
 import kotlin.test.*
 
 class CoroutineSchedulerTest : TestBase() {
+    private val taskModes = listOf(TASK_NON_BLOCKING, TASK_PROBABLY_BLOCKING)
 
     @Test
     fun testModesExternalSubmission() { // Smoke
         CoroutineScheduler(1, 1).use {
-            for (mode in TaskMode.values()) {
+            for (mode in taskModes) {
                 val latch = CountDownLatch(1)
                 it.dispatch(Runnable {
                     latch.countDown()
@@ -30,9 +31,9 @@
     @Test
     fun testModesInternalSubmission() { // Smoke
         CoroutineScheduler(2, 2).use {
-            val latch = CountDownLatch(TaskMode.values().size)
+            val latch = CountDownLatch(taskModes.size)
             it.dispatch(Runnable {
-                for (mode in TaskMode.values()) {
+                for (mode in taskModes) {
                     it.dispatch(Runnable {
                         latch.countDown()
                     }, TaskContextImpl(mode))
@@ -167,7 +168,7 @@
         }
     }
 
-    private class TaskContextImpl(override val taskMode: TaskMode) : TaskContext {
+    private class TaskContextImpl(override val taskMode: Int) : TaskContext {
         override fun afterTask() {}
     }
 }
\ No newline at end of file
diff --git a/ui/kotlinx-coroutines-android/README.md b/ui/kotlinx-coroutines-android/README.md
index 77bd2af..5be286c 100644
--- a/ui/kotlinx-coroutines-android/README.md
+++ b/ui/kotlinx-coroutines-android/README.md
@@ -5,6 +5,23 @@
 Read [Guide to UI programming with coroutines](https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/coroutines-guide-ui.md)
 for tutorial on this module.
 
+# Optimization
+
+R8 and ProGuard rules are bundled into this module. 
+R8 is a replacement for ProGuard in Android ecosystem, it is enabled by default since Android gradle plugin 3.4.0
+(3.3.0-beta also had it enabled). 
+For best results it is recommended to use a recent version of R8, which produces a smaller binary.
+
+When optimizations are enabled with R8 version 1.6.0 or later
+the following debugging features are permanently turned off to reduce the size of the resulting binary:
+
+* [Debugging mode](../../docs/debugging.md#debug-mode)
+* [Stacktrace recovery](../../docs/debugging.md#stacktrace-recovery)
+* The internal assertions in the library are also permanently removed.
+
+You can examine the corresponding rules in this 
+[`coroutines.pro`](resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro) file.
+
 # Package kotlinx.coroutines.android
 
 Provides `Dispatchers.Main` context for Android applications.
diff --git a/ui/kotlinx-coroutines-android/build.gradle b/ui/kotlinx-coroutines-android/build.gradle
index c05881e..68e2232 100644
--- a/ui/kotlinx-coroutines-android/build.gradle
+++ b/ui/kotlinx-coroutines-android/build.gradle
@@ -4,13 +4,6 @@
 
 repositories {
     google()
-    // TODO Remove once R8 is updated to a 1.6.x version.
-    maven {
-        url "http://storage.googleapis.com/r8-releases/raw/master"
-        metadataSources {
-            artifact()
-        }
-    }
 }
 
 configurations {
@@ -25,8 +18,7 @@
     testImplementation "org.robolectric:robolectric:$robolectric_version"
     testImplementation "org.smali:baksmali:$baksmali_version"
 
-    // TODO Replace with a 1.6.x version once released to maven.google.com.
-    r8 'com.android.tools:r8:a7ce65837bec81c62261bf0adac73d9c09d32af2'
+    r8 'com.android.tools.build:builder:4.0.0-alpha06' // Contains r8-2.0.4-dev
 }
 
 class RunR8Task extends JavaExec {
@@ -38,7 +30,7 @@
     File inputConfig
 
     @InputFile
-    final File inputConfigCommon = new File('r8-test-common.pro')
+    final File inputConfigCommon = new File('testdata/r8-test-common.pro')
 
     @InputFiles
     final File jarFile = project.jar.archivePath
@@ -74,30 +66,36 @@
     }
 }
 
-def optimizedDex = new File(buildDir, "dex-optim/")
-def unOptimizedDex = new File(buildDir, "dex-unoptim/")
+def optimizedDexDir = new File(buildDir, "dex-optim/")
+def unOptimizedDexDir = new File(buildDir, "dex-unoptim/")
+
+def optimizedDexFile = new File(optimizedDexDir, "classes.dex")
+def unOptimizedDexFile = new File(unOptimizedDexDir, "classes.dex")
 
 task runR8(type: RunR8Task, dependsOn: 'jar'){
-    outputDex = optimizedDex
-    inputConfig = file('r8-test-rules.pro')
+    outputDex = optimizedDexDir
+    inputConfig = file('testdata/r8-test-rules.pro')
 }
 
 task runR8NoOptim(type: RunR8Task, dependsOn: 'jar'){
-    outputDex = unOptimizedDex
-    inputConfig = file('r8-test-rules-no-optim.pro')
+    outputDex = unOptimizedDexDir
+    inputConfig = file('testdata/r8-test-rules-no-optim.pro')
 }
 
 test {
     // Ensure the R8-processed dex is built and supply its path as a property to the test.
     dependsOn(runR8)
     dependsOn(runR8NoOptim)
-    def dex1 = new File(optimizedDex, "classes.dex")
-    def dex2 = new File(unOptimizedDex, "classes.dex")
 
-    inputs.files(dex1, dex2)
+    inputs.files(optimizedDexFile, unOptimizedDexFile)
 
-    systemProperty 'dexPath', dex1.absolutePath
-    systemProperty 'noOptimDexPath', dex2.absolutePath
+    systemProperty 'dexPath', optimizedDexFile.absolutePath
+    systemProperty 'noOptimDexPath', unOptimizedDexFile.absolutePath
+
+    // Output custom metric with the size of the optimized dex
+    doLast {
+        println("##teamcity[buildStatisticValue key='optimizedDexSize' value='${optimizedDexFile.length()}']")
+    }
 }
 
 tasks.withType(dokka.getClass()) {
diff --git a/ui/kotlinx-coroutines-android/r8-test-rules-no-optim.pro b/ui/kotlinx-coroutines-android/r8-test-rules-no-optim.pro
deleted file mode 100644
index d6bd4a4..0000000
--- a/ui/kotlinx-coroutines-android/r8-test-rules-no-optim.pro
+++ /dev/null
@@ -1,4 +0,0 @@
--include r8-test-common.pro
-
-# Include the shrinker config used by legacy versions of AGP and ProGuard
--include resources/META-INF/com.android.tools/proguard/coroutines.pro
diff --git a/ui/kotlinx-coroutines-android/r8-test-rules.pro b/ui/kotlinx-coroutines-android/r8-test-rules.pro
deleted file mode 100644
index 2e7fdd8..0000000
--- a/ui/kotlinx-coroutines-android/r8-test-rules.pro
+++ /dev/null
@@ -1,7 +0,0 @@
--include r8-test-common.pro
-
-# Ensure the custom, fast service loader implementation is removed. In the case of fast service
-# loader encountering an exception it falls back to regular ServiceLoader in a way that cannot be
-# optimized out by R8.
--include resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
--checkdiscard class kotlinx.coroutines.internal.FastServiceLoader
\ No newline at end of file
diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
index b57b077..fd25b21 100644
--- a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
+++ b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
@@ -5,6 +5,18 @@
     boolean FAST_SERVICE_LOADER_ENABLED return false;
 }
 
--assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoader {
+-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoaderKt {
     boolean ANDROID_DETECTED return true;
+}
+
+# Disable support for "Missing Main Dispatcher", since we always have Android main dispatcher
+-assumenosideeffects class kotlinx.coroutines.internal.MainDispatchersKt {
+    boolean SUPPORT_MISSING return false;
+}
+
+# Statically turn off all debugging facilities and assertions
+-assumenosideeffects class kotlinx.coroutines.DebugKt {
+    boolean getASSERTIONS_ENABLED() return false;
+    boolean getDEBUG() return false;
+    boolean getRECOVER_STACK_TRACES() return false;
 }
\ No newline at end of file
diff --git a/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt b/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt
index 0944326..198fe07 100644
--- a/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt
+++ b/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
 
 package kotlinx.coroutines.android
@@ -12,17 +12,24 @@
 
 @Keep
 internal class AndroidExceptionPreHandler :
-    AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler, Function0<Method?> {
+    AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler
+{
+    @Volatile
+    private var _preHandler: Any? = this // uninitialized marker
 
-    private val preHandler by lazy(this)
-
-    // Reflectively lookup pre-handler. Implement Function0 to avoid generating second class for lambda
-    override fun invoke(): Method? = try {
-        Thread::class.java.getDeclaredMethod("getUncaughtExceptionPreHandler").takeIf {
-            Modifier.isPublic(it.modifiers) && Modifier.isStatic(it.modifiers)
+    // Reflectively lookup pre-handler.
+    private fun preHandler(): Method? {
+        val current = _preHandler
+        if (current !== this) return current as Method?
+        val declared = try {
+            Thread::class.java.getDeclaredMethod("getUncaughtExceptionPreHandler").takeIf {
+                Modifier.isPublic(it.modifiers) && Modifier.isStatic(it.modifiers)
+            }
+        } catch (e: Throwable) {
+            null /* not found */
         }
-    } catch (e: Throwable) {
-        null /* not found */
+        _preHandler = declared
+        return declared
     }
 
     override fun handleException(context: CoroutineContext, exception: Throwable) {
@@ -39,7 +46,7 @@
         if (Build.VERSION.SDK_INT >= 28) {
             thread.uncaughtExceptionHandler.uncaughtException(thread, exception)
         } else {
-            (preHandler?.invoke(null) as? Thread.UncaughtExceptionHandler)
+            (preHandler()?.invoke(null) as? Thread.UncaughtExceptionHandler)
                 ?.uncaughtException(thread, exception)
         }
     }
diff --git a/ui/kotlinx-coroutines-android/r8-test-common.pro b/ui/kotlinx-coroutines-android/testdata/r8-test-common.pro
similarity index 76%
rename from ui/kotlinx-coroutines-android/r8-test-common.pro
rename to ui/kotlinx-coroutines-android/testdata/r8-test-common.pro
index 03f36a8..d29377e 100644
--- a/ui/kotlinx-coroutines-android/r8-test-common.pro
+++ b/ui/kotlinx-coroutines-android/testdata/r8-test-common.pro
@@ -8,5 +8,11 @@
   void handleCoroutineException(...);
 }
 
+# Entry point for the rest of coroutines machinery
+-keep class kotlinx.coroutines.BuildersKt {
+  ** runBlocking(...);
+  ** launch(...);
+}
+
 # We are cheating a bit by not having android.jar on R8's library classpath. Ignore those warnings.
 -ignorewarnings
\ No newline at end of file
diff --git a/ui/kotlinx-coroutines-android/testdata/r8-test-rules-no-optim.pro b/ui/kotlinx-coroutines-android/testdata/r8-test-rules-no-optim.pro
new file mode 100644
index 0000000..61afeed
--- /dev/null
+++ b/ui/kotlinx-coroutines-android/testdata/r8-test-rules-no-optim.pro
@@ -0,0 +1,4 @@
+-include r8-test-common.pro
+
+# Include the shrinker config used by legacy versions of AGP and ProGuard
+-include ../resources/META-INF/com.android.tools/proguard/coroutines.pro
diff --git a/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro b/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro
new file mode 100644
index 0000000..dde8600
--- /dev/null
+++ b/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro
@@ -0,0 +1,14 @@
+-include r8-test-common.pro
+
+-include ../resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
+
+# Validate that service-loader & debugger classes are discarded
+-checkdiscard class kotlinx.coroutines.internal.FastServiceLoader
+-checkdiscard class kotlinx.coroutines.DebugKt
+-checkdiscard class kotlinx.coroutines.internal.StackTraceRecoveryKt
+
+# Real android projects do not keep this class, but somehow it is kept in this test (R8 bug)
+# -checkdiscard class kotlinx.coroutines.internal.MissingMainCoroutineDispatcher
+
+# Should not keep this class, but it is still there (R8 bug)
+#-checkdiscard class kotlinx.coroutines.CoroutineId
\ No newline at end of file