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

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

refactor(updates): tolerate partial enqueue failures

Continue manual update chains when only some apps fail to enqueue so remaining updates can proceed; report failure when an entire chunk cannot be scheduled.
parent 4ecb23cd
Loading
Loading
Loading
Loading
Loading
+12 −2
Original line number Diff line number Diff line
@@ -189,18 +189,28 @@ class UpdatesWorker @AssistedInject constructor(
            completeManualUpdateChain(chainId)
            ResultStatus.OK
        } else {
            triggerUpdateProcessOnSettings(
            val enqueueResults = triggerUpdateProcessOnSettings(
                isConnectedToUnmeteredNetwork = isConnectedToUnMeteredNetwork(applicationContext),
                appsNeededToUpdate = appsNeededToUpdate,
            )
            if (skippedManualUpdateBecausePeriodicWorkIsBlocking) {
                clearBlockedManualUpdateChain(chainId)
                ResultStatus.OK
            } else if (enqueueResults.isEmpty() || enqueueResults.none { it.second }) {
                EventBus.invokeEvent(AppEvent.UpdateEvent(ResultSupreme.WorkError(ResultStatus.UNKNOWN)))
                ResultStatus.UNKNOWN
            } else {
                advanceManualUpdateChain(chainId, appsNeededToUpdate.size)
                // Manual Update All is best-effort per app: failed enqueue attempts remain
                // visible in the updates UI for manual retry while the chain continues.
                if (enqueueResults.any { !it.second }) {
                    Timber.w("Some apps failed to enqueue during manual update chain")
                    EventBus.invokeEvent(AppEvent.UpdateEvent(ResultSupreme.WorkError(ResultStatus.UNKNOWN)))
                }
                advanceManualUpdateChain(chainId, appsNeededToUpdate.size)
                ResultStatus.OK
            }
        }
    }

    private suspend fun requireManualUpdateSnapshot(chainId: String): ManualUpdateChainSnapshot {
        return checkNotNull(manualUpdateChainStore.readSnapshot(chainId)) {
+134 −0
Original line number Diff line number Diff line
@@ -1057,6 +1057,140 @@ class UpdatesWorkerTest {
        assertThat(continuationWork.tags).contains(UpdatesWorkManager.TAG_WORK_USER_INITIATED_UPDATE)
    }

    @Test
    fun doWork_continuesManualChain_whenChunkPartiallyFailsToEnqueue() = runTest {
        val workerContext = ApplicationProvider.getApplicationContext<android.app.Application>()
        shadowOf(workerContext).grantPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
        WorkManagerTestInitHelper.initializeTestWorkManager(workerContext)
        WorkManager.getInstance(workerContext).cancelAllWork().result.get()

        val params = mock<WorkerParameters>()
        val inputData = Data.Builder()
            .putBoolean(UpdatesWorker.IS_AUTO_UPDATE, false)
            .putString(UpdatesWorkManager.INPUT_KEY_CHAIN_ID, MANUAL_CHAIN_ID)
            .build()
        whenever(params.inputData).thenReturn(inputData)

        val updatesManagerRepository = mock<UpdatesManagerRepository>()
        val sessionRepository = createDataStore()
        val playStoreAuthManager = mock<PlayStoreAuthManager>()
        val appInstallationFacade = mock<AppInstallationFacade>()
        val blockedAppRepository = mock<BlockedAppRepository>()
        val systemAppsUpdatesRepository = mock<SystemAppsUpdatesRepository>()
        val manualUpdateChainStore = createManualUpdateChainStore(workerContext)
        val requestedPackages = mutableListOf<String>()
        val snapshotApplications = createManualChainApplications(20)

        manualUpdateChainStore.writeSnapshot(
            buildManualUpdateChainSnapshot(
                chainId = MANUAL_CHAIN_ID,
                applications = snapshotApplications,
                createdAtMillis = 1234L,
            )
        )
        whenever(playStoreAuthManager.getValidatedAuthData()).thenReturn(ResultSupreme.Error("no auth"))
        whenever(systemAppsUpdatesRepository.fetchUpdatableSystemApps(true))
            .thenReturn(ResultSupreme.Success(Unit))
        whenever(appInstallationFacade.initAppInstall(any(), any())).thenAnswer { invocation ->
            val packageName = (invocation.arguments[0] as Application).package_name
            requestedPackages += packageName
            packageName != "foundation.e.apps.10"
        }

        val worker = createWorker(
            workerContext,
            params,
            updatesManagerRepository,
            sessionRepository,
            playStoreAuthManager,
            appInstallationFacade,
            blockedAppRepository,
            systemAppsUpdatesRepository,
            manualUpdateChainStore = manualUpdateChainStore,
            appPreferencesRepository = createAppPreferencesRepository(isOnlyUnmeteredNetworkEnabled = false),
        )

        val result = worker.doWork()

        assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.success())
        assertThat(requestedPackages).containsExactlyElementsIn(
            snapshotApplications.take(15).map { it.package_name }
        ).inOrder()
        assertThat(manualUpdateChainStore.readSnapshot(MANUAL_CHAIN_ID)?.cursor).isEqualTo(15)

        val scheduledWorkInfos = WorkManager.getInstance(workerContext)
            .getWorkInfosForUniqueWork("updates_work_user")
            .get()
        val continuationWork = scheduledWorkInfos.single { it.state == androidx.work.WorkInfo.State.ENQUEUED }
        assertThat(continuationWork.tags).contains(UpdatesWorkManager.TAG_WORK_USER_INITIATED_UPDATE)
    }

    @Test
    fun doWork_failsManualChain_whenWholeChunkFailsToEnqueue() = runTest {
        val workerContext = ApplicationProvider.getApplicationContext<android.app.Application>()
        shadowOf(workerContext).grantPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
        WorkManagerTestInitHelper.initializeTestWorkManager(workerContext)
        WorkManager.getInstance(workerContext).cancelAllWork().result.get()

        val params = mock<WorkerParameters>()
        val inputData = Data.Builder()
            .putBoolean(UpdatesWorker.IS_AUTO_UPDATE, false)
            .putString(UpdatesWorkManager.INPUT_KEY_CHAIN_ID, MANUAL_CHAIN_ID)
            .build()
        whenever(params.inputData).thenReturn(inputData)

        val updatesManagerRepository = mock<UpdatesManagerRepository>()
        val sessionRepository = createDataStore()
        val playStoreAuthManager = mock<PlayStoreAuthManager>()
        val appInstallationFacade = mock<AppInstallationFacade>()
        val blockedAppRepository = mock<BlockedAppRepository>()
        val systemAppsUpdatesRepository = mock<SystemAppsUpdatesRepository>()
        val manualUpdateChainStore = createManualUpdateChainStore(workerContext)
        val requestedPackages = mutableListOf<String>()
        val snapshotApplications = createManualChainApplications(20)

        manualUpdateChainStore.writeSnapshot(
            buildManualUpdateChainSnapshot(
                chainId = MANUAL_CHAIN_ID,
                applications = snapshotApplications,
                createdAtMillis = 1234L,
            )
        )
        whenever(playStoreAuthManager.getValidatedAuthData()).thenReturn(ResultSupreme.Error("no auth"))
        whenever(systemAppsUpdatesRepository.fetchUpdatableSystemApps(true))
            .thenReturn(ResultSupreme.Success(Unit))
        whenever(appInstallationFacade.initAppInstall(any(), any())).thenAnswer { invocation ->
            requestedPackages += (invocation.arguments[0] as Application).package_name
            false
        }

        val worker = createWorker(
            workerContext,
            params,
            updatesManagerRepository,
            sessionRepository,
            playStoreAuthManager,
            appInstallationFacade,
            blockedAppRepository,
            systemAppsUpdatesRepository,
            manualUpdateChainStore = manualUpdateChainStore,
            appPreferencesRepository = createAppPreferencesRepository(isOnlyUnmeteredNetworkEnabled = false),
        )

        val result = worker.doWork()

        assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.failure())
        assertThat(requestedPackages).containsExactlyElementsIn(
            snapshotApplications.take(15).map { it.package_name }
        ).inOrder()
        assertThat(manualUpdateChainStore.readSnapshot(MANUAL_CHAIN_ID)?.cursor).isEqualTo(0)

        val scheduledWorkInfos = WorkManager.getInstance(workerContext)
            .getWorkInfosForUniqueWork("updates_work_user")
            .get()
        assertThat(scheduledWorkInfos.filter { !it.state.isFinished }).isEmpty()
    }

    @Test
    fun doWork_processesFinalManualChainChunk_andClearsSnapshot() = runTest {
        val workerContext = ApplicationProvider.getApplicationContext<android.app.Application>()