compose: details: Improvements to purchase handling

Let the worker handle the purchase so it isn't cancelled even if the user
closes the screen immediately after hitting the install button

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-10-11 16:58:03 +08:00
parent 0167ddbb92
commit 1da8659de7
7 changed files with 40 additions and 53 deletions

View File

@@ -135,7 +135,7 @@ fun AppDetailsScreen(
exodusReport = exodusReport,
onNavigateUp = onNavigateUp,
onNavigateToAppDetails = onNavigateToAppDetails,
onDownload = { viewModel.purchase(app!!) },
onDownload = { viewModel.enqueueDownload(app!!) },
onFavorite = { viewModel.toggleFavourite(app!!) },
onCancelDownload = { viewModel.cancelDownload(app!!) },
onUninstall = { AppInstaller.uninstall(context, packageName) },
@@ -274,14 +274,14 @@ private fun ScreenContentApp(
@Composable
fun SetupActions() {
when (state) {
is AppState.Queued,
is AppState.Purchasing,
is AppState.Downloading -> {
Actions(
primaryActionDisplayName = stringResource(R.string.action_open),
secondaryActionDisplayName = stringResource(R.string.action_cancel),
isPrimaryActionEnabled = false,
onSecondaryAction = onCancelDownload,
isSecondaryActionEnabled = state !is AppState.Purchasing
onSecondaryAction = onCancelDownload
)
}

View File

@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
@@ -33,7 +34,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@@ -49,12 +50,12 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.extensions.isWindowCompact
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composables.InfoComposable
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.data.model.AppState
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import kotlinx.coroutines.android.awaitFrame
import kotlinx.coroutines.launch
@@ -66,26 +67,21 @@ fun ManualDownloadScreen(
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val context = LocalContext.current
val app by viewModel.app.collectAsStateWithLifecycle()
val state by viewModel.state.collectAsStateWithLifecycle()
val topAppBarTitle = when {
windowAdaptiveInfo.isWindowCompact -> app!!.displayName
else -> stringResource(R.string.title_manual_download)
}
LaunchedEffect(key1 = Unit) {
viewModel.purchaseStatus.collect { success ->
if (success) {
context.toast(R.string.toast_manual_available)
onNavigateUp()
} else {
context.toast(R.string.toast_manual_unavailable)
}
LaunchedEffect(key1 = state) {
if (state is AppState.Downloading) {
onNavigateUp()
}
}
ScreenContent(
state = state,
topAppBarTitle = topAppBarTitle,
currentVersionCode = app!!.versionCode,
onNavigateUp = onNavigateUp,
@@ -98,13 +94,14 @@ fun ManualDownloadScreen(
}
)
)
viewModel.purchase(requestedApp)
viewModel.enqueueDownload(requestedApp)
}
)
}
@Composable
private fun ScreenContent(
state: AppState = AppState.Unavailable,
topAppBarTitle: String? = null,
currentVersionCode: Long = 0L,
onNavigateUp: () -> Unit = {},
@@ -116,6 +113,7 @@ private fun ScreenContent(
val snackBarHostState = remember { SnackbarHostState() }
val errorMessage = stringResource(R.string.manual_download_version_error)
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
var versionCode by remember {
val initText = currentVersionCode.toString()
@@ -157,6 +155,7 @@ private fun ScreenContent(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
enabled = !state.inProgress(),
value = versionCode,
onValueChange = {
if (it.text.isDigitsOnly()) {
@@ -167,9 +166,8 @@ private fun ScreenContent(
},
shape = RoundedCornerShape(10.dp),
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
)
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
}
@@ -182,7 +180,7 @@ private fun ScreenContent(
onClick = onNavigateUp
) {
Text(
text = stringResource(R.string.action_cancel),
text = stringResource(R.string.action_close),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -190,7 +188,12 @@ private fun ScreenContent(
Button(
modifier = Modifier.weight(1F),
onClick = { onDownload(versionCode.text.toLong()) }) {
enabled = !state.inProgress(),
onClick = {
onDownload(versionCode.text.toLong())
focusManager.clearFocus()
}
) {
Text(
text = stringResource(R.string.action_install),
maxLines = 1,
@@ -204,9 +207,7 @@ private fun ScreenContent(
@Preview
@Composable
private fun ManualDownloadScreenPreview(
@PreviewParameter(AppPreviewProvider::class) app: App
) {
private fun ManualDownloadScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
ScreenContent(
topAppBarTitle = app.displayName,
currentVersionCode = app.versionCode

View File

@@ -97,9 +97,8 @@ fun Details(
)
}
AppState.Purchasing::class -> {
stringResource(R.string.preparing_to_install)
}
AppState.Queued::class -> stringResource(R.string.status_queued)
AppState.Purchasing::class -> stringResource(R.string.preparing_to_install)
else -> {
stringResource(R.string.version, versionName, versionCode)

View File

@@ -10,10 +10,11 @@ enum class DownloadStatus(@StringRes val localized: Int) {
COMPLETED(R.string.status_completed),
QUEUED(R.string.status_queued),
UNAVAILABLE(R.string.status_unavailable),
VERIFYING(R.string.status_verifying);
VERIFYING(R.string.status_verifying),
PURCHASING(R.string.preparing_to_install);
companion object {
val finished = listOf(FAILED, CANCELLED, COMPLETED)
val running = listOf(QUEUED, DOWNLOADING)
val running = listOf(QUEUED, PURCHASING, DOWNLOADING)
}
}

View File

@@ -55,6 +55,7 @@ sealed class AppState {
val timeRemaining: Long
) : AppState()
data object Queued : AppState()
data object Purchasing : AppState()
data class Installing(val progress: Float) : AppState()
data class Error(val message: String?) : AppState()
@@ -68,7 +69,7 @@ sealed class AppState {
* Whether there is some sort of ongoing process related to the app
*/
fun inProgress(): Boolean {
return this is Downloading || this is Installing || this is Purchasing
return this is Downloading || this is Installing || this is Purchasing || this is Queued
}
/**

View File

@@ -111,6 +111,7 @@ class DownloadWorker @AssistedInject constructor(
setForeground(getForegroundInfo())
// Try to purchase the app if file list is empty
notifyStatus(DownloadStatus.PURCHASING)
download.fileList = download.fileList.ifEmpty {
purchase(download.packageName, download.versionCode, download.offerType)
}

View File

@@ -17,7 +17,6 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.details.TestingProgramStatus
import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper
import com.aurora.gplayapi.network.IHttpClient
@@ -26,6 +25,7 @@ import com.aurora.store.BuildConfig
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.model.AppState
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.PlexusReport
import com.aurora.store.data.model.Report
@@ -40,11 +40,8 @@ import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
@@ -62,7 +59,6 @@ import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
class AppDetailsViewModel @Inject constructor(
val authProvider: AuthProvider,
@ApplicationContext private val context: Context,
private val purchaseHelper: PurchaseHelper,
private val appDetailsHelper: AppDetailsHelper,
private val reviewsHelper: ReviewsHelper,
private val webDataSafetyHelper: WebDataSafetyHelper,
@@ -104,9 +100,6 @@ class AppDetailsViewModel @Inject constructor(
private val _favourite = MutableStateFlow(false)
val favourite = _favourite.asStateFlow()
private val _purchaseStatus = MutableSharedFlow<Boolean>()
val purchaseStatus = _purchaseStatus.asSharedFlow()
private val download = combine(app, downloadHelper.downloadsList) { a, list ->
if (a?.packageName.isNullOrBlank()) return@combine null
list.find { d -> d.packageName == a.packageName }
@@ -219,20 +212,9 @@ class AppDetailsViewModel @Inject constructor(
}
}
fun purchase(app: App) {
fun enqueueDownload(app: App) {
viewModelScope.launch(Dispatchers.IO) {
try {
_state.value = AppState.Purchasing
val files = purchaseHelper.purchase(app.packageName, app.versionCode, app.offerType)
_purchaseStatus.emit(files.isNotEmpty())
if (files.isNotEmpty()) {
downloadHelper.enqueueApp(app.copy(fileList = files.toMutableList()))
}
} catch (exception: Exception) {
_state.value = defaultAppState
_purchaseStatus.emit(false)
Log.e(TAG, "Failed to purchase the app", exception)
}
downloadHelper.enqueueApp(app)
}
}
@@ -271,13 +253,15 @@ class AppDetailsViewModel @Inject constructor(
}.launchIn(viewModelScope)
download.filterNotNull().onEach {
_state.value = when {
it.isRunning -> AppState.Downloading(
_state.value = when (it.status) {
DownloadStatus.DOWNLOADING -> AppState.Downloading(
it.progress.toFloat(),
it.speed,
it.timeRemaining
)
DownloadStatus.QUEUED -> AppState.Queued
DownloadStatus.PURCHASING -> AppState.Purchasing
else -> defaultAppState
}
}.launchIn(viewModelScope)