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

Commit 59d9444d authored by Jonathan Klee's avatar Jonathan Klee
Browse files

fix: fix startup reconciliation of stalled downloads to avoid update deadlock

parent 4fb10348
Loading
Loading
Loading
Loading
Loading
+50 −1
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.await
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.data.DownloadManager
import foundation.e.apps.data.application.AppManager
import foundation.e.apps.data.di.qualifiers.IoCoroutineScope
import foundation.e.apps.data.installation.model.AppInstall
@@ -36,12 +37,14 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import android.app.DownloadManager as PlatformDownloadManager

class InstallOrchestrator @Inject constructor(
    @param:ApplicationContext val context: Context,
    @param:IoCoroutineScope private val scope: CoroutineScope,
    private val appManager: AppManager,
    private val appInstallRepository: AppInstallRepository
    private val appInstallRepository: AppInstallRepository,
    private val downloadManager: DownloadManager,
) {

    fun init() {
@@ -117,6 +120,8 @@ class InstallOrchestrator @Inject constructor(

                when {
                    hasActiveWork || hasActiveSession -> return@forEach
                    app.status == Status.DOWNLOADING && reconcileStaleDownload(app, workManager) ->
                        return@forEach
                    appManager.isAppInstalled(app) -> {
                        appManager.updateDownloadStatus(app, Status.INSTALLED)
                    }
@@ -137,6 +142,50 @@ class InstallOrchestrator @Inject constructor(
        }
    }

    private suspend fun reconcileStaleDownload(
        app: AppInstall,
        workManager: WorkManager
    ): Boolean {
        val downloadIds = app.downloadIdMap.keys
        val statuses = downloadIds.map { id -> downloadManager.isDownloadSuccessful(id).second }
        val hasRunningOrPending = statuses.any {
            it == PlatformDownloadManager.STATUS_PENDING ||
                it == PlatformDownloadManager.STATUS_RUNNING
        }
        val allSuccessful = statuses.isNotEmpty() &&
            statuses.all { it == PlatformDownloadManager.STATUS_SUCCESSFUL }

        return when {
            downloadIds.isEmpty() -> {
                markDownloadAsIssue(app, workManager, "no download ids")
                true
            }
            hasRunningOrPending -> false
            allSuccessful -> {
                app.status = Status.DOWNLOADED
                appManager.updateAppInstall(app)
                Timber.i("INSTALL: Marked ${app.name}/${app.packageName} as DOWNLOADED after reconciliation")
                true
            }
            else -> {
                markDownloadAsIssue(app, workManager, "downloads are paused/failed")
                true
            }
        }
    }

    private suspend fun markDownloadAsIssue(
        app: AppInstall,
        workManager: WorkManager,
        reason: String
    ) {
        workManager.cancelAllWorkByTag(app.id)
        Timber.i(
            "INSTALL: Marking stale download as issue for ${app.name}/${app.packageName} ($reason)"
        )
        appManager.reportInstallationIssue(app)
    }

    private fun hasActiveInstallSession(packageName: String): Boolean {
        return context.packageManager.packageInstaller.allSessions.any { session ->
            session.appPackageName == packageName && session.isActive
+67 −11
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import androidx.work.WorkManager
import androidx.work.testing.WorkManagerTestInitHelper
import androidx.test.core.app.ApplicationProvider
import com.google.common.util.concurrent.Futures
import foundation.e.apps.data.DownloadManager
import foundation.e.apps.data.application.AppManager
import foundation.e.apps.data.installation.model.AppInstall
import foundation.e.apps.data.installation.repository.AppInstallRepository
@@ -63,6 +64,7 @@ import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.util.concurrent.TimeUnit
import android.app.DownloadManager as PlatformDownloadManager

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@@ -75,12 +77,15 @@ class InstallOrchestratorTest {
    private val context: Context = ApplicationProvider.getApplicationContext()
    private val appInstallRepository = mock<AppInstallRepository>()
    private val appManager = mock<AppManager>()
    private val downloadManager = mock<DownloadManager>()

    private var isInstallWorkManagerMocked = false

    @Before
    fun setup() {
        WorkManagerTestInitHelper.initializeTestWorkManager(context)
        whenever(downloadManager.isDownloadSuccessful(any()))
            .thenReturn(Pair(false, PlatformDownloadManager.STATUS_RUNNING))
    }

    @After
@@ -98,7 +103,8 @@ class InstallOrchestratorTest {
        whenever(appInstallRepository.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow())
        whenever(appManager.isAppInstalled(app)).thenReturn(false)

        val installOrchestrator = InstallOrchestrator(context, this, appManager, appInstallRepository)
        val installOrchestrator =
            InstallOrchestrator(context, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()
@@ -107,6 +113,46 @@ class InstallOrchestratorTest {
        verifyMockito(appManager, never()).updateDownloadStatus(any(), any())
    }

    @Test
    fun init_marksStaleDownloadAsInstallationIssue_whenDownloadPaused() = runTest {
        val app = createAppInstall(status = Status.DOWNLOADING).apply {
            downloadIdMap = mutableMapOf(11L to false, 12L to false)
        }

        whenever(downloadManager.isDownloadSuccessful(any()))
            .thenReturn(Pair(false, PlatformDownloadManager.STATUS_PAUSED))
        whenever(appInstallRepository.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow())

        val installOrchestrator =
            InstallOrchestrator(context, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()

        verifyMockito(appManager).reportInstallationIssue(app)
        verifyMockito(appManager, never()).updateAppInstall(any())
    }

    @Test
    fun init_marksDownloadAsDownloaded_whenAllItemsSuccessful() = runTest {
        val app = createAppInstall(status = Status.DOWNLOADING).apply {
            downloadIdMap = mutableMapOf(21L to false, 22L to false)
        }

        whenever(downloadManager.isDownloadSuccessful(any()))
            .thenReturn(Pair(true, PlatformDownloadManager.STATUS_SUCCESSFUL))
        whenever(appInstallRepository.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow())

        val installOrchestrator =
            InstallOrchestrator(context, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()

        verifyMockito(appManager).updateAppInstall(app)
        verifyMockito(appManager, never()).reportInstallationIssue(any())
    }

    @Test
    fun init_updatesStatusToInstalledWhenAppAlreadyInstalled() = runTest {
        val app = createAppInstall(status = Status.DOWNLOADED)
@@ -114,7 +160,8 @@ class InstallOrchestratorTest {
        whenever(appInstallRepository.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow())
        whenever(appManager.isAppInstalled(app)).thenReturn(true)

        val installOrchestrator = InstallOrchestrator(context, this, appManager, appInstallRepository)
        val installOrchestrator =
            InstallOrchestrator(context, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()
@@ -139,7 +186,8 @@ class InstallOrchestratorTest {
        whenever(packageInstaller.allSessions).thenReturn(listOf(sessionInfo))
        whenever(appInstallRepository.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow())

        val installOrchestrator = InstallOrchestrator(wrappedContext, this, appManager, appInstallRepository)
        val installOrchestrator =
            InstallOrchestrator(wrappedContext, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()
@@ -155,7 +203,8 @@ class InstallOrchestratorTest {

        whenever(appInstallRepository.getDownloads()).thenReturn(flowOf(emptyList()), flowOf(listOf(awaiting)))

        val installOrchestrator = InstallOrchestrator(context, this, appManager, appInstallRepository)
        val installOrchestrator =
            InstallOrchestrator(context, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()
@@ -172,7 +221,8 @@ class InstallOrchestratorTest {
        whenever(appInstallRepository.getDownloads())
            .thenReturn(flowOf(emptyList()), flowOf(listOf(active, awaiting)))

        val installOrchestrator = InstallOrchestrator(context, this, appManager, appInstallRepository)
        val installOrchestrator =
            InstallOrchestrator(context, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()
@@ -187,7 +237,8 @@ class InstallOrchestratorTest {

        whenever(appInstallRepository.getDownloads()).thenReturn(flowOf(emptyList()), flowOf(listOf(awaiting)))

        val installOrchestrator = InstallOrchestrator(context, this, appManager, appInstallRepository)
        val installOrchestrator =
            InstallOrchestrator(context, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()
@@ -202,7 +253,8 @@ class InstallOrchestratorTest {

        whenever(appInstallRepository.getDownloads()).thenReturn(flowOf(emptyList()), flowOf(listOf(awaiting)))

        val installOrchestrator = InstallOrchestrator(context, this, appManager, appInstallRepository)
        val installOrchestrator =
            InstallOrchestrator(context, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()
@@ -221,7 +273,8 @@ class InstallOrchestratorTest {

        whenever(appInstallRepository.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow())

        val installOrchestrator = InstallOrchestrator(context, this, appManager, appInstallRepository)
        val installOrchestrator =
            InstallOrchestrator(context, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()
@@ -238,7 +291,8 @@ class InstallOrchestratorTest {

        whenever(appInstallRepository.getDownloads()).thenReturn(flowOf(emptyList()), flowOf(listOf(awaiting)))

        val installOrchestrator = InstallOrchestrator(context, this, appManager, appInstallRepository)
        val installOrchestrator =
            InstallOrchestrator(context, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()
@@ -255,7 +309,8 @@ class InstallOrchestratorTest {
            .thenThrow(RuntimeException("reconcile failed"))
            .thenReturn(flowOf(listOf(awaiting)))

        val installOrchestrator = InstallOrchestrator(context, this, appManager, appInstallRepository)
        val installOrchestrator =
            InstallOrchestrator(context, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()
@@ -267,7 +322,8 @@ class InstallOrchestratorTest {
    fun init_stopsAfterCancellationExceptionDuringReconciliation() = runTest {
        whenever(appInstallRepository.getDownloads()).thenThrow(CancellationException("cancel reconcile"))

        val installOrchestrator = InstallOrchestrator(context, this, appManager, appInstallRepository)
        val installOrchestrator =
            InstallOrchestrator(context, this, appManager, appInstallRepository, downloadManager)

        installOrchestrator.init()
        advanceUntilIdle()