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