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

Commit 18197ef2 authored by dev-12's avatar dev-12
Browse files

Fail update pipeline on enqueue/fetch errors and propagate install outcomes

parent 0e921f53
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -227,6 +227,7 @@ dependencies {

    // WorkManager
    implementation(libs.work.runtime.ktx)
    implementation libs.work.testing

    // Room
    ksp(libs.room.compiler)
+13 −8
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ package foundation.e.apps.data.gitlab
import android.content.Context
import android.os.Build
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.application.ApplicationDataManager
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.enums.Source
@@ -63,7 +64,7 @@ class SystemAppsUpdatesRepository @Inject constructor(
        return systemAppProjectList.map { it.packageName }
    }

    suspend fun fetchUpdatableSystemApps(forceRefresh: Boolean = false) {
    suspend fun fetchUpdatableSystemApps(forceRefresh: Boolean = false): ResultSupreme<Unit> {
        val result = handleNetworkResult {
            if (getUpdatableSystemApps().isNotEmpty() && !forceRefresh) {
                return@handleNetworkResult
@@ -71,18 +72,22 @@ class SystemAppsUpdatesRepository @Inject constructor(

            val endPoint = getUpdatableSystemAppEndPoint()
            val response = updatableSystemAppsApi.getUpdatableSystemApps(endPoint)
            check(response.isSuccessful) {
                "request failed! code : (${response.code()})"
            }
            val body = response.body()
            if (body.isNullOrEmpty()) {
                error("response is empty! despite response indicating success")
            }

            if (response.isSuccessful && !response.body().isNullOrEmpty()) {
            systemAppProjectList.clear()
                response.body()?.let { systemAppProjectList.addAll(it) }
            } else {
                Timber.e("Failed to fetch updatable apps: ${response.errorBody()?.string()}")
            }
            systemAppProjectList.addAll(body)
        }

        if (!result.isSuccess()) {
            Timber.e("Network error when fetching updatable apps - ${result.message}")
            Timber.e("error when fetching updatable apps - ${result.message}")
        }
        return result
    }

    private fun getUpdatableSystemAppEndPoint(): EndPoint {
+101 −102
Original line number Diff line number Diff line
@@ -5,13 +5,13 @@ import android.content.Context
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.annotation.VisibleForTesting
import androidx.hilt.work.HiltWorker
import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker
import androidx.work.WorkInfo.State
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.aurora.gplayapi.data.models.AuthData
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import foundation.e.apps.R
@@ -22,7 +22,6 @@ import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.User
import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository
import foundation.e.apps.data.login.repository.AuthenticatorRepository
import foundation.e.apps.data.login.state.LoginState
import foundation.e.apps.data.preference.AppLoungeDataStore
import foundation.e.apps.data.updates.UpdatesManagerRepository
import foundation.e.apps.install.workmanager.AppInstallProcessor
@@ -47,7 +46,9 @@ class UpdatesWorker @AssistedInject constructor(

    companion object {
        const val IS_AUTO_UPDATE = "IS_AUTO_UPDATE"
        private const val MAX_RETRY_COUNT = 10

        @VisibleForTesting
        const val MAX_RETRY_COUNT = 10
        private const val DELAY_FOR_RETRY = 3000L
    }

@@ -64,9 +65,18 @@ class UpdatesWorker @AssistedInject constructor(
                return Result.success()
            }

            refreshBlockedAppList()
            systemAppsUpdatesRepository.fetchUpdatableSystemApps(forceRefresh = true)
            checkForUpdates()
            if (isAutoUpdate) {
                check(blockedAppRepository.fetchUpdateOfAppWarningList()) {
                    "failed to update app blocklist"
                }
            }

            val systemAppsUpdateTask = systemAppsUpdatesRepository.fetchUpdatableSystemApps(forceRefresh = true)
            check(systemAppsUpdateTask.isSuccess()) { "failed to fetch system apps update!" }
            val enqueueInstall = checkForUpdates()
            check(enqueueInstall == ResultStatus.OK) {
                "failed to enqueue all item"
            }
            Result.success()
        } catch (e: Throwable) {
            Timber.e(e)
@@ -78,13 +88,8 @@ class UpdatesWorker @AssistedInject constructor(
        }
    }

    private suspend fun refreshBlockedAppList() {
        if (isAutoUpdate) {
            blockedAppRepository.fetchUpdateOfAppWarningList()
        }
    }

    private suspend fun checkManualUpdateRunning(): Boolean {
    @VisibleForTesting
    suspend fun checkManualUpdateRunning(): Boolean {
        val workInfos =
            withContext(Dispatchers.IO) {
                WorkManager.getInstance(context).getWorkInfosByTag(UpdatesWorkManager.USER_TAG)
@@ -105,110 +110,98 @@ class UpdatesWorker @AssistedInject constructor(
        return appLoungeDataStore.getUser()
    }

    private fun getLoginState(): LoginState {
        return appLoungeDataStore.getLoginState()
    private suspend fun checkForUpdates(): ResultStatus {
        loadSettings()
        val isConnectedToUnMeteredNetwork = isConnectedToUnMeteredNetwork(applicationContext)
        val (appsNeededToUpdate, resultStatus) = loadWithRetry(emptyList(), ::getAvailableUpdates)

        if (resultStatus != ResultStatus.OK) {
            return resultStatus
        }

    private suspend fun checkForUpdates() {
        Timber.i("Updates found: ${appsNeededToUpdate.size}; $resultStatus")
        if (isAutoUpdate && shouldShowNotification) {
            handleNotification(appsNeededToUpdate.size, isConnectedToUnMeteredNetwork)
        }

        val jobsEnqueued = triggerUpdateProcessOnSettings(isConnectedToUnMeteredNetwork, appsNeededToUpdate)
        return if (jobsEnqueued.all { it.second }) ResultStatus.OK else ResultStatus.UNKNOWN
    }

    @VisibleForTesting
    suspend fun triggerUpdateProcessOnSettings(
        isConnectedToUnmeteredNetwork: Boolean,
        appsNeededToUpdate: List<Application>
    ): List<Pair<Application, Boolean>> {
        fun failedAllResponse() = appsNeededToUpdate.map { app -> Pair(app, false) }
        val hasStoragePermission =
            applicationContext.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
                PackageManager.PERMISSION_GRANTED
        val canContinue = (!isAutoUpdate || automaticInstallEnabled) && hasStoragePermission
        if (!canContinue) return failedAllResponse()
        return if (onlyOnUnmeteredNetwork && isConnectedToUnmeteredNetwork) {
            startUpdateProcess(appsNeededToUpdate)
        } else if (!onlyOnUnmeteredNetwork) {
            startUpdateProcess(appsNeededToUpdate)
        } else {
            failedAllResponse()
        }
    }

    @VisibleForTesting
    suspend fun getAvailableUpdates(): Pair<List<Application>, ResultStatus> {
        loadSettings()
        val isConnectedToUnMeteredNetwork = isConnectedToUnMeteredNetwork(applicationContext)
        val appsNeededToUpdate = mutableListOf<Application>()
        val user = getUser()
        val loginState = getLoginState()
        val authData = appLoungeDataStore.getAuthData()
        val authData = authenticatorRepository.getValidatedAuthData().data
        val resultStatus: ResultStatus

        if (user in listOf(User.ANONYMOUS, User.GOOGLE)) {
        if (user in listOf(User.ANONYMOUS, User.GOOGLE) && authData != null) {
            /*
             * Signifies valid Google user and valid auth data to update
             * apps from Google Play store.
             * The user check will be more useful in No-Google mode.
             */
            val updateData = fetchUpdatesWithAuthRetry()
            appsNeededToUpdate.addAll(updateData.first)
            resultStatus = updateData.second
        } else if (loginState != LoginState.UNAVAILABLE) {
            val (apps, status) = updatesManagerRepository.getUpdates()
            appsNeededToUpdate.addAll(apps)
            resultStatus = status
        } else if (user == User.NO_GOOGLE) {
            /*
             * If authData is null, update apps from cleanapk only.
             */
            val updateData = updatesManagerRepository.getUpdatesOSS()
            appsNeededToUpdate.addAll(updateData.first)
            resultStatus = updateData.second
            val (apps, status) = updatesManagerRepository.getUpdatesOSS()
            appsNeededToUpdate.addAll(apps)
            resultStatus = status
        } else {
            /*
             * If login state is unavailable, don't do anything.
             * If user in UNAVAILABLE, don't do anything.
             */
            Timber.e("Update is aborted for unavailable user!")
            return
        }
        Timber.i("Updates found: ${appsNeededToUpdate.size}; $resultStatus")
        if (isAutoUpdate && shouldShowNotification) {
            handleNotification(appsNeededToUpdate.size, isConnectedToUnMeteredNetwork)
            resultStatus = ResultStatus.UNKNOWN
        }

        if (resultStatus != ResultStatus.OK) {
            manageRetry(resultStatus.toString())
        } else {
            /*
             * Show notification only if enabled.
             * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5376
             */
            retryCount = 0
            if (isAutoUpdate && shouldShowNotification) {
                handleNotification(appsNeededToUpdate.size, isConnectedToUnMeteredNetwork)
            }

            triggerUpdateProcessOnSettings(
                isConnectedToUnMeteredNetwork,
                appsNeededToUpdate,
                authData,
            )
        }
    }

    private suspend fun fetchUpdatesWithAuthRetry(): Pair<List<Application>, ResultStatus> {
        val updateData = updatesManagerRepository.getUpdates()
        if (updateData.second == ResultStatus.OK) {
            return updateData
        return Pair(appsNeededToUpdate, resultStatus)
    }

        val refreshedAuth = authenticatorRepository.getValidatedAuthData().data
        val shouldRetry = refreshedAuth != null

        return if (shouldRetry) updatesManagerRepository.getUpdates() else updateData
    }

    private suspend fun manageRetry(extraMessage: String) {
    @VisibleForTesting
    suspend fun <T> loadWithRetry(
        defaultValue: T,
        loadData: suspend () -> Pair<T, ResultStatus>
    ): Pair<T, ResultStatus> {
        while (retryCount <= MAX_RETRY_COUNT) {
            retryCount++
        if (retryCount == 1) {
            val (data, status) = loadData()
            if (status == ResultStatus.OK) {
                return Pair(data, status)
            }
            delay(DELAY_FOR_RETRY)
            EventBus.invokeEvent(AppEvent.UpdateEvent(ResultSupreme.WorkError(ResultStatus.RETRY)))
        }

        if (retryCount <= MAX_RETRY_COUNT) {
            delay(DELAY_FOR_RETRY)
            checkForUpdates()
        } else {
            val message = "Update is aborted after trying for $MAX_RETRY_COUNT times! message: $extraMessage"
        val message = "Update is aborted after trying for $MAX_RETRY_COUNT times!"
        Timber.e(message)
        EventBus.invokeEvent(AppEvent.UpdateEvent(ResultSupreme.WorkError(ResultStatus.UNKNOWN)))
        }
    }

    private suspend fun triggerUpdateProcessOnSettings(
        isConnectedToUnmeteredNetwork: Boolean,
        appsNeededToUpdate: List<Application>,
        authData: AuthData
    ) {
        val hasStoragePermission =
            applicationContext.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
                PackageManager.PERMISSION_GRANTED
        if ((!isAutoUpdate || automaticInstallEnabled) && hasStoragePermission) {
            if (onlyOnUnmeteredNetwork && isConnectedToUnmeteredNetwork) {
                startUpdateProcess(appsNeededToUpdate, authData)
            } else if (!onlyOnUnmeteredNetwork) {
                startUpdateProcess(appsNeededToUpdate, authData)
            }
        }
        return Pair(defaultValue, ResultStatus.UNKNOWN)
    }

    private fun handleNotification(
@@ -226,17 +219,22 @@ class UpdatesWorker @AssistedInject constructor(
        }
    }

    private suspend fun startUpdateProcess(
        appsNeededToUpdate: List<Application>,
        authData: AuthData
    ) {
        appsNeededToUpdate.forEach { fusedApp ->
            if (!fusedApp.isFree && authData.isAnonymous) {
                return@forEach
            }

            appInstallProcessor.initAppInstall(fusedApp, true)
        }
    // returns list of Pair(app, status(success|failed))
    @VisibleForTesting
    suspend fun startUpdateProcess(appsNeededToUpdate: List<Application>): List<Pair<Application, Boolean>> {
        val response = mutableListOf<Pair<Application, Boolean>>()
        val authData = authenticatorRepository.getValidatedAuthData()
        val isNotLoggedIntoPersonalAccount = !authData.isValidData() || authData.data?.isAnonymous == true
        for (fusedApp in appsNeededToUpdate) {
            val shouldSkip = (!fusedApp.isFree && isNotLoggedIntoPersonalAccount)
            if (shouldSkip.or(isStopped)) { // respect the stop signal as well
                response.add(Pair(fusedApp, false))
                continue
            }
            val status = appInstallProcessor.initAppInstall(fusedApp, true)
            response.add(Pair(fusedApp, status))
        }
        return response
    }

    private fun loadSettings() {
@@ -263,12 +261,13 @@ class UpdatesWorker @AssistedInject constructor(
        )
    }

    /*
    /**
     * Checks if the device is connected to a metered connection or not
     * @param context current Context
     * @return returns true if the connections is not metered, false otherwise
     */
    private fun isConnectedToUnMeteredNetwork(context: Context): Boolean {
    @VisibleForTesting
    fun isConnectedToUnMeteredNetwork(context: Context): Boolean {
        val connectivityManager =
            context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val capabilities =
+42 −29
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@
package foundation.e.apps.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
@@ -85,7 +86,7 @@ class AppInstallProcessor @Inject constructor(
    suspend fun initAppInstall(
        application: Application,
        isAnUpdate: Boolean = false
    ) {
    ): Boolean {
        val appInstall = AppInstall(
            application._id,
            application.source,
@@ -113,7 +114,7 @@ class AppInstallProcessor @Inject constructor(
            application.status == Status.UPDATABLE ||
            appInstallComponents.appManagerWrapper.isFusedDownloadInstalled(appInstall)

        enqueueFusedDownload(appInstall, isUpdate, application.isSystemApp)
        return enqueueFusedDownload(appInstall, isUpdate, application.isSystemApp)
    }

    /**
@@ -127,8 +128,8 @@ class AppInstallProcessor @Inject constructor(
        appInstall: AppInstall,
        isAnUpdate: Boolean = false,
        isSystemApp: Boolean = false
    ) {
        try {
    ): Boolean {
        return try {
            val user = appLoungeDataStore.getUser()
            if (!isSystemApp && (user == User.GOOGLE || user == User.ANONYMOUS)) {
                val authData = appLoungeDataStore.getAuthData()
@@ -137,20 +138,40 @@ class AppInstallProcessor @Inject constructor(
                }
            }

            if (appInstall.type != Type.PWA && !updateDownloadUrls(appInstall)) return
            if (!canEnqueue(appInstall)) return false

            val downloadAdded = appInstallComponents.appManagerWrapper.addDownload(appInstall)
            if (!downloadAdded) {
                Timber.i("Update adding ABORTED! status: $downloadAdded")
                return
            appInstallComponents.appManagerWrapper.updateAwaiting(appInstall)
            InstallWorkManager.enqueueWork(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
        }
    }

            if (!validateAgeLimit(appInstall)) return
    @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 (!validateAgeLimit(appInstall)) {
            return false
        }

        if (!context.isNetworkAvailable()) {
            appInstallComponents.appManagerWrapper.installationIssue(appInstall)
            EventBus.invokeEvent(AppEvent.NoInternetEvent(false))
                return
            return false
        }

        if (StorageComputer.spaceMissing(appInstall) > 0) {
@@ -158,18 +179,10 @@ class AppInstallProcessor @Inject constructor(
            storageNotificationManager.showNotEnoughSpaceNotification(appInstall)
            appInstallComponents.appManagerWrapper.installationIssue(appInstall)
            EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.not_enough_storage))
                return
            return false
        }

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

    private suspend fun validateAgeLimit(appInstall: AppInstall): Boolean {
@@ -270,7 +283,7 @@ class AppInstallProcessor @Inject constructor(
    suspend fun processInstall(
        fusedDownloadId: String,
        isItUpdateWork: Boolean,
        runInForeground: (suspend (String) -> Unit)? = null
        runInForeground: (suspend (String) -> Unit)
    ): Result<ResultStatus> {
        var appInstall: AppInstall? = null
        try {
@@ -306,7 +319,7 @@ class AppInstallProcessor @Inject constructor(
                    )
                }

                runInForeground?.invoke(it.name)
                runInForeground.invoke(it.name)

                startAppInstallationProcess(it)
            }
+2 −2
Original line number Diff line number Diff line
@@ -60,14 +60,14 @@ class InstallAppWorker @AssistedInject constructor(
    override suspend fun doWork(): Result {
        val fusedDownloadId = params.inputData.getString(INPUT_DATA_FUSED_DOWNLOAD) ?: ""
        val isPackageUpdate = params.inputData.getBoolean(IS_UPDATE_WORK, false)
        appInstallProcessor.processInstall(fusedDownloadId, isPackageUpdate) { title ->
        val response = appInstallProcessor.processInstall(fusedDownloadId, isPackageUpdate) { title ->
            setForeground(
                createForegroundInfo(
                    "${context.getString(R.string.installing)} $title"
                )
            )
        }
        return Result.success()
        return if (response.isSuccess) Result.success() else Result.failure()
    }

    private fun createForegroundInfo(progress: String): ForegroundInfo {
Loading