George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2019 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 | |
Chuck Jazdzewski | 0a90de9 | 2020-05-21 10:03:47 -0700 | [diff] [blame] | 17 | @file:Suppress("DEPRECATION", "UNUSED_PARAMETER") |
| 18 | |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 19 | package androidx.ui.core |
| 20 | |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 21 | import androidx.compose.ObserverMap |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 22 | import androidx.compose.frames.FrameCommitObserver |
| 23 | import androidx.compose.frames.FrameReadObserver |
George Mount | 81ddeb1 | 2019-12-18 10:46:26 -0800 | [diff] [blame] | 24 | import androidx.compose.frames.observeAllReads |
George Mount | de2dac7 | 2020-05-18 13:20:08 -0700 | [diff] [blame] | 25 | import androidx.ui.util.fastForEach |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 26 | |
| 27 | /** |
| 28 | * Allows for easy model read observation. To begin observe a change, you must pass a |
Chuck Jazdzewski | 0a90de9 | 2020-05-21 10:03:47 -0700 | [diff] [blame] | 29 | * non-lambda `onCommit` listener to the [observeReads] method. |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 30 | * |
Leland Richardson | fcf76b3 | 2020-05-13 16:58:59 -0700 | [diff] [blame] | 31 | * When a state change has been committed, the `onCommit` listener will be called |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 32 | * with the `targetObject` as the argument. There are no order guarantees for |
George Mount | 81ddeb1 | 2019-12-18 10:46:26 -0800 | [diff] [blame] | 33 | * `onCommit` listener calls. Commit callbacks are made on the thread that model changes |
| 34 | * are committed, so the [commitExecutor] allows the developer to control the thread on which the |
| 35 | * `onCommit`calls are made. An example use would be to have the executor shift to the |
| 36 | * the UI thread for the `onCommit` callbacks to be made. |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 37 | * |
George Mount | 81ddeb1 | 2019-12-18 10:46:26 -0800 | [diff] [blame] | 38 | * A different ModelObserver should be used with each thread that [observeReads] is called on. |
| 39 | * |
| 40 | * @param commitExecutor The executor on which all `onCommit` calls will be made. |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 41 | */ |
Chuck Jazdzewski | 0a90de9 | 2020-05-21 10:03:47 -0700 | [diff] [blame] | 42 | @Deprecated("Frames have been replaced by snapshots", |
| 43 | ReplaceWith( |
| 44 | "SnapshotStateObserver", |
| 45 | "androidx.compose.snapshots" |
| 46 | ) |
| 47 | ) |
George Mount | 81ddeb1 | 2019-12-18 10:46:26 -0800 | [diff] [blame] | 48 | class ModelObserver(private val commitExecutor: (command: () -> Unit) -> Unit) { |
Chuck Jazdzewski | fb6db65 | 2019-11-25 08:30:51 -0800 | [diff] [blame] | 49 | private val commitObserver: FrameCommitObserver = { committed, _ -> |
George Mount | 83c19ce | 2019-12-13 12:59:47 -0800 | [diff] [blame] | 50 | var hasValues = false |
Ralston Da Silva | 231e5cc | 2020-03-05 00:33:03 -0800 | [diff] [blame] | 51 | // This array is in the same order as commitMaps |
Jim Sproch | a88c07a | 2020-06-25 13:00:03 -0700 | [diff] [blame] | 52 | @Suppress("DEPRECATION_ERROR") |
Ralston Da Silva | 231e5cc | 2020-03-05 00:33:03 -0800 | [diff] [blame] | 53 | val targetsArray = synchronized(commitMaps) { |
| 54 | Array(commitMaps.size) { index -> |
| 55 | commitMaps[index].map.get(committed).apply { |
| 56 | if (isNotEmpty()) |
| 57 | hasValues = true |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 58 | } |
| 59 | } |
| 60 | } |
George Mount | 83c19ce | 2019-12-13 12:59:47 -0800 | [diff] [blame] | 61 | if (hasValues) { |
George Mount | 81ddeb1 | 2019-12-18 10:46:26 -0800 | [diff] [blame] | 62 | commitExecutor { |
George Mount | 83c19ce | 2019-12-13 12:59:47 -0800 | [diff] [blame] | 63 | callOnCommit(targetsArray) |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 64 | } |
| 65 | } |
| 66 | } |
| 67 | |
George Mount | 81ddeb1 | 2019-12-18 10:46:26 -0800 | [diff] [blame] | 68 | /** |
| 69 | * The [FrameReadObserver] used by this [ModelObserver] during [observeReads]. |
| 70 | */ |
| 71 | private val readObserver: FrameReadObserver = { model -> |
| 72 | if (!isPaused) { |
Jim Sproch | a88c07a | 2020-06-25 13:00:03 -0700 | [diff] [blame] | 73 | @Suppress("DEPRECATION_ERROR") |
George Mount | 81ddeb1 | 2019-12-18 10:46:26 -0800 | [diff] [blame] | 74 | synchronized(commitMaps) { |
| 75 | currentMap!!.add(model, currentTarget!!) |
| 76 | } |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 77 | } |
| 78 | } |
| 79 | |
| 80 | /** |
George Mount | 81ddeb1 | 2019-12-18 10:46:26 -0800 | [diff] [blame] | 81 | * List of all [CommitMap]s. When [observeReads] is called, there will be |
| 82 | * a [CommitMap] associated with its `onCommit` callback in this list. The list |
| 83 | * only grows. |
| 84 | */ |
| 85 | private val commitMaps = mutableListOf<CommitMap<*>>() |
| 86 | |
| 87 | /** |
| 88 | * Method to call when unsubscribing from the commit observer. |
| 89 | */ |
| 90 | private var commitUnsubscribe: (() -> Unit)? = null |
| 91 | |
| 92 | /** |
| 93 | * `true` when an [observeReads] is in progress and [readObserver] is active and |
| 94 | * `false` when [readObserver] is no longer observing changes. |
| 95 | */ |
| 96 | private var isObserving = false |
| 97 | |
| 98 | /** |
| 99 | * `true` when [pauseObservingReads] is called and read observations should no |
| 100 | * longer be considered invalidations for the `onCommit` callback. |
| 101 | */ |
| 102 | private var isPaused = false |
| 103 | |
| 104 | /** |
| 105 | * The [ObserverMap] that should be added to when a model is read during [observeReads]. |
| 106 | */ |
| 107 | private var currentMap: ObserverMap<Any, Any>? = null |
| 108 | |
| 109 | /** |
| 110 | * The target associated with the active [observeReads] call. |
| 111 | */ |
| 112 | private var currentTarget: Any? = null |
| 113 | |
| 114 | /** |
George Mount | fcdeacb | 2019-12-06 09:24:47 -0800 | [diff] [blame] | 115 | * Test-only access to the internal commit listener. This is used for benchmarking |
| 116 | * the commit notification callback. |
| 117 | * |
Louis Pullen-Freilich | 3672449 | 2020-03-20 13:30:02 +0000 | [diff] [blame] | 118 | * @suppress |
George Mount | fcdeacb | 2019-12-06 09:24:47 -0800 | [diff] [blame] | 119 | */ |
| 120 | val frameCommitObserver: FrameCommitObserver |
Nikolay Igotti | 9ea0c1f | 2020-06-30 12:27:21 +0300 | [diff] [blame] | 121 | @InternalCoreApi |
George Mount | fcdeacb | 2019-12-06 09:24:47 -0800 | [diff] [blame] | 122 | get() = commitObserver |
| 123 | |
| 124 | /** |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 125 | * Executes [block], observing model reads during its execution. |
| 126 | * The [target] is stored as a weak reference to be passed to [onCommit] when a change to the |
| 127 | * model has been detected. |
| 128 | * |
| 129 | * Observation for [target] will be paused when a new [observeReads] call is made or when |
| 130 | * [pauseObservingReads] is called. |
| 131 | * |
| 132 | * Any previous observation with the given [target] and [onCommit] will be |
| 133 | * cleared and only the new observation on [block] will be stored. It is important that |
| 134 | * the same instance of [onCommit] is used between calls or previous references will |
| 135 | * not be cleared. |
| 136 | * |
| 137 | * The [onCommit] will be called when a model that was accessed during [block] has been |
George Mount | 81ddeb1 | 2019-12-18 10:46:26 -0800 | [diff] [blame] | 138 | * committed, and it will be called with [commitExecutor]. |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 139 | */ |
| 140 | fun <T : Any> observeReads(target: T, onCommit: (T) -> Unit, block: () -> Unit) { |
George Mount | 81ddeb1 | 2019-12-18 10:46:26 -0800 | [diff] [blame] | 141 | val oldMap = currentMap |
| 142 | val oldTarget = currentTarget |
| 143 | val oldPaused = isPaused |
Ralston Da Silva | 231e5cc | 2020-03-05 00:33:03 -0800 | [diff] [blame] | 144 | |
Jim Sproch | a88c07a | 2020-06-25 13:00:03 -0700 | [diff] [blame] | 145 | currentMap = @Suppress("DEPRECATION_ERROR") synchronized(commitMaps) { |
Ralston Da Silva | 231e5cc | 2020-03-05 00:33:03 -0800 | [diff] [blame] | 146 | ensureMap(onCommit).apply { removeValue(target) } |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 147 | } |
George Mount | 81ddeb1 | 2019-12-18 10:46:26 -0800 | [diff] [blame] | 148 | currentTarget = target |
| 149 | isPaused = false |
| 150 | if (!isObserving) { |
| 151 | isObserving = true |
| 152 | observeAllReads(readObserver, block) |
| 153 | isObserving = false |
| 154 | } else { |
| 155 | block() |
| 156 | } |
| 157 | currentMap = oldMap |
| 158 | currentTarget = oldTarget |
| 159 | isPaused = oldPaused |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 160 | } |
| 161 | |
| 162 | /** |
| 163 | * Stops observing model reads while executing [block]. Model reads may be restarted |
| 164 | * by calling [observeReads] inside [block]. |
| 165 | */ |
| 166 | fun pauseObservingReads(block: () -> Unit) { |
George Mount | 81ddeb1 | 2019-12-18 10:46:26 -0800 | [diff] [blame] | 167 | val oldPaused = isPaused |
| 168 | isPaused = true |
| 169 | block() |
| 170 | isPaused = oldPaused |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 171 | } |
| 172 | |
| 173 | /** |
| 174 | * Clears all model read observations for a given [target]. This clears values for all |
| 175 | * `onCommit` methods passed in [observeReads]. |
| 176 | */ |
| 177 | fun clear(target: Any) { |
Jim Sproch | a88c07a | 2020-06-25 13:00:03 -0700 | [diff] [blame] | 178 | @Suppress("DEPRECATION_ERROR") |
George Mount | 83c19ce | 2019-12-13 12:59:47 -0800 | [diff] [blame] | 179 | synchronized(commitMaps) { |
George Mount | de2dac7 | 2020-05-18 13:20:08 -0700 | [diff] [blame] | 180 | commitMaps.fastForEach { commitMap -> |
George Mount | 83c19ce | 2019-12-13 12:59:47 -0800 | [diff] [blame] | 181 | commitMap.map.removeValue(target) |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 182 | } |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | /** |
| 187 | * Starts or stops watching for model commits based on [enabled]. |
| 188 | */ |
Chuck Jazdzewski | 0a90de9 | 2020-05-21 10:03:47 -0700 | [diff] [blame] | 189 | fun enableModelUpdatesObserving(enabled: Boolean): Unit = error("deprecated") |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 190 | |
George Mount | 83c19ce | 2019-12-13 12:59:47 -0800 | [diff] [blame] | 191 | /** |
| 192 | * Calls the `onCommit` callback for the given targets. |
| 193 | */ |
| 194 | private fun callOnCommit(targetsArray: Array<List<Any>>) { |
| 195 | for (i in 0..targetsArray.lastIndex) { |
| 196 | val targets = targetsArray[i] |
| 197 | if (targets.isNotEmpty()) { |
Jim Sproch | a88c07a | 2020-06-25 13:00:03 -0700 | [diff] [blame] | 198 | @Suppress("DEPRECATION_ERROR") |
George Mount | 83c19ce | 2019-12-13 12:59:47 -0800 | [diff] [blame] | 199 | val commitCaller = synchronized(commitMaps) { commitMaps[i] } |
| 200 | commitCaller.callOnCommit(targets) |
| 201 | } |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 202 | } |
| 203 | } |
| 204 | |
| 205 | /** |
George Mount | 83c19ce | 2019-12-13 12:59:47 -0800 | [diff] [blame] | 206 | * Returns the [ObserverMap] within [commitMaps] associated with [onCommit] or a newly- |
| 207 | * inserted one if it doesn't exist. |
| 208 | */ |
| 209 | private fun <T : Any> ensureMap(onCommit: (T) -> Unit): ObserverMap<Any, Any> { |
| 210 | val index = commitMaps.indexOfFirst { it.onCommit === onCommit } |
| 211 | if (index == -1) { |
| 212 | val commitMap = CommitMap(onCommit) |
| 213 | commitMaps.add(commitMap) |
| 214 | return commitMap.map |
| 215 | } |
| 216 | return commitMaps[index].map |
| 217 | } |
| 218 | |
| 219 | /** |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 220 | * Used to tie an `onCommit` to its target by type. This works around some difficulties in |
| 221 | * unchecked casts with kotlin. |
| 222 | */ |
| 223 | @Suppress("UNCHECKED_CAST") |
George Mount | 83c19ce | 2019-12-13 12:59:47 -0800 | [diff] [blame] | 224 | private class CommitMap<T : Any>(val onCommit: (T) -> Unit) { |
| 225 | /** |
| 226 | * ObserverMap (key = model, value = target). These are the models that have been |
| 227 | * read during the target's [ModelObserver.observeReads]. |
| 228 | */ |
| 229 | val map = ObserverMap<Any, Any>() |
| 230 | |
| 231 | /** |
| 232 | * Calls the `onCommit` callback for targets affected by the given committed values. |
| 233 | */ |
| 234 | fun callOnCommit(targets: List<Any>) { |
George Mount | 6623eb4 | 2019-12-04 14:38:44 -0800 | [diff] [blame] | 235 | targets.forEach { target -> |
| 236 | onCommit(target as T) |
| 237 | } |
| 238 | } |
| 239 | } |
George Mount | 83c19ce | 2019-12-13 12:59:47 -0800 | [diff] [blame] | 240 | } |