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