Skip to content

Commit 8adbb70

Browse files
JakeWhartonqwwdfsad
authored andcommitted
Send async messages to the Android main looper
1 parent ede2923 commit 8adbb70

File tree

3 files changed

+156
-2
lines changed

3 files changed

+156
-2
lines changed

ui/kotlinx-coroutines-android/build.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ repositories {
99
dependencies {
1010
compileOnly 'com.google.android:android:4.1.1.4'
1111
compileOnly 'com.android.support:support-annotations:26.1.0'
12+
13+
testImplementation 'com.google.android:android:4.1.1.4'
14+
testImplementation 'org.robolectric:robolectric:4.0-alpha-3'
1215
}
1316

1417
tasks.withType(dokka.getClass()) {

ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt

+29-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
package kotlinx.coroutines.experimental.android
88

99
import android.os.*
10+
import android.support.annotation.VisibleForTesting
1011
import android.view.*
1112
import kotlinx.coroutines.experimental.*
13+
import java.lang.reflect.Constructor
1214
import java.util.concurrent.*
1315
import kotlin.coroutines.experimental.*
1416

1517
/**
16-
* Dispatches execution onto Android main UI thread and provides native [delay][Delay.delay] support.
18+
* Dispatches execution onto Android main thread and provides native [delay][Delay.delay] support.
1719
*/
1820
public val Dispatchers.Main: HandlerDispatcher
1921
get() = mainDispatcher
@@ -40,7 +42,32 @@ public fun Handler.asCoroutineDispatcher(): HandlerDispatcher =
4042

4143
private const val MAX_DELAY = Long.MAX_VALUE / 2 // cannot delay for too long on Android
4244

43-
private val mainHandler = Handler(Looper.getMainLooper())
45+
private val mainHandler = Looper.getMainLooper().asHandler(async = true)
46+
47+
@VisibleForTesting
48+
internal fun Looper.asHandler(async: Boolean): Handler {
49+
// Async support was added in API 16.
50+
if (!async || Build.VERSION.SDK_INT < 16) {
51+
return Handler(this)
52+
}
53+
54+
if (Build.VERSION.SDK_INT >= 28) {
55+
// TODO compile against API 28 so this can be invoked without reflection.
56+
val factoryMethod = Handler::class.java.getDeclaredMethod("createAsync", Looper::class.java)
57+
return factoryMethod.invoke(null, this) as Handler
58+
}
59+
60+
val constructor: Constructor<Handler>
61+
try {
62+
constructor = Handler::class.java.getDeclaredConstructor(Looper::class.java,
63+
Handler.Callback::class.java, Boolean::class.javaPrimitiveType)
64+
} catch (ignored: NoSuchMethodException) {
65+
// Hidden constructor absent. Fall back to non-async constructor.
66+
return Handler(this)
67+
}
68+
return constructor.newInstance(this, null, true)
69+
}
70+
4471
private val mainDispatcher = HandlerContext(mainHandler, "Main")
4572

4673
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.experimental.android
6+
7+
import android.os.Build
8+
import android.os.Looper
9+
import android.os.Message
10+
import android.os.MessageQueue
11+
import kotlinx.coroutines.experimental.Dispatchers
12+
import kotlinx.coroutines.experimental.GlobalScope
13+
import kotlinx.coroutines.experimental.launch
14+
import org.junit.Test
15+
import org.junit.runner.RunWith
16+
import org.robolectric.RobolectricTestRunner
17+
import org.robolectric.Shadows.shadowOf
18+
import org.robolectric.annotation.Config
19+
import org.robolectric.shadows.ShadowLooper
20+
import org.robolectric.util.ReflectionHelpers
21+
import kotlin.test.assertFalse
22+
import kotlin.test.assertTrue
23+
24+
@RunWith(RobolectricTestRunner::class)
25+
@Config(manifest = Config.NONE, sdk = [28])
26+
class HandlerDispatcherTest {
27+
28+
/**
29+
* Because [Dispatchers.Main] is a singleton, we cannot vary its initialization behavior. As a
30+
* result we only test its behavior on the newest API level and assert that it uses async
31+
* messages. We rely on the other tests to exercise the variance of the mechanism that the main
32+
* dispatcher uses to ensure it has correct behavior on all API levels.
33+
*/
34+
@Test fun mainIsAsync() {
35+
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
36+
37+
val mainLooper = ShadowLooper.getShadowMainLooper()
38+
mainLooper.pause()
39+
val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
40+
41+
val job = GlobalScope.launch(Dispatchers.Main) {}
42+
43+
val message = mainMessageQueue.head
44+
assertTrue(message.isAsynchronous)
45+
46+
job.cancel()
47+
}
48+
49+
@Test fun asyncMessagesApi14() {
50+
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 14)
51+
52+
val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()
53+
54+
val mainLooper = ShadowLooper.getShadowMainLooper()
55+
mainLooper.pause()
56+
val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
57+
58+
val job = GlobalScope.launch(main) {}
59+
60+
val message = mainMessageQueue.head
61+
assertFalse(message.isAsynchronous)
62+
63+
job.cancel()
64+
}
65+
66+
@Test fun asyncMessagesApi16() {
67+
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 16)
68+
69+
val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()
70+
71+
val mainLooper = ShadowLooper.getShadowMainLooper()
72+
mainLooper.pause()
73+
val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
74+
75+
val job = GlobalScope.launch(main) {}
76+
77+
val message = mainMessageQueue.head
78+
assertTrue(message.isAsynchronous)
79+
80+
job.cancel()
81+
}
82+
83+
@Test fun asyncMessagesApi28() {
84+
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
85+
86+
val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()
87+
88+
val mainLooper = ShadowLooper.getShadowMainLooper()
89+
mainLooper.pause()
90+
val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
91+
92+
val job = GlobalScope.launch(main) {}
93+
94+
val message = mainMessageQueue.head
95+
assertTrue(message.isAsynchronous)
96+
97+
job.cancel()
98+
}
99+
100+
@Test fun noAsyncMessagesIfNotRequested() {
101+
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
102+
103+
val main = Looper.getMainLooper().asHandler(async = false).asCoroutineDispatcher()
104+
105+
val mainLooper = ShadowLooper.getShadowMainLooper()
106+
mainLooper.pause()
107+
val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)
108+
109+
val job = GlobalScope.launch(main) {}
110+
111+
val message = mainMessageQueue.head
112+
assertFalse(message.isAsynchronous)
113+
114+
job.cancel()
115+
}
116+
117+
// TODO compile against API 23+ so this can be invoked without reflection.
118+
private val Looper.queue: MessageQueue
119+
get() = Looper::class.java.getDeclaredMethod("getQueue").invoke(this) as MessageQueue
120+
121+
// TODO compile against API 22+ so this can be invoked without reflection.
122+
private val Message.isAsynchronous: Boolean
123+
get() = Message::class.java.getDeclaredMethod("isAsynchronous").invoke(this) as Boolean
124+
}

0 commit comments

Comments
 (0)