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