1 /*
2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui
18 
19 import androidx.annotation.RestrictTo
20 import kotlin.jvm.JvmInline
21 import kotlinx.coroutines.CoroutineScope
22 import kotlinx.coroutines.Job
23 import kotlinx.coroutines.cancelAndJoin
24 import kotlinx.coroutines.coroutineScope
25 import kotlinx.coroutines.job
26 
27 /**
28  * Helper class for coordinating between mutually-exclusive sessions. A session is represented as an
29  * object of type [T] and a coroutine [Job] whose lifetime is tied to the session.
30  *
31  * Only one session can be active at a time. When a new session is started, the old session will be
32  * cancelled and allowed to finish any cancellation tasks (e.g. `finally` blocks) before the new
33  * session's coroutine starts.
34  */
35 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
36 @InternalComposeUiApi
37 @JvmInline
38 value class SessionMutex<T>
39 private constructor(private val currentSessionHolder: AtomicReference<Session<T>?>) {
40     constructor() : this(AtomicReference(null))
41 
42     /** Returns the current session object. */
43     val currentSession: T?
44         get() = currentSessionHolder.get()?.value
45 
46     /**
47      * Cancels any existing session and then calls [session]. [session] will in turn be cancelled if
48      * this method is called again before it returns.
49      *
50      * @param sessionInitializer Called immediately to create the new session object, before
51      *   cancelling the previous session. Receives a [CoroutineScope] that has the same context as
52      *   [session] will get.
53      * @param session Called with the return value from [sessionInitializer] after cancelling the
54      *   previous session.
55      */
withSessionCancellingPreviousnull56     suspend fun <R> withSessionCancellingPrevious(
57         sessionInitializer: (CoroutineScope) -> T,
58         session: suspend (data: T) -> R
59     ): R = coroutineScope {
60         val newSession = Session(job = coroutineContext.job, value = sessionInitializer(this))
61         currentSessionHolder.getAndSet(newSession)?.job?.cancelAndJoin()
62         try {
63             return@coroutineScope session(newSession.value)
64         } finally {
65             // If this session is being interrupted by another session, the holder will already have
66             // been changed, so this will fail. If the session is getting cancelled externally or
67             // from within, this will ensure we release the session while no session is active.
68             currentSessionHolder.compareAndSet(newSession, null)
69         }
70     }
71 
72     private class Session<T>(val job: Job, val value: T)
73 }
74