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

Verified Commit 0e1892ee authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

refactor: improve install button state mapping to reduce complexity

Introduce a dedicated input model and split mapping logic into focused helpers, updating call sites and tests to improve readability.
parent b1b8c6b0
Loading
Loading
Loading
Loading
Loading
+55 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.ui.compose.state

import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.enums.User
import foundation.e.apps.install.pkg.InstallerService

data class InstallationFault(
    val isFaulty: Boolean,
    val reason: String,
)

data class InstallButtonStateInput(
    val app: Application,
    val user: User,
    val isAnonymousUser: Boolean,
    val isUnsupported: Boolean,
    val installationFault: InstallationFault?,
    val purchaseState: PurchaseState,
    val progressPercent: Int?,
    val isSelfUpdate: Boolean,
    val overrideStatus: Status? = null,
) {
    val resolvedStatus: Status
        get() = overrideStatus ?: app.status

    val percentLabel: String?
        get() = progressPercent?.takeIf { it in 0..PERCENTAGE_MAX }?.let { "$it%" }

    val isFaulty: Boolean
        get() = installationFault?.isFaulty ?: false

    val isUpdateIncompatible: Boolean
        get() = installationFault?.reason == InstallerService.INSTALL_FAILED_UPDATE_INCOMPATIBLE
}

private const val PERCENTAGE_MAX = 100
+159 −151
Original line number Diff line number Diff line
@@ -22,73 +22,81 @@ import foundation.e.apps.R
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.enums.User
import foundation.e.apps.install.pkg.InstallerService

/*
 * Map raw application + contextual signals into a single button state.
 * Keep pure: no side effects; callers handle actions.
 */
