From 7a5a9ddadce9e4b5830f2b027d9ae744d0398871 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Mon, 17 Sep 2018 14:35:47 -0400 Subject: [PATCH] Send async messages to the Android main looper --- ui/kotlinx-coroutines-android/build.gradle | 3 + .../src/HandlerDispatcher.kt | 31 ++++- .../test/HandlerDispatcherTest.kt | 124 ++++++++++++++++++ 3 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt diff --git a/ui/kotlinx-coroutines-android/build.gradle b/ui/kotlinx-coroutines-android/build.gradle index ff819b6643..40d51e4f4a 100644 --- a/ui/kotlinx-coroutines-android/build.gradle +++ b/ui/kotlinx-coroutines-android/build.gradle @@ -9,6 +9,9 @@ repositories { dependencies { compileOnly 'com.google.android:android:4.1.1.4' compileOnly 'com.android.support:support-annotations:26.1.0' + + testImplementation 'com.google.android:android:4.1.1.4' + testImplementation 'org.robolectric:robolectric:4.0-alpha-3' } tasks.withType(dokka.getClass()) { diff --git a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt index 453297a2ad..dcd3e15fd4 100644 --- a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt +++ b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt @@ -7,13 +7,15 @@ package kotlinx.coroutines.experimental.android import android.os.* +import android.support.annotation.VisibleForTesting import android.view.* import kotlinx.coroutines.experimental.* +import java.lang.reflect.Constructor import java.util.concurrent.* import kotlin.coroutines.experimental.* /** - * Dispatches execution onto Android main UI thread and provides native [delay][Delay.delay] support. + * Dispatches execution onto Android main thread and provides native [delay][Delay.delay] support. */ public val Dispatchers.Main: HandlerDispatcher get() = mainDispatcher @@ -40,7 +42,32 @@ public fun Handler.asCoroutineDispatcher(): HandlerDispatcher = private const val MAX_DELAY = Long.MAX_VALUE / 2 // cannot delay for too long on Android -private val mainHandler = Handler(Looper.getMainLooper()) +private val mainHandler = Looper.getMainLooper().asHandler(async = true) + +@VisibleForTesting +internal fun Looper.asHandler(async: Boolean): Handler { + // Async support was added in API 16. + if (!async || Build.VERSION.SDK_INT < 16) { + return Handler(this) + } + + if (Build.VERSION.SDK_INT >= 28) { + // TODO compile against API 28 so this can be invoked without reflection. + val factoryMethod = Handler::class.java.getDeclaredMethod("createAsync", Looper::class.java) + return factoryMethod.invoke(null, this) as Handler + } + + val constructor: Constructor + try { + constructor = Handler::class.java.getDeclaredConstructor(Looper::class.java, + Handler.Callback::class.java, Boolean::class.javaPrimitiveType) + } catch (ignored: NoSuchMethodException) { + // Hidden constructor absent. Fall back to non-async constructor. + return Handler(this) + } + return constructor.newInstance(this, null, true) +} + private val mainDispatcher = HandlerContext(mainHandler, "Main") /** diff --git a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt new file mode 100644 index 0000000000..82d5b20541 --- /dev/null +++ b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.experimental.android + +import android.os.Build +import android.os.Looper +import android.os.Message +import android.os.MessageQueue +import kotlinx.coroutines.experimental.Dispatchers +import kotlinx.coroutines.experimental.GlobalScope +import kotlinx.coroutines.experimental.launch +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowLooper +import org.robolectric.util.ReflectionHelpers +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [28]) +class HandlerDispatcherTest { + + /** + * Because [Dispatchers.Main] is a singleton, we cannot vary its initialization behavior. As a + * result we only test its behavior on the newest API level and assert that it uses async + * messages. We rely on the other tests to exercise the variance of the mechanism that the main + * dispatcher uses to ensure it has correct behavior on all API levels. + */ + @Test fun mainIsAsync() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) + + val mainLooper = ShadowLooper.getShadowMainLooper() + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = GlobalScope.launch(Dispatchers.Main) {} + + val message = mainMessageQueue.head + assertTrue(message.isAsynchronous) + + job.cancel() + } + + @Test fun asyncMessagesApi14() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 14) + + val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() + + val mainLooper = ShadowLooper.getShadowMainLooper() + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = GlobalScope.launch(main) {} + + val message = mainMessageQueue.head + assertFalse(message.isAsynchronous) + + job.cancel() + } + + @Test fun asyncMessagesApi16() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 16) + + val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() + + val mainLooper = ShadowLooper.getShadowMainLooper() + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = GlobalScope.launch(main) {} + + val message = mainMessageQueue.head + assertTrue(message.isAsynchronous) + + job.cancel() + } + + @Test fun asyncMessagesApi28() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) + + val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher() + + val mainLooper = ShadowLooper.getShadowMainLooper() + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = GlobalScope.launch(main) {} + + val message = mainMessageQueue.head + assertTrue(message.isAsynchronous) + + job.cancel() + } + + @Test fun noAsyncMessagesIfNotRequested() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) + + val main = Looper.getMainLooper().asHandler(async = false).asCoroutineDispatcher() + + val mainLooper = ShadowLooper.getShadowMainLooper() + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + + val job = GlobalScope.launch(main) {} + + val message = mainMessageQueue.head + assertFalse(message.isAsynchronous) + + job.cancel() + } + + // TODO compile against API 23+ so this can be invoked without reflection. + private val Looper.queue: MessageQueue + get() = Looper::class.java.getDeclaredMethod("getQueue").invoke(this) as MessageQueue + + // TODO compile against API 22+ so this can be invoked without reflection. + private val Message.isAsynchronous: Boolean + get() = Message::class.java.getDeclaredMethod("isAsynchronous").invoke(this) as Boolean +}