feat: add download confirmation and progress dialogs

This commit is contained in:
Cutiful 2025-06-12 08:40:39 +07:00
parent 8e5e73bdf0
commit 40011a18d4
4 changed files with 207 additions and 19 deletions

View File

@ -35,7 +35,11 @@ fun DownloadProgressDialog(
) { ) {
Text(text = message) Text(text = message)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
LinearProgressIndicator(color = MainGreen, trackColor = MainGreen.copy(alpha = 0.1f)) LinearProgressIndicator(
progress = {progress},
color = MainGreen,
trackColor = MainGreen.copy(alpha = 0.1f),
)
} }
}, },
confirmButton = { confirmButton = {

View File

@ -4,9 +4,8 @@ import com.google.android.gms.tasks.Task
import com.google.android.play.core.splitinstall.SplitInstallManager import com.google.android.play.core.splitinstall.SplitInstallManager
import com.google.android.play.core.splitinstall.SplitInstallRequest import com.google.android.play.core.splitinstall.SplitInstallRequest
import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener
import javax.inject.Inject
class DynamicModuleRepository @Inject constructor( class DynamicModuleRepository (
private val splitInstallManager: SplitInstallManager private val splitInstallManager: SplitInstallManager
) { ) {
private var sessionId = 0 private var sessionId = 0
@ -17,7 +16,7 @@ class DynamicModuleRepository @Inject constructor(
fun startDownload( fun startDownload(
moduleName: String, moduleName: String,
listener: SplitInstallStateUpdatedListener listener: SplitInstallStateUpdatedListener,
): Task<Int> { ): Task<Int> {
val request = SplitInstallRequest.newBuilder() val request = SplitInstallRequest.newBuilder()
.addModule(moduleName) .addModule(moduleName)

View File

@ -1,22 +1,15 @@
package com.syaroful.agrilinkvocpro.di package com.syaroful.agrilinkvocpro.di
import android.content.Context
import com.google.android.play.core.splitinstall.SplitInstallManager import com.google.android.play.core.splitinstall.SplitInstallManager
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory import com.google.android.play.core.splitinstall.SplitInstallManagerFactory
import dagger.Module import com.syaroful.agrilinkvocpro.data.repository.DynamicModuleRepository
import dagger.Provides import org.koin.android.ext.koin.androidContext
import dagger.hilt.InstallIn import org.koin.dsl.module
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module val dynamicFeatureModule = module {
@InstallIn(SingletonComponent::class) single<SplitInstallManager> {
object DynamicFeatureModule { SplitInstallManagerFactory.create(androidContext())
}
@Provides single { DynamicModuleRepository(get()) }
@Singleton
fun provideSplitInstallManager(@ApplicationContext context: Context): SplitInstallManager {
return SplitInstallManagerFactory.create(context)
}
} }

View File

@ -0,0 +1,192 @@
package com.syaroful.agrilinkvocpro.ui.screen.home
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.play.core.splitinstall.SplitInstallException
import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener
import com.google.android.play.core.splitinstall.model.SplitInstallErrorCode
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus
import com.syaroful.agrilinkvocpro.core.utils.DownloadState
import com.syaroful.agrilinkvocpro.data.repository.DynamicModuleRepository
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class DynamicModuleViewModel(
private val dynamicModuleRepository: DynamicModuleRepository
) : ViewModel() {
var currentModuleToDownload = mutableStateOf<String?>(null)
private set
private var onLaunchCallback: ((String) -> Unit)? = null
private val _showProgressDialog = MutableStateFlow(false)
val showProgressDialog: StateFlow<Boolean> = _showProgressDialog
private val _progressMessage = MutableStateFlow("")
val progressMessage: StateFlow<String> = _progressMessage
private val _progressPercent = MutableStateFlow(0f)
val progressPercent: StateFlow<Float> = _progressPercent
fun setDialogVisibility(show: Boolean) {
_showProgressDialog.value = show
}
private val _downloadState = MutableStateFlow<DownloadState>(DownloadState.Idle)
private val downloadState: StateFlow<DownloadState> = _downloadState.asStateFlow()
init {
observeDownloadStatus()
}
private val listener = SplitInstallStateUpdatedListener { state ->
if (state.sessionId() == dynamicModuleRepository.getSessionId()) {
when (state.status()) {
SplitInstallSessionStatus.DOWNLOADING -> {
val progress = if (state.totalBytesToDownload() > 0)
state.bytesDownloaded().toFloat() / state.totalBytesToDownload()
else 0f
_downloadState.value = DownloadState.DownloadingWithProgress(progress)
}
SplitInstallSessionStatus.DOWNLOADED -> {
_downloadState.value = DownloadState.Downloaded
}
SplitInstallSessionStatus.INSTALLED -> {
_downloadState.value = DownloadState.Installed
}
SplitInstallSessionStatus.FAILED -> {
_downloadState.value = DownloadState.Failed("Installation Failed")
}
SplitInstallSessionStatus.CANCELED -> {
_downloadState.value = DownloadState.Failed("Installation Canceled")
}
else -> {}
}
}
}
fun onFeatureClicked(moduleName: String, onLaunch: (String) -> Unit) {
if (isModuleDownloaded(moduleName)) {
startFeatureActivity(moduleName, onLaunch)
} else {
currentModuleToDownload.value = moduleName
}
}
fun confirmDownload(onLaunch: (String) -> Unit) {
currentModuleToDownload.value?.let {
downloadDynamicModule(it, onLaunch)
}
currentModuleToDownload.value = null
}
private fun downloadDynamicModule(moduleName: String, onLaunch: (String) -> Unit) {
onLaunchCallback = onLaunch
_downloadState.value = DownloadState.Starting
dynamicModuleRepository.startDownload(moduleName, listener)
.addOnFailureListener { exception ->
val errorCode = (exception as? SplitInstallException)?.errorCode ?: -1
val errorMessage = when (errorCode) {
SplitInstallErrorCode.NETWORK_ERROR -> "No Internet Connection"
SplitInstallErrorCode.MODULE_UNAVAILABLE -> "Module Unavailable"
SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED -> "Active Session Limit Exceeded"
SplitInstallErrorCode.INSUFFICIENT_STORAGE -> "Insufficient Storage"
SplitInstallErrorCode.PLAY_STORE_NOT_FOUND -> "Play Store Not Found"
else -> "Unknown Error: $errorCode"
}
_downloadState.value = DownloadState.Failed(errorMessage)
}
}
private fun isModuleDownloaded(moduleName: String): Boolean {
return dynamicModuleRepository.isModuleDownloaded(moduleName)
}
private fun observeDownloadStatus() {
viewModelScope.launch {
downloadState.collect { status ->
when (status) {
is DownloadState.Idle -> {
_progressMessage.value = ""
_progressPercent.value = 0f
}
is DownloadState.Starting -> {
_showProgressDialog.value = true
_progressMessage.value = "Starting download..."
_progressPercent.value = 0f
}
is DownloadState.Downloading -> {
_showProgressDialog.value = true
_progressMessage.value = "Downloading module..."
}
is DownloadState.Downloaded -> {
_progressMessage.value = "Download completed"
_showProgressDialog.value = true
_progressPercent.value = 1f
}
is DownloadState.Installed -> {
_progressMessage.value = "Install completed"
_progressPercent.value = 1f
delayAndDismissDialog()
}
is DownloadState.Failed -> {
_progressMessage.value = "Failed: ${status.message}"
_showProgressDialog.value = true
_progressPercent.value = 0f
}
is DownloadState.DownloadingWithProgress -> {
_showProgressDialog.value = true
_progressPercent.value = status.progress
}
}
}
}
}
private fun delayAndDismissDialog() {
viewModelScope.launch {
delay(1500)
_showProgressDialog.value = false
currentModuleToDownload.value?.let {
onLaunchCallback?.invoke(it)
}
currentModuleToDownload.value = null
}
}
private fun startFeatureActivity(moduleName: String, onLaunch: (String) -> Unit) {
val className = when (moduleName) {
"control_feature" -> "com.syaroful.agrilinkvocpro.control_feature.ControlActuatorActivity"
"growth_recipe_feature" -> "com.syaroful.agrilinkvocpro.growth_recipe_feature.GrowthRecipeActivity"
"commodity_price_prediction_feature" -> "com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.PricePredictionActivity"
"plant_disease_detection_feature" -> "com.syaroful.agrilinkvocpro.plant_disease_detection_feature.PlantDiseaseDetectionActivity"
else -> null
}
className?.let { onLaunch(it) }
}
override fun onCleared() {
super.onCleared()
dynamicModuleRepository.unregisterListener(listener)
}
}