diff --git a/app/src/main/java/com/kafka/user/home/MainScreen.kt b/app/src/main/java/com/kafka/user/home/MainScreen.kt index b075cf2e0..9b099358f 100644 --- a/app/src/main/java/com/kafka/user/home/MainScreen.kt +++ b/app/src/main/java/com/kafka/user/home/MainScreen.kt @@ -9,15 +9,25 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState +import com.kafka.base.debug +import com.kafka.common.snackbar.SnackbarManager +import com.kafka.common.snackbar.asString +import com.kafka.common.widgets.LocalSnackbarHostState import com.kafka.data.prefs.Theme +import com.kafka.navigation.Navigator +import com.kafka.navigation.NavigatorHost +import com.kafka.ui.components.snackbar.SnackbarMessagesHost +import com.kafka.user.R import com.kafka.user.home.bottombar.HomeNavigation import com.sarahang.playback.core.PlaybackConnection import com.sarahang.playback.ui.audio.AudioActionHost @@ -27,12 +37,6 @@ import com.sarahang.playback.ui.color.LocalColorExtractor import kotlinx.coroutines.flow.collectLatest import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject -import com.kafka.base.debug -import com.kafka.common.snackbar.SnackbarManager -import com.kafka.common.widgets.LocalSnackbarHostState -import com.kafka.navigation.Navigator -import com.kafka.navigation.NavigatorHost -import com.kafka.ui.components.snackbar.SnackbarMessagesHost import tm.alashow.datmusic.downloader.Downloader import tm.alashow.datmusic.ui.downloader.DownloaderHost import ui.common.theme.theme.LocalTheme @@ -55,11 +59,30 @@ fun MainScreen( ) { val mainViewModel = viewModel { viewModelFactory() } val context = LocalContext.current + val appUpdateConfig by mainViewModel.appUpdateConfig.collectAsStateWithLifecycle() ForceUpdateDialog( - show = mainViewModel.isUpdateRequired, + show = appUpdateConfig == MainViewModel.AppUpdateState.Required, update = { mainViewModel.updateApp(context) }) + LaunchedEffect(appUpdateConfig) { + if (appUpdateConfig == MainViewModel.AppUpdateState.Optional) { + snackbarManager.addMessage( + message = context.getString(R.string.app_update_is_available), + label = context.getString(R.string.update), + onClick = { mainViewModel.updateApp(context) } + ) + } + } + + LaunchedEffect(snackbarManager.actionPerformed) { + snackbarManager.actionPerformed.collectLatest { message -> + if (message.message.asString() == context.getString(R.string.app_update_is_available)) { + mainViewModel.updateApp(context) + } + } + } + LaunchedEffect(mainViewModel, navController) { navController.currentBackStackEntryFlow.collectLatest { entry -> mainViewModel.logScreenView(entry) diff --git a/app/src/main/java/com/kafka/user/home/MainViewModel.kt b/app/src/main/java/com/kafka/user/home/MainViewModel.kt index 463bac7e4..1fdd207d6 100644 --- a/app/src/main/java/com/kafka/user/home/MainViewModel.kt +++ b/app/src/main/java/com/kafka/user/home/MainViewModel.kt @@ -4,30 +4,41 @@ import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavBackStackEntry +import com.kafka.analytics.logger.Analytics +import com.kafka.base.extensions.stateInDefault +import com.kafka.common.goToPlayStore +import com.kafka.domain.interactors.account.SignInAnonymously +import com.kafka.domain.observers.ObserveAppUpdateConfig import com.kafka.remote.config.RemoteConfig import com.kafka.remote.config.getPlayerTheme -import com.kafka.remote.config.minSupportedVersion import com.kafka.user.BuildConfig import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import com.kafka.analytics.logger.Analytics -import com.kafka.common.goToPlayStore -import com.kafka.domain.interactors.account.SignInAnonymously import javax.inject.Inject class MainViewModel @Inject constructor( private val analytics: Analytics, private val signInAnonymously: SignInAnonymously, private val remoteConfig: RemoteConfig, + observeAppUpdateConfig: ObserveAppUpdateConfig ) : ViewModel() { val playerTheme by lazy { remoteConfig.getPlayerTheme() } - val isUpdateRequired by lazy { - val minSupportedVersion = remoteConfig.minSupportedVersion() - minSupportedVersion != 0L && BuildConfig.VERSION_CODE < minSupportedVersion - } + + private val versionCode = BuildConfig.VERSION_CODE + val appUpdateConfig = observeAppUpdateConfig.flow + .map { + when { + it.blockedAppVersions.contains(versionCode) -> AppUpdateState.Required + it.forceUpdateVersion > 0 && versionCode < it.forceUpdateVersion -> AppUpdateState.Required + it.softUpdateVersion > 0 && versionCode < it.softUpdateVersion -> AppUpdateState.Optional + else -> AppUpdateState.None + } + }.stateInDefault(viewModelScope, AppUpdateState.None) init { signInAnonymously() + observeAppUpdateConfig(Unit) } private fun signInAnonymously() { @@ -47,4 +58,10 @@ class MainViewModel @Inject constructor( arguments = entry.arguments, ) } + + enum class AppUpdateState { + Required, + Optional, + None + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ef7ab02eb..246214582 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,4 +10,5 @@ Update Available This version is outdated. Please update the app to continue using it. Update + App update is available diff --git a/core/remote-config/src/commonMain/kotlin/com/kafka/remote/config/RemoteConfigExtensions.kt b/core/remote-config/src/commonMain/kotlin/com/kafka/remote/config/RemoteConfigExtensions.kt index 5c837643f..5c207ea13 100644 --- a/core/remote-config/src/commonMain/kotlin/com/kafka/remote/config/RemoteConfigExtensions.kt +++ b/core/remote-config/src/commonMain/kotlin/com/kafka/remote/config/RemoteConfigExtensions.kt @@ -6,7 +6,6 @@ const val DOWNLOADER_TYPE = "downloader_type" const val GOOGLE_LOGIN_ENABLED = "google_login_enabled" const val RECOMMENDATION_ROW_ENABLED = "recommendation_row_enabled" const val ONLINE_READER_ENABLED = "online_reader_enabled" -const val MIN_SUPPORTED_VERSION = "min_supported_version" const val SHARE_APP_INDEX = "share_app_index" const val DOWNLOADS_WARNING_MESSAGE = "downloads_warning_message" const val ITEM_DETAIL_DYNAMIC_THEME_ENABLED = "item_detail_dynamic_theme_enabled" @@ -28,8 +27,6 @@ fun RemoteConfig.isRecommendationRowEnabled() = getBoolean(RECOMMENDATION_ROW_EN fun RemoteConfig.isOnlineReaderEnabled() = getBoolean(ONLINE_READER_ENABLED) -fun RemoteConfig.minSupportedVersion() = getLong(MIN_SUPPORTED_VERSION) - fun RemoteConfig.shareAppIndex() = getLong(SHARE_APP_INDEX) fun RemoteConfig.downloadsWarningMessage() = get(DOWNLOADS_WARNING_MESSAGE) diff --git a/data/models/src/commonMain/kotlin/com/kafka/data/model/AppConfig.kt b/data/models/src/commonMain/kotlin/com/kafka/data/model/AppConfig.kt new file mode 100644 index 000000000..a80ff9d2d --- /dev/null +++ b/data/models/src/commonMain/kotlin/com/kafka/data/model/AppConfig.kt @@ -0,0 +1,16 @@ +package com.kafka.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AppConfig( + @SerialName("app_update") val appUpdate: AppUpdateConfig, +) + +@Serializable +data class AppUpdateConfig( + @SerialName("soft_update_version") val softUpdateVersion: Int, + @SerialName("force_update_version") val forceUpdateVersion: Int, + @SerialName("blocked_update_version") val blockedAppVersions: List, +) diff --git a/data/repo/src/main/java/com/kafka/data/feature/firestore/FirestoreGraph.kt b/data/repo/src/main/java/com/kafka/data/feature/firestore/FirestoreGraph.kt index f557044cc..7923ae9ff 100644 --- a/data/repo/src/main/java/com/kafka/data/feature/firestore/FirestoreGraph.kt +++ b/data/repo/src/main/java/com/kafka/data/feature/firestore/FirestoreGraph.kt @@ -2,8 +2,9 @@ package com.kafka.data.feature.firestore import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore -import dev.gitlive.firebase.firestore.CollectionReference import com.kafka.base.ApplicationScope +import dev.gitlive.firebase.firestore.CollectionReference +import dev.gitlive.firebase.firestore.DocumentReference import javax.inject.Inject import dev.gitlive.firebase.firestore.FirebaseFirestore as FirebaseFirestoreKt @@ -25,6 +26,11 @@ class FirestoreGraph @Inject constructor( get() = firestoreKt .collection("homepage-collection") + val appUpdateConfig: DocumentReference + get() = firestoreKt + .collection("app_config") + .document("app_update") + val feedbackCollection: CollectionReference get() = firestoreKt.collection("feedback") diff --git a/domain/src/main/java/com/kafka/domain/observers/ObserveAppUpdateConfig.kt b/domain/src/main/java/com/kafka/domain/observers/ObserveAppUpdateConfig.kt new file mode 100644 index 000000000..3630e47d6 --- /dev/null +++ b/domain/src/main/java/com/kafka/domain/observers/ObserveAppUpdateConfig.kt @@ -0,0 +1,17 @@ +package com.kafka.domain.observers + +import com.kafka.base.domain.SubjectInteractor +import com.kafka.data.feature.firestore.FirestoreGraph +import com.kafka.data.model.AppUpdateConfig +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class ObserveAppUpdateConfig @Inject constructor( + private val firestoreGraph: FirestoreGraph +) : SubjectInteractor() { + override fun createObservable(params: Unit): Flow { + return firestoreGraph.appUpdateConfig.snapshots + .map { it.data() } + } +} diff --git a/ui/common/src/commonMain/kotlin/com/kafka/common/snackbar/SnackbarManager.kt b/ui/common/src/commonMain/kotlin/com/kafka/common/snackbar/SnackbarManager.kt index b70d14f33..e944da31e 100644 --- a/ui/common/src/commonMain/kotlin/com/kafka/common/snackbar/SnackbarManager.kt +++ b/ui/common/src/commonMain/kotlin/com/kafka/common/snackbar/SnackbarManager.kt @@ -24,6 +24,7 @@ class SnackbarManager @Inject constructor() { private val actionPerformedMessageChannel = Channel>(Channel.CONFLATED) val messages = messagesChannel.receiveAsFlow() + val actionPerformed = actionPerformedMessageChannel.receiveAsFlow() private val shownMessages = mutableSetOf() suspend fun addError( @@ -38,6 +39,18 @@ class SnackbarManager @Inject constructor() { observeMessageAction(message, onRetry) } + suspend fun addMessage( + message: String, + label: String, + onClick: () -> Unit, + ) { + val action = SnackbarAction(UiMessage.Plain(label), onClick) + val snackbarMessage = SnackbarMessage(UiMessage.Plain(message), action) + addMessage(SnackbarMessage(UiMessage.Plain(message), action)) + + observeMessageAction(snackbarMessage, onClick) + } + fun addMessage(message: UiMessage) = addMessage(SnackbarMessage(message)) private fun addMessage(message: SnackbarMessage<*>) { @@ -65,7 +78,7 @@ class SnackbarManager @Inject constructor() { val result = merge( actionDismissedMessageChannel.receiveAsFlow().filter { it == message } .map { null }, // map to null because it's dismissed - actionPerformedMessageChannel.receiveAsFlow().filter { it == message }, + actionPerformed.filter { it == message }, ).firstOrNull() return if (result == message) message else null } diff --git a/ui/components/src/main/java/com/kafka/ui/components/snackbar/SnackbarMessagesHost.kt b/ui/components/src/main/java/com/kafka/ui/components/snackbar/SnackbarMessagesHost.kt index debf51930..098f44844 100644 --- a/ui/components/src/main/java/com/kafka/ui/components/snackbar/SnackbarMessagesHost.kt +++ b/ui/components/src/main/java/com/kafka/ui/components/snackbar/SnackbarMessagesHost.kt @@ -33,7 +33,6 @@ fun SnackbarMessagesHost( } } - @Composable private fun CollectEvent( flow: Flow, @@ -46,4 +45,4 @@ private fun CollectEvent( collector(it) } } -} \ No newline at end of file +}