SharedPreferencesMigration.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

import android.content.Context
import androidx.datastore.migrations.SharedPreferencesView
import androidx.datastore.migrations.SharedPreferencesMigration
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.toMutablePreferences
import androidx.datastore.preferences.core.toPreferences

/**
 * Creates a SharedPreferencesMigration for DataStore<Preferences>.
 *
 * @param context Context used for getting SharedPreferences.
 * @param sharedPreferencesName The name of the SharedPreferences.
 * @param keysToMigrate The list of keys to migrate. The keys will be mapped to datastore.Preferences with
 * their same values. If the key is already present in the new Preferences, the key
 * will not be migrated again. If the key is not present in the SharedPreferences it
 * will not be migrated. If keysToMigrate is not set, all keys will be migrated from the existing
 * SharedPreferences.
 * @param deleteEmptyPreferences If enabled and the SharedPreferences are empty (i.e. no remaining
 * keys) after this migration runs, the leftover SharedPreferences file is deleted. Note that
 * this cleanup runs only if the migration itself runs, i.e., if the keys were never in
 * SharedPreferences to begin with then the (potentially) empty SharedPreferences
 * won't be cleaned up by this option. This functionality is best effort - if there
 * is an issue deleting the SharedPreferences file it will be silently ignored.
 *
 * TODO(rohitsat): determine whether to remove the deleteEmptyPreferences option.
 */
@JvmOverloads // Generate methods for default params for java users.
public fun SharedPreferencesMigration(
    context: Context,
    sharedPreferencesName: String,
    keysToMigrate: Set<String>? = MIGRATE_ALL_KEYS,
    deleteEmptyPreferences: Boolean = true
): SharedPreferencesMigration<Preferences> {
    return SharedPreferencesMigration(
        context = context,
        sharedPreferencesName = sharedPreferencesName,
        keysToMigrate = keysToMigrate,
        deleteEmptyPreferences = deleteEmptyPreferences,
        shouldRunMigration = { prefs ->
            // If any key hasn't been migrated to currentData, we can't skip the migration. If
            // the key set is not specified, we can't skip the migration.
            val allKeys = prefs.asMap().keys.map { it.name }
            keysToMigrate?.any { it !in allKeys } ?: true
        },
        migrate = { sharedPrefs: SharedPreferencesView, currentData: Preferences ->
            // prefs.getAll is already filtered to our key set, but we don't want to overwrite
            // already existing keys.
            val currentKeys = currentData.asMap().keys.map { it.name }

            val filteredSharedPreferences =
                sharedPrefs.getAll().filter { (key, _) -> key !in currentKeys }

            val mutablePreferences = currentData.toMutablePreferences()
            for ((key, value) in filteredSharedPreferences) {
                when (value) {
                    is Boolean -> mutablePreferences[
                        androidx.datastore.preferences.core.preferencesKey(
                            key
                        )
                    ] = value
                    is Float -> mutablePreferences[
                        androidx.datastore.preferences.core.preferencesKey(
                            key
                        )
                    ] = value
                    is Int -> mutablePreferences[
                        androidx.datastore.preferences.core.preferencesKey(
                            key
                        )
                    ] = value
                    is Long -> mutablePreferences[
                        androidx.datastore.preferences.core.preferencesKey(
                            key
                        )
                    ] = value
                    is String -> mutablePreferences[
                        androidx.datastore.preferences.core.preferencesKey(
                            key
                        )
                    ] = value
                    is Set<*> -> {
                        @Suppress("UNCHECKED_CAST")
                        mutablePreferences[
                            androidx.datastore.preferences.core.preferencesSetKey<String>(
                                key
                            )
                        ] = value as Set<String>
                    }
                }
            }

            mutablePreferences.toPreferences()
        }
    )
}

internal val MIGRATE_ALL_KEYS = null