@Suppress("CyclomaticComplexMethod", "LongMethod", "LongParameterList")
fun mapAppToInstallState(
    app: Application,
    user: User,
    isAnonymousUser: Boolean,
    isUnsupported: Boolean,
    faultyResult: Pair<Boolean, String>?,
    purchaseState: PurchaseState,
    progressPercent: Int?,
    isSelfUpdate: Boolean,
    overrideStatus: Status? = null,
): InstallButtonState {
    val status = overrideStatus ?: app.status
    val percentLabel = progressPercent?.takeIf { it in 0..PERCENTAGE_MAX }?.let { "$it%" }

    return when (status) {
        Status.INSTALLED -> InstallButtonState(
fun mapAppToInstallState(input: InstallButtonStateInput): InstallButtonState {
    return when (val status = input.resolvedStatus) {
        Status.INSTALLED -> mapInstalled()
        Status.UPDATABLE -> mapUpdatable(input)
        Status.UNAVAILABLE -> mapUnavailable(input)
        Status.QUEUED, Status.AWAITING, Status.DOWNLOADING, Status.DOWNLOADED -> mapDownloading(input, status)
        Status.INSTALLING -> mapInstalling(status)
        Status.BLOCKED -> mapBlocked(input)
        Status.INSTALLATION_ISSUE -> mapInstallationIssue(input)
        else -> mapUnknown(input)
    }
}

private fun mapInstalled(): InstallButtonState {
    return InstallButtonState(
        label = ButtonLabel(resId = R.string.open),
        enabled = true,
        style = buildStyleFor(status = Status.INSTALLED, enabled = true),
        actionIntent = InstallButtonAction.OpenAppOrPwa,
        statusTag = StatusTag.Installed,
    )
}

        Status.UPDATABLE -> {
            val unsupported = isUnsupported
            InstallButtonState(
private fun mapUpdatable(input: InstallButtonStateInput): InstallButtonState {
    val unsupported = input.isUnsupported
    return InstallButtonState(
        label = ButtonLabel(resId = if (unsupported) R.string.not_available else R.string.update),
        enabled = true,
        style = buildStyleFor(status = Status.UPDATABLE, enabled = true),
                actionIntent = if (unsupported) {
                    InstallButtonAction.NoOp
                } else if (isSelfUpdate) {
                    InstallButtonAction.UpdateSelfConfirm
                } else {
                    InstallButtonAction.Install
        actionIntent = when {
            unsupported -> InstallButtonAction.NoOp
            input.isSelfUpdate -> InstallButtonAction.UpdateSelfConfirm
            else -> InstallButtonAction.Install
        },
                dialogType = if (!unsupported && isSelfUpdate) InstallDialogType.SelfUpdateConfirmation else null,
        dialogType = if (!unsupported && input.isSelfUpdate) InstallDialogType.SelfUpdateConfirmation else null,
        statusTag = StatusTag.Updatable,
    )
}

        Status.UNAVAILABLE -> {
            when {
                isUnsupported -> InstallButtonState(
private fun mapUnavailable(input: InstallButtonStateInput): InstallButtonState {
    return when {
        input.isUnsupported -> mapUnavailableUnsupported()
        input.app.isFree -> mapUnavailableFree()
        input.isAnonymousUser -> mapUnavailableAnonymous(input.app)
        else -> mapUnavailablePaid(input)
    }
}

private fun mapUnavailableUnsupported(): InstallButtonState {
    return InstallButtonState(
        label = ButtonLabel(resId = R.string.not_available),
        enabled = true,
        style = buildStyleFor(Status.UNAVAILABLE, enabled = true),
        actionIntent = InstallButtonAction.NoOp,
        statusTag = StatusTag.UnavailableUnsupported,
    )
}

                app.isFree -> InstallButtonState(
private fun mapUnavailableFree(): InstallButtonState {
    return InstallButtonState(
        label = ButtonLabel(resId = R.string.install),
        enabled = true,
        style = buildStyleFor(Status.UNAVAILABLE, enabled = true),
        actionIntent = InstallButtonAction.Install,
        statusTag = StatusTag.UnavailableFree,
    )
}

                isAnonymousUser -> InstallButtonState(
private fun mapUnavailableAnonymous(app: Application): InstallButtonState {
    return InstallButtonState(
        label = ButtonLabel(text = app.price),
        enabled = true,
        style = buildStyleFor(Status.UNAVAILABLE, enabled = true),
@@ -96,9 +104,10 @@ fun mapAppToInstallState(
        dialogType = InstallDialogType.PaidAppDialog,
        statusTag = StatusTag.UnavailablePaid,
    )
}

                else -> {
                    when (purchaseState) {
private fun mapUnavailablePaid(input: InstallButtonStateInput): InstallButtonState {
    return when (input.purchaseState) {
        PurchaseState.Loading, PurchaseState.Unknown -> InstallButtonState(
            label = ButtonLabel(text = ""),
            enabled = false,
@@ -117,7 +126,7 @@ fun mapAppToInstallState(
        )

        PurchaseState.NotPurchased -> InstallButtonState(
                            label = ButtonLabel(text = app.price),
            label = ButtonLabel(text = input.app.price),
            enabled = true,
            style = buildStyleFor(Status.UNAVAILABLE, enabled = true),
            actionIntent = InstallButtonAction.ShowPaidDialog,
@@ -126,23 +135,24 @@ fun mapAppToInstallState(
        )
    }
}
            }
        }

        Status.QUEUED, Status.AWAITING, Status.DOWNLOADING, Status.DOWNLOADED -> InstallButtonState(
private fun mapDownloading(input: InstallButtonStateInput, status: Status): InstallButtonState {
    return InstallButtonState(
        label = ButtonLabel(
                resId = if (percentLabel == null) R.string.cancel else null,
                text = percentLabel,
            resId = if (input.percentLabel == null) R.string.cancel else null,
            text = input.percentLabel,
        ),
            progressPercentText = percentLabel,
        progressPercentText = input.percentLabel,
        enabled = true,
        style = buildStyleFor(status, enabled = true),
        actionIntent = InstallButtonAction.CancelDownload,
        statusTag = StatusTag.Downloading,
        rawStatus = status,
    )
}

        Status.INSTALLING -> InstallButtonState(
private fun mapInstalling(status: Status): InstallButtonState {
    return InstallButtonState(
        label = ButtonLabel(resId = R.string.installing),
        enabled = false,
        style = buildStyleFor(status, enabled = false),
@@ -150,48 +160,46 @@ fun mapAppToInstallState(
        statusTag = StatusTag.Installing,
        rawStatus = status,
    )
}

        Status.BLOCKED -> {
            val messageId = when (user) {
private fun mapBlocked(input: InstallButtonStateInput): InstallButtonState {
    val messageId = when (input.user) {
        User.ANONYMOUS, User.NO_GOOGLE -> R.string.install_blocked_anonymous
        User.GOOGLE -> R.string.install_blocked_google
    }
            InstallButtonState(
                label = buildDefaultBlockedLabel(app),
    return InstallButtonState(
        label = buildDefaultBlockedLabel(input.app),
        enabled = true,
        style = buildStyleFor(Status.BLOCKED, enabled = true),
        actionIntent = InstallButtonAction.ShowBlockedSnackbar,
        snackbarMessageId = messageId,
        statusTag = StatusTag.Blocked,
                rawStatus = app.status,
        rawStatus = input.app.status,
    )
}

        Status.INSTALLATION_ISSUE -> {
            val faulty = faultyResult ?: (false to "")
            val incompatible = faulty.second == InstallerService.INSTALL_FAILED_UPDATE_INCOMPATIBLE
            InstallButtonState(
                label = ButtonLabel(resId = if (incompatible) R.string.update else R.string.retry),
                enabled = !faulty.first,
                style = buildStyleFor(Status.INSTALLATION_ISSUE, enabled = !faulty.first),
private fun mapInstallationIssue(input: InstallButtonStateInput): InstallButtonState {
    val enabled = !input.isFaulty
    return InstallButtonState(
        label = ButtonLabel(resId = if (input.isUpdateIncompatible) R.string.update else R.string.retry),
        enabled = enabled,
        style = buildStyleFor(Status.INSTALLATION_ISSUE, enabled = enabled),
        actionIntent = InstallButtonAction.Install,
        statusTag = StatusTag.InstallationIssue,
                rawStatus = app.status,
        rawStatus = input.app.status,
    )
}

        else -> InstallButtonState(
private fun mapUnknown(input: InstallButtonStateInput): InstallButtonState {
    return InstallButtonState(
        label = ButtonLabel(resId = R.string.install),
        enabled = true,
        style = InstallButtonStyle.AccentOutline,
        actionIntent = InstallButtonAction.NoOp,
        statusTag = StatusTag.Unknown,
            rawStatus = app.status,
        rawStatus = input.app.status,
    )
}
}

private const val PERCENTAGE_MAX = 100

private fun buildDefaultBlockedLabel(app: Application): ButtonLabel {
    val literal = app.price.takeIf { it.isNotBlank() }
+12 −9
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment
import foundation.e.apps.ui.compose.screens.SearchScreen
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.PurchaseState
import foundation.e.apps.ui.compose.state.mapAppToInstallState
import foundation.e.apps.ui.compose.theme.AppTheme
@@ -112,16 +113,18 @@ class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) {
                        else -> app
                    }
                    mapAppToInstallState(
                        InstallButtonStateInput(
                            app = effectiveApp,
                            user = user,
                            isAnonymousUser = isAnonymous,
                            isUnsupported = isUnsupported,
                        faultyResult = null,
                            installationFault = null,
                            purchaseState = purchaseState,
                            progressPercent = progressPercent,
                            isSelfUpdate = app.package_name == requireContext().packageName,
                            overrideStatus = overrideStatus,
                        )
                    )
                        .also {
                            // Restore original status to avoid mutating shared paging instance.
                            app.status = originalStatus
+137 −130

File changed.

Preview size limit exceeded, changes collapsed.