1 /* <lambda>null2 * Copyright 2020 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 17 package androidx.datastore.migrations 18 19 import android.content.Context 20 import android.content.SharedPreferences 21 import android.os.Build 22 import androidx.annotation.RequiresApi 23 import androidx.datastore.core.DataMigration 24 import java.io.File 25 import java.io.IOException 26 import kotlin.jvm.Throws 27 28 /** DataMigration instance for migrating from SharedPreferences to DataStore. */ 29 public class SharedPreferencesMigration<T> 30 private constructor( 31 produceSharedPreferences: () -> SharedPreferences, 32 keysToMigrate: Set<String>, 33 private val shouldRunMigration: suspend (T) -> Boolean = { true }, 34 private val migrate: suspend (SharedPreferencesView, T) -> T, 35 private val context: Context?, 36 private val name: String? 37 ) : DataMigration<T> { 38 39 /** 40 * DataMigration from SharedPreferences to DataStore. 41 * 42 * Note: This migration only supports the basic SharedPreferences types: boolean, float, int, 43 * long, string and string set. If the result of getAll contains other types, they will be 44 * ignored. 45 * 46 * Example usage: 47 * ``` 48 * val sharedPrefsMigration = SharedPreferencesMigration( 49 * produceSharedPreferences = { EncryptedSharedPreferences.create(...) } 50 * ) { prefs: SharedPreferencesView, myData: MyData -> 51 * myData.toBuilder().setCounter(prefs.getCounter(COUNTER_KEY, default = 0)).build() 52 * } 53 * ``` 54 * 55 * @param produceSharedPreferences Should return the instance of SharedPreferences to migrate 56 * from. 57 * @param keysToMigrate The list of keys to migrate. The keys will be mapped to datastore 58 * .Preferences with their same values. If the key is already present in the new Preferences, 59 * the key will not be migrated again. If the key is not present in the SharedPreferences it 60 * will not be migrated. If keysToMigrate is not set, all keys will be migrated from the 61 * existing SharedPreferences. 62 * @param shouldRunMigration A lambda which accepts current data of type `T` and returns whether 63 * migration should run. 64 * @param migrate maps SharedPreferences into T. Implementations should be idempotent since this 65 * may be called multiple times. See [DataMigration.migrate] for more information. The lambda 66 * accepts a SharedPreferencesView which is the view of the SharedPreferences to migrate from 67 * (limited to [keysToMigrate] and a T which represent the current data. The function must 68 * return the migrated data. If SharedPreferences is empty or does not contain any keys which 69 * you specified, this callback will not run. 70 */ 71 @JvmOverloads // Generate constructors for default params for java users. 72 public constructor( 73 produceSharedPreferences: () -> SharedPreferences, 74 keysToMigrate: Set<String> = MIGRATE_ALL_KEYS, <lambda>null75 shouldRunMigration: suspend (T) -> Boolean = { true }, 76 migrate: suspend (SharedPreferencesView, T) -> T 77 ) : this( 78 produceSharedPreferences, 79 keysToMigrate, 80 shouldRunMigration, 81 migrate, 82 context = null, 83 name = null 84 ) 85 86 /** 87 * DataMigration from SharedPreferences to DataStore. 88 * 89 * If the SharedPreferences is empty once the migration completes, this migration will attempt 90 * to delete it. 91 * 92 * Example usage: 93 * ``` 94 * val sharedPrefsMigration = SharedPreferencesMigration( 95 * context, 96 * mySharedPreferencesName 97 * ) { prefs: SharedPreferencesView, myData: MyData -> 98 * myData.toBuilder().setCounter(prefs.getCounter(COUNTER_KEY, default = 0)).build() 99 * } 100 * ``` 101 * 102 * @param context Context used for getting SharedPreferences. 103 * @param sharedPreferencesName The name of the SharedPreferences. 104 * @param keysToMigrate The list of keys to migrate. The keys will be mapped to datastore 105 * .Preferences with their same values. If the key is already present in the new Preferences, 106 * the key will not be migrated again. If the key is not present in the SharedPreferences it 107 * will not be migrated. If keysToMigrate is not set, all keys will be migrated from the 108 * existing SharedPreferences. 109 * @param shouldRunMigration A lambda which accepts current data of type `T` and returns whether 110 * migration should run. 111 * @param migrate maps SharedPreferences into T. Implementations should be idempotent since this 112 * may be called multiple times. See [DataMigration.migrate] for more information. The lambda 113 * accepts a SharedPreferencesView which is the view of the SharedPreferences to migrate from 114 * (limited to [keysToMigrate] and a T which represent the current data. The function must 115 * return the migrated data. If SharedPreferences is empty or does not contain any keys which 116 * you specified, this callback will not run. 117 */ 118 @JvmOverloads // Generate constructors for default params for java users. 119 public constructor( 120 context: Context, 121 sharedPreferencesName: String, 122 keysToMigrate: Set<String> = MIGRATE_ALL_KEYS, <lambda>null123 shouldRunMigration: suspend (T) -> Boolean = { true }, 124 migrate: suspend (SharedPreferencesView, T) -> T 125 ) : this( <lambda>null126 { context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE) }, 127 keysToMigrate, 128 shouldRunMigration, 129 migrate, 130 context, 131 sharedPreferencesName 132 ) 133 134 private val sharedPrefs: SharedPreferences by lazy(produceSharedPreferences) 135 136 /** keySet is null if the user specified [MIGRATE_ALL_KEYS]. */ 137 private val keySet: MutableSet<String>? = 138 if (keysToMigrate === MIGRATE_ALL_KEYS) { 139 null 140 } else { 141 keysToMigrate.toMutableSet() 142 } 143 shouldMigratenull144 override suspend fun shouldMigrate(currentData: T): Boolean { 145 if (!shouldRunMigration(currentData)) { 146 return false 147 } 148 149 return if (keySet == null) { 150 sharedPrefs.all.isNotEmpty() 151 } else { 152 keySet.any(sharedPrefs::contains) 153 } 154 } 155 migratenull156 override suspend fun migrate(currentData: T): T = 157 migrate(SharedPreferencesView(sharedPrefs, keySet), currentData) 158 159 @Throws(IOException::class) 160 override suspend fun cleanUp() { 161 val sharedPrefsEditor = sharedPrefs.edit() 162 163 if (keySet == null) { 164 sharedPrefsEditor.clear() 165 } else { 166 keySet.forEach { key -> sharedPrefsEditor.remove(key) } 167 } 168 169 if (!sharedPrefsEditor.commit()) { 170 throw IOException("Unable to delete migrated keys from SharedPreferences.") 171 } 172 173 if (sharedPrefs.all.isEmpty() && context != null && name != null) { 174 deleteSharedPreferences(context, name) 175 } 176 177 keySet?.clear() 178 } 179 deleteSharedPreferencesnull180 private fun deleteSharedPreferences(context: Context, name: String) { 181 if (Build.VERSION.SDK_INT >= 24) { 182 // Silently continue if we aren't able to delete the Shared Preferences. See b/195553816 183 Api24Impl.deleteSharedPreferences(context, name) 184 } else { 185 // Context.deleteSharedPreferences is SDK 24+, so we have to reproduce the definition 186 val prefsFile = getSharedPrefsFile(context, name) 187 val prefsBackup = getSharedPrefsBackup(prefsFile) 188 189 // Silently continue if we aren't able to delete the Shared Preferences File. 190 prefsFile.delete() 191 prefsBackup.delete() 192 } 193 } 194 195 // ContextImpl.getSharedPreferencesPath is private, so we have to reproduce the definition getSharedPrefsFilenull196 private fun getSharedPrefsFile(context: Context, name: String): File { 197 val prefsDir = File(context.applicationInfo.dataDir, "shared_prefs") 198 return File(prefsDir, "$name.xml") 199 } 200 201 // SharedPreferencesImpl.makeBackupFile is private, so we have to reproduce the definition getSharedPrefsBackupnull202 private fun getSharedPrefsBackup(prefsFile: File) = File(prefsFile.path + ".bak") 203 204 @RequiresApi(24) 205 private object Api24Impl { 206 @JvmStatic 207 fun deleteSharedPreferences(context: Context, name: String): Boolean { 208 return context.deleteSharedPreferences(name) 209 } 210 } 211 } 212 213 /** Read-only wrapper around SharedPreferences. This will be passed in to your migration. */ 214 public class SharedPreferencesView 215 internal constructor(private val prefs: SharedPreferences, private val keySet: Set<String>?) { 216 /** 217 * Checks whether the preferences contains a preference. 218 * 219 * @param key the name of the preference to check 220 * @throws IllegalArgumentException if `key` wasn't specified as part of this migration 221 */ containsnull222 public operator fun contains(key: String): Boolean = prefs.contains(checkKey(key)) 223 224 /** 225 * Retrieves a boolean value from the preferences. 226 * 227 * @param key the name of the preference to retrieve 228 * @param defValue value to return if this preference does not exist 229 * @throws IllegalArgumentException if `key` wasn't specified as part of this migration 230 */ 231 public fun getBoolean(key: String, defValue: Boolean): Boolean = 232 prefs.getBoolean(checkKey(key), defValue) 233 234 /** 235 * Retrieves a float value from the preferences. 236 * 237 * @param key the name of the preference to retrieve 238 * @param defValue value to return if this preference does not exist 239 * @throws IllegalArgumentException if `key` wasn't specified as part of this migration 240 */ 241 public fun getFloat(key: String, defValue: Float): Float = 242 prefs.getFloat(checkKey(key), defValue) 243 244 /** 245 * Retrieves a int value from the preferences. 246 * 247 * @param key the name of the preference to retrieve 248 * @param defValue value to return if this preference does not exist 249 * @throws IllegalArgumentException if `key` wasn't specified as part of this migration 250 */ 251 public fun getInt(key: String, defValue: Int): Int = prefs.getInt(checkKey(key), defValue) 252 253 /** 254 * Retrieves a long value from the preferences. 255 * 256 * @param key the name of the preference to retrieve 257 * @param defValue value to return if this preference does not exist 258 * @throws IllegalArgumentException if `key` wasn't specified as part of this migration 259 */ 260 public fun getLong(key: String, defValue: Long): Long = prefs.getLong(checkKey(key), defValue) 261 262 /** 263 * Retrieves a string value from the preferences. 264 * 265 * @param key the name of the preference to retrieve 266 * @param defValue value to return if this preference does not exist 267 * @throws IllegalArgumentException if `key` wasn't specified as part of this migration 268 */ 269 public fun getString(key: String, defValue: String? = null): String? = 270 prefs.getString(checkKey(key), defValue) 271 272 /** 273 * Retrieves a string set value from the preferences. 274 * 275 * @param key the name of the preference to retrieve 276 * @param defValues value to return if this preference does not exist 277 * @throws IllegalArgumentException if `key` wasn't specified as part of this migration 278 */ 279 public fun getStringSet(key: String, defValues: Set<String>? = null): Set<String>? = 280 prefs.getStringSet(checkKey(key), defValues)?.toMutableSet() 281 282 /** Retrieve all values from the preferences that are in the specified keySet. */ 283 public fun getAll(): Map<String, Any?> = 284 prefs.all 285 .filter { (key, _) -> keySet?.contains(key) ?: true } valuenull286 .mapValues { (_, value) -> 287 if (value is Set<*>) { 288 value.toSet() 289 } else { 290 value 291 } 292 } 293 checkKeynull294 private fun checkKey(key: String): String { 295 keySet?.let { check(key in it) { "Can't access key outside migration: $key" } } 296 297 return key 298 } 299 } 300 301 internal val MIGRATE_ALL_KEYS: Set<String> = mutableSetOf() 302