Updated on 2026-08-14
This commit is contained in:
parent
ea780f563e
commit
42c8be234e
18 changed files with 275 additions and 193 deletions
|
|
@ -1,6 +1,9 @@
|
||||||
package com.tangem.tap
|
package com.tangem.tap
|
||||||
|
|
||||||
import android.app.Application
|
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.hilt.work.HiltWorkerFactory
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
|
|
@ -71,9 +74,7 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import com.tangem.wallet.BuildConfig
|
import com.tangem.wallet.BuildConfig
|
||||||
import dagger.hilt.EntryPoints
|
import dagger.hilt.EntryPoints
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.rekotlin.Store
|
import org.rekotlin.Store
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
|
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
|
||||||
|
|
@ -228,12 +229,32 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
private val appScope = MainScope()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
enableStrictModeInDebug()
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
init()
|
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() {
|
private fun updateLogFiles() {
|
||||||
|
|
@ -260,18 +281,31 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
|
||||||
foregroundActivityObserver = ForegroundActivityObserver()
|
foregroundActivityObserver = ForegroundActivityObserver()
|
||||||
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
||||||
|
|
||||||
// TODO: Try to performance and user experience.
|
// We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them.
|
||||||
// [REDACTED_JIRA]
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
awaitAll(
|
awaitAll(
|
||||||
async { featureTogglesManager.init() },
|
async {
|
||||||
async { excludedBlockchainsManager.init() },
|
featureTogglesManager.init()
|
||||||
async { initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) },
|
},
|
||||||
|
async {
|
||||||
|
excludedBlockchainsManager.init()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadNativeLibraries()
|
appScope.launch {
|
||||||
|
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
loadNativeLibraries()
|
||||||
|
walletConnect2Repository.init(
|
||||||
|
projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId,
|
||||||
|
)
|
||||||
|
updateLogFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ExceptionHandler.append(blockchainExceptionHandler)
|
ExceptionHandler.append(blockchainExceptionHandler)
|
||||||
|
|
||||||
if (LogConfig.network.blockchainSdkNetwork) {
|
if (LogConfig.network.blockchainSdkNetwork) {
|
||||||
BlockchainSdkRetrofitBuilder.interceptors = listOf(
|
BlockchainSdkRetrofitBuilder.interceptors = listOf(
|
||||||
createNetworkLoggingInterceptor(),
|
createNetworkLoggingInterceptor(),
|
||||||
|
|
@ -287,9 +321,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
appStateHolder.mainStore = store
|
|
||||||
|
|
||||||
walletConnect2Repository.init(projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId)
|
appStateHolder.mainStore = store
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createReduxStore(): Store<AppState> {
|
private fun createReduxStore(): Store<AppState> {
|
||||||
|
|
|
||||||
|
|
@ -20,18 +20,22 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor(
|
||||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||||
) : VisaAuthTokenStorage {
|
) : VisaAuthTokenStorage {
|
||||||
|
|
||||||
private val secureStorage = AndroidSecureStorage(
|
private val secureStorage by lazy {
|
||||||
preferences = SecureStorage.createEncryptedSharedPreferences(
|
AndroidSecureStorage(
|
||||||
context = applicationContext,
|
preferences = SecureStorage.createEncryptedSharedPreferences(
|
||||||
storageName = "visa_auth_storage",
|
context = applicationContext,
|
||||||
),
|
storageName = "visa_auth_storage",
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private val moshi = Moshi.Builder()
|
private val moshi by lazy {
|
||||||
.add(KotlinJsonAdapterFactory())
|
Moshi.Builder()
|
||||||
.build()
|
.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) {
|
override suspend fun store(cardId: String, tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) {
|
||||||
val json = tokensAdapter.toJson(tokens)
|
val json = tokensAdapter.toJson(tokens)
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,14 @@ class DefaultVisaOTPStorage @Inject constructor(
|
||||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||||
) : VisaOTPStorage {
|
) : VisaOTPStorage {
|
||||||
|
|
||||||
private val secureStorage = AndroidSecureStorage(
|
private val secureStorage by lazy {
|
||||||
preferences = SecureStorage.createEncryptedSharedPreferences(
|
AndroidSecureStorage(
|
||||||
context = applicationContext,
|
preferences = SecureStorage.createEncryptedSharedPreferences(
|
||||||
storageName = "visa_otp_storage",
|
context = applicationContext,
|
||||||
),
|
storageName = "visa_otp_storage",
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun saveOTP(cardId: String, data: VisaOtpData) = withContext(dispatcherProvider.io) {
|
override suspend fun saveOTP(cardId: String, data: VisaOtpData) = withContext(dispatcherProvider.io) {
|
||||||
secureStorage.store(data.rootOTP, VISA_ROOT_OTP_KEY_PREFIX + cardId)
|
secureStorage.store(data.rootOTP, VISA_ROOT_OTP_KEY_PREFIX + cardId)
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,12 @@ internal class DefaultUserWalletsPublicInformationRepository(
|
||||||
moshi: Moshi,
|
moshi: Moshi,
|
||||||
private val secureStorage: SecureStorage,
|
private val secureStorage: SecureStorage,
|
||||||
) : UserWalletsPublicInformationRepository {
|
) : 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> {
|
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,15 @@ internal class DefaultUserWalletsSensitiveInformationRepository(
|
||||||
private val secureStorage: SecureStorage,
|
private val secureStorage: SecureStorage,
|
||||||
) : UserWalletsSensitiveInformationRepository {
|
) : UserWalletsSensitiveInformationRepository {
|
||||||
|
|
||||||
private val sensitiveInformationAdapter: JsonAdapter<UserWalletSensitiveInformation> = moshi.adapter(
|
private val sensitiveInformationAdapter: JsonAdapter<UserWalletSensitiveInformation> by lazy {
|
||||||
UserWalletSensitiveInformation::class.java,
|
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 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> {
|
override suspend fun save(userWallet: UserWallet, encryptionKey: ByteArray?): CompletionResult<Unit> {
|
||||||
if (encryptionKey == null) {
|
if (encryptionKey == null) {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ internal class DevFeatureTogglesManager(
|
||||||
|
|
||||||
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
|
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
|
||||||
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
|
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
|
||||||
) ?: emptyMap()
|
) ?: emptyMap<String, Boolean>()
|
||||||
|
|
||||||
val localFeatureToggles = localTogglesStorage.toggles
|
val localFeatureToggles = localTogglesStorage.toggles
|
||||||
.associateToggles(currentVersion = versionProvider.get().orEmpty())
|
.associateToggles(currentVersion = versionProvider.get().orEmpty())
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.core.configtoggle.manager
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import com.google.common.truth.Truth
|
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.DevFeatureTogglesManager
|
||||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
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.PreferencesKeys
|
||||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -24,7 +26,11 @@ import kotlin.collections.set
|
||||||
internal class DevTogglesManagerTest {
|
internal class DevTogglesManagerTest {
|
||||||
|
|
||||||
private val localTogglesStorage = mockk<TogglesStorage>()
|
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 versionProvider = mockk<VersionProvider>()
|
||||||
private val manager = DevFeatureTogglesManager(
|
private val manager = DevFeatureTogglesManager(
|
||||||
localTogglesStorage = localTogglesStorage,
|
localTogglesStorage = localTogglesStorage,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.squareup.moshi.Types
|
||||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||||
import com.tangem.datasource.asset.loader.AssetLoader
|
import com.tangem.datasource.asset.loader.AssetLoader
|
||||||
import com.tangem.datasource.asset.reader.AssetReader
|
import com.tangem.datasource.asset.reader.AssetReader
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -24,7 +25,11 @@ internal class LocalTogglesStorageTest {
|
||||||
private val jsonAdapter = mockk<JsonAdapter<List<ConfigToggle>>>()
|
private val jsonAdapter = mockk<JsonAdapter<List<ConfigToggle>>>()
|
||||||
|
|
||||||
// Impossible to mockk AssetLoader because it implement inline functions
|
// 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)
|
private val storage = LocalTogglesStorage(assetLoader)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ package com.tangem.datasource.asset.loader
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
import com.squareup.moshi.adapter
|
import com.squareup.moshi.adapter
|
||||||
|
import com.tangem.utils.coroutines.runCatching
|
||||||
import com.tangem.datasource.asset.reader.AssetReader
|
import com.tangem.datasource.asset.reader.AssetReader
|
||||||
import com.tangem.datasource.di.NetworkMoshi
|
import com.tangem.datasource.di.NetworkMoshi
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
@ -23,69 +25,63 @@ import javax.inject.Singleton
|
||||||
class AssetLoader @Inject constructor(
|
class AssetLoader @Inject constructor(
|
||||||
val assetReader: AssetReader,
|
val assetReader: AssetReader,
|
||||||
@NetworkMoshi val moshi: Moshi,
|
@NetworkMoshi val moshi: Moshi,
|
||||||
|
val dispatchers: CoroutineDispatcherProvider,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/** Load content [Content] of asset file [fileName] */
|
/** Load content [Content] of asset file [fileName] */
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
suspend inline fun <reified Content> load(fileName: String): Content? {
|
suspend inline fun <reified Content> load(fileName: String): Content? = runCatching(dispatchers.io) {
|
||||||
return runCatching {
|
val json = assetReader.read(fullFileName = "$fileName.json")
|
||||||
val json = assetReader.read(fullFileName = "$fileName.json")
|
|
||||||
|
|
||||||
moshi.adapter<Content>().fromJson(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
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
.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] */
|
/** Load list [V] values of asset file [fileName] */
|
||||||
suspend inline fun <reified V> loadList(fileName: String): List<V> {
|
suspend inline fun <reified V> loadList(fileName: String): List<V> = runCatching(dispatchers.io) {
|
||||||
return runCatching {
|
val json = assetReader.read(fullFileName = "$fileName.json")
|
||||||
val json = assetReader.read(fullFileName = "$fileName.json")
|
|
||||||
|
|
||||||
val type = Types.newParameterizedType(List::class.java, V::class.java)
|
val type = Types.newParameterizedType(List::class.java, V::class.java)
|
||||||
val adapter = moshi.adapter<List<V>>(type)
|
val adapter = moshi.adapter<List<V>>(type)
|
||||||
|
|
||||||
adapter.fromJson(json)
|
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()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
.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] */
|
/** Load map [String] keys and [V] values of asset file [fileName] */
|
||||||
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> {
|
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> = runCatching(dispatchers.io) {
|
||||||
return runCatching {
|
val json = assetReader.read(fullFileName = "$fileName.json")
|
||||||
val json = assetReader.read(fullFileName = "$fileName.json")
|
|
||||||
|
|
||||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||||
|
|
||||||
adapter.fromJson(json)
|
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()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
.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()
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,23 +1,19 @@
|
||||||
package com.tangem.datasource.asset.reader
|
package com.tangem.datasource.asset.reader
|
||||||
|
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of asset file reader
|
* Implementation of asset file reader
|
||||||
*
|
*
|
||||||
* @property assetManager asset manager
|
* @property assetManager asset manager
|
||||||
* @property dispatchers dispatchers
|
|
||||||
*/
|
*/
|
||||||
internal class AndroidAssetReader(
|
internal class AndroidAssetReader(
|
||||||
private val assetManager: AssetManager,
|
private val assetManager: AssetManager,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
|
||||||
) : AssetReader {
|
) : AssetReader {
|
||||||
|
|
||||||
override suspend fun read(fullFileName: String): String = withContext(dispatchers.io) {
|
override suspend fun read(fullFileName: String): String {
|
||||||
assetManager.open(fullFileName).bufferedReader()
|
return assetManager.open(fullFileName).bufferedReader()
|
||||||
.use(BufferedReader::readText)
|
.use(BufferedReader::readText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,6 @@ package com.tangem.datasource.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.tangem.datasource.local.*
|
|
||||||
import com.tangem.datasource.local.preferences.*
|
import com.tangem.datasource.local.preferences.*
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
|
@ -26,6 +25,7 @@ internal object AppPreferencesStoreModule {
|
||||||
return AppPreferencesStore(
|
return AppPreferencesStore(
|
||||||
preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io),
|
preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io),
|
||||||
moshi = moshi,
|
moshi = moshi,
|
||||||
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,6 @@ package com.tangem.datasource.di
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.tangem.datasource.asset.reader.AndroidAssetReader
|
import com.tangem.datasource.asset.reader.AndroidAssetReader
|
||||||
import com.tangem.datasource.asset.reader.AssetReader
|
import com.tangem.datasource.asset.reader.AssetReader
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -17,10 +16,7 @@ internal object AssetReaderModule {
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun providesAsserReader(
|
fun providesAsserReader(@ApplicationContext context: Context): AssetReader {
|
||||||
@ApplicationContext context: Context,
|
return AndroidAssetReader(context.assets)
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
|
||||||
): AssetReader {
|
|
||||||
return AndroidAssetReader(context.assets, dispatchers)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -37,8 +37,12 @@ class AppLogsStore @Inject constructor(
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private val zipMutex = Mutex()
|
private val zipMutex = Mutex()
|
||||||
|
|
||||||
private val file = File(applicationContext.filesDir, PERMITTED_FILE_NAME)
|
private val logFile by lazy {
|
||||||
private val fileZip = File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP)
|
File(applicationContext.filesDir, PERMITTED_FILE_NAME)
|
||||||
|
}
|
||||||
|
private val logFileZip by lazy {
|
||||||
|
File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP)
|
||||||
|
}
|
||||||
|
|
||||||
private val formatter = DateTimeFormatterBuilder()
|
private val formatter = DateTimeFormatterBuilder()
|
||||||
.appendDayOfMonth(2)
|
.appendDayOfMonth(2)
|
||||||
|
|
@ -55,12 +59,12 @@ class AppLogsStore @Inject constructor(
|
||||||
.toFormatter()
|
.toFormatter()
|
||||||
|
|
||||||
/** Get log file */
|
/** 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? {
|
suspend fun getZipFile(): File? {
|
||||||
return zipMutex.withLock {
|
return zipMutex.withLock {
|
||||||
if (file.exists()) {
|
if (logFile.exists()) {
|
||||||
zip(listOf(file), fileZip)
|
zip(listOf(logFile), logFileZip)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
@ -98,8 +102,8 @@ class AppLogsStore @Inject constructor(
|
||||||
/** Delete deprecated logs if file size exceeds [maxSize] */
|
/** Delete deprecated logs if file size exceeds [maxSize] */
|
||||||
fun deleteDeprecatedLogs(maxSize: Int) {
|
fun deleteDeprecatedLogs(maxSize: Int) {
|
||||||
launchWithLock {
|
launchWithLock {
|
||||||
if (file.exists() && file.length() > maxSize) {
|
if (logFile.exists() && logFile.length() > maxSize) {
|
||||||
file.delete()
|
logFile.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +121,7 @@ class AppLogsStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun writeMessage(tag: String, vararg messages: String) {
|
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(formatter.print(DateTime.now()))
|
||||||
writer.append(": $tag ")
|
writer.append(": $tag ")
|
||||||
messages.forEach(writer::append)
|
messages.forEach(writer::append)
|
||||||
|
|
@ -126,8 +130,8 @@ class AppLogsStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createFileIfNotExist() {
|
private fun createFileIfNotExist() {
|
||||||
if (!file.exists()) {
|
if (!logFile.exists()) {
|
||||||
runCatching { file.createNewFile() }
|
runCatching { logFile.createNewFile() }
|
||||||
.onFailure(Timber::e)
|
.onFailure(Timber::e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application preferences store.
|
* Application preferences store.
|
||||||
|
|
@ -19,6 +20,7 @@ import com.squareup.moshi.Types
|
||||||
*/
|
*/
|
||||||
class AppPreferencesStore(
|
class AppPreferencesStore(
|
||||||
val moshi: Moshi,
|
val moshi: Moshi,
|
||||||
|
val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val preferencesDataStore: DataStore<Preferences>,
|
private val preferencesDataStore: DataStore<Preferences>,
|
||||||
) : DataStore<Preferences> by preferencesDataStore {
|
) : DataStore<Preferences> by preferencesDataStore {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,25 @@ import androidx.datastore.preferences.core.edit
|
||||||
import com.squareup.moshi.JsonDataException
|
import com.squareup.moshi.JsonDataException
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
/** Get flow of nullable data [T] by string [key] */
|
/** Get flow of nullable data [T] by string [key] */
|
||||||
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>): Flow<T?> {
|
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>): Flow<T?> {
|
||||||
val adapter = moshi.adapter(T::class.java)
|
return flow {
|
||||||
return data.map { preferences ->
|
val adapter = moshi.adapter(T::class.java)
|
||||||
preferences[key]?.let {
|
emitAll(
|
||||||
try {
|
data.map { preferences ->
|
||||||
adapter.fromJson(it)
|
preferences[key]?.let {
|
||||||
} catch (e: JsonDataException) {
|
try {
|
||||||
null
|
adapter.fromJson(it)
|
||||||
}
|
} catch (e: JsonDataException) {
|
||||||
}
|
null
|
||||||
}.distinctUntilChanged()
|
}
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,16 +34,19 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
|
||||||
* @see getObjectList
|
* @see getObjectList
|
||||||
* */
|
* */
|
||||||
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>, default: T): Flow<T> {
|
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 flow {
|
||||||
return data.map {
|
val adapter = moshi.adapter(T::class.java)
|
||||||
try {
|
emitAll(
|
||||||
it[key]?.let(adapter::fromJson) ?: default
|
data.map {
|
||||||
} catch (e: JsonDataException) {
|
try {
|
||||||
default
|
it[key]?.let(adapter::fromJson) ?: default
|
||||||
}
|
} catch (e: JsonDataException) {
|
||||||
}.distinctUntilChanged()
|
default
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get nullable data [T] by string [key]
|
* Get nullable data [T] by string [key]
|
||||||
*
|
*
|
||||||
|
|
@ -49,26 +54,27 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
|
||||||
*
|
*
|
||||||
* @see getObjectListSync
|
* @see getObjectListSync
|
||||||
* */
|
* */
|
||||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key<String>): T? {
|
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key<String>): T? =
|
||||||
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
|
withContext(dispatchers.io) {
|
||||||
return data.firstOrNull()
|
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
|
||||||
?.get(key)
|
data.firstOrNull()
|
||||||
?.let {
|
?.get(key)
|
||||||
try {
|
?.let {
|
||||||
adapter.fromJson(it)
|
try {
|
||||||
} catch (e: JsonDataException) {
|
adapter.fromJson(it)
|
||||||
null
|
} catch (e: JsonDataException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** Get data [T] by string [key]. If data is not found, it returns [default] */
|
/** Get data [T] by string [key]. If data is not found, it returns [default] */
|
||||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrDefault(
|
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrDefault(
|
||||||
key: Preferences.Key<String>,
|
key: Preferences.Key<String>,
|
||||||
default: T,
|
default: T,
|
||||||
): T {
|
): T = withContext(dispatchers.io) {
|
||||||
val adapter = moshi.adapter(T::class.java)
|
val adapter = moshi.adapter(T::class.java)
|
||||||
return data.firstOrNull()
|
data.firstOrNull()
|
||||||
?.get(key)
|
?.get(key)
|
||||||
?.let {
|
?.let {
|
||||||
try {
|
try {
|
||||||
|
|
@ -87,37 +93,47 @@ suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrDefault(
|
||||||
*
|
*
|
||||||
* @see storeObjectList
|
* @see storeObjectList
|
||||||
* */
|
* */
|
||||||
suspend inline fun <reified T> AppPreferencesStore.storeObject(key: Preferences.Key<String>, value: T) {
|
@Suppress("OptionalUnit")
|
||||||
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
|
suspend inline fun <reified T> AppPreferencesStore.storeObject(key: Preferences.Key<String>, value: T): Unit =
|
||||||
edit { it[key] = adapter.toJson(value) }
|
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] */
|
/** Store list of data [value] by string [key] */
|
||||||
suspend inline fun <reified T> AppPreferencesStore.storeObjectList(key: Preferences.Key<String>, value: List<T>) {
|
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))
|
withContext(dispatchers.io) {
|
||||||
edit { it[key] = adapter.toJson(value) }
|
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` */
|
/** 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>?> {
|
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 flow {
|
||||||
return data.map { it[key]?.let(adapter::fromJson) }.distinctUntilChanged()
|
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 */
|
/** 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> {
|
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))
|
withContext(dispatchers.io) {
|
||||||
return data.firstOrNull()
|
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
|
||||||
?.get(key)
|
data.firstOrNull()
|
||||||
?.let(adapter::fromJson)
|
?.get(key)
|
||||||
.orEmpty()
|
?.let(adapter::fromJson)
|
||||||
}
|
.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
/** Store map with [String] key and value [V] by string [key] */
|
/** Store map with [String] key and value [V] by string [key] */
|
||||||
suspend inline fun <reified V> AppPreferencesStore.storeObjectMap(
|
suspend inline fun <reified V> AppPreferencesStore.storeObjectMap(
|
||||||
key: Preferences.Key<String>,
|
key: Preferences.Key<String>,
|
||||||
value: Map<String, V>,
|
value: Map<String, V>,
|
||||||
) {
|
) = withContext(dispatchers.io) {
|
||||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
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 */
|
/** 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> {
|
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)
|
withContext(dispatchers.io) {
|
||||||
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)
|
||||||
|
|
||||||
return data.firstOrNull()
|
data.firstOrNull()
|
||||||
?.get(key)
|
?.get(key)
|
||||||
?.let(adapter::fromJson)
|
?.let(adapter::fromJson)
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get flow of map with [String] key and value [V] by string [key], or empty if data is not found */
|
/** 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>> {
|
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)
|
return flow {
|
||||||
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)
|
||||||
|
|
||||||
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 */
|
/** 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> {
|
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))
|
withContext(dispatchers.io) {
|
||||||
return data.firstOrNull()
|
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
|
||||||
?.get(key)
|
data.firstOrNull()
|
||||||
?.let(adapter::fromJson)
|
?.get(key)
|
||||||
.orEmpty()
|
?.let(adapter::fromJson)
|
||||||
}
|
.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
/** Get flow of set of [T] by string [key], or empty if data is not found */
|
/** 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>> {
|
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 flow {
|
||||||
return data.map {
|
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
|
||||||
it[key]?.let(adapter::fromJson) ?: emptySet()
|
emitAll(
|
||||||
|
data.map {
|
||||||
|
it[key]?.let(adapter::fromJson) ?: emptySet()
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -22,7 +22,9 @@ internal class SharedPreferencesKeyMigration(
|
||||||
private val keyName: String,
|
private val keyName: String,
|
||||||
) : DataMigration<Preferences> {
|
) : 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() {
|
override suspend fun cleanUp() {
|
||||||
val sharedPrefsEditor = legacyPrefs.edit()
|
val sharedPrefsEditor = legacyPrefs.edit()
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import com.squareup.moshi.Types
|
||||||
import com.squareup.moshi.adapter
|
import com.squareup.moshi.adapter
|
||||||
import com.tangem.datasource.api.express.models.response.Asset
|
import com.tangem.datasource.api.express.models.response.Asset
|
||||||
import com.tangem.datasource.asset.reader.AssetReader
|
import com.tangem.datasource.asset.reader.AssetReader
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerifyOrder
|
import io.mockk.coVerifyOrder
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
|
@ -22,7 +23,11 @@ class AssetLoaderTest {
|
||||||
|
|
||||||
private val assetReader = mockk<AssetReader>()
|
private val assetReader = mockk<AssetReader>()
|
||||||
private val moshi = mockk<Moshi>()
|
private val moshi = mockk<Moshi>()
|
||||||
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
|
private val assetLoader = AssetLoader(
|
||||||
|
assetReader = assetReader,
|
||||||
|
moshi = moshi,
|
||||||
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun load() = runTest {
|
fun load() = runTest {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package com.tangem.datasource.asset.reader
|
||||||
|
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -15,7 +14,7 @@ import java.io.IOException
|
||||||
internal class AndroidAssetReaderTest {
|
internal class AndroidAssetReaderTest {
|
||||||
|
|
||||||
private val assetManager = mockk<AssetManager>()
|
private val assetManager = mockk<AssetManager>()
|
||||||
private val assetReader = AndroidAssetReader(assetManager, TestingCoroutineDispatcherProvider())
|
private val assetReader = AndroidAssetReader(assetManager)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun read_content() = runTest {
|
fun read_content() = runTest {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue