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
+}