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

Verified Commit 39346f17 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

refactor: move pre-enqueue and enqueue logic from AppInstallProcessor to a separate class

Move enqueue validation and download-link refresh orchestration behind a dedicated coordinator so preflight rules can be exercised directly while the processor stays focused on delegation.
parent d49e0d96
Loading
Loading
Loading
Loading
+3 −147
Original line number Diff line number Diff line
@@ -18,51 +18,21 @@

package foundation.e.apps.data.install.workmanager

import android.content.Context
import androidx.annotation.VisibleForTesting
import com.aurora.gplayapi.exceptions.InternalException
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.R
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.application.ApplicationRepository
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.enums.Type
import foundation.e.apps.data.event.AppEvent
import foundation.e.apps.data.install.AppInstallComponents
import foundation.e.apps.data.install.AppManager
import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.install.notification.StorageNotificationManager
import foundation.e.apps.data.install.wrapper.AppEventDispatcher
import foundation.e.apps.data.install.wrapper.NetworkStatusChecker
import foundation.e.apps.data.install.wrapper.StorageSpaceChecker
import foundation.e.apps.data.playstore.utils.GplayHttpRequestException
import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.domain.model.User
import foundation.e.apps.domain.preferences.SessionRepository
import kotlinx.coroutines.DelicateCoroutinesApi
import timber.log.Timber
import javax.inject.Inject

