Loading app/src/main/java/foundation/e/apps/data/install/workmanager/InstallOrchestrator.kt +50 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() { Loading Loading @@ -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) } Loading @@ -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 Loading app/src/test/java/foundation/e/apps/install/workmanager/InstallOrchestratorTest.kt +67 −11 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading @@ -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 Loading @@ -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() Loading @@ -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) Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading Loading
app/src/main/java/foundation/e/apps/data/install/workmanager/InstallOrchestrator.kt +50 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() { Loading Loading @@ -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) } Loading @@ -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 Loading
app/src/test/java/foundation/e/apps/install/workmanager/InstallOrchestratorTest.kt +67 −11 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading @@ -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 Loading @@ -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() Loading @@ -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) Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading