xref: /aosp_15_r20/external/kotlinx.coroutines/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt (revision 7a7160fed73afa6648ef8aa100d4a336fe921d9a)

<lambda>null1 @file:Suppress("unused")
2 
3 package kotlinx.coroutines.android
4 
5 import android.os.*
6 import android.view.*
7 import androidx.annotation.*
8 import kotlinx.coroutines.*
9 import kotlinx.coroutines.internal.*
10 import java.lang.reflect.*
11 import kotlin.coroutines.*
12 
13 /**
14  * Dispatches execution onto Android [Handler].
15  *
16  * This class provides type-safety and a point for future extensions.
17  */
18 public sealed class HandlerDispatcher : MainCoroutineDispatcher(), Delay {
19     /**
20      * Returns dispatcher that executes coroutines immediately when it is already in the right context
21      * (current looper is the same as this handler's looper) without an additional [re-dispatch][CoroutineDispatcher.dispatch].
22      * This dispatcher does not use [Handler.post] when current looper is the same as looper of the handler.
23      *
24      * Immediate dispatcher is safe from stack overflows and in case of nested invocations forms event-loop similar to [Dispatchers.Unconfined].
25      * The event loop is an advanced topic and its implications can be found in [Dispatchers.Unconfined] documentation.
26      *
27      * Example of usage:
28      * ```
29      * suspend fun updateUiElement(val text: String) {
30      *   /*
31      *    * If it is known that updateUiElement can be invoked both from the Main thread and from other threads,
32      *    * `immediate` dispatcher is used as a performance optimization to avoid unnecessary dispatch.
33      *    *
34      *    * In that case, when `updateUiElement` is invoked from the Main thread, `uiElement.text` will be
35      *    * invoked immediately without any dispatching, otherwise, the `Dispatchers.Main` dispatch cycle via
36      *    * `Handler.post` will be triggered.
37      *    */
38      *   withContext(Dispatchers.Main.immediate) {
39      *     uiElement.text = text
40      *   }
41      *   // Do context-independent logic such as logging
42      * }
43      * ```
44      */
45     public abstract override val immediate: HandlerDispatcher
46 }
47 
48 internal class AndroidDispatcherFactory : MainDispatcherFactory {
49 
createDispatchernull50     override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
51         val mainLooper = Looper.getMainLooper() ?: throw IllegalStateException("The main looper is not available")
52         return HandlerContext(mainLooper.asHandler(async = true))
53     }
54 
hintOnErrornull55     override fun hintOnError(): String = "For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used"
56 
57     override val loadPriority: Int
58         get() = Int.MAX_VALUE / 2
59 }
60 
61 /**
62  * Represents an arbitrary [Handler] as an implementation of [CoroutineDispatcher]
63  * with an optional [name] for nicer debugging
64  *
65  * ## Rejected execution
66  *
67  * If the underlying handler is closed and its message-scheduling methods start to return `false` on
68  * an attempt to submit a continuation task to the resulting dispatcher,
69  * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the
70  * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete.
71  */
72 @JvmName("from") // this is for a nice Java API, see issue #255
73 @JvmOverloads
74 public fun Handler.asCoroutineDispatcher(name: String? = null): HandlerDispatcher =
75     HandlerContext(this, name)
76 
77 private const val MAX_DELAY = Long.MAX_VALUE / 2 // cannot delay for too long on Android
78 
79 @VisibleForTesting
80 internal fun Looper.asHandler(async: Boolean): Handler {
81     // Async support was added in API 16.
82     if (!async || Build.VERSION.SDK_INT < 16) {
83         return Handler(this)
84     }
85 
86     if (Build.VERSION.SDK_INT >= 28) {
87         // TODO compile against API 28 so this can be invoked without reflection.
88         val factoryMethod = Handler::class.java.getDeclaredMethod("createAsync", Looper::class.java)
89         return factoryMethod.invoke(null, this) as Handler
90     }
91 
92     val constructor: Constructor<Handler>
93     try {
94         constructor = Handler::class.java.getDeclaredConstructor(Looper::class.java,
95             Handler.Callback::class.java, Boolean::class.javaPrimitiveType)
96     } catch (ignored: NoSuchMethodException) {
97         // Hidden constructor absent. Fall back to non-async constructor.
98         return Handler(this)
99     }
100     return constructor.newInstance(this, null, true)
101 }
102 
103 @JvmField
104 @Deprecated("Use Dispatchers.Main instead", level = DeprecationLevel.HIDDEN)
<lambda>null105 internal val Main: HandlerDispatcher? = runCatching { HandlerContext(Looper.getMainLooper().asHandler(async = true)) }.getOrNull()
106 
107 /**
108  * Implements [CoroutineDispatcher] on top of an arbitrary Android [Handler].
109  */
110 internal class HandlerContext private constructor(
111     private val handler: Handler,
112     private val name: String?,
113     private val invokeImmediately: Boolean
114 ) : HandlerDispatcher(), Delay {
115     /**
116      * Creates [CoroutineDispatcher] for the given Android [handler].
117      *
118      * @param handler a handler.
119      * @param name an optional name for debugging.
120      */
121     constructor(
122         handler: Handler,
123         name: String? = null
124     ) : this(handler, name, false)
125 
126     override val immediate: HandlerContext = if (invokeImmediately) this else
127         HandlerContext(handler, name, true)
128 
isDispatchNeedednull129     override fun isDispatchNeeded(context: CoroutineContext): Boolean {
130         return !invokeImmediately || Looper.myLooper() != handler.looper
131     }
132 
dispatchnull133     override fun dispatch(context: CoroutineContext, block: Runnable) {
134         if (!handler.post(block)) {
135             cancelOnRejection(context, block)
136         }
137     }
138 
scheduleResumeAfterDelaynull139     override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
140         val block = Runnable {
141             with(continuation) { resumeUndispatched(Unit) }
142         }
143         if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
144             continuation.invokeOnCancellation { handler.removeCallbacks(block) }
145         } else {
146             cancelOnRejection(continuation.context, block)
147         }
148     }
149 
invokeOnTimeoutnull150     override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
151         if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
152             return DisposableHandle { handler.removeCallbacks(block) }
153         }
154         cancelOnRejection(context, block)
155         return NonDisposableHandle
156     }
157 
cancelOnRejectionnull158     private fun cancelOnRejection(context: CoroutineContext, block: Runnable) {
159         context.cancel(CancellationException("The task was rejected, the handler underlying the dispatcher '${toString()}' was closed"))
160         Dispatchers.IO.dispatch(context, block)
161     }
162 
<lambda>null163     override fun toString(): String = toStringInternalImpl() ?: run {
164         val str = name ?: handler.toString()
165         if (invokeImmediately) "$str.immediate" else str
166     }
167 
equalsnull168     override fun equals(other: Any?): Boolean =
169         other is HandlerContext && other.handler === handler && other.invokeImmediately == invokeImmediately
170     // inlining `Boolean.hashCode()` for Android compatibility, as requested by Animal Sniffer
171     override fun hashCode(): Int = System.identityHashCode(handler) xor if (invokeImmediately) 1231 else 1237
172 }
173 
174 @Volatile
175 private var choreographer: Choreographer? = null
176 
177 /**
178  * Awaits the next animation frame and returns frame time in nanoseconds.
179  */
180 public suspend fun awaitFrame(): Long {
181     // fast path when choreographer is already known
182     val choreographer = choreographer
183     return if (choreographer != null) {
184         suspendCancellableCoroutine { cont ->
185             postFrameCallback(choreographer, cont)
186         }
187     } else {
188         awaitFrameSlowPath()
189     }
190 }
191 
awaitFrameSlowPathnull192 private suspend fun awaitFrameSlowPath(): Long = suspendCancellableCoroutine { cont ->
193     if (Looper.myLooper() === Looper.getMainLooper()) { // Check if we are already in the main looper thread
194         updateChoreographerAndPostFrameCallback(cont)
195     } else { // post into looper thread to figure it out
196         Dispatchers.Main.dispatch(cont.context, Runnable {
197             updateChoreographerAndPostFrameCallback(cont)
198         })
199     }
200 }
201 
updateChoreographerAndPostFrameCallbacknull202 private fun updateChoreographerAndPostFrameCallback(cont: CancellableContinuation<Long>) {
203     val choreographer = choreographer ?: Choreographer.getInstance()!!.also { choreographer = it }
204     postFrameCallback(choreographer, cont)
205 }
206 
postFrameCallbacknull207 private fun postFrameCallback(choreographer: Choreographer, cont: CancellableContinuation<Long>) {
208     choreographer.postFrameCallback { nanos ->
209         with(cont) { Dispatchers.Main.resumeUndispatched(nanos) }
210     }
211 }
212