[go: nahoru, domu]

blob: 2252461693f13b1b1eecb12f43a5a484c65ce85d [file] [log] [blame]
/*
* 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.migrations
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import androidx.datastore.core.DataMigration
import java.io.File
import java.io.IOException
import kotlin.jvm.Throws
/**
* DataMigration instance for migrating from SharedPreferences to DataStore.
*/
public class SharedPreferencesMigration<T>
private constructor(
produceSharedPreferences: () -> SharedPreferences,
keysToMigrate: Set<String>?,
private val shouldRunMigration: suspend (T) -> Boolean = { true },
private val migrate: suspend (SharedPreferencesView, T) -> T,
private val context: Context?,
private val name: String?
) : DataMigration<T> {
/**
* DataMigration from SharedPreferences to DataStore.
*
* Note: This migration only supports the basic SharedPreferences types: boolean, float, int,
* long, string and string set. If the result of getAll contains other types, they will be
* ignored.
*
*
* Example usage:
* ```
* val sharedPrefsMigration = SharedPreferencesMigration(
* produceSharedPreferences = { EncryptedSharedPreferences.create(...) }
* ) { prefs: SharedPreferencesView, myData: MyData ->
* myData.toBuilder().setCounter(prefs.getCounter(COUNTER_KEY, default = 0)).build()
* }
* ```
* @param produceSharedPreferences Should return the instance of SharedPreferences to migrate
* from.
* @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 null, all keys will be migrated from the existing
* SharedPreferences.
* @param migrate maps SharedPreferences into T. Implementations should be idempotent
* since this may be called multiple times. See [DataMigration.migrate] for more
* information. The lambda accepts a SharedPreferencesView which is the view of the
* SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
* the current data. The function must return the migrated data.
*/
@JvmOverloads // Generate constructors for default params for java users.
public constructor(
produceSharedPreferences: () -> SharedPreferences,
keysToMigrate: Set<String>? = MIGRATE_ALL_KEYS,
shouldRunMigration: suspend (T) -> Boolean = { true },
migrate: suspend (SharedPreferencesView, T) -> T
) : this(
produceSharedPreferences,
keysToMigrate,
shouldRunMigration,
migrate,
context = null,
name = null
)
/**
* DataMigration from SharedPreferences to DataStore.
*
* If the SharedPreferences is empty once the migration completes, this migration will
* attempt to delete it.
*
* Example usage:
*
* ```
* val sharedPrefsMigration = SharedPreferencesMigration(
* context,
* mySharedPreferencesName
* ) { prefs: SharedPreferencesView, myData: MyData ->
* myData.toBuilder().setCounter(prefs.getCounter(COUNTER_KEY, default = 0)).build()
* }
* ```
*
* @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 null, all keys will be migrated from the existing
* SharedPreferences.
* @param migrate maps SharedPreferences into T. Implementations should be idempotent
* since this may be called multiple times. See [DataMigration.migrate] for more
* information. The lambda accepts a SharedPreferencesView which is the view of the
* SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
* the current data. The function must return the migrated data.
*/
@JvmOverloads // Generate constructors for default params for java users.
public constructor(
context: Context,
sharedPreferencesName: String,
keysToMigrate: Set<String>? = MIGRATE_ALL_KEYS,
shouldRunMigration: suspend (T) -> Boolean = { true },
migrate: suspend (SharedPreferencesView, T) -> T
) : this(
{ context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE) },
keysToMigrate,
shouldRunMigration,
migrate,
context,
sharedPreferencesName
)
private val sharedPrefs: SharedPreferences by lazy(produceSharedPreferences)
private val keySet: MutableSet<String> by lazy {
(keysToMigrate ?: sharedPrefs.all.keys).toMutableSet()
}
override suspend fun shouldMigrate(currentData: T): Boolean {
if (!shouldRunMigration(currentData)) {
return false
}
return keySet.any(sharedPrefs::contains)
}
override suspend fun migrate(currentData: T): T =
migrate(
SharedPreferencesView(
sharedPrefs,
keySet
),
currentData
)
@Throws(IOException::class)
override suspend fun cleanUp() {
val sharedPrefsEditor = sharedPrefs.edit()
for (key in keySet) {
sharedPrefsEditor.remove(key)
}
if (!sharedPrefsEditor.commit()) {
throw IOException("Unable to delete migrated keys from SharedPreferences.")
}
if (sharedPrefs.all.isEmpty() && context != null && name != null) {
deleteSharedPreferences(context, name)
}
keySet.clear()
}
// TODO(b/170429111): Fix the unsafe new API call.
@SuppressLint("UnsafeNewApiCall")
private fun deleteSharedPreferences(context: Context, name: String) {
if (android.os.Build.VERSION.SDK_INT >= 24) {
if (!context.deleteSharedPreferences(name)) {
throw IOException("Unable to delete SharedPreferences: $name")
}
return
}
// Context.deleteSharedPreferences is SDK 24+, so we have to reproduce the definition
val prefsFile = getSharedPrefsFile(context, name)
val prefsBackup = getSharedPrefsBackup(prefsFile)
// Silently continue if we aren't able to delete the Shared Preferences File.
prefsFile.delete()
prefsBackup.delete()
}
// ContextImpl.getSharedPreferencesPath is private, so we have to reproduce the definition
private fun getSharedPrefsFile(context: Context, name: String): File {
val prefsDir = File(context.applicationInfo.dataDir, "shared_prefs")
return File(prefsDir, "$name.xml")
}
// SharedPreferencesImpl.makeBackupFile is private, so we have to reproduce the definition
private fun getSharedPrefsBackup(prefsFile: File) = File(prefsFile.path + ".bak")
}
/**
* Read-only wrapper around SharedPreferences. This will be passed in to your migration. The
* constructor is public to enable easier testing of migrations.
*/
public class SharedPreferencesView internal constructor(
private val prefs: SharedPreferences,
private val keySet: Set<String>
) {
/**
* Checks whether the preferences contains a preference.
*
* @param key the name of the preference to check
* @throws IllegalArgumentException if `key` wasn't specified as part of this migration
*/
public operator fun contains(key: String): Boolean = prefs.contains(checkKey(key))
/**
* Retrieves a boolean value from the preferences.
*
* @param key the name of the preference to retrieve
* @param defValue value to return if this preference does not exist
* @throws IllegalArgumentException if `key` wasn't specified as part of this migration
*/
public fun getBoolean(key: String, defValue: Boolean): Boolean =
prefs.getBoolean(checkKey(key), defValue)
/**
* Retrieves a float value from the preferences.
*
* @param key the name of the preference to retrieve
* @param defValue value to return if this preference does not exist
* @throws IllegalArgumentException if `key` wasn't specified as part of this migration
*/
public fun getFloat(key: String, defValue: Float): Float =
prefs.getFloat(checkKey(key), defValue)
/**
* Retrieves a int value from the preferences.
*
* @param key the name of the preference to retrieve
* @param defValue value to return if this preference does not exist
* @throws IllegalArgumentException if `key` wasn't specified as part of this migration
*/
public fun getInt(key: String, defValue: Int): Int = prefs.getInt(checkKey(key), defValue)
/**
* Retrieves a long value from the preferences.
*
* @param key the name of the preference to retrieve
* @param defValue value to return if this preference does not exist
* @throws IllegalArgumentException if `key` wasn't specified as part of this migration
*/
public fun getLong(key: String, defValue: Long): Long = prefs.getLong(checkKey(key), defValue)
/**
* Retrieves a string value from the preferences.
*
* @param key the name of the preference to retrieve
* @param defValue value to return if this preference does not exist
* @throws IllegalArgumentException if `key` wasn't specified as part of this migration
*/
public fun getString(key: String, defValue: String? = null): String? =
prefs.getString(checkKey(key), defValue)
/**
* Retrieves a string set value from the preferences.
*
* @param key the name of the preference to retrieve
* @param defValues value to return if this preference does not exist
* @throws IllegalArgumentException if `key` wasn't specified as part of this migration
*/
public fun getStringSet(key: String, defValues: Set<String>? = null): Set<String>? =
prefs.getStringSet(checkKey(key), defValues)?.toMutableSet()
/** Retrieve all values from the preferences that are in the specified keySet. */
public fun getAll(): Map<String, Any?> = prefs.all.filter { (key, _) ->
key in keySet
}.mapValues { (_, value) ->
if (value is Set<*>) {
value.toSet()
} else {
value
}
}
private fun checkKey(key: String): String {
check(key in keySet) { "Can't access key outside migration: $key" }
return key
}
}
internal val MIGRATE_ALL_KEYS = null