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

Verified Commit 970a3c15 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

refactor: improve SearchFragmentV2 code organization

Break out Compose setup, progress collection, and install-state derivation into focused helpers to improve readability while preserving behavior.
parent 4a2760ec
Loading
Loading
Loading
Loading
Loading
+155 −78
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ package foundation.e.apps.ui.search.v2

import android.os.Bundle
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -63,73 +64,54 @@ class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) {
    private val appProgressViewModel: AppProgressViewModel by viewModels()
    private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels()

    @Suppress("LongMethod")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val composeView = view.findViewById<ComposeView>(R.id.composeView)
        composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
        val composeView = setupComposeView(view)
        startDownloadProgressCollection()
        setComposeContent(composeView)
    }

    private fun setupComposeView(view: View): ComposeView {
        return view.findViewById<ComposeView>(R.id.composeView).apply {
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
        }
    }

    // Ensure DownloadProgress emissions reach the ViewModel even if Compose does not recompose.
    private fun startDownloadProgressCollection() {
        viewLifecycleOwner.lifecycleScope.launch {
            appProgressViewModel.downloadProgress.asFlow().collect { progress ->
                searchViewModel.updateDownloadProgress(copyProgress(progress))
            }
        }
    }

    private fun setComposeContent(composeView: ComposeView) {
        composeView.setContent {
            AppTheme {
                SearchScreenContent()
            }
        }
    }

    @Composable
    private fun SearchScreenContent() {
        val uiState by searchViewModel.uiState.collectAsStateWithLifecycle()
        val user = mainActivityViewModel.getUser()
        val isAnonymous = user == User.ANONYMOUS
        val downloadProgress by appProgressViewModel.downloadProgress.observeAsState()
        val progressPercentMap by searchViewModel.progressPercentByKey.collectAsState()
        val statusByKey by searchViewModel.statusByKey.collectAsState()
        val selfPackageName = requireContext().packageName

                LaunchedEffect(downloadProgress) {
                    // Retain Compose-based updates as a secondary path for safety.
                    downloadProgress?.let {
                        searchViewModel.updateDownloadProgress(copyProgress(it))
                    }
                }
        DownloadProgressEffect(downloadProgress)

                val installButtonStateProvider: (Application) -> InstallButtonState = { app ->
                    val progressKey = app.package_name.takeIf { it.isNotBlank() } ?: app._id
                    val progressPercent = progressPercentMap[progressKey]
                    val overrideStatus = statusByKey[progressKey]
                    val purchaseState = when {
                        app.isFree -> PurchaseState.Unknown
                        app.isPurchased -> PurchaseState.Purchased
                        else -> PurchaseState.NotPurchased
                    }
                    val isBlocked = appInfoFetchViewModel.isAppInBlockedList(app)
                    val isUnsupported =
                        app.filterLevel.isInitialized() && !app.filterLevel.isUnFiltered()
                    val originalStatus = app.status
                    val effectiveApp = when {
                        isBlocked -> {
                            app.status = Status.BLOCKED
                            app
                        }

                        else -> app
                    }
                    mapAppToInstallState(
                        InstallButtonStateInput(
                            app = effectiveApp,
        val installButtonStateProvider = buildInstallButtonStateProvider(
            user = user,
                            isAnonymousUser = isAnonymous,
                            isUnsupported = isUnsupported,
                            installationFault = null,
                            purchaseState = purchaseState,
                            progressPercent = progressPercent,
                            isSelfUpdate = app.package_name == requireContext().packageName,
                            overrideStatus = overrideStatus,
                        )
            isAnonymous = isAnonymous,
            progressPercentMap = progressPercentMap,
            statusByKey = statusByKey,
            selfPackageName = selfPackageName,
        )
                        .also {
                            // Restore original status to avoid mutating shared paging instance.
                            app.status = originalStatus
                        }
                }

        SearchScreen(
            uiState = uiState,
@@ -154,9 +136,104 @@ class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) {
            installButtonStateProvider = installButtonStateProvider,
        )
    }

    @Composable
    private fun DownloadProgressEffect(downloadProgress: DownloadProgress?) {
        LaunchedEffect(downloadProgress) {
            // Retain Compose-based updates as a secondary path for safety.
            downloadProgress?.let {
                searchViewModel.updateDownloadProgress(copyProgress(it))
            }
        }
    }

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

            mapInstallButtonState(
                app = app,
                installButtonContext = InstallButtonContext(
                    user = user,
                    isAnonymous = isAnonymous,
                    isUnsupported = isUnsupported,
                    purchaseState = purchaseState,
                    progressPercent = progressPercent,
                    overrideStatus = overrideStatus,
                    isBlocked = isBlocked,
                    selfPackageName = selfPackageName,
                )
            )
        }
    }

    private fun progressKeyFor(app: Application): String {
        return app.package_name.takeIf { it.isNotBlank() } ?: app._id
    }

    private fun purchaseStateFor(app: Application): PurchaseState {
        return when {
            app.isFree -> PurchaseState.Unknown
            app.isPurchased -> PurchaseState.Purchased
            else -> PurchaseState.NotPurchased
        }
    }

    private fun isUnsupportedApp(app: Application): Boolean {
        return app.filterLevel.isInitialized() && !app.filterLevel.isUnFiltered()
    }

    private fun mapInstallButtonState(
        app: Application,
        installButtonContext: InstallButtonContext,
    ): InstallButtonState {
        val originalStatus = app.status
        if (installButtonContext.isBlocked) {
            app.status = Status.BLOCKED
        }

        return try {
            mapAppToInstallState(
                InstallButtonStateInput(
                    app = app,
                    user = installButtonContext.user,
                    isAnonymousUser = installButtonContext.isAnonymous,
                    isUnsupported = installButtonContext.isUnsupported,
                    installationFault = null,
                    purchaseState = installButtonContext.purchaseState,
                    progressPercent = installButtonContext.progressPercent,
                    isSelfUpdate = app.package_name == installButtonContext.selfPackageName,
                    overrideStatus = installButtonContext.overrideStatus,
                )
            )
        } finally {
            // Restore original status to avoid mutating shared paging instance.
            app.status = originalStatus
        }
    }

    private data class InstallButtonContext(
        val user: User,
        val isAnonymous: Boolean,
        val isUnsupported: Boolean,
        val purchaseState: PurchaseState,
        val progressPercent: Int?,
        val overrideStatus: Status?,
        val isBlocked: Boolean,
        val selfPackageName: String,
    )

    private fun copyProgress(progress: DownloadProgress): DownloadProgress {
        return DownloadProgress(
            totalSizeBytes = progress.totalSizeBytes.toMutableMap(),