1 /*
2 * Copyright 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.ui.platform
18
19 import android.os.Looper
20 import android.view.Choreographer
21 import androidx.compose.runtime.MonotonicFrameClock
22 import androidx.core.os.HandlerCompat
23 import kotlin.coroutines.CoroutineContext
24 import kotlinx.coroutines.CoroutineDispatcher
25 import kotlinx.coroutines.Dispatchers
26 import kotlinx.coroutines.runBlocking
27
28 /**
29 * A [CoroutineDispatcher] that will perform dispatch during a [handler] callback or
30 * [choreographer]'s animation frame stage, whichever comes first. Use [Main] to obtain a dispatcher
31 * for the process's main thread (i.e. the activity thread) or [CurrentThread] to obtain a
32 * dispatcher for the current thread.
33 */
34 // Implementation note: the constructor is private to direct users toward the companion object
35 // accessors for the main/current threads. A choreographer must be obtained from its current
36 // thread as per the only public API surface for obtaining one as of this writing, and the
37 // choreographer and handler must match. Constructing an AndroidUiDispatcher with a handler
38 // not marked as async will adversely affect dispatch behavior but not to the point of
39 // incorrectness; more operations would be deferred to the choreographer frame as racing handler
40 // messages would wait behind a frame barrier.
41 class AndroidUiDispatcher
42 private constructor(val choreographer: Choreographer, private val handler: android.os.Handler) :
43 CoroutineDispatcher() {
44
45 // Guards all properties in this class
46 private val lock = Any()
47
48 private val toRunTrampolined = ArrayDeque<Runnable>()
49 private var toRunOnFrame = mutableListOf<Choreographer.FrameCallback>()
50 private var spareToRunOnFrame = mutableListOf<Choreographer.FrameCallback>()
51 private var scheduledTrampolineDispatch = false
52 private var scheduledFrameDispatch = false
53
54 private val dispatchCallback =
55 object : Choreographer.FrameCallback, Runnable {
runnull56 override fun run() {
57 performTrampolineDispatch()
58 synchronized(lock) {
59 if (toRunOnFrame.isEmpty()) {
60 choreographer.removeFrameCallback(this)
61 scheduledFrameDispatch = false
62 }
63 }
64 }
65
doFramenull66 override fun doFrame(frameTimeNanos: Long) {
67 handler.removeCallbacks(this)
68 performTrampolineDispatch()
69 performFrameDispatch(frameTimeNanos)
70 }
71 }
72
<lambda>null73 private fun nextTask(): Runnable? = synchronized(lock) { toRunTrampolined.removeFirstOrNull() }
74
performTrampolineDispatchnull75 private fun performTrampolineDispatch() {
76 do {
77 var task = nextTask()
78 while (task != null) {
79 task.run()
80 task = nextTask()
81 }
82 } while (
83 // We don't dispatch holding the lock so that other tasks can get in on our
84 // trampolining time slice, but once we're done, make sure nothing added a new task
85 // before we set scheduledDispatch = false, which would prevent the next dispatch
86 // from being correctly scheduled. Loop to run these stragglers now.
87 synchronized(lock) {
88 if (toRunTrampolined.isEmpty()) {
89 scheduledTrampolineDispatch = false
90 false
91 } else true
92 }
93 )
94 }
95
performFrameDispatchnull96 private fun performFrameDispatch(frameTimeNanos: Long) {
97 val toRun =
98 synchronized(lock) {
99 if (!scheduledFrameDispatch) return
100 scheduledFrameDispatch = false
101 val result = toRunOnFrame
102 toRunOnFrame = spareToRunOnFrame
103 spareToRunOnFrame = result
104 result
105 }
106 for (i in 0 until toRun.size) {
107 // This callback will not and must not throw, see AndroidUiFrameClock
108 toRun[i].doFrame(frameTimeNanos)
109 }
110 toRun.clear()
111 }
112
postFrameCallbacknull113 internal fun postFrameCallback(callback: Choreographer.FrameCallback) {
114 synchronized(lock) {
115 toRunOnFrame.add(callback)
116 if (!scheduledFrameDispatch) {
117 scheduledFrameDispatch = true
118 choreographer.postFrameCallback(dispatchCallback)
119 }
120 }
121 }
122
removeFrameCallbacknull123 internal fun removeFrameCallback(callback: Choreographer.FrameCallback) {
124 synchronized(lock) { toRunOnFrame.remove(callback) }
125 }
126
127 /**
128 * A [MonotonicFrameClock] associated with this [AndroidUiDispatcher]'s [choreographer] that may
129 * be used to await [Choreographer] frame dispatch.
130 */
131 val frameClock: MonotonicFrameClock = AndroidUiFrameClock(choreographer, this)
132
dispatchnull133 override fun dispatch(context: CoroutineContext, block: Runnable) {
134 synchronized(lock) {
135 toRunTrampolined.addLast(block)
136 if (!scheduledTrampolineDispatch) {
137 scheduledTrampolineDispatch = true
138 handler.post(dispatchCallback)
139 if (!scheduledFrameDispatch) {
140 scheduledFrameDispatch = true
141 choreographer.postFrameCallback(dispatchCallback)
142 }
143 }
144 }
145 }
146
147 companion object {
148 /**
149 * The [CoroutineContext] containing the [AndroidUiDispatcher] and its [frameClock] for the
150 * process's main thread.
151 */
<lambda>null152 val Main: CoroutineContext by lazy {
153 val dispatcher =
154 AndroidUiDispatcher(
155 if (isMainThread()) Choreographer.getInstance()
156 else runBlocking(Dispatchers.Main) { Choreographer.getInstance() },
157 HandlerCompat.createAsync(Looper.getMainLooper())
158 )
159
160 dispatcher + dispatcher.frameClock
161 }
162
163 private val currentThread: ThreadLocal<CoroutineContext> =
164 object : ThreadLocal<CoroutineContext>() {
initialValuenull165 override fun initialValue(): CoroutineContext =
166 AndroidUiDispatcher(
167 Choreographer.getInstance(),
168 HandlerCompat.createAsync(
169 Looper.myLooper() ?: error("no Looper on this thread")
170 )
171 )
172 .let { it + it.frameClock }
173 }
174
175 /**
176 * The canonical [CoroutineContext] containing the [AndroidUiDispatcher] and its
177 * [frameClock] for the calling thread. Returns [Main] if accessed from the process's main
178 * thread.
179 *
180 * Throws [IllegalStateException] if the calling thread does not have both a [Choreographer]
181 * and an active [Looper].
182 */
183 val CurrentThread: CoroutineContext
184 get() =
185 if (isMainThread()) Main
186 else {
187 currentThread.get() ?: error("no AndroidUiDispatcher for this thread")
188 }
189 }
190 }
191
isMainThreadnull192 private fun isMainThread() = Looper.myLooper() === Looper.getMainLooper()
193