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.SideEffect
21 import androidx.compose.runtime.currentCompositeKeyHash
22 import androidx.compose.runtime.getValue
23 import androidx.compose.runtime.mutableStateOf
24 import androidx.compose.runtime.saveable.Saver
25 import androidx.compose.runtime.saveable.SaverScope
26 import androidx.compose.runtime.saveable.mapSaver
27 import androidx.compose.runtime.saveable.rememberSaveable
28 import androidx.compose.runtime.setValue
29 import com.android.systemui.scene.session.shared.SessionStorage
30 import com.android.systemui.util.kotlin.mapValuesNotNullTo
31
32 /**
33 * An explicit storage for remembering composable state outside of the lifetime of a composition.
34 *
35 * Specifically, this allows easy conversion of standard
36 * [remember][androidx.compose.runtime.remember] invocations to ones that are preserved beyond the
37 * callsite's existence in the composition.
38 *
39 * ```kotlin
40 * @Composable
41 * fun Parent() {
42 * val session = remember { Session() }
43 * ...
44 * if (someCondition) {
45 * Child(session)
46 * }
47 * }
48 *
49 * @Composable
50 * fun Child(session: Session) {
51 * val state by session.rememberSession { mutableStateOf(0f) }
52 * ...
53 * }
54 * ```
55 */
56 interface Session {
57 /**
58 * Remember the value returned by [init] if all [inputs] are equal (`==`) to the values they had
59 * in the previous composition, otherwise produce and remember a new value by calling [init].
60 *
61 * @param inputs A set of inputs such that, when any of them have changed, will cause the state
62 * to reset and [init] to be rerun
63 * @param key An optional key to be used as a key for the saved value. If `null`, we use the one
64 * automatically generated by the Compose runtime which is unique for the every exact code
65 * location in the composition tree
66 * @param init A factory function to create the initial value of this state
67 * @see androidx.compose.runtime.remember
68 */
69 @Composable fun <T> rememberSession(key: String?, vararg inputs: Any?, init: () -> T): T
70 }
71
72 /** Returns a new [Session], optionally backed by the provided [SessionStorage]. */
Sessionnull73 fun Session(storage: SessionStorage = SessionStorage()): Session = SessionImpl(storage)
74
75 /**
76 * Remember the value returned by [init] if all [inputs] are equal (`==`) to the values they had in
77 * the previous composition, otherwise produce and remember a new value by calling [init].
78 *
79 * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
80 * reset and [init] to be rerun
81 * @param key An optional key to be used as a key for the saved value. If not provided we use the
82 * one automatically generated by the Compose runtime which is unique for the every exact code
83 * location in the composition tree
84 * @param init A factory function to create the initial value of this state
85 * @see androidx.compose.runtime.remember
86 */
87 @Composable
88 fun <T> Session.rememberSession(vararg inputs: Any?, key: String? = null, init: () -> T): T =
89 rememberSession(key, inputs, init = init)
90
91 /**
92 * An explicit storage for remembering composable state outside of the lifetime of a composition.
93 *
94 * Specifically, this allows easy conversion of standard [rememberSession] invocations to ones that
95 * are preserved beyond the callsite's existence in the composition.
96 *
97 * ```kotlin
98 * @Composable
99 * fun Parent() {
100 * val session = rememberSaveableSession()
101 * ...
102 * if (someCondition) {
103 * Child(session)
104 * }
105 * }
106 *
107 * @Composable
108 * fun Child(session: SaveableSession) {
109 * val state by session.rememberSaveableSession { mutableStateOf(0f) }
110 * ...
111 * }
112 * ```
113 */
114 interface SaveableSession : Session {
115 /**
116 * Remember the value produced by [init].
117 *
118 * It behaves similarly to [rememberSession], but the stored value will survive the activity or
119 * process recreation using the saved instance state mechanism (for example it happens when the
120 * screen is rotated in the Android application).
121 *
122 * @param inputs A set of inputs such that, when any of them have changed, will cause the state
123 * to reset and [init] to be rerun
124 * @param saver The [Saver] object which defines how the state is saved and restored.
125 * @param key An optional key to be used as a key for the saved value. If not provided we use
126 * the automatically generated by the Compose runtime which is unique for the every exact code
127 * location in the composition tree
128 * @param init A factory function to create the initial value of this state
129 * @see rememberSaveable
130 */
131 @Composable
132 fun <T : Any> rememberSaveableSession(
133 vararg inputs: Any?,
134 saver: Saver<T, out Any>,
135 key: String?,
136 init: () -> T,
137 ): T
138 }
139
140 /**
141 * Returns a new [SaveableSession] that is preserved across configuration changes.
142 *
143 * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
144 * reset.
145 * @param key An optional key to be used as a key for the saved value. If not provided we use the
146 * automatically generated by the Compose runtime which is unique for the every exact code
147 * location in the composition tree.
148 */
149 @Composable
rememberSaveableSessionnull150 fun rememberSaveableSession(
151 vararg inputs: Any?,
152 key: String? = null,
153 ): SaveableSession =
154 rememberSaveable(inputs, SaveableSessionImpl.SessionSaver, key) { SaveableSessionImpl() }
155
156 private class SessionImpl(
157 private val storage: SessionStorage = SessionStorage(),
158 ) : Session {
159 @Composable
rememberSessionnull160 override fun <T> rememberSession(key: String?, vararg inputs: Any?, init: () -> T): T {
161 val storage = storage.storage
162 val compositeKey = currentCompositeKeyHash
163 // key is the one provided by the user or the one generated by the compose runtime
164 val finalKey =
165 if (!key.isNullOrEmpty()) {
166 key
167 } else {
168 compositeKey.toString(MAX_SUPPORTED_RADIX)
169 }
170 if (finalKey !in storage) {
171 val value = init()
172 SideEffect { storage[finalKey] = SessionStorage.StorageEntry(inputs, value) }
173 return value
174 }
175 val entry = storage[finalKey]!!
176 if (!inputs.contentEquals(entry.keys)) {
177 val value = init()
178 SideEffect { entry.stored = value }
179 return value
180 }
181 @Suppress("UNCHECKED_CAST") return entry.stored as T
182 }
183 }
184
185 private class SaveableSessionImpl(
186 saveableStorage: MutableMap<String, StorageEntry> = mutableMapOf(),
187 sessionStorage: SessionStorage = SessionStorage(),
<lambda>null188 ) : SaveableSession, Session by Session(sessionStorage) {
189
190 var saveableStorage: MutableMap<String, StorageEntry> by mutableStateOf(saveableStorage)
191
192 @Composable
193 override fun <T : Any> rememberSaveableSession(
194 vararg inputs: Any?,
195 saver: Saver<T, out Any>,
196 key: String?,
197 init: () -> T,
198 ): T {
199 val compositeKey = currentCompositeKeyHash
200 // key is the one provided by the user or the one generated by the compose runtime
201 val finalKey =
202 if (!key.isNullOrEmpty()) {
203 key
204 } else {
205 compositeKey.toString(MAX_SUPPORTED_RADIX)
206 }
207
208 @Suppress("UNCHECKED_CAST") (saver as Saver<T, Any>)
209
210 if (finalKey !in saveableStorage) {
211 val value = init()
212 SideEffect { saveableStorage[finalKey] = StorageEntry.Restored(inputs, value, saver) }
213 return value
214 }
215 when (val entry = saveableStorage[finalKey]!!) {
216 is StorageEntry.Unrestored -> {
217 val value = saver.restore(entry.unrestored) ?: init()
218 SideEffect {
219 saveableStorage[finalKey] = StorageEntry.Restored(inputs, value, saver)
220 }
221 return value
222 }
223 is StorageEntry.Restored<*> -> {
224 if (!inputs.contentEquals(entry.inputs)) {
225 val value = init()
226 SideEffect {
227 saveableStorage[finalKey] = StorageEntry.Restored(inputs, value, saver)
228 }
229 return value
230 }
231 @Suppress("UNCHECKED_CAST") return entry.stored as T
232 }
233 }
234 }
235
236 sealed class StorageEntry {
237 class Unrestored(val unrestored: Any) : StorageEntry()
238
239 class Restored<T>(val inputs: Array<out Any?>, var stored: T, val saver: Saver<T, Any>) :
240 StorageEntry() {
241 fun SaverScope.saveEntry() {
242 with(saver) { stored?.let { save(it) } }
243 }
244 }
245 }
246
247 object SessionSaver :
248 Saver<SaveableSessionImpl, Any> by mapSaver(
249 save = { sessionScope: SaveableSessionImpl ->
250 sessionScope.saveableStorage.mapValues { (k, v) ->
251 when (v) {
252 is StorageEntry.Unrestored -> v.unrestored
253 is StorageEntry.Restored<*> -> {
254 with(v) { saveEntry() }
255 }
256 }
257 }
258 },
259 restore = { savedMap: Map<String, Any?> ->
260 SaveableSessionImpl(
261 saveableStorage =
262 savedMap.mapValuesNotNullTo(mutableMapOf()) { (k, v) ->
263 v?.let { StorageEntry.Unrestored(v) }
264 }
265 )
266 }
267 )
268 }
269
270 private const val MAX_SUPPORTED_RADIX = 36
271