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