1 /*
<lambda>null2 * Copyright (C) 2024 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 com.android.systemui.scene.session.ui.composable
18
19 import androidx.compose.runtime.Composable
20 import androidx.compose.runtime.DisposableEffectResult
21 import androidx.compose.runtime.DisposableEffectScope
22 import androidx.compose.runtime.RememberObserver
23 import androidx.compose.runtime.SideEffect
24 import androidx.compose.runtime.currentCompositeKeyHash
25 import androidx.compose.runtime.getValue
26 import androidx.compose.runtime.mutableStateOf
27 import androidx.compose.runtime.rememberCompositionContext
28 import androidx.compose.runtime.saveable.Saver
29 import androidx.compose.runtime.saveable.SaverScope
30 import androidx.compose.runtime.saveable.mapSaver
31 import androidx.compose.runtime.saveable.rememberSaveable
32 import androidx.compose.runtime.setValue
33 import com.android.systemui.scene.session.shared.SessionStorage
34 import com.android.systemui.util.kotlin.mapValuesNotNullTo
35 import kotlin.coroutines.CoroutineContext
36 import kotlin.coroutines.EmptyCoroutineContext
37 import kotlinx.coroutines.CoroutineScope
38 import kotlinx.coroutines.Job
39
40 /**
41 * An explicit storage for remembering composable state outside of the lifetime of a composition.
42 *
43 * Specifically, this allows easy conversion of standard
44 * [remember][androidx.compose.runtime.remember] invocations to ones that are preserved beyond the
45 * callsite's existence in the composition.
46 *
47 * ```kotlin
48 * @Composable
49 * fun Parent() {
50 * val session = remember { Session() }
51 * ...
52 * if (someCondition) {
53 * Child(session)
54 * }
55 * }
56 *
57 * @Composable
58 * fun Child(session: Session) {
59 * val state by session.rememberSession { mutableStateOf(0f) }
60 * ...
61 * }
62 * ```
63 */
64 interface Session {
65 /**
66 * Remember the value returned by [init] if all [inputs] are equal (`==`) to the values they had
67 * in the previous composition, otherwise produce and remember a new value by calling [init].
68 *
69 * @param inputs A set of inputs such that, when any of them have changed, will cause the state
70 * to reset and [init] to be rerun
71 * @param key An optional key to be used as a key for the saved value. If `null`, we use the one
72 * automatically generated by the Compose runtime which is unique for the every exact code
73 * location in the composition tree
74 * @param init A factory function to create the initial value of this state
75 * @see androidx.compose.runtime.remember
76 */
77 @Composable fun <T> rememberSession(key: String?, vararg inputs: Any?, init: () -> T): T
78 }
79
80 /** Returns a new [Session], optionally backed by the provided [SessionStorage]. */
Sessionnull81 fun Session(storage: SessionStorage = SessionStorage()): Session = SessionImpl(storage)
82
83 /**
84 * Remember the value returned by [init] if all [inputs] are equal (`==`) to the values they had in
85 * the previous composition, otherwise produce and remember a new value by calling [init].
86 *
87 * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
88 * reset and [init] to be rerun
89 * @param key An optional key to be used as a key for the saved value. If not provided we use the
90 * one automatically generated by the Compose runtime which is unique for the every exact code
91 * location in the composition tree
92 * @param init A factory function to create the initial value of this state
93 * @see androidx.compose.runtime.remember
94 */
95 @Composable
96 fun <T> Session.rememberSession(vararg inputs: Any?, key: String? = null, init: () -> T): T =
97 rememberSession(key, *inputs, init = init)
98
99 /**
100 * A side effect of composition that must be reversed or cleaned up if the [Session] ends.
101 *
102 * @see androidx.compose.runtime.DisposableEffect
103 */
104 @Composable
105 fun Session.SessionDisposableEffect(
106 vararg inputs: Any?,
107 key: String? = null,
108 effect: DisposableEffectScope.() -> DisposableEffectResult,
109 ) {
110 rememberSession(inputs, key) {
111 object : RememberObserver {
112
113 var onDispose: DisposableEffectResult? = null
114
115 override fun onAbandoned() {
116 // no-op
117 }
118
119 override fun onForgotten() {
120 onDispose?.dispose()
121 onDispose = null
122 }
123
124 override fun onRemembered() {
125 onDispose = DisposableEffectScope().effect()
126 }
127 }
128 }
129 }
130
131 /**
132 * Return a [CoroutineScope] bound to this [Session] using the optional [CoroutineContext] provided
133 * by [getContext]. [getContext] will only be called once and the same [CoroutineScope] instance
134 * will be returned for the duration of the [Session].
135 *
136 * @see androidx.compose.runtime.rememberCoroutineScope
137 */
138 @Composable
sessionCoroutineScopenull139 fun Session.sessionCoroutineScope(
140 getContext: () -> CoroutineContext = { EmptyCoroutineContext }
141 ): CoroutineScope {
142 val effectContext: CoroutineContext = rememberCompositionContext().effectCoroutineContext
<lambda>null143 val job = rememberSession { Job() }
<lambda>null144 SessionDisposableEffect { onDispose { job.cancel() } }
<lambda>null145 return rememberSession { CoroutineScope(effectContext + job + getContext()) }
146 }
147
148 /**
149 * An explicit storage for remembering composable state outside of the lifetime of a composition.
150 *
151 * Specifically, this allows easy conversion of standard [rememberSession] invocations to ones that
152 * are preserved beyond the callsite's existence in the composition.
153 *
154 * ```kotlin
155 * @Composable
156 * fun Parent() {
157 * val session = rememberSaveableSession()
158 * ...
159 * if (someCondition) {
160 * Child(session)
161 * }
162 * }
163 *
164 * @Composable
165 * fun Child(session: SaveableSession) {
166 * val state by session.rememberSaveableSession { mutableStateOf(0f) }
167 * ...
168 * }
169 * ```
170 */
171 interface SaveableSession : Session {
172 /**
173 * Remember the value produced by [init].
174 *
175 * It behaves similarly to [rememberSession], but the stored value will survive the activity or
176 * process recreation using the saved instance state mechanism (for example it happens when the
177 * screen is rotated in the Android application).
178 *
179 * @param inputs A set of inputs such that, when any of them have changed, will cause the state
180 * to reset and [init] to be rerun
181 * @param saver The [Saver] object which defines how the state is saved and restored.
182 * @param key An optional key to be used as a key for the saved value. If not provided we use
183 * the automatically generated by the Compose runtime which is unique for the every exact code
184 * location in the composition tree
185 * @param init A factory function to create the initial value of this state
186 * @see rememberSaveable
187 */
188 @Composable
rememberSaveableSessionnull189 fun <T : Any> rememberSaveableSession(
190 vararg inputs: Any?,
191 saver: Saver<T, out Any>,
192 key: String?,
193 init: () -> T,
194 ): T
195 }
196
197 /**
198 * Returns a new [SaveableSession] that is preserved across configuration changes.
199 *
200 * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
201 * reset.
202 * @param key An optional key to be used as a key for the saved value. If not provided we use the
203 * automatically generated by the Compose runtime which is unique for the every exact code
204 * location in the composition tree.
205 */
206 @Composable
207 fun rememberSaveableSession(vararg inputs: Any?, key: String? = null): SaveableSession =
208 rememberSaveable(*inputs, SaveableSessionImpl.SessionSaver, key) { SaveableSessionImpl() }
209
210 private class SessionImpl(private val storage: SessionStorage = SessionStorage()) : Session {
211 @Composable
rememberSessionnull212 override fun <T> rememberSession(key: String?, vararg inputs: Any?, init: () -> T): T {
213 val storage = storage.storage
214 val compositeKey = currentCompositeKeyHash
215 // key is the one provided by the user or the one generated by the compose runtime
216 val finalKey =
217 if (!key.isNullOrEmpty()) {
218 key
219 } else {
220 compositeKey.toString(MAX_SUPPORTED_RADIX)
221 }
222 if (finalKey !in storage) {
223 val value = init()
224 SideEffect {
225 storage[finalKey] = SessionStorage.StorageEntry(inputs, value)
226 if (value is RememberObserver) {
227 value.onRemembered()
228 }
229 }
230 return value
231 }
232 val entry = storage[finalKey]!!
233 if (!inputs.contentEquals(entry.keys)) {
234 val value = init()
235 SideEffect {
236 val oldValue = entry.stored
237 if (oldValue is RememberObserver) {
238 oldValue.onForgotten()
239 }
240 entry.stored = value
241 if (value is RememberObserver) {
242 value.onRemembered()
243 }
244 }
245 return value
246 }
247 @Suppress("UNCHECKED_CAST")
248 return entry.stored as T
249 }
250 }
251
252 private class SaveableSessionImpl(
253 saveableStorage: MutableMap<String, StorageEntry> = mutableMapOf(),
254 sessionStorage: SessionStorage = SessionStorage(),
<lambda>null255 ) : SaveableSession, Session by Session(sessionStorage) {
256
257 var saveableStorage: MutableMap<String, StorageEntry> by mutableStateOf(saveableStorage)
258
259 @Composable
260 override fun <T : Any> rememberSaveableSession(
261 vararg inputs: Any?,
262 saver: Saver<T, out Any>,
263 key: String?,
264 init: () -> T,
265 ): T {
266 val compositeKey = currentCompositeKeyHash
267 // key is the one provided by the user or the one generated by the compose runtime
268 val finalKey =
269 if (!key.isNullOrEmpty()) {
270 key
271 } else {
272 compositeKey.toString(MAX_SUPPORTED_RADIX)
273 }
274
275 @Suppress("UNCHECKED_CAST") (saver as Saver<T, Any>)
276
277 if (finalKey !in saveableStorage) {
278 val value = init()
279 SideEffect { saveableStorage[finalKey] = StorageEntry.Restored(inputs, value, saver) }
280 return value
281 }
282 when (val entry = saveableStorage[finalKey]!!) {
283 is StorageEntry.Unrestored -> {
284 val value = saver.restore(entry.unrestored) ?: init()
285 SideEffect {
286 saveableStorage[finalKey] = StorageEntry.Restored(inputs, value, saver)
287 }
288 return value
289 }
290 is StorageEntry.Restored<*> -> {
291 if (!inputs.contentEquals(entry.inputs)) {
292 val value = init()
293 SideEffect {
294 saveableStorage[finalKey] = StorageEntry.Restored(inputs, value, saver)
295 }
296 return value
297 }
298 @Suppress("UNCHECKED_CAST")
299 return entry.stored as T
300 }
301 }
302 }
303
304 sealed class StorageEntry {
305 class Unrestored(val unrestored: Any) : StorageEntry()
306
307 class Restored<T>(val inputs: Array<out Any?>, var stored: T, val saver: Saver<T, Any>) :
308 StorageEntry() {
309 fun SaverScope.saveEntry() {
310 with(saver) { stored?.let { save(it) } }
311 }
312 }
313 }
314
315 object SessionSaver :
316 Saver<SaveableSessionImpl, Any> by mapSaver(
317 save = { sessionScope: SaveableSessionImpl ->
318 sessionScope.saveableStorage.mapValues { (k, v) ->
319 when (v) {
320 is StorageEntry.Unrestored -> v.unrestored
321 is StorageEntry.Restored<*> -> {
322 with(v) { saveEntry() }
323 }
324 }
325 }
326 },
327 restore = { savedMap: Map<String, Any?> ->
328 SaveableSessionImpl(
329 saveableStorage =
330 savedMap.mapValuesNotNullTo(mutableMapOf()) { (k, v) ->
331 v?.let { StorageEntry.Unrestored(v) }
332 }
333 )
334 },
335 )
336 }
337
338 private const val MAX_SUPPORTED_RADIX = 36
339