1 /*
2  * Copyright 2020 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.foundation
18 
19 import androidx.compose.foundation.internal.PlatformOptimizedCancellationException
20 import androidx.compose.runtime.Stable
21 import kotlinx.coroutines.CancellationException
22 import kotlinx.coroutines.Job
23 import kotlinx.coroutines.coroutineScope
24 import kotlinx.coroutines.sync.Mutex
25 import kotlinx.coroutines.sync.withLock
26 
27 /**
28  * Priorities for performing mutation on state.
29  *
30  * [MutatePriority] values follow the natural ordering of `enum class` values; a value that compares
31  * as `>` to another has a higher priority. A mutation of equal or greater priority will interrupt
32  * the current mutation in progress.
33  */
34 enum class MutatePriority {
35     /**
36      * The default priority for mutations. Can be interrupted by other [Default], [UserInput] or
37      * [PreventUserInput] priority operations. [Default] priority should be used for programmatic
38      * animations or changes that should not interrupt user input.
39      */
40     Default,
41 
42     /**
43      * An elevated priority for mutations meant for implementing direct user interactions. Can be
44      * interrupted by other [UserInput] or [PreventUserInput] priority operations.
45      */
46     UserInput,
47 
48     /**
49      * A high-priority mutation that can only be interrupted by other [PreventUserInput] priority
50      * operations. [PreventUserInput] priority should be used for operations that user input should
51      * not be able to interrupt.
52      */
53     PreventUserInput
54 }
55 
56 /**
57  * Used in place of the standard Job cancellation pathway to avoid reflective javaClass.simpleName
58  * lookups to build the exception message and stack trace collection. Remove if these are changed in
59  * kotlinx.coroutines.
60  */
61 internal class MutationInterruptedException :
62     PlatformOptimizedCancellationException("Mutation interrupted")
63 
64 /**
65  * Mutual exclusion for UI state mutation over time.
66  *
67  * [mutate] permits interruptible state mutation over time using a standard [MutatePriority]. A
68  * [MutatorMutex] enforces that only a single writer can be active at a time for a particular state
69  * resource. Instead of queueing callers that would acquire the lock like a traditional [Mutex], new
70  * attempts to [mutate] the guarded state will either cancel the current mutator or if the current
71  * mutator has a higher priority, the new caller will throw [CancellationException].
72  *
73  * [MutatorMutex] should be used for implementing hoisted state objects that many mutators may want
74  * to manipulate over time such that those mutators can coordinate with one another. The
75  * [MutatorMutex] instance should be hidden as an implementation detail. For example:
76  *
77  * @sample androidx.compose.foundation.samples.mutatorMutexStateObject
78  */
79 @Stable
80 class MutatorMutex {
81     private class Mutator(val priority: MutatePriority, val job: Job) {
canInterruptnull82         fun canInterrupt(other: Mutator) = priority >= other.priority
83 
84         fun cancel() = job.cancel(MutationInterruptedException())
85     }
86 
87     private val currentMutator = AtomicReference<Mutator?>(null)
88     private val mutex = Mutex()
89 
90     private fun tryMutateOrCancel(mutator: Mutator) {
91         while (true) {
92             val oldMutator = currentMutator.get()
93             if (oldMutator == null || mutator.canInterrupt(oldMutator)) {
94                 if (currentMutator.compareAndSet(oldMutator, mutator)) {
95                     oldMutator?.cancel()
96                     break
97                 }
98             } else throw CancellationException("Current mutation had a higher priority")
99         }
100     }
101 
102     /**
103      * Enforce that only a single caller may be active at a time.
104      *
105      * If [mutate] is called while another call to [mutate] or [mutateWith] is in progress, their
106      * [priority] values are compared. If the new caller has a [priority] equal to or higher than
107      * the call in progress, the call in progress will be cancelled, throwing
108      * [CancellationException] and the new caller's [block] will be invoked. If the call in progress
109      * had a higher [priority] than the new caller, the new caller will throw
110      * [CancellationException] without invoking [block].
111      *
112      * @param priority the priority of this mutation; [MutatePriority.Default] by default. Higher
113      *   priority mutations will interrupt lower priority mutations.
114      * @param block mutation code to run mutually exclusive with any other call to [mutate] or
115      *   [mutateWith].
116      */
mutatenull117     suspend fun <R> mutate(
118         priority: MutatePriority = MutatePriority.Default,
119         block: suspend () -> R
120     ) = coroutineScope {
121         val mutator = Mutator(priority, coroutineContext[Job]!!)
122 
123         tryMutateOrCancel(mutator)
124 
125         mutex.withLock {
126             try {
127                 block()
128             } finally {
129                 currentMutator.compareAndSet(mutator, null)
130             }
131         }
132     }
133 
134     /**
135      * Enforce that only a single caller may be active at a time.
136      *
137      * If [mutateWith] is called while another call to [mutate] or [mutateWith] is in progress,
138      * their [priority] values are compared. If the new caller has a [priority] equal to or higher
139      * than the call in progress, the call in progress will be cancelled, throwing
140      * [CancellationException] and the new caller's [block] will be invoked. If the call in progress
141      * had a higher [priority] than the new caller, the new caller will throw
142      * [CancellationException] without invoking [block].
143      *
144      * This variant of [mutate] calls its [block] with a [receiver], removing the need to create an
145      * additional capturing lambda to invoke it with a receiver object. This can be used to expose a
146      * mutable scope to the provided [block] while leaving the rest of the state object read-only.
147      * For example:
148      *
149      * @sample androidx.compose.foundation.samples.mutatorMutexStateObjectWithReceiver
150      * @param receiver the receiver `this` that [block] will be called with
151      * @param priority the priority of this mutation; [MutatePriority.Default] by default. Higher
152      *   priority mutations will interrupt lower priority mutations.
153      * @param block mutation code to run mutually exclusive with any other call to [mutate] or
154      *   [mutateWith].
155      */
mutateWithnull156     suspend fun <T, R> mutateWith(
157         receiver: T,
158         priority: MutatePriority = MutatePriority.Default,
159         block: suspend T.() -> R
160     ) = coroutineScope {
161         val mutator = Mutator(priority, coroutineContext[Job]!!)
162 
163         tryMutateOrCancel(mutator)
164 
165         mutex.withLock {
166             try {
167                 receiver.block()
168             } finally {
169                 currentMutator.compareAndSet(mutator, null)
170             }
171         }
172     }
173 
174     /**
175      * Attempt to mutate synchronously if there is no other active caller. If there is no other
176      * active caller, the [block] will be executed in a lock. If there is another active caller,
177      * this method will return false, indicating that the active caller needs to be cancelled
178      * through a [mutate] or [mutateWith] call with an equal or higher mutation priority.
179      *
180      * Calls to [mutate] and [mutateWith] will suspend until execution of the [block] has finished.
181      *
182      * @param block mutation code to run mutually exclusive with any other call to [mutate],
183      *   [mutateWith] or [tryMutate].
184      * @return true if the [block] was executed, false if there was another active caller and the
185      *   [block] was not executed.
186      */
tryMutatenull187     inline fun tryMutate(block: () -> Unit): Boolean {
188         val didLock = tryLock()
189         if (didLock) {
190             try {
191                 block()
192             } finally {
193                 unlock()
194             }
195         }
196         return didLock
197     }
198 
tryLocknull199     @PublishedApi internal fun tryLock(): Boolean = mutex.tryLock()
200 
201     @PublishedApi
202     internal fun unlock() {
203         mutex.unlock()
204     }
205 }
206