@Suppress("LongParameterList") // FIXME: Remove suppression and fix detekt
class AppInstallProcessor @Inject constructor(
    @ApplicationContext private val context: Context,
    private val appInstallComponents: AppInstallComponents,
    private val applicationRepository: ApplicationRepository,
    private val sessionRepository: SessionRepository,
    private val playStoreAuthStore: PlayStoreAuthStore,
    private val storageNotificationManager: StorageNotificationManager,
    private val appEventDispatcher: AppEventDispatcher,
    private val storageSpaceChecker: StorageSpaceChecker,
    private val networkStatusChecker: NetworkStatusChecker,
    private val appInstallAgeLimitGate: AppInstallAgeLimitGate,
    private val appInstallStartCoordinator: AppInstallStartCoordinator,
    private val appInstallWorkRunner: AppInstallWorkRunner,
    private val appInstallRequestFactory: AppInstallRequestFactory,
) {
    @Inject
    lateinit var appManager: AppManager

    /**
     * creates [AppInstall] from [Application] and enqueues into WorkManager to run install process.
     * @param application represents the app info which will be installed
@@ -94,128 +64,14 @@ class AppInstallProcessor @Inject constructor(
        isAnUpdate: Boolean = false,
        isSystemApp: Boolean = false
    ): Boolean {
        return try {
            val user = sessionRepository.awaitUser()
            if (!isSystemApp && (user == User.GOOGLE || user == User.ANONYMOUS)) {
                val authData = playStoreAuthStore.awaitAuthData()
                if (!appInstall.isFree && authData?.isAnonymous == true) {
                    appEventDispatcher.dispatch(
                        AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message)
                    )
                }
            }

            if (!canEnqueue(appInstall)) return false

            appInstallComponents.appManagerWrapper.updateAwaiting(appInstall)
            InstallWorkManager.enqueueWork(context, appInstall, isAnUpdate)
            true
        } catch (e: Exception) {
            Timber.e(
                e,
                "Enqueuing App install work is failed for ${appInstall.packageName} exception: ${e.localizedMessage}"
            )
            appInstallComponents.appManagerWrapper.installationIssue(appInstall)
            false
        }
        return appInstallStartCoordinator.enqueue(appInstall, isAnUpdate, isSystemApp)
    }

    @VisibleForTesting
    suspend fun canEnqueue(appInstall: AppInstall): Boolean {
        if (appInstall.type != Type.PWA && !updateDownloadUrls(appInstall)) {
            return false
        }

        if (!appInstallComponents.appManagerWrapper.addDownload(appInstall)) {
            Timber.i("Update adding ABORTED! status")
            return false
        }

        if (!appInstallAgeLimitGate.allow(appInstall)) {
            return false
        }

        if (!networkStatusChecker.isNetworkAvailable()) {
            appInstallComponents.appManagerWrapper.installationIssue(appInstall)
            appEventDispatcher.dispatch(AppEvent.NoInternetEvent(false))
            return false
        }

        if (storageSpaceChecker.spaceMissing(appInstall) > 0) {
            Timber.d("Storage is not available for: ${appInstall.name} size: ${appInstall.appSize}")
            storageNotificationManager.showNotEnoughSpaceNotification(appInstall)
            appInstallComponents.appManagerWrapper.installationIssue(appInstall)
            appEventDispatcher.dispatch(AppEvent.ErrorMessageEvent(R.string.not_enough_storage))
            return false
        }

        return true
    }

    // returns TRUE if updating urls is successful, otherwise false.
    private suspend fun updateDownloadUrls(appInstall: AppInstall): Boolean {
        try {
            updateFusedDownloadWithAppDownloadLink(appInstall)
        } catch (e: InternalException.AppNotPurchased) {
            if (appInstall.isFree) {
                handleAppRestricted(appInstall)
                return false
            }
            appInstallComponents.appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall)
            appEventDispatcher.dispatch(AppEvent.AppPurchaseEvent(appInstall))
            return false
        } catch (e: GplayHttpRequestException) {
            handleUpdateDownloadError(
                appInstall,
                "${appInstall.packageName} code: ${e.status} exception: ${e.localizedMessage}",
                e
            )
            return false
        } catch (e: IllegalStateException) {
            Timber.e(e)
        } catch (e: Exception) {
            handleUpdateDownloadError(
                appInstall,
                "${appInstall.packageName} exception: ${e.localizedMessage}",
                e
            )
            return false
        }
        return true
    }

    private suspend fun handleAppRestricted(appInstall: AppInstall) {
        appEventDispatcher.dispatch(AppEvent.AppRestrictedOrUnavailable(appInstall))
        appManager.addDownload(appInstall)
        appManager.updateUnavailable(appInstall)
    }

    private suspend fun handleUpdateDownloadError(
        appInstall: AppInstall,
        message: String,
        e: Exception
    ) {
        Timber.e(e, "Updating download Urls failed for $message")
        appEventDispatcher.dispatch(
            AppEvent.UpdateEvent(
                ResultSupreme.WorkError(
                    ResultStatus.UNKNOWN,
                    appInstall
                )
            )
        )
    }

    private suspend fun updateFusedDownloadWithAppDownloadLink(
        appInstall: AppInstall
    ) {
        applicationRepository.updateFusedDownloadWithDownloadingInfo(
            appInstall.source,
            appInstall
        )
        return appInstallStartCoordinator.canEnqueue(appInstall)
    }

    @OptIn(DelicateCoroutinesApi::class)
    suspend fun processInstall(
        fusedDownloadId: String,
        isItUpdateWork: Boolean,
+179 −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.data.install.workmanager

import android.content.Context
import com.aurora.gplayapi.exceptions.InternalException
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.R
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.application.ApplicationRepository
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Type
import foundation.e.apps.data.event.AppEvent
import foundation.e.apps.data.install.AppManager
import foundation.e.apps.data.install.AppManagerWrapper
import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.install.notification.StorageNotificationManager
import foundation.e.apps.data.install.wrapper.AppEventDispatcher
import foundation.e.apps.data.install.wrapper.NetworkStatusChecker
import foundation.e.apps.data.install.wrapper.StorageSpaceChecker
import foundation.e.apps.data.playstore.utils.GplayHttpRequestException
import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.domain.model.User
import foundation.e.apps.domain.preferences.SessionRepository
import timber.log.Timber
import javax.inject.Inject

@Suppress("LongParameterList", "TooGenericExceptionCaught", "ReturnCount") // FIXME: Remove suppression and fix detekt
class AppInstallStartCoordinator @Inject constructor(
    @ApplicationContext private val context: Context,
    private val appManagerWrapper: AppManagerWrapper,
    private val applicationRepository: ApplicationRepository,
    private val sessionRepository: SessionRepository,
    private val playStoreAuthStore: PlayStoreAuthStore,
    private val storageNotificationManager: StorageNotificationManager,
    private val appInstallAgeLimitGate: AppInstallAgeLimitGate,
    private val appEventDispatcher: AppEventDispatcher,
    private val storageSpaceChecker: StorageSpaceChecker,
    private val networkStatusChecker: NetworkStatusChecker,
    private val appManager: AppManager,
) {
    suspend fun enqueue(
        appInstall: AppInstall,
        isAnUpdate: Boolean = false,
        isSystemApp: Boolean = false
    ): Boolean {
        return try {
            val user = sessionRepository.awaitUser()
            if (!isSystemApp && (user == User.GOOGLE || user == User.ANONYMOUS)) {
                val authData = playStoreAuthStore.awaitAuthData()
                if (!appInstall.isFree && authData?.isAnonymous == true) {
                    appEventDispatcher.dispatch(
                        AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message)
                    )
                }
            }

            if (!canEnqueue(appInstall)) return false

            appManagerWrapper.updateAwaiting(appInstall)
            InstallWorkManager.enqueueWork(context, appInstall, isAnUpdate)
            true
        } catch (e: Exception) {
            Timber.e(
                e,
                "Enqueuing App install work is failed for ${appInstall.packageName} exception: ${e.localizedMessage}"
            )
            appManagerWrapper.installationIssue(appInstall)
            false
        }
    }

    suspend fun canEnqueue(appInstall: AppInstall): Boolean {
        if (appInstall.type != Type.PWA && !updateDownloadUrls(appInstall)) {
            return false
        }

        if (!appManagerWrapper.addDownload(appInstall)) {
            Timber.i("Update adding ABORTED! status")
            return false
        }

        if (!appInstallAgeLimitGate.allow(appInstall)) {
            return false
        }

        if (!networkStatusChecker.isNetworkAvailable()) {
            appManagerWrapper.installationIssue(appInstall)
            appEventDispatcher.dispatch(AppEvent.NoInternetEvent(false))
            return false
        }

        if (storageSpaceChecker.spaceMissing(appInstall) > 0) {
            Timber.d("Storage is not available for: ${appInstall.name} size: ${appInstall.appSize}")
            storageNotificationManager.showNotEnoughSpaceNotification(appInstall)
            appManagerWrapper.installationIssue(appInstall)
            appEventDispatcher.dispatch(AppEvent.ErrorMessageEvent(R.string.not_enough_storage))
            return false
        }

        return true
    }

    private suspend fun updateDownloadUrls(appInstall: AppInstall): Boolean {
        try {
            updateFusedDownloadWithAppDownloadLink(appInstall)
        } catch (_: InternalException.AppNotPurchased) {
            if (appInstall.isFree) {
                handleAppRestricted(appInstall)
                return false
            }
            appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall)
            appEventDispatcher.dispatch(AppEvent.AppPurchaseEvent(appInstall))
            return false
        } catch (e: GplayHttpRequestException) {
            handleUpdateDownloadError(
                appInstall,
                "${appInstall.packageName} code: ${e.status} exception: ${e.localizedMessage}",
                e
            )
            return false
        } catch (e: IllegalStateException) {
            Timber.e(e)
        } catch (e: Exception) {
            handleUpdateDownloadError(
                appInstall,
                "${appInstall.packageName} exception: ${e.localizedMessage}",
                e
            )
            return false
        }
        return true
    }

    private suspend fun handleAppRestricted(appInstall: AppInstall) {
        appEventDispatcher.dispatch(AppEvent.AppRestrictedOrUnavailable(appInstall))
        appManager.addDownload(appInstall)
        appManager.updateUnavailable(appInstall)
    }

    private suspend fun handleUpdateDownloadError(
        appInstall: AppInstall,
        message: String,
        e: Exception
    ) {
        Timber.e(e, "Updating download Urls failed for $message")
        appEventDispatcher.dispatch(
            AppEvent.UpdateEvent(
                ResultSupreme.WorkError(
                    ResultStatus.UNKNOWN,
                    appInstall
                )
            )
        )
    }

    private suspend fun updateFusedDownloadWithAppDownloadLink(appInstall: AppInstall) {
        applicationRepository.updateFusedDownloadWithDownloadingInfo(
            appInstall.source,
            appInstall
        )
    }
}
+31 −19
Original line number Diff line number Diff line
@@ -36,8 +36,9 @@ import foundation.e.apps.data.install.AppManagerWrapper
import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.install.notification.StorageNotificationManager
import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate
import foundation.e.apps.data.install.workmanager.AppInstallRequestFactory
import foundation.e.apps.data.install.workmanager.AppInstallProcessor
import foundation.e.apps.data.install.workmanager.AppInstallRequestFactory
import foundation.e.apps.data.install.workmanager.AppInstallStartCoordinator
import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner
import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler
import foundation.e.apps.data.install.workmanager.InstallWorkManager
@@ -118,6 +119,7 @@ class AppInstallProcessorTest {
    private lateinit var updatesNotificationSender: UpdatesNotificationSender
    private lateinit var networkStatusChecker: NetworkStatusChecker
    private lateinit var appInstallAgeLimitGate: AppInstallAgeLimitGate
    private lateinit var appInstallStartCoordinator: AppInstallStartCoordinator
    private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler
    private lateinit var appInstallWorkRunner: AppInstallWorkRunner
    private lateinit var appInstallRequestFactory: AppInstallRequestFactory
@@ -150,6 +152,19 @@ class AppInstallProcessorTest {
            appEventDispatcher,
            parentalControlAuthGateway
        )
        appInstallStartCoordinator = AppInstallStartCoordinator(
            context,
            fakeFusedManagerRepository,
            applicationRepository,
            sessionRepository,
            playStoreAuthStore,
            storageNotificationManager,
            appInstallAgeLimitGate,
            appEventDispatcher,
            storageSpaceChecker,
            networkStatusChecker,
            fakeFusedManager
        )
        appUpdateCompletionHandler = AppUpdateCompletionHandler(
            context,
            appInstallRepository,
@@ -168,16 +183,8 @@ class AppInstallProcessorTest {
        )

        appInstallProcessor = AppInstallProcessor(
            context,
            appInstallComponents,
            applicationRepository,
            sessionRepository,
            playStoreAuthStore,
            storageNotificationManager,
            appEventDispatcher,
            storageSpaceChecker,
            networkStatusChecker,
            appInstallAgeLimitGate,
            appInstallStartCoordinator,
            appInstallWorkRunner,
            appInstallRequestFactory
        )
@@ -653,23 +660,28 @@ class AppInstallProcessorTest {
            appEventDispatcher,
            parentalControlAuthGateway
        )
        val workRunner = AppInstallWorkRunner(
            appInstallRepository,
            appManagerWrapper,
            mockk(relaxed = true),
            appUpdateCompletionHandler
        )
        return AppInstallProcessor(
        val startCoordinator = AppInstallStartCoordinator(
            context,
            appInstallComponents,
            appManagerWrapper,
            applicationRepository,
            sessionRepository,
            playStoreAuthStore,
            storageNotificationManager,
            ageLimitGate,
            appEventDispatcher,
            storageSpaceChecker,
            networkStatusChecker,
            ageLimitGate,
            fakeFusedManager
        )
        val workRunner = AppInstallWorkRunner(
            appInstallRepository,
            appManagerWrapper,
            mockk(relaxed = true),
            appUpdateCompletionHandler
        )
        return AppInstallProcessor(
            appInstallComponents,
            startCoordinator,
            workRunner,
            appInstallRequestFactory
        )
+282 −0

File added.

Preview size limit exceeded, changes collapsed.