From 029179cad7e8525e4144d6e9dde8147fc36f4c10 Mon Sep 17 00:00:00 2001 From: dev-12 Date: Tue, 17 Feb 2026 22:16:04 +0530 Subject: [PATCH 1/3] refactor: return job unique id for installation task --- .../install/workmanager/InstallWorkManager.kt | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/InstallWorkManager.kt b/app/src/main/java/foundation/e/apps/install/workmanager/InstallWorkManager.kt index 82db14f76..a4ebe0844 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/InstallWorkManager.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/InstallWorkManager.kt @@ -8,23 +8,26 @@ import androidx.work.WorkManager import foundation.e.apps.data.install.models.AppInstall import timber.log.Timber import java.lang.Exception +import java.util.UUID object InstallWorkManager { const val INSTALL_WORK_NAME = "APP_LOUNGE_INSTALL_APP" lateinit var context: Application - fun enqueueWork(appInstall: AppInstall, isUpdateWork: Boolean = false) { + fun enqueueWork(appInstall: AppInstall, isUpdateWork: Boolean = false): UUID { + val request = OneTimeWorkRequestBuilder().setInputData( + Data.Builder() + .putString(InstallAppWorker.INPUT_DATA_FUSED_DOWNLOAD, appInstall.id) + .putBoolean(InstallAppWorker.IS_UPDATE_WORK, isUpdateWork) + .build() + ).addTag(appInstall.id) + .build() WorkManager.getInstance(context).enqueueUniqueWork( INSTALL_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, - OneTimeWorkRequestBuilder().setInputData( - Data.Builder() - .putString(InstallAppWorker.INPUT_DATA_FUSED_DOWNLOAD, appInstall.id) - .putBoolean(InstallAppWorker.IS_UPDATE_WORK, isUpdateWork) - .build() - ).addTag(appInstall.id) - .build() + request ) + return request.id } fun cancelWork(tag: String) { -- GitLab From 99eb645c0791461cc06b2ccf7d09535aff87e0eb Mon Sep 17 00:00:00 2001 From: dev-12 Date: Tue, 17 Feb 2026 22:23:30 +0530 Subject: [PATCH 2/3] refactor: sequential start installation jobs now the update worker will start installation jobs sequentially instead of enqueuing every app at once, and will wait for job results before marking job as completed --- .../workmanager/AppInstallProcessor.kt | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt index 198a18c4d..9dba5a1b3 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt @@ -20,6 +20,8 @@ package foundation.e.apps.install.workmanager import android.content.Context import androidx.annotation.VisibleForTesting +import androidx.work.WorkInfo +import androidx.work.WorkManager import com.aurora.gplayapi.exceptions.InternalException import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R @@ -50,10 +52,16 @@ import foundation.e.apps.utils.getFormattedString import foundation.e.apps.utils.isNetworkAvailable import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.transformWhile +import kotlinx.coroutines.time.withTimeout import timber.log.Timber import java.text.NumberFormat +import java.time.Duration import java.util.Date +import java.util.UUID import javax.inject.Inject class AppInstallProcessor @Inject constructor( @@ -75,6 +83,7 @@ class AppInstallProcessor @Inject constructor( companion object { private const val TAG = "AppInstallProcessor" private const val DATE_FORMAT = "dd/MM/yyyy-HH:mm" + private val INSTALL_TASK_TIMEOUT = Duration.ofMinutes(30) } /** @@ -141,8 +150,8 @@ class AppInstallProcessor @Inject constructor( if (!canEnqueue(appInstall)) return false appInstallComponents.appManagerWrapper.updateAwaiting(appInstall) - InstallWorkManager.enqueueWork(appInstall, isAnUpdate) - true + val id = InstallWorkManager.enqueueWork(appInstall, isAnUpdate) + waitForTaskCompletionOrCancelOnTimeout(workId = id) } catch (e: Exception) { Timber.e( e, @@ -153,6 +162,25 @@ class AppInstallProcessor @Inject constructor( } } + private suspend fun waitForTaskCompletionOrCancelOnTimeout( + workManager: WorkManager = WorkManager.getInstance(context), + timeout: Duration = INSTALL_TASK_TIMEOUT, + workId: UUID + ): Boolean { + return try { + withTimeout(timeout) { + workManager.getWorkInfoByIdFlow(workId) + .filterNotNull() + .first { it.state.isFinished } + .state == WorkInfo.State.SUCCEEDED + } + } catch (e: TimeoutCancellationException) { + Timber.e(e, "install work timed-out") + workManager.cancelWorkById(workId) + false + } + } + @VisibleForTesting suspend fun canEnqueue(appInstall: AppInstall): Boolean { if (appInstall.type != Type.PWA && !updateDownloadUrls(appInstall)) { -- GitLab From 23cc15067f5ac235713e6c37ce4e1fd2afe1541e Mon Sep 17 00:00:00 2001 From: dev-12 Date: Wed, 18 Feb 2026 08:12:41 +0530 Subject: [PATCH 3/3] test: expend coverage for `waitForTaskCompletion` --- .../workmanager/AppInstallProcessor.kt | 3 +- .../AppInstallProcessorTest.kt | 69 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt index 9dba5a1b3..58974fc1d 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt @@ -162,7 +162,8 @@ class AppInstallProcessor @Inject constructor( } } - private suspend fun waitForTaskCompletionOrCancelOnTimeout( + @VisibleForTesting + suspend fun waitForTaskCompletionOrCancelOnTimeout( workManager: WorkManager = WorkManager.getInstance(context), timeout: Duration = INSTALL_TASK_TIMEOUT, workId: UUID diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt index 67544b5c9..7219c3341 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -23,6 +23,8 @@ import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.work.WorkInfo +import androidx.work.WorkManager import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fdroid.FDroidRepository @@ -42,10 +44,14 @@ import foundation.e.apps.utils.StorageComputer import foundation.e.apps.util.MainCoroutineRule import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject import io.mockk.unmockkObject import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -57,6 +63,9 @@ import org.mockito.Mockito import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import java.time.Duration +import java.util.UUID +import io.mockk.verify @OptIn(ExperimentalCoroutinesApi::class) class AppInstallProcessorTest { @@ -370,6 +379,66 @@ class AppInstallProcessorTest { } } + @Test + fun waitForTaskCompletionOrCancelOnTimeout_returnsTrueWhenWorkSucceeds() = runTest { + val workManager = mockk() + val workId = UUID.randomUUID() + val workInfo = mockk() + + every { workInfo.state } returns WorkInfo.State.SUCCEEDED + every { workManager.getWorkInfoByIdFlow(workId) } returns flowOf(workInfo) + + val result = appInstallProcessor.waitForTaskCompletionOrCancelOnTimeout( + workManager = workManager, + timeout = Duration.ofMillis(100), + workId = workId + ) + + assertTrue(result) + verify(exactly = 0) { workManager.cancelWorkById(workId) } + } + + @Test + fun waitForTaskCompletionOrCancelOnTimeout_returnsFalseWhenWorkFailed() = runTest { + val workManager = mockk() + val workId = UUID.randomUUID() + val workInfo = mockk() + + every { workInfo.state } returns WorkInfo.State.FAILED + every { workManager.getWorkInfoByIdFlow(workId) } returns flowOf(workInfo) + + val result = appInstallProcessor.waitForTaskCompletionOrCancelOnTimeout( + workManager = workManager, + timeout = Duration.ofMillis(100), + workId = workId + ) + + assertEquals(false, result) + verify(exactly = 0) { workManager.cancelWorkById(workId) } + } + + @Test + fun waitForTaskCompletionOrCancelOnTimeout_cancelsWorkOnTimeout() = runTest { + val workManager = mockk(relaxed = true) + val workId = UUID.randomUUID() + val workInfo = mockk() + + every { workInfo.state } returns WorkInfo.State.RUNNING + every { workManager.getWorkInfoByIdFlow(workId) } returns flow { + emit(workInfo) + awaitCancellation() + } + + val result = appInstallProcessor.waitForTaskCompletionOrCancelOnTimeout( + workManager = workManager, + timeout = Duration.ofMillis(1), + workId = workId + ) + + assertEquals(false, result) + verify { workManager.cancelWorkById(workId) } + } + private suspend fun runProcessInstall(appInstall: AppInstall): AppInstall? { appInstallProcessor.processInstall(appInstall.id, false) { // _ignored_ -- GitLab