mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 06:42:07 +01:00
Introduce a MainCoroutineDispatcher (#1744)
* Introduce a `MainCoroutineDispatcher`
This is almost entirely based on
8c27d51025/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt,
which is the implementation of `kotlinx-coroutines-android`
* Lint
* Move dispatcher to AndroidCompat
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
@file:Suppress("UNUSED")
|
||||
|
||||
package kotlinx.coroutines.android
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Delay
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.DisposableHandle
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.InternalCoroutinesApi
|
||||
import kotlinx.coroutines.MainCoroutineDispatcher
|
||||
import kotlinx.coroutines.NonDisposableHandle
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.internal.MainDispatcherFactory
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Dispatches execution onto Android [Handler].
|
||||
*
|
||||
* This class provides type-safety and a point for future extensions.
|
||||
*/
|
||||
@OptIn(InternalCoroutinesApi::class)
|
||||
public sealed class HandlerDispatcher :
|
||||
MainCoroutineDispatcher(),
|
||||
Delay {
|
||||
/**
|
||||
* Returns dispatcher that executes coroutines immediately when it is already in the right context
|
||||
* (current looper is the same as this handler's looper) without an additional [re-dispatch][CoroutineDispatcher.dispatch].
|
||||
* This dispatcher does not use [Handler.post] when current looper is the same as looper of the handler.
|
||||
*
|
||||
* Immediate dispatcher is safe from stack overflows and in case of nested invocations forms event-loop similar to [Dispatchers.Unconfined].
|
||||
* The event loop is an advanced topic and its implications can be found in [Dispatchers.Unconfined] documentation.
|
||||
*
|
||||
* Example of usage:
|
||||
* ```
|
||||
* suspend fun updateUiElement(val text: String) {
|
||||
* /*
|
||||
* * If it is known that updateUiElement can be invoked both from the Main thread and from other threads,
|
||||
* * `immediate` dispatcher is used as a performance optimization to avoid unnecessary dispatch.
|
||||
* *
|
||||
* * In that case, when `updateUiElement` is invoked from the Main thread, `uiElement.text` will be
|
||||
* * invoked immediately without any dispatching, otherwise, the `Dispatchers.Main` dispatch cycle via
|
||||
* * `Handler.post` will be triggered.
|
||||
* */
|
||||
* withContext(Dispatchers.Main.immediate) {
|
||||
* uiElement.text = text
|
||||
* }
|
||||
* // Do context-independent logic such as logging
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public abstract override val immediate: HandlerDispatcher
|
||||
}
|
||||
|
||||
@OptIn(InternalCoroutinesApi::class)
|
||||
internal class AndroidDispatcherFactory : MainDispatcherFactory {
|
||||
override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
|
||||
val mainLooper = Looper.getMainLooper() ?: throw IllegalStateException("The main looper is not available")
|
||||
return HandlerContext(mainLooper.asHandler())
|
||||
}
|
||||
|
||||
override fun hintOnError(): String = "For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used"
|
||||
|
||||
override val loadPriority: Int
|
||||
get() = Int.MAX_VALUE / 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an arbitrary [Handler] as an implementation of [CoroutineDispatcher]
|
||||
* with an optional [name] for nicer debugging
|
||||
*
|
||||
* ## Rejected execution
|
||||
*
|
||||
* If the underlying handler is closed and its message-scheduling methods start to return `false` on
|
||||
* an attempt to submit a continuation task to the resulting dispatcher,
|
||||
* then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the
|
||||
* [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete.
|
||||
*/
|
||||
@JvmName("from") // this is for a nice Java API, see issue #255
|
||||
@JvmOverloads
|
||||
public fun Handler.asCoroutineDispatcher(name: String? = null): HandlerDispatcher =
|
||||
HandlerContext(this, name)
|
||||
|
||||
private const val MAX_DELAY = Long.MAX_VALUE / 2 // cannot delay for too long on Android
|
||||
|
||||
internal fun Looper.asHandler(): Handler = Handler(this)
|
||||
|
||||
@JvmField
|
||||
@Deprecated("Use Dispatchers.Main instead", level = DeprecationLevel.HIDDEN)
|
||||
internal val Main: HandlerDispatcher? = runCatching { HandlerContext(Looper.getMainLooper().asHandler()) }.getOrNull()
|
||||
|
||||
/**
|
||||
* Implements [CoroutineDispatcher] on top of an arbitrary Android [Handler].
|
||||
*/
|
||||
@OptIn(InternalCoroutinesApi::class)
|
||||
internal class HandlerContext private constructor(
|
||||
private val handler: Handler,
|
||||
private val name: String?,
|
||||
private val invokeImmediately: Boolean,
|
||||
) : HandlerDispatcher(),
|
||||
Delay {
|
||||
/**
|
||||
* Creates [CoroutineDispatcher] for the given Android [handler].
|
||||
*
|
||||
* @param handler a handler.
|
||||
* @param name an optional name for debugging.
|
||||
*/
|
||||
constructor(
|
||||
handler: Handler,
|
||||
name: String? = null,
|
||||
) : this(handler, name, false)
|
||||
|
||||
override val immediate: HandlerContext =
|
||||
if (invokeImmediately) {
|
||||
this
|
||||
} else {
|
||||
HandlerContext(handler, name, true)
|
||||
}
|
||||
|
||||
override fun isDispatchNeeded(context: CoroutineContext): Boolean = !invokeImmediately || Looper.myLooper() != handler.looper
|
||||
|
||||
override fun dispatch(
|
||||
context: CoroutineContext,
|
||||
block: Runnable,
|
||||
) {
|
||||
if (!handler.post(block)) {
|
||||
cancelOnRejection(context, block)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun scheduleResumeAfterDelay(
|
||||
timeMillis: Long,
|
||||
continuation: CancellableContinuation<Unit>,
|
||||
) {
|
||||
val block =
|
||||
Runnable {
|
||||
with(continuation) { resumeUndispatched(Unit) }
|
||||
}
|
||||
if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
|
||||
continuation.invokeOnCancellation { handler.removeCallbacks(block) }
|
||||
} else {
|
||||
cancelOnRejection(continuation.context, block)
|
||||
}
|
||||
}
|
||||
|
||||
override fun invokeOnTimeout(
|
||||
timeMillis: Long,
|
||||
block: Runnable,
|
||||
context: CoroutineContext,
|
||||
): DisposableHandle {
|
||||
if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
|
||||
return DisposableHandle { handler.removeCallbacks(block) }
|
||||
}
|
||||
cancelOnRejection(context, block)
|
||||
return NonDisposableHandle
|
||||
}
|
||||
|
||||
private fun cancelOnRejection(
|
||||
context: CoroutineContext,
|
||||
block: Runnable,
|
||||
) {
|
||||
context.cancel(CancellationException("The task was rejected, the handler underlying the dispatcher '${toString()}' was closed"))
|
||||
Dispatchers.IO.dispatch(context, block)
|
||||
}
|
||||
|
||||
override fun toString(): String =
|
||||
toStringInternalImpl() ?: run {
|
||||
val str = name ?: handler.toString()
|
||||
if (invokeImmediately) "$str.immediate" else str
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean =
|
||||
other is HandlerContext && other.handler === handler && other.invokeImmediately == invokeImmediately
|
||||
|
||||
// inlining `Boolean.hashCode()` for Android compatibility, as requested by Animal Sniffer
|
||||
override fun hashCode(): Int = System.identityHashCode(handler) xor if (invokeImmediately) 1231 else 1237
|
||||
}
|
||||
Reference in New Issue
Block a user