ManualFrameClock.kt

/*
 * Copyright 2020 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.compose.animation.core

import androidx.compose.runtime.dispatch.BroadcastFrameClock
import androidx.compose.runtime.dispatch.MonotonicFrameClock

/**
 * A [MonotonicFrameClock] built on a [BroadcastFrameClock] that keeps track of the current time.
 *
 * For backwards compatibility with the older [ManualAnimationClock][androidx.animation
 * .ManualAnimationClock], which immediately calls the subscriber's callback with the current
 * time, supply [dispatchOnSubscribe] to the constructor.
 */
// TODO(b/163462047): Consider propagating the onNewAwaiters callback from BroadcastFrameClock
class ManualFrameClock
@Deprecated(
    message = "dispatchOnSubscribe should only be used for backwards compatibility when this " +
        "MonotonicFrameClock is used as an AnimationClockObservable in places where " +
        "ManualAnimationClock was used before.",
    replaceWith = ReplaceWith("ManualFrameClock(initialTime)")
)
constructor(
    initialTime: Long = 0L,
    internal val dispatchOnSubscribe: Boolean
) : MonotonicFrameClock {
    @Suppress("DEPRECATION")
    constructor(initialTime: Long = 0L) : this(initialTime, false)

    private val broadcastClock = BroadcastFrameClock()

    /**
     * The current time of this [frame clock][MonotonicFrameClock], in nanoseconds.
     */
    var currentTime: Long = initialTime
        private set

    /**
     * Whether or not there are currently routines awaiting a frame from this clock.
     *
     * Note that immediately after [advanceClock], coroutines that have received a frame time
     * might not have had their continuation run yet. This can lead to [hasAwaiters] returning
     * false, even though those coroutines may request another frame immediately when they are
     * continued. To work around this caveat, make sure that those coroutines have run before
     * calling [hasAwaiters]. For example, `withContext(AndroidUiDispatcher.Main) {}` will finish
     * when all work currently scheduled on the AndroidUiDispatcher is done.
     */
    val hasAwaiters: Boolean get() = broadcastClock.hasAwaiters

    /**
     * Advances the clock by the given number of [nanoseconds][nanos]. One frame will be sent,
     * with a frame time that is [nanos] nanoseconds after the last frame time (or after the
     * initial time, if no frames have been sent yet).
     */
    fun advanceClock(nanos: Long) {
        require(nanos > 0) {
            "Cannot advance the clock by $nanos ns, only values greater than 0 are allowed"
        }
        // Make sure multiple invocations of advanceClock can't run concurrently
        @Suppress("DEPRECATION_ERROR")
        synchronized(broadcastClock) {
            currentTime += nanos
            broadcastClock.sendFrame(currentTime)
        }
    }

    override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
        return broadcastClock.withFrameNanos(onFrame)
    }
}

/**
 * Advances the clock by the given number of [milliseconds][millis]. See
 * [ManualFrameClock.advanceClock] for more information.
 */
fun ManualFrameClock.advanceClockMillis(millis: Long) = advanceClock(millis * 1_000_000)