diff --git a/README.md b/README.md index 7577d9c..94b72e2 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,10 @@ https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_envi # Build +## Build debug + +- `./gradlew clean assembleDevDebug` + ## Deploy debug to Firebase App Distribution - `./gradlew clean bundleDevDebug` @@ -78,6 +82,8 @@ https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_envi ## Code integration +* Switch the `isProductFlavorFilterEnabled` property to `false` in the + [BuildTypeAndroidApplicationPlugin.kt](build-logic/convention/src/main/kotlin/com/yugyd/buildlogic/convention/buildtype/BuildTypeAndroidApplicationPlugin.kt) * Switch the `IS_BASED_ON_PLATFORM_APP` property to `true` in the [build.gradle](app/build.gradle) file. * Add the path to the [google-services.json](app/src/dev/google-services.json) file to diff --git a/app/.gitignore b/app/.gitignore index a5252f4..9411cb7 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,3 +1,5 @@ /build # Hide sensitive information for production ads ids /src/**/res/values/ad-ids.xml +!src/main/res/values/ad-ids.xml +!src/dev/res/values/ad-ids.xml \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 91ee00f..db4da7b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -187,6 +187,9 @@ dependencies { // Work Manager implementation libs.work.manager.ktx + // Serialization + implementation libs.kotlinx.serialization + // Logging implementation libs.timber } diff --git a/app/src/main/java/com/yugyd/quiz/ContentProviderImpl.kt b/app/src/main/java/com/yugyd/quiz/ContentProviderImpl.kt index c8d0905..02f9127 100644 --- a/app/src/main/java/com/yugyd/quiz/ContentProviderImpl.kt +++ b/app/src/main/java/com/yugyd/quiz/ContentProviderImpl.kt @@ -23,7 +23,6 @@ import com.yugyd.quiz.core.ResIdProvider import com.yugyd.quiz.featuretoggle.domain.RemoteConfigRepository import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -import javax.inject.Singleton internal class ContentProviderImpl @Inject constructor( @ApplicationContext private val context: Context, @@ -40,4 +39,14 @@ internal class ContentProviderImpl @Inject constructor( } ?.link ?: context.getString(resIdProvider.appTelegramChat()) } + + override suspend fun getUpdateLink(): String? { + val config = remoteConfigRepository.fetchUpdateConfig() + return config + ?.links + ?.firstOrNull { + GlobalConfig.APPLICATION_ID.contains(it.packageX) + } + ?.link + } } diff --git a/app/src/main/java/com/yugyd/quiz/YandexAdProviderImpl.kt b/app/src/main/java/com/yugyd/quiz/YandexAdProviderImpl.kt index c6fa073..9be6a9b 100644 --- a/app/src/main/java/com/yugyd/quiz/YandexAdProviderImpl.kt +++ b/app/src/main/java/com/yugyd/quiz/YandexAdProviderImpl.kt @@ -1,14 +1,44 @@ package com.yugyd.quiz import com.yugyd.quiz.core.AdIdProvider +import com.yugyd.quiz.core.GlobalConfig import com.yugyd.quiz.core.TextModel import javax.inject.Inject internal class YandexAdProviderImpl @Inject constructor() : AdIdProvider { - override fun idAdBannerGame() = R.string.id_yandex_ad_banner_game - override fun idAdRewardedGame() = R.string.id_yandex_ad_rewarded_game - override fun idAdInterstitialGameEnd() = R.string.id_yandex_ad_interstitial_game_end - override fun idAdRewardedTheme() = R.string.id_yandex_ad_rewarded_theme + + override fun idAdBannerGame(): Int { + return if (GlobalConfig.DEBUG) { + R.string.test_id_yandex_ad_banner_game + } else { + R.string.id_yandex_ad_banner_game + } + } + + override fun idAdRewardedGame(): Int { + return if (GlobalConfig.DEBUG) { + R.string.test_id_yandex_ad_rewarded_game + } else { + R.string.id_yandex_ad_rewarded_game + } + } + + override fun idAdInterstitialGameEnd(): Int { + return if (GlobalConfig.DEBUG) { + R.string.test_id_yandex_ad_interstitial_game_end + } else { + R.string.id_yandex_ad_interstitial_game_end + } + } + + override fun idAdRewardedTheme(): Int { + return if (GlobalConfig.DEBUG) { + R.string.test_id_yandex_ad_rewarded_theme + } else { + R.string.id_yandex_ad_rewarded_theme + } + } + override fun gameBannerAdId() = TextModel.ResTextModel(res = idAdBannerGame()) override fun gameRewardedAdId() = TextModel.ResTextModel(res = idAdRewardedGame()) override fun themeRewardedAdId() = TextModel.ResTextModel(res = idAdRewardedTheme()) diff --git a/app/src/main/java/com/yugyd/quiz/di/SerializationModule.kt b/app/src/main/java/com/yugyd/quiz/di/SerializationModule.kt new file mode 100644 index 0000000..a11f83e --- /dev/null +++ b/app/src/main/java/com/yugyd/quiz/di/SerializationModule.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Roman Likhachev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yugyd.quiz.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object SerializationModule { + + @Singleton + @Provides + fun providesJson(): Json = Json { + ignoreUnknownKeys = true + isLenient = true + } +} diff --git a/app/src/main/res/values/ad-ids.xml b/app/src/main/res/values/ad-ids.xml new file mode 100644 index 0000000..2abf766 --- /dev/null +++ b/app/src/main/res/values/ad-ids.xml @@ -0,0 +1,23 @@ + + + + + demo-banner-yandex + demo-interstitial-yandex + demo-rewarded-yandex + demo-rewarded-yandex + diff --git a/build-logic/convention/src/main/kotlin/com/yugyd/buildlogic/convention/buildtype/BuildTypeAndroidApplicationPlugin.kt b/build-logic/convention/src/main/kotlin/com/yugyd/buildlogic/convention/buildtype/BuildTypeAndroidApplicationPlugin.kt index 229b1ec..1a16b41 100644 --- a/build-logic/convention/src/main/kotlin/com/yugyd/buildlogic/convention/buildtype/BuildTypeAndroidApplicationPlugin.kt +++ b/build-logic/convention/src/main/kotlin/com/yugyd/buildlogic/convention/buildtype/BuildTypeAndroidApplicationPlugin.kt @@ -30,6 +30,8 @@ class BuildTypeAndroidApplicationPlugin : Plugin { private val ACTIVE_PRODUCT_FLAVOR_VARIANTS = arrayOf("devDebug", "devStaging", "devRelease") } + private val isProductFlavorFilterEnabled = true + override fun apply(target: Project) { with(target) { checkPlugin(ANDROID_APPLICATION_ALIAS) @@ -46,7 +48,10 @@ class BuildTypeAndroidApplicationPlugin : Plugin { // https://developer.android.com/build/build-variants#filter-variants extensions.configure { beforeVariants { variantBuilder -> - if (!ACTIVE_PRODUCT_FLAVOR_VARIANTS.contains(variantBuilder.name)) { + if ( + isProductFlavorFilterEnabled && + !ACTIVE_PRODUCT_FLAVOR_VARIANTS.contains(variantBuilder.name) + ) { variantBuilder.enable = false } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e28e5cb..d55d847 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ kotlin = '2.0.21' # UI appcompat = '1.7.0' material = '1.12.0' -compose-bom = '2024.10.01' +compose-bom = '2024.11.00' # DI hilt = '2.52' # Annotation processor @@ -21,13 +21,13 @@ androidxTest = '1.6.1' core-ktx = '1.15.0' roomVersion = '2.6.1' workManagerVersion = '2.10.0' -firebaseVersion = '33.5.1' +firebaseVersion = '33.6.0' accompanistVersion = '0.36.0' versionsUpdates = '0.51.0' # Build logic compile-sdk = '35' target-sdk = '35' -min-sdk = '24' +min-sdk = '26' convention = "1.0.0" [libraries] @@ -74,8 +74,8 @@ compose-activity = { module = 'androidx.activity:activity-compose', version = '1 compose-viewmodel = { module = 'androidx.lifecycle:lifecycle-viewmodel-compose', version = '2.8.7' } compose-lifecycle = { module = 'androidx.lifecycle:lifecycle-runtime-compose', version = '2.8.7' } # Navigation -navigation-runtime = { module = 'androidx.navigation:navigation-runtime-ktx', version = '2.8.3' } -compose-navigation = { module = 'androidx.navigation:navigation-compose', version = '2.8.3' } +navigation-runtime = { module = 'androidx.navigation:navigation-runtime-ktx', version = '2.8.4' } +compose-navigation = { module = 'androidx.navigation:navigation-compose', version = '2.8.4' } compose-hilt-navigation = { module = 'androidx.hilt:hilt-navigation-compose', version = '1.2.0' } # Accompanist accompanist-drawablepainter = { module = 'com.google.accompanist:accompanist-drawablepainter', version.ref = 'accompanistVersion' } diff --git a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/RemoteConfigRepositoryImpl.kt b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/RemoteConfigRepositoryImpl.kt index f103d8f..9a36f07 100644 --- a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/RemoteConfigRepositoryImpl.kt +++ b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/RemoteConfigRepositoryImpl.kt @@ -18,10 +18,13 @@ package com.yugyd.quiz.featuretoggle.data import android.content.Context import com.yugyd.quiz.featuretoggle.data.mapper.TelegramConfigMapper +import com.yugyd.quiz.featuretoggle.data.mapper.UpdateConfigMapper import com.yugyd.quiz.featuretoggle.data.model.TelegramConfigDto +import com.yugyd.quiz.featuretoggle.data.model.UpdateConfigDto import com.yugyd.quiz.featuretoggle.domain.RemoteConfigRepository import com.yugyd.quiz.featuretoggle.domain.model.FeatureToggle import com.yugyd.quiz.featuretoggle.domain.model.telegram.TelegramConfig +import com.yugyd.quiz.featuretoggle.domain.model.update.UpdateConfig import com.yugyd.quiz.remoteconfig.api.RemoteConfig import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.serialization.json.Json @@ -33,6 +36,8 @@ internal class RemoteConfigRepositoryImpl @Inject internal constructor( @ApplicationContext private val context: Context, private val remoteConfig: RemoteConfig, private val telegramConfigMapper: TelegramConfigMapper, + private val updateConfigMapper: UpdateConfigMapper, + private val json: Json, ) : RemoteConfigRepository { override suspend fun fetchFeatureToggle( @@ -45,7 +50,7 @@ internal class RemoteConfigRepositoryImpl @Inject internal constructor( override suspend fun fetchTelegramConfig(): TelegramConfig? { return try { val dtoList = remoteConfig.fetchStringValue(CONFIG_TELEGRAM_KEY).run { - Json.decodeFromString>(this) + json.decodeFromString>(this) } val mappedDtoList = dtoList.map(telegramConfigMapper::map) @@ -63,8 +68,30 @@ internal class RemoteConfigRepositoryImpl @Inject internal constructor( } } + override suspend fun fetchUpdateConfig(): UpdateConfig? { + return try { + val dtoList = remoteConfig.fetchStringValue(CONFIG_UPDATE_KEY).run { + json.decodeFromString>(this) + } + + val mappedDtoList = dtoList.map(updateConfigMapper::map) + + val currentLocale = context.resources.configuration.locales.get(0) + + mappedDtoList.firstOrNull { + it.locale == currentLocale + } ?: mappedDtoList.firstOrNull { + it.locale == Locale.ENGLISH + } + } catch (expected: Throwable) { + Timber.e(expected) + null + } + } + companion object { private const val FORCE_UPDATE_KEY = "force_update_version" private const val CONFIG_TELEGRAM_KEY = "config_telegram" + private const val CONFIG_UPDATE_KEY = "config_update" } } diff --git a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/LinkMapper.kt b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/LinkMapper.kt new file mode 100644 index 0000000..77c3a42 --- /dev/null +++ b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/LinkMapper.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Roman Likhachev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yugyd.quiz.featuretoggle.data.mapper + +import com.yugyd.quiz.featuretoggle.data.model.LinkDto +import com.yugyd.quiz.featuretoggle.domain.model.telegram.Link +import javax.inject.Inject + +internal class LinkMapper @Inject constructor() { + + fun mapToLinks(links: List): List { + return links.map { Link(link = it.link, packageX = it.packageX) } + } +} diff --git a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/LocaleMapper.kt b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/LocaleMapper.kt new file mode 100644 index 0000000..3eb6635 --- /dev/null +++ b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/LocaleMapper.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Roman Likhachev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yugyd.quiz.featuretoggle.data.mapper + +import java.util.Locale +import javax.inject.Inject + +internal class LocaleMapper @Inject constructor() { + + fun mapValueToLocale(value: String): Locale { + val language = if (value.contains(LOCALE_VALUE_SEPARATOR)) { + value.substringBefore(LOCALE_VALUE_SEPARATOR) + } else { + value + } + return Locale(language) + } + + companion object { + private const val LOCALE_VALUE_SEPARATOR = "-" + } +} diff --git a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/TelegramConfigMapper.kt b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/TelegramConfigMapper.kt index 24712dc..155e9be 100644 --- a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/TelegramConfigMapper.kt +++ b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/TelegramConfigMapper.kt @@ -18,25 +18,26 @@ package com.yugyd.quiz.featuretoggle.data.mapper import com.yugyd.quiz.featuretoggle.data.model.TelegramConfigDto import com.yugyd.quiz.featuretoggle.domain.model.telegram.GameEnd -import com.yugyd.quiz.featuretoggle.domain.model.telegram.Link import com.yugyd.quiz.featuretoggle.domain.model.telegram.MainPopup import com.yugyd.quiz.featuretoggle.domain.model.telegram.ProfileCell import com.yugyd.quiz.featuretoggle.domain.model.telegram.TelegramConfig import com.yugyd.quiz.featuretoggle.domain.model.telegram.TrainPopup -import java.util.Locale import javax.inject.Inject -class TelegramConfigMapper @Inject constructor() { +class TelegramConfigMapper @Inject internal constructor( + private val localeMapper: LocaleMapper, + private val linkMapper: LinkMapper, +) { fun map(telegramConfigDto: TelegramConfigDto): TelegramConfig = telegramConfigDto.run { return TelegramConfig( - locale = locale.toLocale(), + locale = localeMapper.mapValueToLocale(locale), gameEnd = GameEnd( buttonTitle = gameEnd.buttonTitle, message = gameEnd.message, title = gameEnd.title, ), - links = links.map { Link(link = it.link, packageX = it.packageX) }, + links = linkMapper.mapToLinks(links), mainPopup = MainPopup( buttonTitle = mainPopup.buttonTitle, title = mainPopup.title, @@ -54,17 +55,4 @@ class TelegramConfigMapper @Inject constructor() { ) ) } - - private fun String.toLocale(): Locale { - val language = if (contains(LOCALE_VALUE_SEPARATOR)) { - substringBefore(LOCALE_VALUE_SEPARATOR) - } else { - this - } - return Locale(language) - } - - companion object { - private const val LOCALE_VALUE_SEPARATOR = "-" - } } diff --git a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/UpdateConfigMapper.kt b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/UpdateConfigMapper.kt new file mode 100644 index 0000000..30c88e3 --- /dev/null +++ b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/mapper/UpdateConfigMapper.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Roman Likhachev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yugyd.quiz.featuretoggle.data.mapper + +import com.yugyd.quiz.featuretoggle.data.model.UpdateConfigDto +import com.yugyd.quiz.featuretoggle.domain.model.update.UpdateConfig +import javax.inject.Inject + +class UpdateConfigMapper @Inject internal constructor( + private val localeMapper: LocaleMapper, + private val linkMapper: LinkMapper, +) { + + fun map(configDto: UpdateConfigDto): UpdateConfig { + return UpdateConfig( + locale = localeMapper.mapValueToLocale(configDto.locale), + buttonTitle = configDto.mainScreen.buttonTitle, + message = configDto.mainScreen.message, + title = configDto.mainScreen.title, + links = linkMapper.mapToLinks(configDto.links), + ) + } +} diff --git a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/model/UpdateConfigDto.kt b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/model/UpdateConfigDto.kt new file mode 100644 index 0000000..25558d1 --- /dev/null +++ b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/model/UpdateConfigDto.kt @@ -0,0 +1,14 @@ +package com.yugyd.quiz.featuretoggle.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateConfigDto( + @SerialName("locale") + val locale: String, + @SerialName("mainScreen") + val mainScreen: UpdateMainScreenDto, + @SerialName("links") + val links: List, +) diff --git a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/model/UpdateMainScreenDto.kt b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/model/UpdateMainScreenDto.kt new file mode 100644 index 0000000..5e0a5b2 --- /dev/null +++ b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/data/model/UpdateMainScreenDto.kt @@ -0,0 +1,14 @@ +package com.yugyd.quiz.featuretoggle.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateMainScreenDto( + @SerialName("buttonTitle") + val buttonTitle: String, + @SerialName("message") + val message: String, + @SerialName("title") + val title: String, +) diff --git a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/domain/RemoteConfigRepository.kt b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/domain/RemoteConfigRepository.kt index 66bf90a..e71ed66 100644 --- a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/domain/RemoteConfigRepository.kt +++ b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/domain/RemoteConfigRepository.kt @@ -18,9 +18,11 @@ package com.yugyd.quiz.featuretoggle.domain import com.yugyd.quiz.featuretoggle.domain.model.FeatureToggle import com.yugyd.quiz.featuretoggle.domain.model.telegram.TelegramConfig +import com.yugyd.quiz.featuretoggle.domain.model.update.UpdateConfig interface RemoteConfigRepository { suspend fun fetchFeatureToggle(featureToggle: FeatureToggle): Boolean suspend fun fetchForceUpdateVersion(): Int suspend fun fetchTelegramConfig(): TelegramConfig? + suspend fun fetchUpdateConfig(): UpdateConfig? } diff --git a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/domain/model/FeatureToggle.kt b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/domain/model/FeatureToggle.kt index 7430ae3..f7507d8 100644 --- a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/domain/model/FeatureToggle.kt +++ b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/domain/model/FeatureToggle.kt @@ -17,7 +17,10 @@ package com.yugyd.quiz.featuretoggle.domain.model enum class FeatureToggle(val key: String, val isLocal: Boolean, val localValue: Boolean = false) { - AD(key = "feature_ad", isLocal = true, localValue = true), + AD(key = "feature_ad", isLocal = false), + AD_INTERSTITIAL_GAME_END(key = "feature_ad_interstitial_game_end", isLocal = false), + AD_BANNER_GAME(key = "feature_ad_banner_game", isLocal = false), + AD_REWARDED_THEME(key = "feature_ad_rewarded_theme", isLocal = false), PRO(key = "feature_pro", isLocal = false), CORRECT(key = "feature_correct", isLocal = false), TELEGRAM(key = "feature_telegram", isLocal = false), diff --git a/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/domain/model/update/UpdateConfig.kt b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/domain/model/update/UpdateConfig.kt new file mode 100644 index 0000000..74fa9f9 --- /dev/null +++ b/product/core/featuretoggle/src/main/java/com/yugyd/quiz/featuretoggle/domain/model/update/UpdateConfig.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Roman Likhachev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yugyd.quiz.featuretoggle.domain.model.update + +import com.yugyd.quiz.featuretoggle.domain.model.telegram.Link +import java.util.Locale + +data class UpdateConfig( + val locale: Locale, + val buttonTitle: String, + val message: String, + val title: String, + val links: List?, +) diff --git a/product/core/featuretoggle/src/main/res/xml/remote_config_defaults.xml b/product/core/featuretoggle/src/main/res/xml/remote_config_defaults.xml index 107c686..ef77774 100644 --- a/product/core/featuretoggle/src/main/res/xml/remote_config_defaults.xml +++ b/product/core/featuretoggle/src/main/res/xml/remote_config_defaults.xml @@ -29,18 +29,38 @@ feature_ad - false + true + + + feature_ad_interstitial_game_end + true + + + feature_ad_banner_game + true + + + feature_ad_rewarded_theme + true config_telegram - [{"locale":"en","gameEnd":{"title":"New record!","message":"Did you like it? Subscribe to the Telegram channel, daily questions and a lot of interesting things await you!","buttonTitle":"Follow"},"profileCell":{"title":"","message":"Daily questions and feedback"},"mainPopup":{"title":"We have opened a telegram channel!","message":"Daily questions and a lot of interesting things await you!","buttonTitle":"Follow"},"trainPopup":{"title":"We have opened a telegram channel!","message":"Daily questions and a lot of interesting things await you!","positiveButtonTitle":"Follow","negativeButtonTitle":"Later"},"links":[{"package":"com.yugyd.quiz","link":"quizplatformapp"},{"package":"com.yugyd.russianlanguagequiz","link":"russian_language_quiz"},{"package":"com.yugyd.russianhistoryquiz","link":"russian_history_quiz "},{"package":"com.yugyd.sociologyquiz","link":"sociology_quiz"},{"package":"com.yugyd.biologyquiz","link":"biology_quiz_app"},{"package":"com.yugyd.geographyquiz","link":"geography_quiz_app"}]},{"locale":"ru-RU","gameEnd":{"title":"Новый рекорд!","message":"Вам понравилось? Подпишитесь на телеграм канал, вас ждут ежедневные вопросы и много интересного!","buttonTitle":"Подписаться"},"profileCell":{"title":"","message":"Ежедневные вопросы и обратная связь"},"mainPopup":{"title":"Мы открыли телеграм канал!","message":"Вас ждут ежедневные вопросы и много интересного!","buttonTitle":"Подписаться"},"trainPopup":{"title":"Мы открыли телеграм канал!","message":"Вас ждут ежедневные вопросы и много интересного!","positiveButtonTitle":"Подписаться","negativeButtonTitle":"Позже"},"links":[{"package":"com.yugyd.quiz","link":"quizplatformapp"},{"package":"com.yugyd.russianlanguagequiz","link":"russian_language_quiz"},{"package":"com.yugyd.russianhistoryquiz","link":"russian_history_quiz "},{"package":"com.yugyd.sociologyquiz","link":"sociology_quiz"},{"package":"com.yugyd.biologyquiz","link":"biology_quiz_app"},{"package":"com.yugyd.geographyquiz","link":"geography_quiz_app"}]}] + [{"locale":"en","gameEnd":{"title":"New record!","message":"Did you like it? Subscribe to the Telegram channel, daily questions and a lot of interesting things await you!","buttonTitle":"Follow"},"profileCell":{"title":"","message":"Daily questions and feedback"},"mainPopup":{"title":"We have opened a telegram channel!","message":"Daily questions and a lot of interesting things await you!","buttonTitle":"Follow"},"trainPopup":{"title":"We have opened a telegram channel!","message":"Daily questions and a lot of interesting things await you!","positiveButtonTitle":"Follow","negativeButtonTitle":"Later"},"links":[{"package":"com.yugyd.quiz","link":"quizplatformapp"}]},{"locale":"ru-RU","gameEnd":{"title":"Новый рекорд!","message":"Вам понравилось? Подпишитесь на телеграм канал, вас ждут ежедневные вопросы и много интересного!","buttonTitle":"Подписаться"},"profileCell":{"title":"","message":"Ежедневные вопросы и обратная связь"},"mainPopup":{"title":"Мы открыли телеграм канал!","message":"Вас ждут ежедневные вопросы и много интересного!","buttonTitle":"Подписаться"},"trainPopup":{"title":"Мы открыли телеграм канал!","message":"Вас ждут ежедневные вопросы и много интересного!","positiveButtonTitle":"Подписаться","negativeButtonTitle":"Позже"},"links":[{"package":"com.yugyd.quiz","link":"quizplatformapp"}]}] force_update_version - 0 + 47 feature_telegram true + + quest_query_format + https://www.google.ru/search?q=%s%20%s + + + config_update + [{"locale":"en","mainScreen":{"title":"Update is out","message":"New features and content available to you","buttonTitle":"Update"},"links":[{"package":"com.yugyd.quiz","link":""}]},{"locale":"ru-RU","mainScreen":{"title":"Вышло обновление","message":"Новые функции и контент доступны для вас","buttonTitle":"Обновить"},"links":[{"package":"com.yugyd.quiz","link":""}]}] + \ No newline at end of file diff --git a/product/core/search-utils/.gitignore b/product/core/search-utils/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/product/core/search-utils/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/product/core/search-utils/build.gradle b/product/core/search-utils/build.gradle new file mode 100644 index 0000000..24577bf --- /dev/null +++ b/product/core/search-utils/build.gradle @@ -0,0 +1,21 @@ +plugins { + alias(libs.plugins.convention.library) + alias(libs.plugins.convention.library.buildtype) + alias(libs.plugins.convention.library.jacoco) + alias(libs.plugins.convention.library.lint) + alias(libs.plugins.convention.library.test) + alias(libs.plugins.convention.hilt) +} + +android { + namespace 'com.yugyd.quiz.core.searchutils' +} + +dependencies { + implementation project(':product:core:coroutines-utils') + implementation project(':product:core') + implementation(project(":product:services:remoteconfig-api")) + + // Logging + implementation libs.timber +} diff --git a/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/QueryBlModule.kt b/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/QueryBlModule.kt new file mode 100644 index 0000000..4c4c2fc --- /dev/null +++ b/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/QueryBlModule.kt @@ -0,0 +1,19 @@ +package com.yugyd.quiz.core.searchutils + +import com.yugyd.quiz.core.searchutils.data.QueryFormatRepositoryImpl +import com.yugyd.quiz.core.searchutils.data.QuestQueryUrlBuilderImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +abstract class QueryBlModule { + + @Binds + internal abstract fun bindQueryUrlBuilder(impl: QuestQueryUrlBuilderImpl): QueryUrlBuilder + + @Binds + internal abstract fun bindFormatRepository(impl: QueryFormatRepositoryImpl): QueryFormatRepository +} diff --git a/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/QueryFormatRepository.kt b/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/QueryFormatRepository.kt new file mode 100644 index 0000000..526d46a --- /dev/null +++ b/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/QueryFormatRepository.kt @@ -0,0 +1,5 @@ +package com.yugyd.quiz.core.searchutils + +interface QueryFormatRepository { + suspend fun getFormatFromRemoteConfig(): String +} diff --git a/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/QueryUrlBuilder.kt b/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/QueryUrlBuilder.kt new file mode 100644 index 0000000..985bf1a --- /dev/null +++ b/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/QueryUrlBuilder.kt @@ -0,0 +1,5 @@ +package com.yugyd.quiz.core.searchutils + +interface QueryUrlBuilder { + fun buildUrl(quest: SearchQuest, queryFormat: String): String +} diff --git a/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/SearchQuest.kt b/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/SearchQuest.kt new file mode 100644 index 0000000..e515bf6 --- /dev/null +++ b/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/SearchQuest.kt @@ -0,0 +1,6 @@ +package com.yugyd.quiz.core.searchutils + +data class SearchQuest( + val quest: String, + val trueAnswer: String, +) diff --git a/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/data/QueryFormatRepositoryImpl.kt b/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/data/QueryFormatRepositoryImpl.kt similarity index 85% rename from product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/data/QueryFormatRepositoryImpl.kt rename to product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/data/QueryFormatRepositoryImpl.kt index c53ef8c..d1c0ac7 100644 --- a/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/data/QueryFormatRepositoryImpl.kt +++ b/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/data/QueryFormatRepositoryImpl.kt @@ -1,7 +1,7 @@ -package com.yugyd.quiz.domain.tasks.data +package com.yugyd.quiz.core.searchutils.data import com.yugyd.quiz.core.coroutinesutils.DispatchersProvider -import com.yugyd.quiz.domain.tasks.QueryFormatRepository +import com.yugyd.quiz.core.searchutils.QueryFormatRepository import com.yugyd.quiz.remoteconfig.api.RemoteConfig import kotlinx.coroutines.withContext import javax.inject.Inject diff --git a/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/QuestQueryUrlBuilderImpl.kt b/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/data/QuestQueryUrlBuilderImpl.kt similarity index 63% rename from product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/QuestQueryUrlBuilderImpl.kt rename to product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/data/QuestQueryUrlBuilderImpl.kt index 7ce5ff8..01c2970 100644 --- a/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/QuestQueryUrlBuilderImpl.kt +++ b/product/core/search-utils/src/main/java/com/yugyd/quiz/core/searchutils/data/QuestQueryUrlBuilderImpl.kt @@ -1,7 +1,8 @@ -package com.yugyd.quiz.domain.tasks +package com.yugyd.quiz.core.searchutils.data import com.yugyd.quiz.core.Logger -import com.yugyd.quiz.domain.game.api.model.Quest +import com.yugyd.quiz.core.searchutils.QueryUrlBuilder +import com.yugyd.quiz.core.searchutils.SearchQuest import java.util.IllegalFormatException import javax.inject.Inject @@ -9,13 +10,13 @@ internal class QuestQueryUrlBuilderImpl @Inject constructor( private val logger: Logger, ) : QueryUrlBuilder { - override fun buildUrl(quest: Quest, queryFormat: String): String { + override fun buildUrl(quest: SearchQuest, queryFormat: String): String { if (queryFormat.isEmpty()) { return "" } return try { - String.format(queryFormat, quest.quest) + String.format(queryFormat, quest.quest, quest.trueAnswer) } catch (error: IllegalFormatException) { logger.logError(TAG, error) "" diff --git a/product/core/src/main/java/com/yugyd/quiz/core/ContentProvider.kt b/product/core/src/main/java/com/yugyd/quiz/core/ContentProvider.kt index 1d48117..c3ab973 100644 --- a/product/core/src/main/java/com/yugyd/quiz/core/ContentProvider.kt +++ b/product/core/src/main/java/com/yugyd/quiz/core/ContentProvider.kt @@ -18,4 +18,5 @@ package com.yugyd.quiz.core interface ContentProvider { suspend fun getTelegramChannel(): String + suspend fun getUpdateLink(): String? } diff --git a/product/end/end-ui/src/main/java/com/yugyd/quiz/ui/end/gameend/GameEndViewModel.kt b/product/end/end-ui/src/main/java/com/yugyd/quiz/ui/end/gameend/GameEndViewModel.kt index a531975..834b688 100644 --- a/product/end/end-ui/src/main/java/com/yugyd/quiz/ui/end/gameend/GameEndViewModel.kt +++ b/product/end/end-ui/src/main/java/com/yugyd/quiz/ui/end/gameend/GameEndViewModel.kt @@ -110,7 +110,8 @@ internal class GameEndViewModel @Inject constructor( vmScopeErrorHandled.launch { screenState = screenState.copy(isLoading = true) - val isAdFeatureEnabled = featureManager.isFeatureEnabled(FeatureToggle.AD) + val isAdFeatureEnabled = featureManager.isFeatureEnabled(FeatureToggle.AD) && + featureManager.isFeatureEnabled(FeatureToggle.AD_INTERSTITIAL_GAME_END) runCatch( block = { diff --git a/product/errors/errors-bl/build.gradle b/product/errors/errors-bl/build.gradle index 4c791f4..18e2bff 100644 --- a/product/errors/errors-bl/build.gradle +++ b/product/errors/errors-bl/build.gradle @@ -14,6 +14,7 @@ android { dependencies { // Modules implementation project(":product:core:coroutines-utils") + implementation project(':product:core:search-utils') implementation project(':product:shared:domain-api') implementation project(':product:shared:domain-utils') implementation project(':product:game:game-bl-api') diff --git a/product/errors/errors-bl/src/main/java/com/yugyd/quiz/domain/errors/ErrorInteractorImpl.kt b/product/errors/errors-bl/src/main/java/com/yugyd/quiz/domain/errors/ErrorInteractorImpl.kt index 9897725..a3e637a 100644 --- a/product/errors/errors-bl/src/main/java/com/yugyd/quiz/domain/errors/ErrorInteractorImpl.kt +++ b/product/errors/errors-bl/src/main/java/com/yugyd/quiz/domain/errors/ErrorInteractorImpl.kt @@ -17,6 +17,9 @@ package com.yugyd.quiz.domain.errors import com.yugyd.quiz.core.coroutinesutils.DispatchersProvider +import com.yugyd.quiz.core.searchutils.QueryFormatRepository +import com.yugyd.quiz.core.searchutils.QueryUrlBuilder +import com.yugyd.quiz.core.searchutils.SearchQuest import com.yugyd.quiz.domain.api.model.tasks.TaskModel import com.yugyd.quiz.domain.api.repository.ErrorSource import com.yugyd.quiz.domain.api.repository.QuestSource @@ -30,12 +33,14 @@ internal class ErrorInteractorImpl @Inject constructor( private val errorSource: ErrorSource, private val separatorParser: SeparatorParser, private val dispatcherProvider: DispatchersProvider, + private val queryUrlBuilder: QueryUrlBuilder, + private val queryFormatRepository: QueryFormatRepository, ) : ErrorInteractor { override suspend fun getErrorsModels(errors: List) = withContext(dispatcherProvider.io) { - questSource - .getQuestIdsByErrors(errors.toIntArray()) - .let(::mapQuests) + val queryFormat = queryFormatRepository.getFormatFromRemoteConfig() + val errorsModels = questSource.getQuestIdsByErrors(errors.toIntArray()) + mapQuests(errorsModels, queryFormat) } override suspend fun getErrors() = withContext(dispatcherProvider.io) { @@ -54,18 +59,19 @@ internal class ErrorInteractorImpl @Inject constructor( errorSource.removeErrors(errors) } - private fun mapQuests(quests: List) = quests + private fun mapQuests(quests: List, queryFormat: String) = quests .map(separatorParser::parseErrorQuest) - .map { it.toErrorModel() } + .map { it.toErrorModel(queryFormat) } - private fun Quest.toErrorModel() = TaskModel( + private fun Quest.toErrorModel(queryFormat: String) = TaskModel( id = id, quest = quest, trueAnswer = trueAnswer, - queryLink = "$GOOGLE_SEARCH_URL$quest$trueAnswer" + queryLink = queryUrlBuilder.buildUrl(this.toSearchQuest(), queryFormat), ) - companion object { - private const val GOOGLE_SEARCH_URL = "https://www.google.ru/search?q=" - } + private fun Quest.toSearchQuest() = SearchQuest( + quest = quest, + trueAnswer = trueAnswer, + ) } diff --git a/product/game/game-ui/src/main/java/com/yugyd/quiz/gameui/game/GameViewModel.kt b/product/game/game-ui/src/main/java/com/yugyd/quiz/gameui/game/GameViewModel.kt index 7780bc0..3cbd98d 100644 --- a/product/game/game-ui/src/main/java/com/yugyd/quiz/gameui/game/GameViewModel.kt +++ b/product/game/game-ui/src/main/java/com/yugyd/quiz/gameui/game/GameViewModel.kt @@ -232,7 +232,8 @@ internal class GameViewModel @Inject constructor( block = { val gameData = gameInteractor.startGame(screenState.payload) - val isAdFeatureEnabled = featureManager.isFeatureEnabled(FeatureToggle.AD) + val isAdFeatureEnabled = featureManager.isFeatureEnabled(FeatureToggle.AD) && + featureManager.isFeatureEnabled(FeatureToggle.AD_BANNER_GAME) val isProFeatureEnabled = featureManager.isFeatureEnabled(FeatureToggle.PRO) processGameData( diff --git a/product/main/main-ui/src/main/java/com/yugyd/quiz/ui/main/QuizNavHost.kt b/product/main/main-ui/src/main/java/com/yugyd/quiz/ui/main/QuizNavHost.kt index 1bd29ea..7377def 100644 --- a/product/main/main-ui/src/main/java/com/yugyd/quiz/ui/main/QuizNavHost.kt +++ b/product/main/main-ui/src/main/java/com/yugyd/quiz/ui/main/QuizNavHost.kt @@ -76,6 +76,9 @@ internal fun QuizNavHost( navigateToGooglePlay = { navigateToExternalScreen(GlobalScreens.rate()) }, + navigateToBrowser = { + navigateToExternalScreen(GlobalScreens.externalBrowser(it)) + }, ) sectionScreen( diff --git a/product/profile/profile-ui/src/main/java/com/yugyd/quiz/ui/profile/model/ProfileUiMapper.kt b/product/profile/profile-ui/src/main/java/com/yugyd/quiz/ui/profile/model/ProfileUiMapper.kt index c0d255d..d49507c 100644 --- a/product/profile/profile-ui/src/main/java/com/yugyd/quiz/ui/profile/model/ProfileUiMapper.kt +++ b/product/profile/profile-ui/src/main/java/com/yugyd/quiz/ui/profile/model/ProfileUiMapper.kt @@ -52,7 +52,14 @@ internal class ProfileUiMapper @Inject constructor( header(content, isProFeatureEnabled), item(TypeProfile.TASKS, R.string.profile_tasks), mapContentToValueItem(contentTitle, isContentFeatureEnabled), - section(TypeProfile.SOCIAL_SECTION, R.string.title_social, isTelegramFeatureEnabled), + section( + type = TypeProfile.SOCIAL_SECTION, + titleRes = R.string.title_social, + isSectionEnabled = isTelegramEnabledAndTelegramConfigNotNull( + isTelegramFeatureEnabled = isTelegramFeatureEnabled, + telegramConfig = telegramConfig, + ), + ), social(TypeProfile.TELEGRAM_SOCIAL, isTelegramFeatureEnabled, telegramConfig), section(TypeProfile.SETTINGS_SECTION, R.string.title_settings), value(TypeProfile.TRANSITION, R.string.title_show_answer, transition.value), @@ -148,8 +155,10 @@ internal class ProfileUiMapper @Inject constructor( isTelegramFeatureEnabled: Boolean, telegramConfig: TelegramConfig? ): SocialItemProfileUiModel? { - return if (isTelegramFeatureEnabled && telegramConfig != null) { - val telegramTitle = telegramConfig.profileCell.title.ifBlank { + return if ( + isTelegramEnabledAndTelegramConfigNotNull(isTelegramFeatureEnabled, telegramConfig) + ) { + val telegramTitle = requireNotNull(telegramConfig).profileCell.title.ifBlank { context.getString(R.string.profile_title_telegram) } val telegramMsg = telegramConfig.profileCell.message.ifBlank { @@ -167,6 +176,11 @@ internal class ProfileUiMapper @Inject constructor( } } + private fun isTelegramEnabledAndTelegramConfigNotNull( + isTelegramFeatureEnabled: Boolean, + telegramConfig: TelegramConfig?, + ) = isTelegramFeatureEnabled && telegramConfig != null + private fun getPurchaseSection(isProFeatureEnabled: Boolean): SelectItemProfileUiModel? { return if (isProFeatureEnabled) { item(TypeProfile.PRO, R.string.title_pro_version) diff --git a/product/shared/data/schemas/com.yugyd.quiz.data.database.user.UserDatabase/2.json b/product/shared/data/schemas/com.yugyd.quiz.data.database.user.UserDatabase/2.json new file mode 100644 index 0000000..f8b38da --- /dev/null +++ b/product/shared/data/schemas/com.yugyd.quiz.data.database.user.UserDatabase/2.json @@ -0,0 +1,194 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "6d0f7e0684fc2e518f18996db471ae40", + "entities": [ + { + "tableName": "error", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER NOT NULL, PRIMARY KEY(`_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "mode", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER NOT NULL, `title` TEXT NOT NULL, PRIMARY KEY(`_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "record", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `theme_id` INTEGER NOT NULL, `mode_id` INTEGER NOT NULL, `record` INTEGER NOT NULL, `complexity` INTEGER, `total_time` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "themeId", + "columnName": "theme_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modeId", + "columnName": "mode_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "record", + "columnName": "record", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "complexity", + "columnName": "complexity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalTime", + "columnName": "total_time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "section", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER NOT NULL, PRIMARY KEY(`_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "train", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER NOT NULL, PRIMARY KEY(`_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "content", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `file_path` TEXT NOT NULL, `is_checked` INTEGER NOT NULL, `content_marker` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isChecked", + "columnName": "is_checked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentMarker", + "columnName": "content_marker", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6d0f7e0684fc2e518f18996db471ae40')" + ] + } +} \ No newline at end of file diff --git a/product/shared/data/src/main/java/com/yugyd/quiz/data/crypto/CryptoHelperImpl.kt b/product/shared/data/src/main/java/com/yugyd/quiz/data/crypto/CryptoHelperImpl.kt index ebe5743..3f3f2df 100644 --- a/product/shared/data/src/main/java/com/yugyd/quiz/data/crypto/CryptoHelperImpl.kt +++ b/product/shared/data/src/main/java/com/yugyd/quiz/data/crypto/CryptoHelperImpl.kt @@ -16,17 +16,35 @@ package com.yugyd.quiz.data.crypto +import java.util.concurrent.locks.ReentrantLock import javax.inject.Inject +import javax.inject.Singleton +import kotlin.concurrent.withLock +@Singleton internal class CryptoHelperImpl @Inject constructor() : CryptoHelper { + private val lock = ReentrantLock() + override fun decrypt(encrypted: String?): String { - // Add your encrypted logic - return encrypted.orEmpty() + return if (encrypted?.isNotEmpty() == true) { + lock.withLock { + // Add your encrypted logic + encrypted + } + } else { + "" + } } override fun encrypt(decrypted: String?): String { - // Add your encrypted logic - return decrypted.orEmpty() + return if (decrypted?.isNotEmpty() == true) { + lock.withLock { + // Add your encrypted logic + decrypted + } + } else { + "" + } } } diff --git a/product/shared/data/src/main/java/com/yugyd/quiz/data/database/user/UserDatabase.kt b/product/shared/data/src/main/java/com/yugyd/quiz/data/database/user/UserDatabase.kt index 4f3def4..65f309e 100644 --- a/product/shared/data/src/main/java/com/yugyd/quiz/data/database/user/UserDatabase.kt +++ b/product/shared/data/src/main/java/com/yugyd/quiz/data/database/user/UserDatabase.kt @@ -15,7 +15,7 @@ import com.yugyd.quiz.data.model.RecordEntity import com.yugyd.quiz.data.model.SectionEntity import com.yugyd.quiz.data.model.TrainEntity -private const val USER_DB_VERSION = 1 +private const val USER_DB_VERSION = 2 @Database( entities = [ @@ -27,7 +27,7 @@ private const val USER_DB_VERSION = 1 ContentEntity::class, ], version = USER_DB_VERSION, - exportSchema = false + exportSchema = true ) internal abstract class UserDatabase : RoomDatabase() { abstract fun errorDao(): ErrorDao diff --git a/product/shared/data/src/main/java/com/yugyd/quiz/data/database/user/migrations/Migration1To2.kt b/product/shared/data/src/main/java/com/yugyd/quiz/data/database/user/migrations/Migration1To2.kt new file mode 100644 index 0000000..ebf77c9 --- /dev/null +++ b/product/shared/data/src/main/java/com/yugyd/quiz/data/database/user/migrations/Migration1To2.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Roman Likhachev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yugyd.quiz.data.database.user.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + // Create the new 'content' table + db.execSQL( + """ + CREATE TABLE content ( + _id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT NOT NULL, + file_path TEXT NOT NULL, + is_checked INTEGER NOT NULL, + content_marker TEXT NOT NULL + ) + """.trimIndent() + ) + } +} diff --git a/product/shared/data/src/main/java/com/yugyd/quiz/data/di/UserDatabaseModule.kt b/product/shared/data/src/main/java/com/yugyd/quiz/data/di/UserDatabaseModule.kt index 1de01ad..46765b0 100644 --- a/product/shared/data/src/main/java/com/yugyd/quiz/data/di/UserDatabaseModule.kt +++ b/product/shared/data/src/main/java/com/yugyd/quiz/data/di/UserDatabaseModule.kt @@ -9,6 +9,7 @@ import com.yugyd.quiz.data.database.user.dao.RecordDao import com.yugyd.quiz.data.database.user.dao.SectionDao import com.yugyd.quiz.data.database.user.dao.TrainDao import com.yugyd.quiz.data.database.user.dao.UserResetDao +import com.yugyd.quiz.data.database.user.migrations.MIGRATION_1_2 import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -28,6 +29,7 @@ object UserDatabaseModule { @ApplicationContext appContext: Context ) = Room .databaseBuilder(appContext, UserDatabase::class.java, USER_DB_NAME) + .addMigrations(MIGRATION_1_2) .fallbackToDestructiveMigration() .build() diff --git a/product/tasks/tasks-bl/build.gradle b/product/tasks/tasks-bl/build.gradle index aa17ce6..7082df2 100644 --- a/product/tasks/tasks-bl/build.gradle +++ b/product/tasks/tasks-bl/build.gradle @@ -19,6 +19,7 @@ dependencies { implementation project(':product:shared:domain-utils') implementation project(':product:services:remoteconfig-api') implementation project(':product:game:game-bl-api') + implementation project(':product:core:search-utils') // Kotlin implementation libs.kotlinx.coroutines.android diff --git a/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/QueryFormatRepository.kt b/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/QueryFormatRepository.kt deleted file mode 100644 index e2bcfe7..0000000 --- a/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/QueryFormatRepository.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.yugyd.quiz.domain.tasks - -internal interface QueryFormatRepository { - suspend fun getFormatFromRemoteConfig(): String -} diff --git a/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/QueryUrlBuilder.kt b/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/QueryUrlBuilder.kt deleted file mode 100644 index 1366041..0000000 --- a/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/QueryUrlBuilder.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.yugyd.quiz.domain.tasks - -import com.yugyd.quiz.domain.game.api.model.Quest - -internal interface QueryUrlBuilder { - fun buildUrl(quest: Quest, queryFormat: String): String -} \ No newline at end of file diff --git a/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/TasksBlModule.kt b/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/TasksBlModule.kt index ea0a0f3..19676b1 100644 --- a/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/TasksBlModule.kt +++ b/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/TasksBlModule.kt @@ -1,6 +1,5 @@ package com.yugyd.quiz.domain.tasks -import com.yugyd.quiz.domain.tasks.data.QueryFormatRepositoryImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -15,10 +14,4 @@ abstract class TasksBlModule { @Binds internal abstract fun bindFilterInteractor(impl: FilterInteractorImpl): FilterInteractor - - @Binds - internal abstract fun bindQueryUrlBuilder(impl: QuestQueryUrlBuilderImpl): QueryUrlBuilder - - @Binds - internal abstract fun bindFormatRepository(impl: QueryFormatRepositoryImpl): QueryFormatRepository } diff --git a/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/TasksInteractorImpl.kt b/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/TasksInteractorImpl.kt index adf162f..1277961 100644 --- a/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/TasksInteractorImpl.kt +++ b/product/tasks/tasks-bl/src/main/java/com/yugyd/quiz/domain/tasks/TasksInteractorImpl.kt @@ -17,6 +17,9 @@ package com.yugyd.quiz.domain.tasks import com.yugyd.quiz.core.coroutinesutils.DispatchersProvider +import com.yugyd.quiz.core.searchutils.QueryFormatRepository +import com.yugyd.quiz.core.searchutils.QueryUrlBuilder +import com.yugyd.quiz.core.searchutils.SearchQuest import com.yugyd.quiz.domain.api.repository.QuestSource import com.yugyd.quiz.domain.game.api.model.Quest import com.yugyd.quiz.domain.tasks.model.TaskModel @@ -46,8 +49,13 @@ internal class TasksInteractorImpl @Inject constructor( id = id, quest = quest, trueAnswer = trueAnswer, - queryLink = queryUrlBuilder.buildUrl(this, queryFormat), + queryLink = queryUrlBuilder.buildUrl(this.toSearchQuest(), queryFormat), complexity = complexity, ) + + private fun Quest.toSearchQuest() = SearchQuest( + quest = quest, + trueAnswer = trueAnswer, + ) } diff --git a/product/theme/theme-ui/src/main/java/com/yugyd/quiz/ui/theme/ThemeViewModel.kt b/product/theme/theme-ui/src/main/java/com/yugyd/quiz/ui/theme/ThemeViewModel.kt index c613c7f..1b30ce3 100644 --- a/product/theme/theme-ui/src/main/java/com/yugyd/quiz/ui/theme/ThemeViewModel.kt +++ b/product/theme/theme-ui/src/main/java/com/yugyd/quiz/ui/theme/ThemeViewModel.kt @@ -80,7 +80,8 @@ internal class ThemeViewModel @Inject constructor( recordController.subscribe(this) vmScopeErrorHandled.launch { - val isAdFeatureEnabled = featureManager.isFeatureEnabled(FeatureToggle.AD) + val isAdFeatureEnabled = featureManager.isFeatureEnabled(FeatureToggle.AD) && + featureManager.isFeatureEnabled(FeatureToggle.AD_REWARDED_THEME) val isTelegramFeatureEnabled = featureManager.isFeatureEnabled(FeatureToggle.TELEGRAM) loadData( diff --git a/product/theme/theme-ui/src/main/java/com/yugyd/quiz/ui/theme/model/ThemeUiMapper.kt b/product/theme/theme-ui/src/main/java/com/yugyd/quiz/ui/theme/model/ThemeUiMapper.kt index 11789da..881cccf 100644 --- a/product/theme/theme-ui/src/main/java/com/yugyd/quiz/ui/theme/model/ThemeUiMapper.kt +++ b/product/theme/theme-ui/src/main/java/com/yugyd/quiz/ui/theme/model/ThemeUiMapper.kt @@ -28,8 +28,13 @@ internal class ThemeUiMapper @Inject constructor( fun map(model: Theme) = model.run { val progressPercent = percent(progress, count) + val imageUri = if (model.image != null) { - Uri.parse(image) + if (model.image!!.isHttpUrl()) { + Uri.parse(model.image) + } else { + Uri.parse("file:///android_asset/${model.image}") + } } else { null } @@ -43,4 +48,8 @@ internal class ThemeUiMapper @Inject constructor( record = progress ) } + + private fun String.isHttpUrl(): Boolean { + return startsWith("http://") || startsWith("https://") + } } diff --git a/product/update/update-ui/build.gradle b/product/update/update-ui/build.gradle index 56b002c..7862074 100644 --- a/product/update/update-ui/build.gradle +++ b/product/update/update-ui/build.gradle @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.convention.library.jacoco) alias(libs.plugins.convention.library.lint) alias(libs.plugins.convention.library.test) + alias(libs.plugins.convention.hilt) } android { @@ -13,12 +14,20 @@ android { dependencies { // Module - implementation project(':product:designsystem:uikit') + implementation project(':product:core:featuretoggle') + implementation project(':product:core:common-ui') + implementation project(':product:core:coroutines-utils') implementation project(':product:core:navigation') + implementation project(':product:core') + implementation project(':product:designsystem:uikit') // UI - Compose implementation libs.compose.material3 + implementation libs.compose.material.icons + implementation libs.compose.viewmodel + implementation libs.compose.lifecycle // Navigation + implementation libs.compose.hilt.navigation implementation libs.compose.navigation } diff --git a/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateNavigation.kt b/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateNavigation.kt index a67f1ae..6f260e4 100644 --- a/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateNavigation.kt +++ b/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateNavigation.kt @@ -30,12 +30,14 @@ fun NavController.navigateToUpdate() { fun NavGraphBuilder.updateScreen( navigateToGooglePlay: () -> Unit, + navigateToBrowser: (String) -> Unit, ) { composable( route = UPDATE_ROUTE, arguments = listOf(hideBottomBarArgument), ) { UpdateRoute( + navigateToBrowser = navigateToBrowser, navigateToGooglePlay = navigateToGooglePlay, ) } diff --git a/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateScreen.kt b/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateScreen.kt index 00d1ec1..3ba1a23 100644 --- a/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateScreen.kt +++ b/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateScreen.kt @@ -28,30 +28,53 @@ import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.yugyd.quiz.uikit.common.ThemePreviews import com.yugyd.quiz.uikit.component.QuizBackground import com.yugyd.quiz.uikit.theme.QuizApplicationTheme +import com.yugyd.quiz.update.UpdateView.Action +import com.yugyd.quiz.update.UpdateView.State +import com.yugyd.quiz.update.UpdateView.State.NavigationState +import com.yugyd.quiz.update.UpdateView.State.UpdateConfigUiModel import com.yugyd.quiz.uikit.R as UiKitR @Composable -fun UpdateRoute( +internal fun UpdateRoute( + viewModel: UpdateViewModel = hiltViewModel(), navigateToGooglePlay: () -> Unit, + navigateToBrowser: (String) -> Unit, ) { + val state by viewModel.state.collectAsStateWithLifecycle() + UpdateScreen( + model = state, navigateToGooglePlay = navigateToGooglePlay, + navigateToBrowser = navigateToBrowser, + onUpdateClicked = { + viewModel.onAction(Action.OnUpdateClicked) + }, + onNavigationHandled = { + viewModel.onAction(Action.OnNavigationHandled) + } ) } @Composable internal fun UpdateScreen( + model: State, navigateToGooglePlay: () -> Unit, + navigateToBrowser: (String) -> Unit, + onUpdateClicked: () -> Unit, + onNavigationHandled: () -> Unit, ) { Column( modifier = Modifier @@ -72,7 +95,7 @@ internal fun UpdateScreen( Spacer(modifier = Modifier.height(32.dp)) Text( - text = stringResource(id = R.string.title_update), + text = model.updateConfig.title, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground, ) @@ -80,7 +103,7 @@ internal fun UpdateScreen( Spacer(modifier = Modifier.height(16.dp)) Text( - text = stringResource(id = R.string.title_update_description), + text = model.updateConfig.message, color = MaterialTheme.colorScheme.onBackground, textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyLarge, @@ -89,11 +112,43 @@ internal fun UpdateScreen( Spacer(modifier = Modifier.height(16.dp)) Button( - onClick = navigateToGooglePlay, + onClick = onUpdateClicked, ) { - Text(text = stringResource(id = R.string.action_update)) + Text(text = model.updateConfig.buttonTitle) } } + + NavigationHandler( + navigationState = model.navigationState, + onNavigationHandled = onNavigationHandled, + navigateToGooglePlay = navigateToGooglePlay, + navigateToBrowser = navigateToBrowser, + ) +} + +@Composable +internal fun NavigationHandler( + navigationState: NavigationState?, + navigateToGooglePlay: () -> Unit, + navigateToBrowser: (String) -> Unit, + onNavigationHandled: () -> Unit, +) { + LaunchedEffect(key1 = navigationState) { + when (navigationState) { + is NavigationState.NavigateToGooglePlay -> { + if (!navigationState.storeLink.isNullOrEmpty()) { + navigateToBrowser(navigationState.storeLink) + } else { + navigateToGooglePlay() + + } + } + + null -> Unit + } + + navigationState?.let { onNavigationHandled() } + } } @ThemePreviews @@ -102,7 +157,19 @@ private fun UpdateScreenPreview() { QuizApplicationTheme { QuizBackground { UpdateScreen( - navigateToGooglePlay = {} + State( + updateConfig = UpdateConfigUiModel( + buttonTitle = "Button", + message = "Message", + title = "Title", + ), + isLoading = false, + navigationState = null, + ), + onNavigationHandled = {}, + onUpdateClicked = {}, + navigateToGooglePlay = {}, + navigateToBrowser = {}, ) } } diff --git a/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateView.kt b/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateView.kt new file mode 100644 index 0000000..8d8355c --- /dev/null +++ b/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateView.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2023 Roman Likhachev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yugyd.quiz.update + +internal interface UpdateView { + + data class State( + val updateConfig: UpdateConfigUiModel = UpdateConfigUiModel(), + val isLoading: Boolean = false, + val navigationState: NavigationState? = null, + ) { + + data class UpdateConfigUiModel( + val buttonTitle: String = "", + val message: String = "", + val title: String = "", + ) + + sealed interface NavigationState { + data class NavigateToGooglePlay(val storeLink: String? = null) : NavigationState + } + } + + sealed interface Action { + data object OnUpdateClicked : Action + data object OnNavigationHandled : Action + } +} diff --git a/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateViewModel.kt b/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateViewModel.kt new file mode 100644 index 0000000..7e258b1 --- /dev/null +++ b/product/update/update-ui/src/main/java/com/yugyd/quiz/update/UpdateViewModel.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2023 Roman Likhachev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yugyd.quiz.update + +import com.yugyd.quiz.commonui.base.BaseViewModel +import com.yugyd.quiz.core.ContentProvider +import com.yugyd.quiz.core.Logger +import com.yugyd.quiz.core.coroutinesutils.DispatchersProvider +import com.yugyd.quiz.core.runCatch +import com.yugyd.quiz.featuretoggle.domain.RemoteConfigRepository +import com.yugyd.quiz.update.UpdateView.Action +import com.yugyd.quiz.update.UpdateView.State +import com.yugyd.quiz.update.UpdateView.State.NavigationState +import com.yugyd.quiz.update.UpdateView.State.UpdateConfigUiModel +import com.yugyd.quiz.update.model.UpdateUiMapper +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class UpdateViewModel @Inject constructor( + private val remoteConfigRepository: RemoteConfigRepository, + private val updateUiMapper: UpdateUiMapper, + private val contentProvider: ContentProvider, + private val logger: Logger, + dispatchersProvider: DispatchersProvider, +) : + BaseViewModel( + logger = logger, + dispatchersProvider = dispatchersProvider, + initialState = State(), + ) { + + init { + loadData() + } + + override fun handleAction(action: Action) { + when (action) { + is Action.OnUpdateClicked -> onUpdateClicked() + + Action.OnNavigationHandled -> { + screenState = screenState.copy(navigationState = null) + } + } + } + + private fun loadData() { + vmScopeErrorHandled.launch { + screenState = screenState.copy( + isLoading = true, + updateConfig = UpdateConfigUiModel(), + ) + + runCatch( + block = { + val updateConfig = remoteConfigRepository.fetchUpdateConfig() + screenState = updateUiMapper.map(updateConfig) + }, + catch = ::processDataError, + ) + } + } + + private fun processDataError(error: Throwable) { + screenState = updateUiMapper.makeDefaultState() + processError(error) + } + + private fun onUpdateClicked() { + vmScopeErrorHandled.launch { + runCatch( + block = { + val link = contentProvider.getUpdateLink() + screenState = screenState.copy( + navigationState = NavigationState.NavigateToGooglePlay(link), + ) + }, + catch = { + logger.logError(TAG, it) + screenState = screenState.copy( + navigationState = NavigationState.NavigateToGooglePlay(), + ) + } + ) + } + } + + private companion object { + private const val TAG = "UpdateViewModel" + } +} diff --git a/product/update/update-ui/src/main/java/com/yugyd/quiz/update/model/UpdateUiMapper.kt b/product/update/update-ui/src/main/java/com/yugyd/quiz/update/model/UpdateUiMapper.kt new file mode 100644 index 0000000..10c2447 --- /dev/null +++ b/product/update/update-ui/src/main/java/com/yugyd/quiz/update/model/UpdateUiMapper.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Roman Likhachev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yugyd.quiz.update.model + +import android.content.Context +import com.yugyd.quiz.featuretoggle.domain.model.update.UpdateConfig +import com.yugyd.quiz.update.R +import com.yugyd.quiz.update.UpdateView.State +import com.yugyd.quiz.update.UpdateView.State.UpdateConfigUiModel +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +internal class UpdateUiMapper @Inject constructor( + @ApplicationContext private val context: Context, +) { + + fun map(updateConfig: UpdateConfig?): State { + return State( + updateConfig = UpdateConfigUiModel( + title = updateConfig?.title ?: context.getString(R.string.title_update), + message = updateConfig?.message + ?: context.getString(R.string.title_update_description), + buttonTitle = updateConfig?.buttonTitle + ?: context.getString(R.string.action_update), + ), + isLoading = false, + ) + } + + fun makeDefaultState(): State { + return State( + updateConfig = UpdateConfigUiModel( + buttonTitle = context.getString(R.string.action_update), + message = context.getString(R.string.title_update_description), + title = context.getString(R.string.title_update), + ), + isLoading = false, + ) + } +} diff --git a/settings.gradle b/settings.gradle index 958a9ff..6c1fddd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,6 +28,7 @@ include ':product:core:coroutines-utils' include ':product:core:featuretoggle' include ':product:core:file' include ':product:core:navigation' +include ':product:core:search-utils' include ':product:core:test' // Services include ':product:services:ad-api'