Preferences.kt

/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.datastore.preferences.core

import androidx.datastore.core.DataStore
import java.util.Collections
import java.util.concurrent.atomic.AtomicBoolean

/**
 * Preferences and MutablePreferences are a lot like a generic Map and MutableMap keyed by the
 * Preferences.Key class. These are intended for use with DataStore. Construct a
 * DataStore<Preferences> instance using [PreferenceDataStoreFactory.create].
 */
public abstract class Preferences internal constructor() {
    /**
     * Key for values stored in Preferences. Type T is the type of the value associated with the
     * Key.
     *
     * T must be one of the following: Boolean, Int, Long, Float, String, Set<String>.
     *
     * Construct Keys for your data type using: [booleanPreferencesKey], [intPreferencesKey],
     * [longPreferencesKey], [floatPreferencesKey], [stringPreferencesKey], [stringSetPreferencesKey]
     */
    public class Key<T>
    internal constructor(public val name: String) {
        /**
         * Infix function to create a Preferences.Pair.
         * This is used to support [preferencesOf] and [MutablePreferences.putAll]
         * @param value is the value this preferences key should point to.
         */
        public infix fun to(value: T): Preferences.Pair<T> = Preferences.Pair(this, value)

        override fun equals(other: Any?): Boolean =
            if (other is Key<*>) {
                name == other.name
            } else {
                false
            }

        override fun hashCode(): Int {
            return name.hashCode()
        }

        override fun toString(): String = name
    }

    /**
     * Key Value pairs for Preferences. Type T is the type of the value.
     *
     * Construct these using the infix function [to].
     */
    public class Pair<T> internal constructor(internal val key: Key<T>, internal val value: T)

    /**
     * Returns true if this Preferences contains the specified key.
     *
     * @param key the key to check for
     */
    public abstract operator fun <T> contains(key: Key<T>): Boolean

    /**
     * Get a preference with a key. If the key is not set, returns null.
     *
     * If T is Set<String>, this returns an unmodifiable set which will throw a runtime exception
     * when mutated. Do not try to mutate the returned set.
     *
     * Use [MutablePreferences.set] to change the value of a preference (inside a
     * [DataStore<Preferences>.edit] block).
     *
     * @param T the type of the preference
     * @param key the key for the preference
     * @throws ClassCastException if there is something stored with the same name as [key] but
     * it cannot be cast to T
     */
    public abstract operator fun <T> get(key: Key<T>): T?

    /**
     * Retrieve a map of all key preference pairs. The returned map is unmodifiable, and attempts
     * to mutate it will throw runtime exceptions.
     *
     * @return a map containing all the preferences in this Preferences
     */
    public abstract fun asMap(): Map<Key<*>, Any>

    /**
     * Gets a mutable copy of Preferences which contains all the preferences in this Preferences.
     * This can be used to update your preferences without building a new Preferences object from
     * scratch in [DataStore.updateData].
     *
     * This is similar to [Map.toMutableMap].
     *
     * @return a MutablePreferences with all the preferences from this Preferences
     */
    public fun toMutablePreferences(): MutablePreferences {
        return MutablePreferences(asMap().toMutableMap(), startFrozen = false)
    }

    /**
     * Gets a read-only copy of Preferences which contains all the preferences in this Preferences.
     *
     * This is similar to [Map.toMap].
     *
     * @return a copy of this Preferences
     */
    public fun toPreferences(): Preferences {
        return MutablePreferences(asMap().toMutableMap(), startFrozen = true)
    }
}

/**
 * Mutable version of [Preferences]. Allows for creating Preferences with different key-value pairs.
 */
