Updated on 2026-08-14
This commit is contained in:
parent
6c84e0b309
commit
6796e868c6
9 changed files with 331 additions and 1 deletions
|
|
@ -48,10 +48,12 @@ dependencies {
|
|||
/** Security */
|
||||
implementation(deps.spongecastle.core)
|
||||
|
||||
/** Chucker */
|
||||
/** Chucker */
|
||||
debugImplementation(deps.chucker)
|
||||
externalImplementation(deps.chuckerStub)
|
||||
internalImplementation(deps.chuckerStub)
|
||||
releaseImplementation(deps.chuckerStub)
|
||||
|
||||
/** Local storages */
|
||||
implementation(deps.androidx.datastore)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.local.*
|
||||
import com.tangem.datasource.local.preferences.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object AppPreferencesStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppPreferencesStore(
|
||||
@ApplicationContext appContext: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
): AppPreferencesStore {
|
||||
return AppPreferencesStore(
|
||||
preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io),
|
||||
moshi = moshi,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.datasource.local.preferences
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.MutablePreferences
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import com.squareup.moshi.Moshi
|
||||
|
||||
/**
|
||||
* Application preferences store.
|
||||
* AppPreferencesStore is wrapper around DataStore<Preferences> that supports json serialization and deserialization.
|
||||
*
|
||||
* @property moshi Moshi instance. Property has 'public' modifier because it is used
|
||||
* by Public-API inline function. Don't use it directly.
|
||||
* @property preferencesDataStore DataStore<Preferences> instance
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AppPreferencesStore(
|
||||
val moshi: Moshi,
|
||||
private val preferencesDataStore: DataStore<Preferences>,
|
||||
) : DataStore<Preferences> by preferencesDataStore {
|
||||
|
||||
/**
|
||||
* Edit data according with transaction [transform].
|
||||
*
|
||||
* @param transform transaction. It has receiver [AppPreferencesStore] that allows to use [getObject], [setObject]
|
||||
* functions when creating transaction.
|
||||
*/
|
||||
suspend fun editData(transform: suspend AppPreferencesStore.(MutablePreferences) -> Unit): Preferences {
|
||||
return edit { transform(it) }
|
||||
}
|
||||
|
||||
/** Get nullable data [T] by string [key] from [MutablePreferences] */
|
||||
inline fun <reified T> MutablePreferences.getObject(key: Preferences.Key<String>): T? {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
return this[key]?.let(adapter::fromJson)
|
||||
}
|
||||
|
||||
/** Set data [T] by string [key] to [MutablePreferences] */
|
||||
inline fun <reified T> MutablePreferences.setObject(key: Preferences.Key<String>, value: T) {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
this[key] = adapter.toJson(value)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.datasource.local.preferences
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataMigration
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import com.tangem.datasource.local.preferences.PreferencesDataStore.INSTANCE
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Application preferences data store 'DataStore<Preferences>'.
|
||||
* Implements the singleton pattern [INSTANCE] under the hood.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object PreferencesDataStore {
|
||||
|
||||
private const val PREFERENCES_FILE_NAME = "TAP_PREFS"
|
||||
|
||||
private var INSTANCE: DataStore<Preferences>? = null
|
||||
|
||||
fun getInstance(context: Context, dispatcher: CoroutineContext): DataStore<Preferences> {
|
||||
return INSTANCE ?: create(context, dispatcher).also { INSTANCE = it }
|
||||
}
|
||||
|
||||
private fun create(context: Context, dispatcher: CoroutineContext): DataStore<Preferences> {
|
||||
return PreferenceDataStoreFactory.create(
|
||||
corruptionHandler = createCorruptionHandler(),
|
||||
migrations = createMigrations(),
|
||||
scope = CoroutineScope(context = dispatcher + SupervisorJob()),
|
||||
produceFile = { context.preferencesDataStoreFile(name = PREFERENCES_FILE_NAME) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun createCorruptionHandler(): ReplaceFileCorruptionHandler<Preferences> {
|
||||
return ReplaceFileCorruptionHandler(
|
||||
produceNewData = {
|
||||
Timber.w(it)
|
||||
emptyPreferences()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMigrations(): List<DataMigration<Preferences>> {
|
||||
return listOf()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.local.preferences
|
||||
|
||||
/**
|
||||
* All preferences keys that DataStore<Preferences> is stored.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object PreferencesKeys
|
||||
|
||||
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */
|
||||
internal fun getTapPrefKeysToMigrate(): Set<String> {
|
||||
return setOf()
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.datasource.local.preferences.utils
|
||||
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/** Get flow of nullable data [T] by string [key] */
|
||||
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>): Flow<T?> {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
return data.map { it[key]?.let(adapter::fromJson) }
|
||||
}
|
||||
|
||||
/** Get flow of data [T] by string [key]. If data is not found, it returns [default] */
|
||||
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>, default: T): Flow<T> {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
return data.map { it[key]?.let(adapter::fromJson) ?: default }
|
||||
}
|
||||
|
||||
/** Get nullable data [T] by string [key] */
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key<String>): T? {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
return data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
}
|
||||
|
||||
/** Get data [T] by string [key]. If data is not found, it returns [default] */
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrDefault(
|
||||
key: Preferences.Key<String>,
|
||||
default: T,
|
||||
): T {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
return data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
?: default
|
||||
}
|
||||
|
||||
/** Store data [value] by string [key] */
|
||||
suspend inline fun <reified T> AppPreferencesStore.storeObject(key: Preferences.Key<String>, value: T) {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
edit { it[key] = adapter.toJson(value) }
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.datasource.local.preferences.utils
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/** Get flow of nullable data [T] by [key] */
|
||||
fun <T> DataStore<Preferences>.get(key: Preferences.Key<T>): Flow<T?> {
|
||||
return data.map { it[key] }
|
||||
}
|
||||
|
||||
/** Get flow of data [T] by [key]. If data is not found, it returns [default] */
|
||||
fun <T> DataStore<Preferences>.get(key: Preferences.Key<T>, default: T): Flow<T> {
|
||||
return data.map { it[key] ?: default }
|
||||
}
|
||||
|
||||
/** Get nullable data [T] by [key] */
|
||||
suspend fun <T> DataStore<Preferences>.getSyncOrNull(key: Preferences.Key<T>): T? {
|
||||
return data.firstOrNull()?.get(key)
|
||||
}
|
||||
|
||||
/** Get data [T] by [key]. If data is not found, it returns [default] */
|
||||
suspend fun <T> DataStore<Preferences>.getSyncOrDefault(key: Preferences.Key<T>, default: T): T {
|
||||
return data.firstOrNull()?.get(key) ?: default
|
||||
}
|
||||
|
||||
/** Store data [value] by [key] */
|
||||
suspend fun <T> DataStore<Preferences>.store(key: Preferences.Key<T>, value: T) {
|
||||
edit { it[key] = value }
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package com.tangem.datasource.local.preferences.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.DoNotInline
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.datastore.core.DataMigration
|
||||
import androidx.datastore.preferences.core.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Migration of a specified key with name changing.
|
||||
* Example, migrate the "key1" from "pref1" to the "key2" from "pref2".
|
||||
*
|
||||
* @property context context
|
||||
* @property legacyPrefsName legacy SharedPreferences name
|
||||
* @property legacyKeyName legacy SharedPreferences key name
|
||||
* @property keyName new SharedPreferences key name
|
||||
*/
|
||||
internal class SharedPreferencesKeyMigration(
|
||||
private val context: Context,
|
||||
private val legacyPrefsName: String,
|
||||
private val legacyKeyName: String,
|
||||
private val keyName: String,
|
||||
) : DataMigration<Preferences> {
|
||||
|
||||
private val legacyPrefs = context.getSharedPreferences(legacyPrefsName, Context.MODE_PRIVATE)
|
||||
|
||||
override suspend fun cleanUp() {
|
||||
val sharedPrefsEditor = legacyPrefs.edit()
|
||||
|
||||
sharedPrefsEditor.remove(legacyKeyName)
|
||||
|
||||
if (!sharedPrefsEditor.commit()) {
|
||||
throw IOException("Unable to delete migrated keys from SharedPreferences.")
|
||||
}
|
||||
|
||||
if (legacyPrefs.all.isEmpty()) {
|
||||
deleteSharedPreferences(context = context, name = legacyPrefsName)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun shouldMigrate(currentData: Preferences): Boolean = true
|
||||
|
||||
override suspend fun migrate(currentData: Preferences): Preferences {
|
||||
val currentKeys = currentData.asMap().keys.map(Preferences.Key<*>::name)
|
||||
|
||||
// If migration is already happened, return
|
||||
if (currentKeys.contains(keyName)) return currentData
|
||||
|
||||
val value = legacyPrefs.all[legacyKeyName]
|
||||
if (value != null) {
|
||||
val mutablePreferences = currentData.toMutablePreferences()
|
||||
|
||||
when (value) {
|
||||
is Boolean -> mutablePreferences[booleanPreferencesKey(keyName)] = value
|
||||
is Float -> mutablePreferences[floatPreferencesKey(keyName)] = value
|
||||
is Int -> mutablePreferences[intPreferencesKey(keyName)] = value
|
||||
is Long -> mutablePreferences[longPreferencesKey(keyName)] = value
|
||||
is String -> mutablePreferences[stringPreferencesKey(keyName)] = value
|
||||
is Set<*> -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
mutablePreferences[stringSetPreferencesKey(keyName)] = value as Set<String>
|
||||
}
|
||||
}
|
||||
|
||||
return mutablePreferences.toPreferences()
|
||||
}
|
||||
|
||||
return currentData
|
||||
}
|
||||
|
||||
private fun deleteSharedPreferences(context: Context, name: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
if (!Api24Impl.deleteSharedPreferences(context, name)) {
|
||||
throw IOException("Unable to delete SharedPreferences: $name")
|
||||
}
|
||||
} else {
|
||||
val prefsFile = getSharedPrefsFile(context, name)
|
||||
val prefsBackup = getSharedPrefsBackup(prefsFile)
|
||||
|
||||
prefsFile.delete()
|
||||
prefsBackup.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private object Api24Impl {
|
||||
|
||||
@JvmStatic
|
||||
@DoNotInline
|
||||
fun deleteSharedPreferences(context: Context, name: String): Boolean {
|
||||
return context.deleteSharedPreferences(name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSharedPrefsFile(context: Context, name: String): File {
|
||||
val prefsDir = File(context.applicationInfo.dataDir, "shared_prefs")
|
||||
return File(prefsDir, "$name.xml")
|
||||
}
|
||||
|
||||
private fun getSharedPrefsBackup(prefsFile: File) = File(prefsFile.path + ".bak")
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ androidxFragment = "1.5.3"
|
|||
androidxLifecycle = "2.5.1"
|
||||
androidx-paging = "3.1.1"
|
||||
androidx-palette = "1.0.0"
|
||||
androidx-datastore = "1.0.0"
|
||||
# endregion AndroidX
|
||||
|
||||
# region Compose
|
||||
|
|
@ -136,6 +137,7 @@ lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", v
|
|||
lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
|
||||
lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compoese-lifecycle-runtime" }
|
||||
androidx-palette = { module = "androidx.palette:palette", version.ref = "androidx-palette" }
|
||||
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
|
||||
# region AndroidX
|
||||
|
||||
# region Compose
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue