Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bceff7f2 authored by Saalim Quadri's avatar Saalim Quadri Committed by Nishith Khanna
Browse files

feat: Properly handle button pending/install states

- Squash of following commits:
  012e2fb5,
  f545d61b,
  d4f98835, 5784a2ec



Signed-off-by: default avatarSaalim Quadri <danascape@gmail.com>
parent 8a9ef397
Loading
Loading
Loading
Loading
+29 −28
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -239,6 +240,7 @@ private fun PrivacyBadge(
                    .size(16.dp)
                    .testTag(SearchResultListItemTestTags.PRIVACY_PROGRESS),
                strokeWidth = 2.dp,
                color = MaterialTheme.colorScheme.tertiary,
            )
        } else {
            Text(
@@ -270,14 +272,12 @@ private fun PrimaryActionArea(
                .clickable(onClick = onShowMoreClick),
        )
        return
    } else {
        // render the primary action button
    }

    Column(horizontalAlignment = Alignment.End) {
        val accentColor = MaterialTheme.colorScheme.tertiary

        val labelTextColor = when {
        uiState.isFilledStyle -> MaterialTheme.colorScheme.onPrimary
            uiState.isFilledStyle -> Color.White
            else -> accentColor
        }

@@ -301,7 +301,6 @@ private fun PrimaryActionArea(
            }
        }

    Column(horizontalAlignment = Alignment.End) {
        val borderColor = when {
            uiState.isFilledStyle -> Color.Transparent
            uiState.enabled -> accentColor
@@ -311,6 +310,7 @@ private fun PrimaryActionArea(
            onClick = onPrimaryClick,
            enabled = uiState.enabled,
            modifier = Modifier
                .widthIn(min = 88.dp)
                .height(40.dp)
                .testTag(SearchResultListItemTestTags.PRIMARY_BUTTON),
            shape = RoundedCornerShape(4.dp),
@@ -353,7 +353,7 @@ private fun PlaceholderRow(modifier: Modifier = Modifier) {
            .testTag(SearchResultListItemTestTags.PLACEHOLDER),
        contentAlignment = Alignment.Center,
    ) {
        CircularProgressIndicator()
        CircularProgressIndicator(color = MaterialTheme.colorScheme.tertiary)
    }
}

@@ -379,6 +379,7 @@ data class PrimaryActionUiState(
    val isFilledStyle: Boolean,
    val showMore: Boolean = false,
    val actionIntent: InstallButtonAction = InstallButtonAction.NoOp,
    val progressFraction: Float = 0f,
)

internal object SearchResultListItemTestTags {
+1 −0
Original line number Diff line number Diff line
@@ -595,6 +595,7 @@ private fun Application.toSearchResultUiState(buttonState: InstallButtonState):
            isFilledStyle = buttonState.style == InstallButtonStyle.AccentFill,
            showMore = false,
            actionIntent = buttonState.actionIntent,
            progressFraction = buttonState.progressFraction,
        ),
        iconUrl = iconUrl,
        placeholderResId = null,
+1 −0
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ data class InstallButtonState(
    val style: InstallButtonStyle = InstallButtonStyle.AccentOutline,
    val showProgressBar: Boolean = false,
    val progressPercentText: String? = null,
    val progressFraction: Float = 0f,
    val actionIntent: InstallButtonAction = InstallButtonAction.NoOp,
    @StringRes val snackbarMessageId: Int? = null,
    val dialogType: InstallDialogType? = null,
+24 −5
Original line number Diff line number Diff line
@@ -32,7 +32,8 @@ fun mapAppToInstallState(input: InstallButtonStateInput): InstallButtonState {
        Status.INSTALLED -> mapInstalled()
        Status.UPDATABLE -> mapUpdatable(input)
        Status.UNAVAILABLE -> mapUnavailable(input)
        Status.QUEUED, Status.AWAITING, Status.DOWNLOADING, Status.DOWNLOADED -> mapDownloading(input, status)
        Status.QUEUED, Status.AWAITING -> mapQueued(status)
        Status.DOWNLOADING, Status.DOWNLOADED -> mapDownloading(input, status)
        Status.INSTALLING -> mapInstalling(status)
        Status.BLOCKED -> mapBlocked(input)
        Status.INSTALLATION_ISSUE -> mapInstallationIssue(input)
@@ -136,13 +137,27 @@ private fun mapUnavailablePaid(input: InstallButtonStateInput): InstallButtonSta
    }
}

private fun mapQueued(status: Status): InstallButtonState {
    return InstallButtonState(
        label = ButtonLabel(resId = R.string.cancel),
        progressPercentText = null,
        enabled = true,
        style = buildStyleFor(status, enabled = true),
        actionIntent = InstallButtonAction.CancelDownload,
        statusTag = StatusTag.Downloading,
        rawStatus = status,
    )
}

private fun mapDownloading(input: InstallButtonStateInput, status: Status): InstallButtonState {
    val fraction = input.progressPercent
        ?.coerceIn(PROGRESS_MIN, PROGRESS_MAX)
        ?.div(PROGRESS_MAX_FLOAT)
        ?: 0f
    return InstallButtonState(
        label = ButtonLabel(
            resId = if (input.percentLabel == null) R.string.cancel else null,
            text = input.percentLabel,
        ),
        label = ButtonLabel(resId = R.string.cancel),
        progressPercentText = input.percentLabel,
        progressFraction = fraction,
        enabled = true,
        style = buildStyleFor(status, enabled = true),
        actionIntent = InstallButtonAction.CancelDownload,
@@ -216,3 +231,7 @@ private fun buildStyleFor(status: Status, enabled: Boolean): InstallButtonStyle
        else -> InstallButtonStyle.Disabled
    }
}

private const val PROGRESS_MIN = 0
private const val PROGRESS_MAX = 100
private const val PROGRESS_MAX_FLOAT = 100f
+53 −25
Original line number Diff line number Diff line
@@ -48,9 +48,11 @@ import foundation.e.apps.ui.AppProgressViewModel
import foundation.e.apps.ui.MainActivityViewModel
import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment
import foundation.e.apps.ui.compose.screens.SearchScreen
import foundation.e.apps.ui.compose.state.ButtonLabel
import foundation.e.apps.ui.compose.state.InstallButtonAction
import foundation.e.apps.ui.compose.state.InstallButtonState
import foundation.e.apps.ui.compose.state.InstallButtonStateInput
import foundation.e.apps.ui.compose.state.InstallButtonStyle
import foundation.e.apps.ui.compose.state.PurchaseState
import foundation.e.apps.ui.compose.state.mapAppToInstallState
import foundation.e.apps.ui.compose.theme.AppTheme
@@ -101,17 +103,21 @@ class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) {
        val downloadProgress by appProgressViewModel.downloadProgress.observeAsState()
        val progressPercentMap by searchViewModel.progressPercentByKey.collectAsState()
        val statusByKey by searchViewModel.statusByKey.collectAsState()
        val pendingInstalls by searchViewModel.pendingInstalls.collectAsState()
        val selfPackageName = requireContext().packageName

        DownloadProgressEffect(downloadProgress)

        val installButtonStateProvider = buildInstallButtonStateProvider(
            InstallButtonProviderParams(
                user = user,
                isAnonymous = isAnonymous,
                progressPercentMap = progressPercentMap,
                statusByKey = statusByKey,
                pendingInstalls = pendingInstalls,
                selfPackageName = selfPackageName,
            )
        )

        SearchScreen(
            uiState = uiState,
@@ -148,35 +154,43 @@ class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) {
    }

    private fun buildInstallButtonStateProvider(
        user: User,
        isAnonymous: Boolean,
        progressPercentMap: Map<String, Int>,
        statusByKey: Map<String, Status>,
        selfPackageName: String,
        params: InstallButtonProviderParams,
    ): (Application) -> InstallButtonState {
        return { app ->
            val progressKey = progressKeyFor(app)
            val progressPercent = progressPercentMap[progressKey]
            val overrideStatus = statusByKey[progressKey]
            val progressPercent = params.progressPercentMap[progressKey]
            val overrideStatus = params.statusByKey[progressKey]
            val isPending = progressKey in params.pendingInstalls
            val purchaseState = purchaseStateFor(app)
            val isBlocked = appInfoFetchViewModel.isAppInBlockedList(app)
            val isUnsupported = isUnsupportedApp(app)

            // Show disabled state while waiting for install to register
            if (isPending && (overrideStatus == null || overrideStatus == Status.UNAVAILABLE)) {
                InstallButtonState(
                    label = ButtonLabel(),
                    enabled = false,
                    style = InstallButtonStyle.AccentOutline,
                    showProgressBar = true,
                    actionIntent = InstallButtonAction.NoOp,
                )
            } else {
                mapInstallButtonState(
                    app = app,
                    installButtonContext = InstallButtonContext(
                    user = user,
                    isAnonymous = isAnonymous,
                        user = params.user,
                        isAnonymous = params.isAnonymous,
                        isUnsupported = isUnsupported,
                        purchaseState = purchaseState,
                        progressPercent = progressPercent,
                        overrideStatus = overrideStatus,
                        isBlocked = isBlocked,
                    selfPackageName = selfPackageName,
                        selfPackageName = params.selfPackageName,
                    )
                )
            }
        }
    }

    private fun progressKeyFor(app: Application): String {
        return app.package_name.takeIf { it.isNotBlank() } ?: app._id
@@ -234,6 +248,15 @@ class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) {
        val selfPackageName: String,
    )

    private data class InstallButtonProviderParams(
        val user: User,
        val isAnonymous: Boolean,
        val progressPercentMap: Map<String, Int>,
        val statusByKey: Map<String, Status>,
        val pendingInstalls: Set<String>,
        val selfPackageName: String,
    )

    private fun copyProgress(progress: DownloadProgress): DownloadProgress {
        return DownloadProgress(
            totalSizeBytes = progress.totalSizeBytes.toMutableMap(),
@@ -247,15 +270,20 @@ class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) {
        when (action) {
            InstallButtonAction.Install,
            InstallButtonAction.UpdateSelfConfirm -> {
                searchViewModel.markPendingInstall(app)
                mainActivityViewModel.verifyUiFilter(app) {
                    if (mainActivityViewModel.shouldShowPaidAppsSnackBar(app)) {
                        searchViewModel.clearPendingInstall(app)
                        return@verifyUiFilter
                    }
                    mainActivityViewModel.getApplication(app)
                }
            }

            InstallButtonAction.CancelDownload -> mainActivityViewModel.cancelDownload(app)
            InstallButtonAction.CancelDownload -> {
                searchViewModel.clearPendingInstall(app)
                mainActivityViewModel.cancelDownload(app)
            }
            InstallButtonAction.OpenAppOrPwa -> {
                if (app.is_pwa) {
                    mainActivityViewModel.launchPwa(app)
Loading