From c118781f5a9d87f83a84825b182116740affe191 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Mar 2023 17:18:36 +0800 Subject: [PATCH] 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")