Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-09 15:03:19 +05:00
parent ea780f563e
commit 42c8be234e
18 changed files with 275 additions and 193 deletions

View file

@ -1,6 +1,9 @@
package com.tangem.tap
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import coil.ImageLoader
@ -71,9 +74,7 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.*
import org.rekotlin.Store
import kotlin.collections.set
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
@ -228,12 +229,32 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
// endregion
private val appScope = MainScope()
override fun onCreate() {
enableStrictModeInDebug()
super.onCreate()
init()
}
updateLogFiles()
private fun enableStrictModeInDebug() {
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectAll()
.penaltyLog()
.build(),
)
StrictMode.setVmPolicy(
VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build(),
)
}
}
private fun updateLogFiles() {
@ -260,18 +281,31 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
// TODO: Try to performance and user experience.
// [REDACTED_JIRA]
// We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them.
runBlocking {
awaitAll(
async { featureTogglesManager.init() },
async { excludedBlockchainsManager.init() },
async { initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) },
async {
featureTogglesManager.init()
},
async {
excludedBlockchainsManager.init()
},
)
}
loadNativeLibraries()
appScope.launch {
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
launch(Dispatchers.IO) {
loadNativeLibraries()
walletConnect2Repository.init(
projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId,
)
updateLogFiles()
}
}
ExceptionHandler.append(blockchainExceptionHandler)
if (LogConfig.network.blockchainSdkNetwork) {
BlockchainSdkRetrofitBuilder.interceptors = listOf(
createNetworkLoggingInterceptor(),
@ -287,9 +321,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
appStateHolder.mainStore = store
walletConnect2Repository.init(projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId)
appStateHolder.mainStore = store
}
private fun createReduxStore(): Store<AppState> {

View file

@ -20,18 +20,22 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor(
private val dispatcherProvider: CoroutineDispatcherProvider,
) : VisaAuthTokenStorage {
private val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_auth_storage",
),
)
private val secureStorage by lazy {
AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_auth_storage",
),
)
}
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val moshi by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
private val tokensAdapter = moshi.adapter(VisaAuthTokens::class.java)
private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) }
override suspend fun store(cardId: String, tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) {
val json = tokensAdapter.toJson(tokens)

View file

@ -20,12 +20,14 @@ class DefaultVisaOTPStorage @Inject constructor(
private val dispatcherProvider: CoroutineDispatcherProvider,
) : VisaOTPStorage {
private val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_otp_storage",
),
)
private val secureStorage by lazy {
AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_otp_storage",
),
)
}
override suspend fun saveOTP(cardId: String, data: VisaOtpData) = withContext(dispatcherProvider.io) {
secureStorage.store(data.rootOTP, VISA_ROOT_OTP_KEY_PREFIX + cardId)

View file

@ -20,9 +20,12 @@ internal class DefaultUserWalletsPublicInformationRepository(
moshi: Moshi,
private val secureStorage: SecureStorage,
) : UserWalletsPublicInformationRepository {
private val publicInformationAdapter: JsonAdapter<List<UserWalletPublicInformation>> = moshi.adapter(
Types.newParameterizedType(List::class.java, UserWalletPublicInformation::class.java),
)
private val publicInformationAdapter: JsonAdapter<List<UserWalletPublicInformation>> by lazy {
moshi.adapter(
Types.newParameterizedType(List::class.java, UserWalletPublicInformation::class.java),
)
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return withContext(Dispatchers.IO) {

View file

@ -24,12 +24,15 @@ internal class DefaultUserWalletsSensitiveInformationRepository(
private val secureStorage: SecureStorage,
) : UserWalletsSensitiveInformationRepository {
private val sensitiveInformationAdapter: JsonAdapter<UserWalletSensitiveInformation> = moshi.adapter(
UserWalletSensitiveInformation::class.java,
)
private val encryptedSensitiveInformationMapAdapter: JsonAdapter<Map<String, ByteArray>> = moshi.adapter(
Types.newParameterizedType(Map::class.java, String::class.java, ByteArray::class.java),
)
private val sensitiveInformationAdapter: JsonAdapter<UserWalletSensitiveInformation> by lazy {
moshi.adapter(UserWalletSensitiveInformation::class.java)
}
private val encryptedSensitiveInformationMapAdapter: JsonAdapter<Map<String, ByteArray>> by lazy {
moshi.adapter(
Types.newParameterizedType(Map::class.java, String::class.java, ByteArray::class.java),
)
}
override suspend fun save(userWallet: UserWallet, encryptionKey: ByteArray?): CompletionResult<Unit> {
if (encryptionKey == null) {

View file

@ -32,7 +32,7 @@ internal class DevFeatureTogglesManager(
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
) ?: emptyMap()
) ?: emptyMap<String, Boolean>()
val localFeatureToggles = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())

View file

@ -2,6 +2,7 @@ package com.tangem.core.configtoggle.manager
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.squareup.moshi.Moshi
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
import com.tangem.core.configtoggle.storage.ConfigToggle
@ -12,6 +13,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -24,7 +26,11 @@ import kotlin.collections.set
internal class DevTogglesManagerTest {
private val localTogglesStorage = mockk<TogglesStorage>()
private val appPreferenceStore = mockk<AppPreferencesStore>(relaxed = true)
private val appPreferenceStore = AppPreferencesStore(
moshi = Moshi.Builder().build(),
dispatchers = TestingCoroutineDispatcherProvider(),
preferencesDataStore = mockk(relaxed = true),
)
private val versionProvider = mockk<VersionProvider>()
private val manager = DevFeatureTogglesManager(
localTogglesStorage = localTogglesStorage,

View file

@ -8,6 +8,7 @@ import com.squareup.moshi.Types
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -24,7 +25,11 @@ internal class LocalTogglesStorageTest {
private val jsonAdapter = mockk<JsonAdapter<List<ConfigToggle>>>()
// Impossible to mockk AssetLoader because it implement inline functions
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
private val assetLoader = AssetLoader(
assetReader = assetReader,
moshi = moshi,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val storage = LocalTogglesStorage(assetLoader)

View file

@ -3,8 +3,10 @@ package com.tangem.datasource.asset.loader
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.adapter
import com.tangem.utils.coroutines.runCatching
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -23,69 +25,63 @@ import javax.inject.Singleton
class AssetLoader @Inject constructor(
val assetReader: AssetReader,
@NetworkMoshi val moshi: Moshi,
val dispatchers: CoroutineDispatcherProvider,
) {
/** Load content [Content] of asset file [fileName] */
@OptIn(ExperimentalStdlibApi::class)
suspend inline fun <reified Content> load(fileName: String): Content? {
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
suspend inline fun <reified Content> load(fileName: String): Content? = runCatching(dispatchers.io) {
val json = assetReader.read(fullFileName = "$fileName.json")
moshi.adapter<Content>().fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
null
},
)
moshi.adapter<Content>().fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
null
},
)
/** Load list [V] values of asset file [fileName] */
suspend inline fun <reified V> loadList(fileName: String): List<V> {
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
suspend inline fun <reified V> loadList(fileName: String): List<V> = runCatching(dispatchers.io) {
val json = assetReader.read(fullFileName = "$fileName.json")
val type = Types.newParameterizedType(List::class.java, V::class.java)
val adapter = moshi.adapter<List<V>>(type)
val type = Types.newParameterizedType(List::class.java, V::class.java)
val adapter = moshi.adapter<List<V>>(type)
adapter.fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig.orEmpty()
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
emptyList()
},
)
adapter.fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig.orEmpty()
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
emptyList()
},
)
/** Load map [String] keys and [V] values of asset file [fileName] */
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> {
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> = runCatching(dispatchers.io) {
val json = assetReader.read(fullFileName = "$fileName.json")
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
adapter.fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig.orEmpty()
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
emptyMap()
},
)
adapter.fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig.orEmpty()
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
emptyMap()
},
)
}

View file

@ -1,23 +1,19 @@
package com.tangem.datasource.asset.reader
import android.content.res.AssetManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.io.BufferedReader
/**
* Implementation of asset file reader
*
* @property assetManager asset manager
* @property dispatchers dispatchers
*/
internal class AndroidAssetReader(
private val assetManager: AssetManager,
private val dispatchers: CoroutineDispatcherProvider,
) : AssetReader {
override suspend fun read(fullFileName: String): String = withContext(dispatchers.io) {
assetManager.open(fullFileName).bufferedReader()
override suspend fun read(fullFileName: String): String {
return assetManager.open(fullFileName).bufferedReader()
.use(BufferedReader::readText)
}
}

View file

@ -2,7 +2,6 @@ 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
@ -26,6 +25,7 @@ internal object AppPreferencesStoreModule {
return AppPreferencesStore(
preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io),
moshi = moshi,
dispatchers = dispatchers,
)
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.datasource.di
import android.content.Context
import com.tangem.datasource.asset.reader.AndroidAssetReader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -17,10 +16,7 @@ internal object AssetReaderModule {
@Singleton
@Provides
fun providesAsserReader(
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): AssetReader {
return AndroidAssetReader(context.assets, dispatchers)
fun providesAsserReader(@ApplicationContext context: Context): AssetReader {
return AndroidAssetReader(context.assets)
}
}

View file

@ -37,8 +37,12 @@ class AppLogsStore @Inject constructor(
private val mutex = Mutex()
private val zipMutex = Mutex()
private val file = File(applicationContext.filesDir, PERMITTED_FILE_NAME)
private val fileZip = File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP)
private val logFile by lazy {
File(applicationContext.filesDir, PERMITTED_FILE_NAME)
}
private val logFileZip by lazy {
File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP)
}
private val formatter = DateTimeFormatterBuilder()
.appendDayOfMonth(2)
@ -55,12 +59,12 @@ class AppLogsStore @Inject constructor(
.toFormatter()
/** Get log file */
fun getFile(): File? = if (file.exists()) file else null
fun getFile(): File? = if (logFile.exists()) logFile else null
suspend fun getZipFile(): File? {
return zipMutex.withLock {
if (file.exists()) {
zip(listOf(file), fileZip)
if (logFile.exists()) {
zip(listOf(logFile), logFileZip)
} else {
null
}
@ -98,8 +102,8 @@ class AppLogsStore @Inject constructor(
/** Delete deprecated logs if file size exceeds [maxSize] */
fun deleteDeprecatedLogs(maxSize: Int) {
launchWithLock {
if (file.exists() && file.length() > maxSize) {
file.delete()
if (logFile.exists() && logFile.length() > maxSize) {
logFile.delete()
}
}
}
@ -117,7 +121,7 @@ class AppLogsStore @Inject constructor(
}
private fun writeMessage(tag: String, vararg messages: String) {
BufferedWriter(FileWriter(file, true)).use { writer ->
BufferedWriter(FileWriter(logFile, true)).use { writer ->
writer.append(formatter.print(DateTime.now()))
writer.append(": $tag ")
messages.forEach(writer::append)
@ -126,8 +130,8 @@ class AppLogsStore @Inject constructor(
}
private fun createFileIfNotExist() {
if (!file.exists()) {
runCatching { file.createNewFile() }
if (!logFile.exists()) {
runCatching { logFile.createNewFile() }
.onFailure(Timber::e)
}
}

View file

@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
/**
* Application preferences store.
@ -19,6 +20,7 @@ import com.squareup.moshi.Types
*/
class AppPreferencesStore(
val moshi: Moshi,
val dispatchers: CoroutineDispatcherProvider,
private val preferencesDataStore: DataStore<Preferences>,
) : DataStore<Preferences> by preferencesDataStore {

View file

@ -5,23 +5,25 @@ import androidx.datastore.preferences.core.edit
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Types
import com.tangem.datasource.local.preferences.AppPreferencesStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
/** 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 { preferences ->
preferences[key]?.let {
try {
adapter.fromJson(it)
} catch (e: JsonDataException) {
null
}
}
}.distinctUntilChanged()
return flow {
val adapter = moshi.adapter(T::class.java)
emitAll(
data.map { preferences ->
preferences[key]?.let {
try {
adapter.fromJson(it)
} catch (e: JsonDataException) {
null
}
}
}.distinctUntilChanged(),
)
}
}
/**
@ -32,16 +34,19 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
* @see getObjectList
* */
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>, default: T): Flow<T> {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
return data.map {
try {
it[key]?.let(adapter::fromJson) ?: default
} catch (e: JsonDataException) {
default
}
}.distinctUntilChanged()
return flow {
val adapter = moshi.adapter(T::class.java)
emitAll(
data.map {
try {
it[key]?.let(adapter::fromJson) ?: default
} catch (e: JsonDataException) {
default
}
}.distinctUntilChanged(),
)
}
}
/**
* Get nullable data [T] by string [key]
*
@ -49,26 +54,27 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
*
* @see getObjectListSync
* */
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key<String>): T? {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
return data.firstOrNull()
?.get(key)
?.let {
try {
adapter.fromJson(it)
} catch (e: JsonDataException) {
null
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key<String>): T? =
withContext(dispatchers.io) {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
data.firstOrNull()
?.get(key)
?.let {
try {
adapter.fromJson(it)
} catch (e: JsonDataException) {
null
}
}
}
}
}
/** 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 {
): T = withContext(dispatchers.io) {
val adapter = moshi.adapter(T::class.java)
return data.firstOrNull()
data.firstOrNull()
?.get(key)
?.let {
try {
@ -87,37 +93,47 @@ suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrDefault(
*
* @see storeObjectList
* */
suspend inline fun <reified T> AppPreferencesStore.storeObject(key: Preferences.Key<String>, value: T) {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
edit { it[key] = adapter.toJson(value) }
}
@Suppress("OptionalUnit")
suspend inline fun <reified T> AppPreferencesStore.storeObject(key: Preferences.Key<String>, value: T): Unit =
withContext(dispatchers.io) {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
edit { it[key] = adapter.toJson(value) }
}
/** Store list of data [value] by string [key] */
suspend inline fun <reified T> AppPreferencesStore.storeObjectList(key: Preferences.Key<String>, value: List<T>) {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
edit { it[key] = adapter.toJson(value) }
}
suspend inline fun <reified T> AppPreferencesStore.storeObjectList(key: Preferences.Key<String>, value: List<T>) =
withContext(dispatchers.io) {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
edit { it[key] = adapter.toJson(value) }
}
/** Get flow of list of data [T] by string [key]. If data is not found, it returns `null` */
inline fun <reified T> AppPreferencesStore.getObjectList(key: Preferences.Key<String>): Flow<List<T>?> {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
return data.map { it[key]?.let(adapter::fromJson) }.distinctUntilChanged()
return flow {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
emitAll(
data.map {
it[key]?.let(adapter::fromJson)
}.distinctUntilChanged(),
)
}
}
/** Get list of data [T] by string [key], or empty if data is not found */
suspend inline fun <reified T> AppPreferencesStore.getObjectListSync(key: Preferences.Key<String>): List<T> {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
return data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
suspend inline fun <reified T> AppPreferencesStore.getObjectListSync(key: Preferences.Key<String>): List<T> =
withContext(dispatchers.io) {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
/** Store map with [String] key and value [V] by string [key] */
suspend inline fun <reified V> AppPreferencesStore.storeObjectMap(
key: Preferences.Key<String>,
value: Map<String, V>,
) {
) = withContext(dispatchers.io) {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
@ -125,37 +141,47 @@ suspend inline fun <reified V> AppPreferencesStore.storeObjectMap(
}
/** Get map with [String] key and value [V] by string [key], or empty if data is not found */
suspend inline fun <reified V> AppPreferencesStore.getObjectMapSync(key: Preferences.Key<String>): Map<String, V> {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
suspend inline fun <reified V> AppPreferencesStore.getObjectMapSync(key: Preferences.Key<String>): Map<String, V> =
withContext(dispatchers.io) {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
return data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
/** Get flow of map with [String] key and value [V] by string [key], or empty if data is not found */
inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences.Key<String>): Flow<Map<String, V>> {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
return flow {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
return data.map { it[key]?.let(adapter::fromJson) ?: emptyMap() }
emitAll(
data.map { it[key]?.let(adapter::fromJson) ?: emptyMap() },
)
}
}
/** Get set of data [T] by string [key], or empty if data is not found */
suspend inline fun <reified T> AppPreferencesStore.getObjectSetSync(key: Preferences.Key<String>): Set<T> {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
return data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
suspend inline fun <reified T> AppPreferencesStore.getObjectSetSync(key: Preferences.Key<String>): Set<T> =
withContext(dispatchers.io) {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
/** Get flow of set of [T] by string [key], or empty if data is not found */
inline fun <reified T> AppPreferencesStore.getObjectSet(key: Preferences.Key<String>): Flow<Set<T>> {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
return data.map {
it[key]?.let(adapter::fromJson) ?: emptySet()
return flow {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
emitAll(
data.map {
it[key]?.let(adapter::fromJson) ?: emptySet()
},
)
}
}

View file

@ -22,7 +22,9 @@ internal class SharedPreferencesKeyMigration(
private val keyName: String,
) : DataMigration<Preferences> {
private val legacyPrefs = context.getSharedPreferences(legacyPrefsName, Context.MODE_PRIVATE)
private val legacyPrefs by lazy {
context.getSharedPreferences(legacyPrefsName, Context.MODE_PRIVATE)
}
override suspend fun cleanUp() {
val sharedPrefsEditor = legacyPrefs.edit()

View file

@ -7,6 +7,7 @@ import com.squareup.moshi.Types
import com.squareup.moshi.adapter
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.every
@ -22,7 +23,11 @@ class AssetLoaderTest {
private val assetReader = mockk<AssetReader>()
private val moshi = mockk<Moshi>()
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
private val assetLoader = AssetLoader(
assetReader = assetReader,
moshi = moshi,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@Test
fun load() = runTest {

View file

@ -2,7 +2,6 @@ package com.tangem.datasource.asset.reader
import android.content.res.AssetManager
import com.google.common.truth.Truth
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
@ -15,7 +14,7 @@ import java.io.IOException
internal class AndroidAssetReaderTest {
private val assetManager = mockk<AssetManager>()
private val assetReader = AndroidAssetReader(assetManager, TestingCoroutineDispatcherProvider())
private val assetReader = AndroidAssetReader(assetManager)
@Test
fun read_content() = runTest {