From d3f3e86fa360fb289438edc77a64da17188c13c7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Mar 2023 23:27:34 +0800 Subject: [PATCH 1/2] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 11 +++++---- .../com/tangem/tap/common/redux/AppReducer.kt | 2 ++ .../com/tangem/tap/common/redux/AppState.kt | 4 ++++ .../tokens/LoadAvailableCoinsService.kt | 6 ++--- .../tap/domain/twins/TwinCardsManager.kt | 4 ++-- .../features/di/redux/DaggerGraphAction.kt | 9 ++++++++ .../di/redux/DaggerGraphMiddleware.kt | 12 ++++++++++ .../features/di/redux/DaggerGraphReducer.kt | 20 ++++++++++++++++ .../tap/features/di/redux/DaggerGraphState.kt | 6 +++++ .../products/twins/redux/TwinCardsAction.kt | 2 +- .../products/twins/ui/TwinsCardsFragment.kt | 12 +++++++--- .../features/tokens/redux/TokensMiddleware.kt | 15 +++--------- .../datasource/asset/AndroidAssetReader.kt | 23 +++++++++++++++++++ .../tangem/datasource/asset/AssetReader.kt | 12 ++++++++++ .../datasource/config/FeaturesLocalLoader.kt | 6 ++--- .../com/tangem/datasource/di/AssetModule.kt | 18 +++++++++++++++ .../tangem/datasource/utils/AssetReader.kt | 23 ------------------- 17 files changed, 134 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphAction.kt create mode 100644 app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphReducer.kt create mode 100644 app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphState.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/utils/AssetReader.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 56b14e2732..29e19dad86 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -12,11 +12,10 @@ import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.core.analytics.Analytics import com.tangem.datasource.api.common.MoshiConverter +import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config -import com.tangem.datasource.utils.AndroidAssetReader -import com.tangem.datasource.utils.AssetReader import com.tangem.domain.DomainLayer import com.tangem.domain.common.LogConfig import com.tangem.tap.common.IntentHandler @@ -49,6 +48,7 @@ import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation import com.tangem.tap.domain.walletconnect.WalletConnectRepository +import com.tangem.tap.features.di.redux.DaggerGraphAction import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.persistence.PreferencesStorage import com.tangem.tap.proxy.AppStateHolder @@ -66,7 +66,6 @@ lateinit var activityResultCaller: ActivityResultCaller lateinit var preferencesStorage: PreferencesStorage lateinit var walletConnectRepository: WalletConnectRepository lateinit var shopService: TangemShopService -lateinit var assetReader: AssetReader lateinit var userTokensRepository: UserTokensRepository private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() } @@ -117,6 +116,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var configManager: ConfigManager + @Inject + lateinit var assetReader: AssetReader + override fun onCreate() { super.onCreate() @@ -141,7 +143,6 @@ class TapApplication : Application(), ImageLoaderFactory { preferencesStorage = PreferencesStorage(this) walletConnectRepository = WalletConnectRepository(this) - assetReader = AndroidAssetReader(this) val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) initConfigManager(configLoader, ::initWithConfigDependency) initWarningMessagesManager() @@ -159,6 +160,8 @@ class TapApplication : Application(), ImageLoaderFactory { appStateHolder.mainStore = store appStateHolder.userTokensRepository = userTokensRepository appStateHolder.walletStoresManager = walletStoresManager + + store.dispatch(DaggerGraphAction.SetDependencies(assetReader)) } override fun newImageLoader(): ImageLoader { diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 3bcb47a092..0a85acf922 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -4,6 +4,7 @@ import com.tangem.tap.common.redux.global.globalReducer import com.tangem.tap.common.redux.navigation.NavigationReducer import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer +import com.tangem.tap.features.di.redux.DaggerGraphReducer import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer import com.tangem.tap.features.home.redux.HomeReducer import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer @@ -44,6 +45,7 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) saveWalletState = SaveWalletReducer.reduce(action, state), walletSelectorState = WalletSelectorReducer.reduce(action, state), sprinklrState = SprinklrReducer.reduce(action, state), + daggerGraphState = DaggerGraphReducer.reduce(action, state), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index e9b253d90d..cd4e19b15f 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -11,6 +11,8 @@ import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState +import com.tangem.tap.features.di.redux.DaggerGraphMiddleware +import com.tangem.tap.features.di.redux.DaggerGraphState import com.tangem.tap.features.disclaimer.redux.DisclaimerMiddleware import com.tangem.tap.features.disclaimer.redux.DisclaimerState import com.tangem.tap.features.home.redux.HomeMiddleware @@ -64,6 +66,7 @@ data class AppState( val saveWalletState: SaveWalletState = SaveWalletState(), val walletSelectorState: WalletSelectorState = WalletSelectorState(), val sprinklrState: SprinklrState = SprinklrState(), + val daggerGraphState: DaggerGraphState = DaggerGraphState(), ) : StateType { val domainState: DomainState @@ -105,6 +108,7 @@ data class AppState( LockUserWalletsTimerMiddleware().middleware, AccessCodeRequestPolicyMiddleware().middleware, SprinklrMiddleware().middleware, + DaggerGraphMiddleware.daggerGraphMiddleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt index bc1f1dccc5..e2cf3b1355 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt @@ -6,8 +6,8 @@ import com.tangem.common.services.Result import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.datasource.asset.AssetReader import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.datasource.utils.AssetReader import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -74,8 +74,8 @@ class LoadAvailableCoinsService( } } - fun getTestnetCoins(): List { - val json = assetReader.readAssetAsString(FILE_NAME_TESTNET_COINS) + private fun getTestnetCoins(): List { + val json = assetReader.readJson(FILE_NAME_TESTNET_COINS) return currenciesAdapter.fromJson(json)!!.coins .map { Currency.fromJsonObject(it) } } diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index 8e2cea35fa..3795a0cf00 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -9,10 +9,10 @@ import com.tangem.common.KeyPair import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.datasource.api.common.MoshiConverter +import com.tangem.datasource.asset.AssetReader import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.operations.wallet.CreateWalletResponse -import com.tangem.datasource.utils.AssetReader import com.tangem.tap.tangemSdkManager class TwinCardsManager( @@ -89,7 +89,7 @@ class TwinCardsManager( } private fun getIssuers(reader: AssetReader): List { - val file = reader.readAssetAsString("tangem-app-config/issuers") + val file = reader.readJson(fileName = "tangem-app-config/issuers") return getAdapter().fromJson(file)!! } } diff --git a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphAction.kt new file mode 100644 index 0000000000..6019e1d5b3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphAction.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.features.di.redux + +import com.tangem.datasource.asset.AssetReader +import org.rekotlin.Action + +sealed interface DaggerGraphAction : Action { + + data class SetDependencies(val assetReader: AssetReader) : DaggerGraphAction +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphMiddleware.kt b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphMiddleware.kt new file mode 100644 index 0000000000..d88f8da24e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphMiddleware.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.features.di.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Middleware + +object DaggerGraphMiddleware { + val daggerGraphMiddleware: Middleware = { _, _ -> + { next -> + { action -> next(action) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphReducer.kt new file mode 100644 index 0000000000..f060e845f9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphReducer.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.features.di.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Action + +object DaggerGraphReducer { + fun reduce(action: Action, state: AppState): DaggerGraphState { + if (action !is DaggerGraphAction) return state.daggerGraphState + + return internalReduce(action, state) + } + + private fun internalReduce(action: DaggerGraphAction, state: AppState): DaggerGraphState { + return when (action) { + is DaggerGraphAction.SetDependencies -> state.daggerGraphState.copy( + assetReader = action.assetReader, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphState.kt new file mode 100644 index 0000000000..fe726f8dc0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphState.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.features.di.redux + +import com.tangem.datasource.asset.AssetReader +import org.rekotlin.StateType + +data class DaggerGraphState(val assetReader: AssetReader? = null) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt index ac58136a05..c552a354a4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.Message import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.VoidCallback -import com.tangem.datasource.utils.AssetReader +import com.tangem.datasource.asset.AssetReader import com.tangem.domain.common.ScanResponse import com.tangem.tap.domain.TapError import com.tangem.tap.domain.twins.TwinCardsManager diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index f471878c85..416dbb70e3 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -15,7 +15,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.VoidCallback import com.tangem.core.analytics.Analytics import com.tangem.core.ui.fragments.setStatusBarColor -import com.tangem.datasource.utils.AndroidAssetReader +import com.tangem.datasource.asset.AssetReader import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TwinCardNumber import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget @@ -39,10 +39,16 @@ import com.tangem.tap.features.wallet.redux.Artwork import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @Suppress("LargeClass") +@AndroidEntryPoint class TwinsCardsFragment : BaseOnboardingFragment() { + @Inject + lateinit var assetReader: AssetReader + private val mainBinding by lazy { binding.vMain } private var previousStep: TwinCardsStep = TwinCardsStep.None @@ -253,8 +259,8 @@ class TwinsCardsFragment : BaseOnboardingFragment() { Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) store.dispatch( TwinCardsAction.Wallet.LaunchFirstStep( - Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), - AndroidAssetReader(requireContext()), + initialMessage = Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), + reader = assetReader, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index 27aa168f02..d072b93e1f 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -21,7 +21,6 @@ import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction import com.tangem.domain.redux.domainStore import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE -import com.tangem.tap.assetReader import com.tangem.tap.common.analytics.events.ManageTokens import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain @@ -87,7 +86,7 @@ class TokensMiddleware { val loadCoinsService = LoadAvailableCoinsService( tangemTechApi = store.state.domainNetworks.tangemTechService.api, dispatchers = AppCoroutineDispatcherProvider(), - assetReader = assetReader, + assetReader = requireNotNull(store.state.daggerGraphState.assetReader) { "Asset reader is null" }, ) scope.launch { @@ -266,18 +265,10 @@ class TokensMiddleware { val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } if (toDerive.isEmpty()) return null - return DerivationData( - derivations = mapKeyOfWalletPublicKey to toDerive, - alreadyDerivedKeys = alreadyDerivedKeys, - mapKeyOfWalletPublicKey = mapKeyOfWalletPublicKey, - ) + return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) } - private class DerivationData( - val derivations: Pair>, - val alreadyDerivedKeys: ExtendedPublicKeysMap, - val mapKeyOfWalletPublicKey: ByteArrayKey, - ) + private class DerivationData(val derivations: Pair>) private fun submitAdd(scanResponse: ScanResponse, currencyList: List) { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt new file mode 100644 index 0000000000..7e33fefae3 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.asset + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.BufferedReader +import javax.inject.Inject + +/** + * Implementation of asset file reader + * + * @property context application context + */ +internal class AndroidAssetReader @Inject constructor( + @ApplicationContext private val context: Context, +) : AssetReader { + + override fun readJson(fileName: String): String { + return context.assets + .open("$fileName.json") + .bufferedReader() + .use(BufferedReader::readText) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt new file mode 100644 index 0000000000..6475ad3909 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.asset + +/** + * Asset file reader + * +[REDACTED_AUTHOR] + */ +interface AssetReader { + + /** Read content of json file [fileName] from asset */ + fun readJson(fileName: String): String +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt index 8ae508cbd3..829b539552 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt @@ -2,10 +2,10 @@ package com.tangem.datasource.config import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi +import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.config.models.ConfigModel import com.tangem.datasource.config.models.ConfigValueModel import com.tangem.datasource.config.models.FeatureModel -import com.tangem.datasource.utils.AssetReader import timber.log.Timber /** @@ -25,8 +25,8 @@ class FeaturesLocalLoader( val featureAdapter: JsonAdapter = moshi.adapter(FeatureModel::class.java) val valuesAdapter: JsonAdapter = moshi.adapter(ConfigValueModel::class.java) - val jsonFeatures = assetReader.readAssetAsString(featuresName) - val jsonConfigValues = assetReader.readAssetAsString(configValuesName) + val jsonFeatures = assetReader.readJson(featuresName) + val jsonConfigValues = assetReader.readJson(configValuesName) ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues)) } catch (ex: Exception) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt new file mode 100644 index 0000000000..7df5502310 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.asset.AndroidAssetReader +import com.tangem.datasource.asset.AssetReader +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface AssetModule { + + @Binds + @Singleton + fun bindAsserReader(androidAssetReader: AndroidAssetReader): AssetReader +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/AssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/AssetReader.kt deleted file mode 100644 index 919d273aea..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/AssetReader.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.datasource.utils - -import android.content.Context - -/** -[REDACTED_AUTHOR] - */ -interface AssetReader { - fun readAssetAsString(name: String): String -} - -class AndroidAssetReader( - private val context: Context, -) : AssetReader { - - override fun readAssetAsString(name: String): String { - return context.readAssetAsString(name) - } -} - -fun Context.readAssetAsString(fileName: String): String { - return this.assets.open("$fileName.json").bufferedReader().readText() -} \ No newline at end of file From c118781f5a9d87f83a84825b182116740affe191 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Mar 2023 17:18:36 +0800 Subject: [PATCH 2/2] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../java/com/tangem/tap/TapApplication.kt | 10 +++ buildSrc/src/main/java/AppConfig.kt | 2 +- buildSrc/src/main/java/ConfigFieldBuilder.kt | 16 ++++- buildSrc/src/main/java/Dependency.kt | 1 + buildSrc/src/main/java/Versions.kt | 1 + core/datasource/build.gradle.kts | 5 +- .../di/AppPreferenceStorageModule.kt | 18 ++++++ .../datasource/local/AppPreferenceStorage.kt | 12 ++++ .../local/AppPreferenceStorageImpl.kt | 24 +++++++ core/featuretoggles/.gitignore | 1 + core/featuretoggles/build.gradle.kts | 62 ++++++++++++++++++ .../src/main/AndroidManifest.xml | 2 + .../configs/feature_toggles_config.json | 10 +++ .../core/featuretoggle/FeatureToggle.kt | 12 ++++ .../core/featuretoggle/contract/Version.kt | 63 +++++++++++++++++++ .../contract/VersionAvailabilityContract.kt | 20 ++++++ .../di/FeatureTogglesManagerModule.kt | 55 ++++++++++++++++ .../manager/DevFeatureTogglesManager.kt | 54 ++++++++++++++++ .../manager/FeatureTogglesManager.kt | 17 +++++ .../manager/MutableFeatureTogglesManager.kt | 15 +++++ .../manager/ProdFeatureTogglesManager.kt | 29 +++++++++ .../featuretoggle/storage/FeatureToggle.kt | 13 ++++ .../storage/FeatureTogglesStorage.kt | 15 +++++ .../storage/LocalFeatureTogglesStorage.kt | 35 +++++++++++ .../core/featuretoggle/utils/CollectionExt.kt | 13 ++++ .../version/DefaultVersionProvider.kt | 37 +++++++++++ .../featuretoggle/version/VersionProvider.kt | 8 +++ features/tester/impl/build.gradle.kts | 15 +++++ settings.gradle.kts | 1 + 30 files changed, 561 insertions(+), 6 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt create mode 100644 core/featuretoggles/.gitignore create mode 100644 core/featuretoggles/build.gradle.kts create mode 100644 core/featuretoggles/src/main/AndroidManifest.xml create mode 100644 core/featuretoggles/src/main/assets/configs/feature_toggles_config.json create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureToggle.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/utils/CollectionExt.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/VersionProvider.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1fff3a2a88..9de8e3ac3e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -112,6 +112,7 @@ dependencies { implementation(project(":domain")) implementation(project(":common")) implementation(project(":core:analytics")) + implementation(project(":core:featuretoggles")) implementation(project(":core:res")) implementation(project(":core:ui")) implementation(project(":core:datasource")) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 29e19dad86..69fc9f25f6 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -11,6 +11,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.core.analytics.Analytics +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.config.ConfigManager @@ -54,6 +55,7 @@ import com.tangem.tap.persistence.PreferencesStorage import com.tangem.tap.proxy.AppStateHolder import com.tangem.wallet.BuildConfig import dagger.hilt.android.HiltAndroidApp +import kotlinx.coroutines.launch import okhttp3.logging.HttpLoggingInterceptor import org.rekotlin.Store import timber.log.Timber @@ -119,6 +121,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var assetReader: AssetReader + @Inject + lateinit var featureTogglesManager: FeatureTogglesManager + override fun onCreate() { super.onCreate() @@ -162,6 +167,10 @@ class TapApplication : Application(), ImageLoaderFactory { appStateHolder.walletStoresManager = walletStoresManager store.dispatch(DaggerGraphAction.SetDependencies(assetReader)) + + scope.launch { + featureTogglesManager.init() + } } override fun newImageLoader(): ImageLoader { @@ -211,6 +220,7 @@ class TapApplication : Application(), ImageLoaderFactory { ) { fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply { appVersion = try { + // TODO don't use deprecated method val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) pInfo.versionName } catch (e: PackageManager.NameNotFoundException) { diff --git a/buildSrc/src/main/java/AppConfig.kt b/buildSrc/src/main/java/AppConfig.kt index 9a7e91768d..5c966326f1 100644 --- a/buildSrc/src/main/java/AppConfig.kt +++ b/buildSrc/src/main/java/AppConfig.kt @@ -1,7 +1,7 @@ object AppConfig { const val packageName = "com.tangem.wallet" const val versionCode = 1 - const val versionName = "1.0.0.-SNAPSHOT" + const val versionName = "1.0.0-SNAPSHOT" const val minSdkVersion = 21 const val targetSdkVersion = 33 const val compileSdkVersion = 33 diff --git a/buildSrc/src/main/java/ConfigFieldBuilder.kt b/buildSrc/src/main/java/ConfigFieldBuilder.kt index 42c1f16c56..596ae047d6 100644 --- a/buildSrc/src/main/java/ConfigFieldBuilder.kt +++ b/buildSrc/src/main/java/ConfigFieldBuilder.kt @@ -18,9 +18,19 @@ class BuildConfigFieldFactory( */ sealed class Field(val type: String, val name: String, val value: String) { - class Environment(value: String) : Field("String", "ENVIRONMENT", "\"$value\"") + class Environment(value: String) : Field(type = "String", name = "ENVIRONMENT", value = "\"$value\"") - class TestActionEnabled(isEnabled: Boolean) : Field("Boolean","TEST_ACTION_ENABLED", isEnabled.toString()) + class TestActionEnabled(isEnabled: Boolean) : Field( + type = "Boolean", + name = "TEST_ACTION_ENABLED", + value = isEnabled.toString(), + ) - class LogEnabled(isEnabled: Boolean) : Field("Boolean","LOG_ENABLED", isEnabled.toString()) + class LogEnabled(isEnabled: Boolean) : Field(type = "Boolean", name = "LOG_ENABLED", value = isEnabled.toString()) + + class TesterMenuAvailability(isEnabled: Boolean) : Field( + type = "Boolean", + name = "TESTER_MENU_ENABLED", + value = isEnabled.toString(), + ) } \ No newline at end of file diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt index eaf82306b2..f739b70479 100644 --- a/buildSrc/src/main/java/Dependency.kt +++ b/buildSrc/src/main/java/Dependency.kt @@ -47,6 +47,7 @@ object Library { const val hiltCore = "com.google.dagger:hilt-core:" + Versions.hilt const val hiltKapt = "com.google.dagger:hilt-compiler:" + Versions.hilt const val jodatime = "joda-time:joda-time:" + Versions.jodatime + const val krateSharedPref = "hu.autsoft:krate:" + Versions.krateSharedPref const val kotsonGsonExt = "com.github.salomonbrys.kotson:kotson:" + Versions.kotsonGsonExt const val lottie = "com.airbnb.android:lottie:" + Versions.lottie const val materialComponent = "com.google.android.material:material:" + Versions.googleMaterialComponent diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index 32ba2da6ab..ff548135ef 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -39,6 +39,7 @@ object Versions { const val googlePlayServicesWallet = "19.1.0" const val hilt = "2.44" const val jodatime = "2.12.1" + const val krateSharedPref = "2.0.0" const val kotsonGsonExt = "2.5.0" const val lottie = "3.4.0" const val moshi = "1.12.0" diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 4cd3a19a15..1199f1f518 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -58,12 +58,13 @@ dependencies { implementation(Library.timber) /** Network */ - implementation(Library.retrofit) - implementation(Library.retrofitMoshiConverter) + implementation(Library.krateSharedPref) implementation(Library.moshi) implementation(Library.moshiKotlin) implementation(Library.okHttp) implementation(Library.okHttpLogging) + implementation(Library.retrofit) + implementation(Library.retrofitMoshiConverter) /** Time */ implementation(Library.jodatime) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt new file mode 100644 index 0000000000..2eefec13ed --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.AppPreferenceStorage +import com.tangem.datasource.local.AppPreferenceStorageImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface AppPreferenceStorageModule { + + @Binds + @Singleton + fun bindAppPreferenceStorage(appPreferenceStorageImpl: AppPreferenceStorageImpl): AppPreferenceStorage +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt new file mode 100644 index 0000000000..3e30c7344b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.local + +/** + * Application local storage + * +[REDACTED_AUTHOR] + */ +interface AppPreferenceStorage { + + /** Json config with feature toggles 'ToggleName: String - Availability: Boolean' */ + var featureToggles: String +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt new file mode 100644 index 0000000000..3ce9a8aca9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt @@ -0,0 +1,24 @@ +package com.tangem.datasource.local + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import hu.autsoft.krate.SimpleKrate +import hu.autsoft.krate.default.withDefault +import hu.autsoft.krate.stringPref +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Implementation of application local storage + * + * @param context application context + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class AppPreferenceStorageImpl @Inject constructor( + @ApplicationContext context: Context, +) : SimpleKrate(context = context), AppPreferenceStorage { + + override var featureToggles: String by stringPref().withDefault("") +} \ No newline at end of file diff --git a/core/featuretoggles/.gitignore b/core/featuretoggles/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/featuretoggles/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/featuretoggles/build.gradle.kts b/core/featuretoggles/build.gradle.kts new file mode 100644 index 0000000000..84a19a537f --- /dev/null +++ b/core/featuretoggles/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("kapt") + id("com.google.dagger.hilt.android") +} + +android { + defaultConfig { + compileSdk = AppConfig.compileSdkVersion + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + buildTypes { + release { + BuildConfigFieldFactory( + fields = listOf(Field.TesterMenuAvailability(false)), + builder = ::buildConfigField, + ).create() + } + + debug { + BuildConfigFieldFactory( + fields = listOf(Field.TesterMenuAvailability(true)), + builder = ::buildConfigField, + ).create() + } + + create("debug_beta") { + initWith(getByName("release")) + BuildConfigFieldFactory( + fields = listOf( + Field.Environment("release"), + Field.TestActionEnabled(true), + Field.LogEnabled(true), + Field.TesterMenuAvailability(true), + ), + builder = ::buildConfigField, + ).create() + } + } +} + +dependencies { + implementation(Library.moshi) + implementation(Library.moshiKotlin) + implementation(Library.hilt) + kapt(Library.hiltKapt) + implementation(Library.timber) + + implementation(project(":core:datasource")) +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/AndroidManifest.xml b/core/featuretoggles/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..f96c64f1db --- /dev/null +++ b/core/featuretoggles/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json new file mode 100644 index 0000000000..34473131eb --- /dev/null +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -0,0 +1,10 @@ +[ + { + "name": "INACTIVE_TEST_FEATURE_ENABLED", + "version": "undefined" + }, + { + "name": "ACTIVE2_TEST_FEATURE_ENABLED", + "version": "1.0.0" + } +] \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt new file mode 100644 index 0000000000..6b2c82cc51 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt @@ -0,0 +1,12 @@ +package com.tangem.core.featuretoggle + +/** + * Feature toggle + * +[REDACTED_AUTHOR] + */ +interface FeatureToggle { + + /** Feature toggle name */ + val name: String +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt new file mode 100644 index 0000000000..6e5075c705 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt @@ -0,0 +1,63 @@ +package com.tangem.core.featuretoggle.contract + +import timber.log.Timber + +/** + * Presentation of application version (..). + * + * @param value version value as string + * +[REDACTED_AUTHOR] + */ +internal class Version private constructor(value: String) : Comparable { + + private val major: Int + private val minor: Int + private val fix: Int? + + init { + val versions = value.split(VERSION_DELIMITER).map(String::toInt) + + major = versions.getVersionValue(index = MAJOR_VERSION_POSITION) + minor = versions.getVersionValue(index = MINOR_VERSION_POSITION) + fix = versions.getOrNull(index = FIX_VERSION_POSITION) + } + + override fun compareTo(other: Version): Int { + var result = major.compareTo(other.major) + if (result == 0) result = minor.compareTo(other.minor) + if (result == 0) { + when { + fix == null && other.fix == null -> result = 0 + fix == null && other.fix != null -> result = -1 + fix != null && other.fix == null -> result = 1 + fix != null && other.fix != null -> result = fix.compareTo(other.fix) + } + } + return result + } + + private fun List.getVersionValue(index: Int): Int { + return getOrNull(index) ?: error("Invalid version") + } + + companion object { + private const val MAJOR_VERSION_POSITION = 0 + private const val MINOR_VERSION_POSITION = 1 + private const val FIX_VERSION_POSITION = 2 + private const val VERSION_DELIMITER = "." + + /** + * Create instance with value [value]. + * If [value] doesn't meet all requirements, the function returns null. + */ + fun create(value: String): Version? { + return try { + Version(value) + } catch (exception: Exception) { + Timber.e(exception, "Version %s is null", value) + return null + } + } + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt new file mode 100644 index 0000000000..2ee5c72e14 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt @@ -0,0 +1,20 @@ +package com.tangem.core.featuretoggle.contract + +/** + * Version contract to evaluate availability of feature toggle + * +[REDACTED_AUTHOR] + */ +internal object VersionAvailabilityContract { + + private const val DISABLED_FEATURE_TOGGLE_VERSION = "undefined" + + /** Evaluate availability of feature toggles using [currentVersion] and [localVersion] */ + operator fun invoke(currentVersion: String, localVersion: String): Boolean { + if (localVersion == DISABLED_FEATURE_TOGGLE_VERSION) return false + val current = Version.create(currentVersion) ?: return false + val local = Version.create(localVersion) ?: return false + + return current >= local + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt new file mode 100644 index 0000000000..eadb4bf0fb --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt @@ -0,0 +1,55 @@ +package com.tangem.core.featuretoggle.di + +import android.content.Context +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.core.featuretoggle.manager.DevFeatureTogglesManager +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.featuretoggle.manager.ProdFeatureTogglesManager +import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage +import com.tangem.core.featuretoggle.version.DefaultVersionProvider +import com.tangem.core.featuretoggles.BuildConfig +import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.local.AppPreferenceStorage +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 FeatureTogglesManagerModule { + + @Provides + @Singleton + @OptIn(ExperimentalStdlibApi::class) + fun provideFeatureTogglesManager( + @ApplicationContext context: Context, + assetReader: AssetReader, + appPreferenceStorage: AppPreferenceStorage, + ): FeatureTogglesManager { + val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() + val localFeatureTogglesStorage = LocalFeatureTogglesStorage( + assetReader = assetReader, + jsonAdapter = moshi.adapter(), + ) + val versionProvider = DefaultVersionProvider(context) + + return if (BuildConfig.TESTER_MENU_ENABLED) { + DevFeatureTogglesManager( + localFeatureTogglesStorage = localFeatureTogglesStorage, + appPreferenceStorage = appPreferenceStorage, + jsonAdapter = moshi.adapter(), + versionProvider = versionProvider, + ) + } else { + ProdFeatureTogglesManager( + localFeatureTogglesStorage = localFeatureTogglesStorage, + versionProvider = versionProvider, + ) + } + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt new file mode 100644 index 0000000000..81cf4b9a9d --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt @@ -0,0 +1,54 @@ +package com.tangem.core.featuretoggle.manager + +import com.squareup.moshi.JsonAdapter +import com.tangem.core.featuretoggle.FeatureToggle +import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage +import com.tangem.core.featuretoggle.utils.associateToggles +import com.tangem.core.featuretoggle.version.VersionProvider +import com.tangem.datasource.local.AppPreferenceStorage +import kotlin.properties.Delegates + +/** + * Feature toggles manager implementation in DEV build + * + * @property localFeatureTogglesStorage local feature toggles storage + * @property appPreferenceStorage application local storage + * @property jsonAdapter adapter for parsing json + * @property versionProvider application version provider + */ +internal class DevFeatureTogglesManager( + private val localFeatureTogglesStorage: FeatureTogglesStorage, + private val appPreferenceStorage: AppPreferenceStorage, + private val jsonAdapter: JsonAdapter>, + private val versionProvider: VersionProvider, +) : MutableFeatureTogglesManager { + + private var featureTogglesMap: MutableMap by Delegates.notNull() + + override suspend fun init() { + localFeatureTogglesStorage.init() + + val savedFeatureToggles = if (appPreferenceStorage.featureToggles.isNotEmpty()) { + jsonAdapter.fromJson(appPreferenceStorage.featureToggles).orEmpty() + } else { + emptyMap() + } + + featureTogglesMap = localFeatureTogglesStorage.featureToggles + .associateToggles(currentVersion = versionProvider.get().orEmpty()) + .mapValues { resultToggle -> + savedFeatureToggles[resultToggle.key] ?: resultToggle.value + } + .toMutableMap() + } + + override fun isFeatureEnabled(toggle: FeatureToggle): Boolean = featureTogglesMap.any { it.key == toggle.name } + + override fun getFeatureToggles(): Map = featureTogglesMap + + override fun changeToggle(name: String, isEnabled: Boolean) { + featureTogglesMap[name] ?: return + featureTogglesMap[name] = isEnabled + appPreferenceStorage.featureToggles = jsonAdapter.toJson(featureTogglesMap) + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt new file mode 100644 index 0000000000..37cb34b343 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt @@ -0,0 +1,17 @@ +package com.tangem.core.featuretoggle.manager + +import com.tangem.core.featuretoggle.FeatureToggle + +/** + * Component for getting information about the availability of feature toggles + * +[REDACTED_AUTHOR] + */ +interface FeatureTogglesManager { + + /** Initialize manager */ + suspend fun init() + + /** Check feature toggle [toggle] availability */ + fun isFeatureEnabled(toggle: FeatureToggle): Boolean +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt new file mode 100644 index 0000000000..abf6ff5b72 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt @@ -0,0 +1,15 @@ +package com.tangem.core.featuretoggle.manager + +/** + * Component for change information about the availability of feature toggles + * +[REDACTED_AUTHOR] + */ +interface MutableFeatureTogglesManager : FeatureTogglesManager { + + /** Get feature toggles */ + fun getFeatureToggles(): Map + + /** Change availability [isEnabled] of toggle with name [name] */ + fun changeToggle(name: String, isEnabled: Boolean) +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt new file mode 100644 index 0000000000..bbfe8641d2 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt @@ -0,0 +1,29 @@ +package com.tangem.core.featuretoggle.manager + +import com.tangem.core.featuretoggle.FeatureToggle +import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage +import com.tangem.core.featuretoggle.utils.associateToggles +import com.tangem.core.featuretoggle.version.VersionProvider +import kotlin.properties.Delegates + +/** + * Feature toggles manager implementation in PROD build + * + * @property localFeatureTogglesStorage local feature toggles storage + * @property versionProvider application version provider + */ +internal class ProdFeatureTogglesManager( + private val localFeatureTogglesStorage: FeatureTogglesStorage, + private val versionProvider: VersionProvider, +) : FeatureTogglesManager { + + private var featureToggles: Map by Delegates.notNull() + + override suspend fun init() { + localFeatureTogglesStorage.init() + featureToggles = localFeatureTogglesStorage.featureToggles + .associateToggles(currentVersion = versionProvider.get() ?: "") + } + + override fun isFeatureEnabled(toggle: FeatureToggle): Boolean = featureToggles.any { it.key == toggle.name } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureToggle.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureToggle.kt new file mode 100644 index 0000000000..69de7b76e7 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureToggle.kt @@ -0,0 +1,13 @@ +package com.tangem.core.featuretoggle.storage + +/** + * Data model with information about feature toggle + * + * @property name feature toggle name + * @property version version number in which the feature will be included + * + * IMPORTANT: if the version is "undefined", it means that feature toggle is disabled! + * +[REDACTED_AUTHOR] + */ +internal data class FeatureToggle(val name: String, val version: String) \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt new file mode 100644 index 0000000000..dd788efacb --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt @@ -0,0 +1,15 @@ +package com.tangem.core.featuretoggle.storage + +/** + * Component that initializes and stores a list of feature toggles + * +[REDACTED_AUTHOR] + */ +internal interface FeatureTogglesStorage { + + /** List of feature toggles */ + val featureToggles: List + + /** Initialize storage */ + suspend fun init() +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt new file mode 100644 index 0000000000..8cd19f39d4 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt @@ -0,0 +1,35 @@ +package com.tangem.core.featuretoggle.storage + +import com.squareup.moshi.JsonAdapter +import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage.Companion.LOCAL_CONFIG_PATH +import com.tangem.datasource.asset.AssetReader +import timber.log.Timber +import kotlin.properties.Delegates + +/** + * Storage implementation for storing local feature toggles. + * Feature toggles are declared in file [LOCAL_CONFIG_PATH]. + * + * @property assetReader asset reader + * @property jsonAdapter adapter for parsing local json config + * +[REDACTED_AUTHOR] + */ +internal class LocalFeatureTogglesStorage( + private val assetReader: AssetReader, + private val jsonAdapter: JsonAdapter>, +) : FeatureTogglesStorage { + + override var featureToggles: List by Delegates.notNull() + private set + + override suspend fun init() { + runCatching { requireNotNull(jsonAdapter.fromJson(assetReader.readJson(LOCAL_CONFIG_PATH))) } + .onSuccess { featureToggles = it } + .onFailure { Timber.e(LocalFeatureTogglesStorage::class.java.name, "Failed to parse $LOCAL_CONFIG_PATH") } + } + + private companion object { + const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config" + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/utils/CollectionExt.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/utils/CollectionExt.kt new file mode 100644 index 0000000000..990f95cf18 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/utils/CollectionExt.kt @@ -0,0 +1,13 @@ +package com.tangem.core.featuretoggle.utils + +import com.tangem.core.featuretoggle.contract.VersionAvailabilityContract +import com.tangem.core.featuretoggle.storage.FeatureToggle + +internal fun List.associateToggles(currentVersion: String): Map { + return associate { localToggle -> + Pair( + first = localToggle.name, + second = VersionAvailabilityContract(currentVersion, localToggle.version), + ) + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt new file mode 100644 index 0000000000..ab449b92ec --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt @@ -0,0 +1,37 @@ +package com.tangem.core.featuretoggle.version + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +/** + * Implementation of application version provider + * + * @property context application context + */ +internal class DefaultVersionProvider @Inject constructor( + @ApplicationContext private val context: Context, +) : VersionProvider { + + override fun get(): String? { + val versionName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager + .getPackageInfo( + context.packageName, + PackageManager.PackageInfoFlags.of(0), + ) + .versionName + } else { + context.packageManager.getPackageInfo(context.packageName, 0).versionName + } + + return runCatching { versionName.substringBefore(VERSION_NAME_DELIMITER) } + .fold(onSuccess = { it }, onFailure = { null }) + } + + private companion object { + const val VERSION_NAME_DELIMITER = "-" + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/VersionProvider.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/VersionProvider.kt new file mode 100644 index 0000000000..585caf85b8 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/VersionProvider.kt @@ -0,0 +1,8 @@ +package com.tangem.core.featuretoggle.version + +/** Application version provider */ +internal interface VersionProvider { + + /** Get application version */ + fun get(): String? +} \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 8466265b89..d32128a56b 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -25,6 +25,20 @@ android { } buildTypes { + release { + BuildConfigFieldFactory( + fields = listOf(Field.TesterMenuAvailability(false)), + builder = ::buildConfigField, + ).create() + } + + debug { + BuildConfigFieldFactory( + fields = listOf(Field.TesterMenuAvailability(true)), + builder = ::buildConfigField, + ).create() + } + create("debug_beta") { initWith(getByName("release")) BuildConfigFieldFactory( @@ -32,6 +46,7 @@ android { Field.Environment("release"), Field.TestActionEnabled(true), Field.LogEnabled(true), + Field.TesterMenuAvailability(true) ), builder = ::buildConfigField, ).create() diff --git a/settings.gradle.kts b/settings.gradle.kts index 75add91dd0..da8fd1aacc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,7 @@ include(":common") // region Core modules include(":core:analytics") include(":core:datasource") +include(":core:featuretoggles") include(":core:res") include(":core:utils") include(":core:ui")