1 /*
<lambda>null2 * Copyright 2022 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 package androidx.datastore.preferences.core
17
18 import androidx.datastore.core.DataStore
19
20 /**
21 * Preferences and MutablePreferences are a lot like a generic Map and MutableMap keyed by the
22 * Preferences.Key class. These are intended for use with DataStore. Construct a
23 * DataStore<Preferences> instance using [PreferenceDataStoreFactory.create].
24 */
25 public abstract class Preferences internal constructor() {
26 /**
27 * Key for values stored in Preferences. Type T is the type of the value associated with the
28 * Key.
29 *
30 * T must be one of the following: Boolean, Int, Long, Float, String, Set<String>.
31 *
32 * Construct Keys for your data type using: [booleanPreferencesKey], [intPreferencesKey],
33 * [longPreferencesKey], [floatPreferencesKey], [stringPreferencesKey],
34 * [stringSetPreferencesKey]
35 */
36 public class Key<T> internal constructor(public val name: String) {
37 /**
38 * Infix function to create a Preferences.Pair. This is used to support [preferencesOf] and
39 * [MutablePreferences.putAll]
40 *
41 * @param value is the value this preferences key should point to.
42 */
43 public infix fun to(value: T): Preferences.Pair<T> = Preferences.Pair(this, value)
44
45 override fun equals(other: Any?): Boolean =
46 if (other is Key<*>) {
47 name == other.name
48 } else {
49 false
50 }
51
52 override fun hashCode(): Int {
53 return name.hashCode()
54 }
55
56 override fun toString(): String = name
57 }
58
59 /**
60 * Key Value pairs for Preferences. Type T is the type of the value.
61 *
62 * Construct these using the infix function [to].
63 */
64 public class Pair<T> internal constructor(internal val key: Key<T>, internal val value: T)
65
66 /**
67 * Returns true if this Preferences contains the specified key.
68 *
69 * @param key the key to check for
70 */
71 public abstract operator fun <T> contains(key: Key<T>): Boolean
72
73 /**
74 * Get a preference with a key. If the key is not set, returns null.
75 *
76 * If T is Set<String>, this returns an unmodifiable set which will throw a runtime exception
77 * when mutated. Do not try to mutate the returned set.
78 *
79 * Use [MutablePreferences.set] to change the value of a preference (inside a
80 * [DataStore<Preferences>.edit] block).
81 *
82 * @param T the type of the preference
83 * @param key the key for the preference
84 * @throws ClassCastException if there is something stored with the same name as [key] but it
85 * cannot be cast to T
86 */
87 public abstract operator fun <T> get(key: Key<T>): T?
88
89 /**
90 * Retrieve a map of all key preference pairs. The returned map is unmodifiable, and attempts to
91 * mutate it will throw runtime exceptions.
92 *
93 * @return a map containing all the preferences in this Preferences
94 */
95 public abstract fun asMap(): Map<Key<*>, Any>
96
97 /**
98 * Gets a mutable copy of Preferences which contains all the preferences in this Preferences.
99 * This can be used to update your preferences without building a new Preferences object from
100 * scratch in [DataStore.updateData].
101 *
102 * This is similar to [Map.toMutableMap].
103 *
104 * @return a MutablePreferences with all the preferences from this Preferences
105 */
106 public fun toMutablePreferences(): MutablePreferences {
107 return MutablePreferences(asMap().toMutableMap(), startFrozen = false)
108 }
109
110 /**
111 * Gets a read-only copy of Preferences which contains all the preferences in this Preferences.
112 *
113 * This is similar to [Map.toMap].
114 *
115 * @return a copy of this Preferences
116 */
117 public fun toPreferences(): Preferences {
118 return MutablePreferences(asMap().toMutableMap(), startFrozen = true)
119 }
120 }
121
122 /**
123 * Mutable version of [Preferences]. Allows for creating Preferences with different key-value pairs.
124 */
125 public class MutablePreferences
126 internal constructor(
127 internal val preferencesMap: MutableMap<Key<*>, Any> = mutableMapOf(),
128 startFrozen: Boolean = true
129 ) : Preferences() {
130
131 /** If frozen, mutating methods will throw. */
132 private val frozen = AtomicBoolean(startFrozen)
133
checkNotFrozennull134 internal fun checkNotFrozen() {
135 check(!frozen.get()) { "Do mutate preferences once returned to DataStore." }
136 }
137
138 /** Causes any future mutations to result in an exception being thrown. */
freezenull139 internal fun freeze() {
140 frozen.set(true)
141 }
142
containsnull143 override operator fun <T> contains(key: Key<T>): Boolean {
144 return preferencesMap.containsKey(key)
145 }
146
getnull147 override operator fun <T> get(key: Key<T>): T? {
148 @Suppress("UNCHECKED_CAST")
149 return when (val value = preferencesMap[key]) {
150 is ByteArray -> value.copyOf()
151 else -> value
152 }
153 as T?
154 }
155
asMapnull156 override fun asMap(): Map<Key<*>, Any> {
157 return immutableMap(
158 preferencesMap.entries.associate { entry ->
159 when (val value = entry.value) {
160 is ByteArray -> Pair(entry.key, value.copyOf())
161 else -> Pair(entry.key, entry.value)
162 }
163 }
164 )
165 }
166
167 // Mutating methods below:
168
169 /**
170 * Set a key value pair in MutablePreferences.
171 *
172 * Example usage: val COUNTER_KEY = intPreferencesKey("counter")
173 *
174 * // Once edit completes successfully, preferenceStore will contain the incremented counter.
175 * preferenceStore.edit { prefs: MutablePreferences -> prefs\[COUNTER_KEY\] =
176 * prefs\[COUNTER_KEY\] :? 0 + 1 }
177 *
178 * @param key the preference to set
179 * @param value the value to set the preference to
180 */
setnull181 public operator fun <T> set(key: Key<T>, value: T) {
182 setUnchecked(key, value)
183 }
184
185 /** Private setter function. The type of key and value *must* be the same. */
setUncheckednull186 internal fun setUnchecked(key: Key<*>, value: Any?) {
187 checkNotFrozen()
188
189 when (value) {
190 null -> remove(key)
191 // Copy set so changes to input don't change Preferences. Wrap in unmodifiableSet so
192 // returned instances can't be changed.
193 is Set<*> -> preferencesMap[key] = immutableCopyOfSet(value)
194 is ByteArray -> preferencesMap[key] = value.copyOf()
195 else -> preferencesMap[key] = value
196 }
197 }
198
199 /**
200 * Appends or replaces all pairs from [prefs] to this MutablePreferences. Keys in [prefs] will
201 * overwrite keys in this Preferences.
202 *
203 * Example usage: mutablePrefs += preferencesOf(COUNTER_KEY to 100, NAME to "abcdef")
204 *
205 * @param prefs Preferences to append to this MutablePreferences
206 */
plusAssignnull207 public operator fun plusAssign(prefs: Preferences) {
208 checkNotFrozen()
209 preferencesMap += prefs.asMap()
210 }
211
212 /**
213 * Appends or replaces all [pair] to this MutablePreferences.
214 *
215 * Example usage: mutablePrefs += COUNTER_KEY to 100
216 *
217 * @param pair the Preference.Pair to add to this MutablePreferences
218 */
plusAssignnull219 public operator fun plusAssign(pair: Preferences.Pair<*>) {
220 checkNotFrozen()
221 putAll(pair)
222 }
223
224 /**
225 * Removes the preference with the given key from this MutablePreferences. If this Preferences
226 * does not contain the key, this is a no-op.
227 *
228 * Example usage: mutablePrefs -= COUNTER_KEY
229 *
230 * @param key the key to remove from this MutablePreferences
231 */
minusAssignnull232 public operator fun minusAssign(key: Preferences.Key<*>) {
233 checkNotFrozen()
234 remove(key)
235 }
236
237 /**
238 * Appends or replaces all [pairs] to this MutablePreferences.
239 *
240 * @param pairs the pairs to append to this MutablePreferences
241 */
putAllnull242 public fun putAll(vararg pairs: Preferences.Pair<*>) {
243 checkNotFrozen()
244 pairs.forEach { setUnchecked(it.key, it.value) }
245 }
246
247 /**
248 * Remove a preferences from this MutablePreferences.
249 *
250 * @param key the key to remove this MutablePreferences
251 * @return the original value of this preference key.
252 */
253 @Suppress("UNCHECKED_CAST")
removenull254 public fun <T> remove(key: Preferences.Key<T>): T {
255 checkNotFrozen()
256 return preferencesMap.remove(key) as T
257 }
258
259 /* Removes all preferences from this MutablePreferences. */
clearnull260 public fun clear() {
261 checkNotFrozen()
262 preferencesMap.clear()
263 }
264
265 // Equals and hash code for use by DataStore
equalsnull266 override fun equals(other: Any?): Boolean {
267 if (other !is MutablePreferences) {
268 return false
269 }
270
271 if (other.preferencesMap === preferencesMap) return true
272
273 if (other.preferencesMap.size != preferencesMap.size) return false
274
275 return other.preferencesMap.all { otherEntry ->
276 preferencesMap[otherEntry.key]?.let { value ->
277 when (val otherVal = otherEntry.value) {
278 is ByteArray -> value is ByteArray && otherVal.contentEquals(value)
279 else -> otherVal == value
280 }
281 } ?: false
282 }
283 }
284
hashCodenull285 override fun hashCode(): Int {
286 return preferencesMap.entries.sumOf { entry ->
287 when (val value = entry.value) {
288 is ByteArray -> value.contentHashCode()
289 else -> value.hashCode()
290 }
291 }
292 }
293
294 /** For better debugging. */
toStringnull295 override fun toString(): String =
296 preferencesMap.entries.joinToString(separator = ",\n", prefix = "{\n", postfix = "\n}") {
297 entry ->
298 val value =
299 when (val value = entry.value) {
300 is ByteArray -> value.joinToString(", ", "[", "]")
301 else -> "${entry.value}"
302 }
303 " ${entry.key.name} = $value"
304 }
305 }
306
307 /**
308 * Edit the value in DataStore transactionally in an atomic read-modify-write operation. All
309 * operations are serialized.
310 *
311 * The coroutine completes when the data has been persisted durably to disk (after which
312 * [DataStore.data] will reflect the update). If the transform or write to disk fails, the
313 * transaction is aborted and an exception is thrown.
314 *
315 * Note: values that are changed in [transform] are NOT updated in DataStore until after the
316 * transform completes. Do not assume that the data has been successfully persisted until after edit
317 * returns successfully.
318 *
319 * Note: do NOT store a reference to the MutablePreferences provided to transform. Mutating this
320 * after [transform] returns will NOT change the data in DataStore. Future versions of this may
321 * throw exceptions if the MutablePreferences object is mutated outside of [transform].
322 *
323 * See [DataStore.updateData].
324 *
325 * Example usage: val COUNTER_KEY = intPreferencesKey("my_counter")
326 *
327 * dataStore.edit { prefs -> prefs\[COUNTER_KEY\] = prefs\[COUNTER_KEY\] :? 0 + 1 }
328 *
329 * @param transform block which accepts MutablePreferences that contains all the preferences
330 * currently in DataStore. Changes to this MutablePreferences object will be persisted once
331 * transform completes.
332 * @throws androidx.datastore.core.IOException when an exception is encountered when writing data to
333 * disk
334 * @throws Exception when thrown by the transform block
335 */
editnull336 public suspend fun DataStore<Preferences>.edit(
337 transform: suspend (MutablePreferences) -> Unit
338 ): Preferences {
339 return this.updateData {
340 // It's safe to return MutablePreferences since we freeze it in
341 // PreferencesDataStore.updateData()
342 it.toMutablePreferences().apply { transform(this) }
343 }
344 }
345