DispatchQueue.kt

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.lifecycle

import android.annotation.SuppressLint
import androidx.annotation.AnyThread
import androidx.annotation.MainThread
import kotlinx.coroutines.Dispatchers
import java.util.ArrayDeque
import java.util.Queue
import kotlin.coroutines.CoroutineContext

/**
 * Helper class for [PausingDispatcher] that tracks runnables which are enqueued to the dispatcher
 * and also calls back the [PausingDispatcher] when the runnable should run.
 */
internal class DispatchQueue {
    // handler thread
    private var paused: Boolean = true
    // handler thread
    private var finished: Boolean = false
    private var isDraining: Boolean = false

    private val queue: Queue<Runnable> = ArrayDeque<Runnable>()

    @MainThread
    fun pause() {
        paused = true
    }

    @MainThread
    fun resume() {
        if (!paused) {
            return
        }
        check(!finished) {
            "Cannot resume a finished dispatcher"
        }
        paused = false
        drainQueue()
    }

    @MainThread
    fun finish() {
        finished = true
        drainQueue()
    }

    @MainThread
    fun drainQueue() {
        if (isDraining) {
            // Block re-entrant calls to avoid deep stacks
            return
        }
        try {
            isDraining = true
            while (queue.isNotEmpty()) {
                if (!canRun()) {
                    break
                }
                queue.poll()?.run()
            }
        } finally {
            isDraining = false
        }
    }

    @MainThread
    fun canRun() = finished || !paused

    @AnyThread
    @SuppressLint("WrongThread") // false negative, we are checking the thread
    fun dispatchAndEnqueue(context: CoroutineContext, runnable: Runnable) {
        with(Dispatchers.Main.immediate) {
            // This check is here to handle a special but important case. If for example
            // launchWhenCreated is used while not created it's expected that it will run
            // synchronously when the lifecycle is created. If we called `dispatch` here
            // it launches made before the required state is reached would always be deferred
            // which is not the intended behavior.
            //
            // This means that calling `yield()` while paused and then receiving `resume` right
            // after leads to the runnable being run immediately but that is indeed intended.
            // This could be solved by implementing `dispatchYield` in the dispatcher but it's
            // marked as internal API.
            if (isDispatchNeeded(context) || canRun()) {
                dispatch(context, Runnable { enqueue(runnable) })
            } else {
                enqueue(runnable)
            }
        }
    }

    @MainThread
    private fun enqueue(runnable: Runnable) {
        check(queue.offer(runnable)) {
            "cannot enqueue any more runnables"
        }
        drainQueue()
    }
}