public class MutablePreferences internal constructor(
    internal val preferencesMap: MutableMap<Key<*>, Any> = mutableMapOf(),
    startFrozen: Boolean = true
) : Preferences() {

    /**
     * If frozen, mutating methods will throw.
     */
    private val frozen = AtomicBoolean(startFrozen)

    internal fun checkNotFrozen() {
        check(!frozen.get()) { "Do mutate preferences once returned to DataStore." }
    }

    /**
     * Causes any future mutations to result in an exception being thrown.
     */
    internal fun freeze() {
        frozen.set(true)
    }

    override operator fun <T> contains(key: Key<T>): Boolean {
        return preferencesMap.containsKey(key)
    }

    override operator fun <T> get(key: Key<T>): T? {
        @Suppress("UNCHECKED_CAST")
        return preferencesMap[key] as T?
    }

    override fun asMap(): Map<Key<*>, Any> {
        return Collections.unmodifiableMap(preferencesMap)
    }

    // Mutating methods below:

    /**
     * Set a key value pair in MutablePreferences.
     *
     * Example usage:
     * val COUNTER_KEY = intPreferencesKey("counter")
     *
     * // Once edit completes successfully, preferenceStore will contain the incremented counter.
     * preferenceStore.edit { prefs: MutablePreferences ->
     *   prefs\[COUNTER_KEY\] = prefs\[COUNTER_KEY\] :? 0 + 1
     * }
     *
     * @param key the preference to set
     * @param key the value to set the preference to
     */
    public operator fun <T> set(key: Key<T>, value: T) {
        setUnchecked(key, value)
    }

    /**
     * Private setter function. The type of key and value *must* be the same.
     */
    internal fun setUnchecked(key: Key<*>, value: Any?) {
        checkNotFrozen()

        when (value) {
            null -> remove(key)
            // Copy set so changes to input don't change Preferences. Wrap in unmodifiableSet so
            // returned instances can't be changed.
            is Set<*> -> preferencesMap[key] = Collections.unmodifiableSet(value.toSet())
            else -> preferencesMap[key] = value
        }
    }

    /**
     * Appends or replaces all pairs from [prefs] to this MutablePreferences. Keys in [prefs]
     * will overwrite keys in this Preferences.
     *
     * Example usage:
     * mutablePrefs += preferencesOf(COUNTER_KEY to 100, NAME to "abcdef")
     *
     * @param prefs Preferences to append to this MutablePreferences
     */
    public operator fun plusAssign(prefs: Preferences) {
        checkNotFrozen()
        preferencesMap += prefs.asMap()
    }

    /**
     * Appends or replaces all [pair] to this MutablePreferences.
     *
     * Example usage:
     * mutablePrefs += COUNTER_KEY to 100
     *
     * @param pair the Preference.Pair to add to this MutablePreferences
     */
    public operator fun plusAssign(pair: Preferences.Pair<*>) {
        checkNotFrozen()
        putAll(pair)
    }

    /**
     * Removes the preference with the given key from this MutablePreferences. If this
     * Preferences does not contain the key, this is a no-op.
     *
     * Example usage:
     * mutablePrefs -= COUNTER_KEY
     *
     * @param key the key to remove from this MutablePreferences
     */
    public operator fun minusAssign(key: Preferences.Key<*>) {
        checkNotFrozen()
        remove(key)
    }

    /**
     * Appends or replaces all [pairs] to this MutablePreferences.
     *
     * @param pairs the pairs to append to this MutablePreferences
     */
    public fun putAll(vararg pairs: Preferences.Pair<*>) {
        checkNotFrozen()
        pairs.forEach {
            setUnchecked(it.key, it.value)
        }
    }

    /**
     * Remove a preferences from this MutablePreferences.
     *
     * @param key the key to remove this MutablePreferences
     * @return the original value of this preference key.
     */
    @Suppress("UNCHECKED_CAST")
    public fun <T> remove(key: Preferences.Key<T>): T {
        checkNotFrozen()
        return preferencesMap.remove(key) as T
    }

    /* Removes all preferences from this MutablePreferences. */
    public fun clear() {
        checkNotFrozen()
        preferencesMap.clear()
    }

    // Equals and hash code for use by DataStore
    override fun equals(other: Any?): Boolean {
        if (other is MutablePreferences) {
            return preferencesMap == other.preferencesMap
        }
        return false
    }

    override fun hashCode(): Int {
        return preferencesMap.hashCode()
    }

    /**
     * For better debugging.
     */
    override fun toString(): String =
        preferencesMap.entries.joinToString(
            separator = ",\n",
            prefix = "{\n",
            postfix = "\n}"
        ) { entry ->
            "  ${entry.key.name} = ${entry.value}"
        }
}

/**
 * Edit the value in DataStore transactionally in an atomic read-modify-write operation. All
 * operations are serialized.
 *
 * The coroutine completes when the data has been persisted durably to disk (after which
 * [DataStore.data] will reflect the update). If the transform or write to disk fails, the
 * transaction is aborted and an exception is thrown.
 *
 * Note: values that are changed in [transform] are NOT updated in DataStore until after the
 * transform completes. Do not assume that the data has been successfully persisted until after
 * edit returns successfully.
 *
 * Note: do NOT store a reference to the MutablePreferences provided to transform. Mutating this
 * after [transform] returns will NOT change the data in DataStore. Future versions of this may
 * throw exceptions if the MutablePreferences object is mutated outside of [transform].
 *
 * See [DataStore.updateData].
 *
 * Example usage:
 * val COUNTER_KEY = intPreferencesKey("my_counter")
 *
 * dataStore.edit { prefs ->
 *   prefs\[COUNTER_KEY\] = prefs\[COUNTER_KEY\] :? 0 + 1
 * }
 *
 * @param transform block which accepts MutablePreferences that contains all the preferences
 * currently in DataStore. Changes to this MutablePreferences object will be persisted once
 * transform completes.
 * @throws IOException when an exception is encountered when writing data to disk
 * @throws Exception when thrown by the transform block
 */

public suspend fun DataStore<Preferences>.edit(
    transform: suspend (MutablePreferences) -> Unit
): Preferences {
    return this.updateData {
        // It's safe to return MutablePreferences since we freeze it in
        // PreferencesDataStore.updateData()
        it.toMutablePreferences().apply { transform(this) }
    }
}