| /* |
| * 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.ui.test |
| |
| import androidx.compose.runtime.MonotonicFrameClock |
| import kotlinx.coroutines.CancellableContinuation |
| import kotlinx.coroutines.CoroutineScope |
| import kotlinx.coroutines.ExperimentalCoroutinesApi |
| import kotlinx.coroutines.delay |
| import kotlinx.coroutines.launch |
| import kotlinx.coroutines.suspendCancellableCoroutine |
| import kotlinx.coroutines.test.DelayController |
| import kotlin.coroutines.ContinuationInterceptor |
| |
| private const val DefaultFrameDelay = 16_000_000L |
| |
| /** |
| * Construct a [TestMonotonicFrameClock] for [coroutineScope], obtaining the [DelayController] |
| * from the scope's [context][CoroutineScope.coroutineContext]. This frame clock may be used to |
| * consistently drive time under controlled tests. |
| * |
| * Calls to [TestMonotonicFrameClock.withFrameNanos] will schedule an upcoming frame |
| * [frameDelayNanos] nanoseconds in the future by launching into [coroutineScope] if such a frame |
| * has not yet been scheduled. |
| */ |
| @Suppress("MethodNameUnits") // Nanos for high-precision animation clocks |
| @ExperimentalCoroutinesApi |
| fun TestMonotonicFrameClock( |
| coroutineScope: CoroutineScope, |
| frameDelayNanos: Long = DefaultFrameDelay |
| ): TestMonotonicFrameClock = TestMonotonicFrameClock( |
| coroutineScope = coroutineScope, |
| delayController = coroutineScope.coroutineContext[ContinuationInterceptor].let { interceptor -> |
| requireNotNull(interceptor as? DelayController) { |
| "ContinuationInterceptor $interceptor of supplied scope must implement DelayController" |
| } |
| }, |
| frameDelayNanos = frameDelayNanos |
| ) |
| |
| /** |
| * A [MonotonicFrameClock] with a time source controlled by a `kotlinx-coroutines-test` |
| * [DelayController]. This frame clock may be used to consistently drive time under controlled |
| * tests. |
| * |
| * Calls to [withFrameNanos] will schedule an upcoming frame [frameDelayNanos] nanoseconds in the |
| * future by launching into [coroutineScope] if such a frame has not yet been scheduled. The |
| * current frame time for [withFrameNanos] is provided by [delayController]. It is strongly |
| * suggested that [coroutineScope] contain the test dispatcher controlled by [delayController]. |
| */ |
| @ExperimentalCoroutinesApi |
| class TestMonotonicFrameClock( |
| private val coroutineScope: CoroutineScope, |
| private val delayController: DelayController, |
| @get:Suppress("MethodNameUnits") // Nanos for high-precision animation clocks |
| val frameDelayNanos: Long = DefaultFrameDelay |
| ) : MonotonicFrameClock { |
| private val lock = Any() |
| private val awaiters = mutableListOf<Awaiter<*>>() |
| private var posted = false |
| |
| /** |
| * Returns whether there are any awaiters on this clock. |
| */ |
| val hasAwaiters: Boolean get() = synchronized(lock) { awaiters.isNotEmpty() } |
| |
| private class Awaiter<R>( |
| private val onFrame: (Long) -> R, |
| private val continuation: CancellableContinuation<R> |
| ) { |
| fun runFrame(frameTimeNanos: Long): () -> Unit { |
| val result = runCatching { onFrame(frameTimeNanos) } |
| return { continuation.resumeWith(result) } |
| } |
| } |
| |
| override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R = |
| suspendCancellableCoroutine { co -> |
| synchronized(lock) { |
| awaiters.add(Awaiter(onFrame, co)) |
| maybeLaunchTickRunner() |
| } |
| } |
| |
| private fun maybeLaunchTickRunner() { |
| if (!posted) { |
| posted = true |
| coroutineScope.launch { |
| delay(frameDelayMillis) |
| synchronized(lock) { |
| posted = false |
| val toRun = awaiters.toList() |
| awaiters.clear() |
| val frameTime = delayController.currentTime * 1_000_000 |
| // In case of awaiters on an immediate dispatcher, run all frame callbacks |
| // before resuming any associated continuations with the results. |
| toRun.map { it.runFrame(frameTime) }.forEach { it() } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * The frame delay time for the [TestMonotonicFrameClock] in milliseconds. |
| */ |
| @ExperimentalCoroutinesApi |
| val TestMonotonicFrameClock.frameDelayMillis: Long |
| get() = frameDelayNanos / 1_000_000 |