From 36d7c9b67dfa1371827721a5b15700b7712796a6 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 12 Mar 2026 21:37:40 +0600 Subject: [PATCH 01/29] refactor: isolate AppInstallProcessor platform dependencies Created thin wrappers for global EventBus, storage, parental control auth checking, updates DAO, updates notifier, and network connectivity check. These wrappers replace direct static calls without changing existing behaviour and introduce mockable seams for tests in AppInstallProcessorTest. --- .../AppInstallProcessorBindingsModule.kt | 66 +++ .../workmanager/AppInstallProcessor.kt | 53 +-- .../install/wrapper/AppEventDispatcher.kt | 33 ++ .../install/wrapper/NetworkStatusChecker.kt | 36 ++ .../wrapper/ParentalControlAuthGateway.kt | 32 ++ .../install/wrapper/StorageSpaceChecker.kt | 33 ++ .../wrapper/UpdatesNotificationSender.kt | 36 ++ .../data/install/wrapper/UpdatesTracker.kt | 51 +++ .../AppInstallProcessorTest.kt | 386 ++++++++---------- 9 files changed, 482 insertions(+), 244 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/di/bindings/AppInstallProcessorBindingsModule.kt create mode 100644 app/src/main/java/foundation/e/apps/data/install/wrapper/AppEventDispatcher.kt create mode 100644 app/src/main/java/foundation/e/apps/data/install/wrapper/NetworkStatusChecker.kt create mode 100644 app/src/main/java/foundation/e/apps/data/install/wrapper/ParentalControlAuthGateway.kt create mode 100644 app/src/main/java/foundation/e/apps/data/install/wrapper/StorageSpaceChecker.kt create mode 100644 app/src/main/java/foundation/e/apps/data/install/wrapper/UpdatesNotificationSender.kt create mode 100644 app/src/main/java/foundation/e/apps/data/install/wrapper/UpdatesTracker.kt diff --git a/app/src/main/java/foundation/e/apps/data/di/bindings/AppInstallProcessorBindingsModule.kt b/app/src/main/java/foundation/e/apps/data/di/bindings/AppInstallProcessorBindingsModule.kt new file mode 100644 index 000000000..ed205c78b --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/di/bindings/AppInstallProcessorBindingsModule.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.di.bindings + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.install.wrapper.AppEventDispatcher +import foundation.e.apps.data.install.wrapper.DefaultAppEventDispatcher +import foundation.e.apps.data.install.wrapper.DeviceNetworkStatusChecker +import foundation.e.apps.data.install.wrapper.NetworkStatusChecker +import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway +import foundation.e.apps.data.install.wrapper.ParentalControlAuthGatewayImpl +import foundation.e.apps.data.install.wrapper.StorageSpaceChecker +import foundation.e.apps.data.install.wrapper.StorageSpaceCheckerImpl +import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender +import foundation.e.apps.data.install.wrapper.UpdatesNotificationSenderImpl +import foundation.e.apps.data.install.wrapper.UpdatesTracker +import foundation.e.apps.data.install.wrapper.UpdatesTrackerImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface AppInstallProcessorBindingsModule { + + @Binds + @Singleton + fun bindAppEventDispatcher(dispatcher: DefaultAppEventDispatcher): AppEventDispatcher + + @Binds + @Singleton + fun bindStorageSpaceChecker(checker: StorageSpaceCheckerImpl): StorageSpaceChecker + + @Binds + @Singleton + fun bindParentalControlAuthGateway(gateway: ParentalControlAuthGatewayImpl): ParentalControlAuthGateway + + @Binds + @Singleton + fun bindUpdatesTracker(tracker: UpdatesTrackerImpl): UpdatesTracker + + @Binds + @Singleton + fun bindUpdatesNotificationSender(sender: UpdatesNotificationSenderImpl): UpdatesNotificationSender + + @Binds + @Singleton + fun bindNetworkStatusChecker(checker: DeviceNetworkStatusChecker): NetworkStatusChecker +} diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt index e2d33d90c..e65837d73 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt @@ -25,24 +25,24 @@ import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.application.UpdatesDao import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Type import foundation.e.apps.data.event.AppEvent -import foundation.e.apps.data.event.EventBus import foundation.e.apps.data.install.AppInstallComponents import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.download.DownloadManagerUtils import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager -import foundation.e.apps.data.install.updates.UpdatesNotifier +import foundation.e.apps.data.install.wrapper.AppEventDispatcher +import foundation.e.apps.data.install.wrapper.NetworkStatusChecker +import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway +import foundation.e.apps.data.install.wrapper.StorageSpaceChecker +import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender +import foundation.e.apps.data.install.wrapper.UpdatesTracker import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.PlayStoreAuthStore -import foundation.e.apps.data.system.ParentalControlAuthenticator -import foundation.e.apps.data.system.StorageComputer -import foundation.e.apps.data.system.isNetworkAvailable import foundation.e.apps.data.utils.getFormattedString import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.domain.model.ContentRatingValidity @@ -66,6 +66,12 @@ class AppInstallProcessor @Inject constructor( private val sessionRepository: SessionRepository, private val playStoreAuthStore: PlayStoreAuthStore, private val storageNotificationManager: StorageNotificationManager, + private val appEventDispatcher: AppEventDispatcher, + private val storageSpaceChecker: StorageSpaceChecker, + private val parentalControlAuthGateway: ParentalControlAuthGateway, + private val updatesTracker: UpdatesTracker, + private val updatesNotificationSender: UpdatesNotificationSender, + private val networkStatusChecker: NetworkStatusChecker, ) { @Inject lateinit var downloadManager: DownloadManagerUtils @@ -139,7 +145,9 @@ class AppInstallProcessor @Inject constructor( if (!isSystemApp && (user == User.GOOGLE || user == User.ANONYMOUS)) { val authData = playStoreAuthStore.awaitAuthData() if (!appInstall.isFree && authData?.isAnonymous == true) { - EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message)) + appEventDispatcher.dispatch( + AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message) + ) } } @@ -175,17 +183,17 @@ class AppInstallProcessor @Inject constructor( return false } - if (!context.isNetworkAvailable()) { + if (!networkStatusChecker.isNetworkAvailable()) { appInstallComponents.appManagerWrapper.installationIssue(appInstall) - EventBus.invokeEvent(AppEvent.NoInternetEvent(false)) + appEventDispatcher.dispatch(AppEvent.NoInternetEvent(false)) return false } - if (StorageComputer.spaceMissing(appInstall) > 0) { + if (storageSpaceChecker.spaceMissing(appInstall) > 0) { Timber.d("Storage is not available for: ${appInstall.name} size: ${appInstall.appSize}") storageNotificationManager.showNotEnoughSpaceNotification(appInstall) appInstallComponents.appManagerWrapper.installationIssue(appInstall) - EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.not_enough_storage)) + appEventDispatcher.dispatch(AppEvent.ErrorMessageEvent(R.string.not_enough_storage)) return false } @@ -200,13 +208,13 @@ class AppInstallProcessor @Inject constructor( if (ageLimitValidationResult.isSuccess()) { awaitInvokeAgeLimitEvent(appInstall.name) if (ageLimitValidationResult.data?.requestPin == true) { - val isAuthenticated = ParentalControlAuthenticator.awaitAuthentication() + val isAuthenticated = parentalControlAuthGateway.awaitAuthentication() if (isAuthenticated) { ageLimitValidationResult.setData(ContentRatingValidity(true)) } } } else { - EventBus.invokeEvent(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc)) + appEventDispatcher.dispatch(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc)) } var ageIsValid = true @@ -219,7 +227,7 @@ class AppInstallProcessor @Inject constructor( suspend fun awaitInvokeAgeLimitEvent(type: String) { val deferred = CompletableDeferred() - EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent(type, deferred)) + appEventDispatcher.dispatch(AppEvent.AgeLimitRestrictionEvent(type, deferred)) deferred.await() // await closing dialog box } @@ -233,7 +241,7 @@ class AppInstallProcessor @Inject constructor( return false } appInstallComponents.appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) - EventBus.invokeEvent(AppEvent.AppPurchaseEvent(appInstall)) + appEventDispatcher.dispatch(AppEvent.AppPurchaseEvent(appInstall)) return false } catch (e: GplayHttpRequestException) { handleUpdateDownloadError( @@ -256,7 +264,7 @@ class AppInstallProcessor @Inject constructor( } private suspend fun handleAppRestricted(appInstall: AppInstall) { - EventBus.invokeEvent(AppEvent.AppRestrictedOrUnavailable(appInstall)) + appEventDispatcher.dispatch(AppEvent.AppRestrictedOrUnavailable(appInstall)) appManager.addDownload(appInstall) appManager.updateUnavailable(appInstall) } @@ -267,7 +275,7 @@ class AppInstallProcessor @Inject constructor( e: Exception ) { Timber.e(e, "Updating download Urls failed for $message") - EventBus.invokeEvent( + appEventDispatcher.dispatch( AppEvent.UpdateEvent( ResultSupreme.WorkError( ResultStatus.UNKNOWN, @@ -367,12 +375,12 @@ class AppInstallProcessor @Inject constructor( appInstallComponents.appManagerWrapper.getFusedDownloadPackageStatus(appInstall) if (packageStatus == Status.INSTALLED) { - UpdatesDao.addSuccessfullyUpdatedApp(it) + updatesTracker.addSuccessfullyUpdatedApp(it) } if (isUpdateCompleted()) { // show notification for ended update showNotificationOnUpdateEnded() - UpdatesDao.clearSuccessfullyUpdatedApps() + updatesTracker.clearSuccessfullyUpdatedApps() } } } @@ -387,18 +395,17 @@ class AppInstallProcessor @Inject constructor( ).contains(it.status) } - return UpdatesDao.successfulUpdatedApps.isNotEmpty() && downloadListWithoutAnyIssue.isEmpty() + return updatesTracker.hasSuccessfulUpdatedApps() && downloadListWithoutAnyIssue.isEmpty() } private suspend fun showNotificationOnUpdateEnded() { val locale = playStoreAuthStore.awaitAuthData()?.locale ?: java.util.Locale.getDefault() val date = Date().getFormattedString(DATE_FORMAT, locale) val numberOfUpdatedApps = - NumberFormat.getNumberInstance(locale).format(UpdatesDao.successfulUpdatedApps.size) + NumberFormat.getNumberInstance(locale).format(updatesTracker.successfulUpdatedAppsCount()) .toString() - UpdatesNotifier.showNotification( - context, + updatesNotificationSender.showNotification( context.getString(R.string.update), context.getString( R.string.message_last_update_triggered, diff --git a/app/src/main/java/foundation/e/apps/data/install/wrapper/AppEventDispatcher.kt b/app/src/main/java/foundation/e/apps/data/install/wrapper/AppEventDispatcher.kt new file mode 100644 index 000000000..e9c44ea58 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/wrapper/AppEventDispatcher.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.wrapper + +import foundation.e.apps.data.event.AppEvent +import foundation.e.apps.data.event.EventBus +import javax.inject.Inject + +interface AppEventDispatcher { + suspend fun dispatch(event: AppEvent) +} + +class DefaultAppEventDispatcher @Inject constructor() : AppEventDispatcher { + override suspend fun dispatch(event: AppEvent) { + EventBus.invokeEvent(event) + } +} diff --git a/app/src/main/java/foundation/e/apps/data/install/wrapper/NetworkStatusChecker.kt b/app/src/main/java/foundation/e/apps/data/install/wrapper/NetworkStatusChecker.kt new file mode 100644 index 000000000..612ed2b0c --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/wrapper/NetworkStatusChecker.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.wrapper + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.data.system.isNetworkAvailable +import javax.inject.Inject + +interface NetworkStatusChecker { + fun isNetworkAvailable(): Boolean +} + +class DeviceNetworkStatusChecker @Inject constructor( + @ApplicationContext private val context: Context +) : NetworkStatusChecker { + override fun isNetworkAvailable(): Boolean { + return context.isNetworkAvailable() + } +} diff --git a/app/src/main/java/foundation/e/apps/data/install/wrapper/ParentalControlAuthGateway.kt b/app/src/main/java/foundation/e/apps/data/install/wrapper/ParentalControlAuthGateway.kt new file mode 100644 index 000000000..36829868b --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/wrapper/ParentalControlAuthGateway.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.wrapper + +import foundation.e.apps.data.system.ParentalControlAuthenticator +import javax.inject.Inject + +interface ParentalControlAuthGateway { + suspend fun awaitAuthentication(): Boolean +} + +class ParentalControlAuthGatewayImpl @Inject constructor() : ParentalControlAuthGateway { + override suspend fun awaitAuthentication(): Boolean { + return ParentalControlAuthenticator.awaitAuthentication() + } +} diff --git a/app/src/main/java/foundation/e/apps/data/install/wrapper/StorageSpaceChecker.kt b/app/src/main/java/foundation/e/apps/data/install/wrapper/StorageSpaceChecker.kt new file mode 100644 index 000000000..e4b330af9 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/wrapper/StorageSpaceChecker.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.wrapper + +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.system.StorageComputer +import javax.inject.Inject + +interface StorageSpaceChecker { + fun spaceMissing(appInstall: AppInstall): Long +} + +class StorageSpaceCheckerImpl @Inject constructor() : StorageSpaceChecker { + override fun spaceMissing(appInstall: AppInstall): Long { + return StorageComputer.spaceMissing(appInstall) + } +} diff --git a/app/src/main/java/foundation/e/apps/data/install/wrapper/UpdatesNotificationSender.kt b/app/src/main/java/foundation/e/apps/data/install/wrapper/UpdatesNotificationSender.kt new file mode 100644 index 000000000..97a226c93 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/wrapper/UpdatesNotificationSender.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.wrapper + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.data.install.updates.UpdatesNotifier +import javax.inject.Inject + +interface UpdatesNotificationSender { + fun showNotification(title: String, message: String) +} + +class UpdatesNotificationSenderImpl @Inject constructor( + @ApplicationContext private val context: Context +) : UpdatesNotificationSender { + override fun showNotification(title: String, message: String) { + UpdatesNotifier.showNotification(context, title, message) + } +} diff --git a/app/src/main/java/foundation/e/apps/data/install/wrapper/UpdatesTracker.kt b/app/src/main/java/foundation/e/apps/data/install/wrapper/UpdatesTracker.kt new file mode 100644 index 000000000..26bf2e1ba --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/wrapper/UpdatesTracker.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.wrapper + +import foundation.e.apps.data.application.UpdatesDao +import foundation.e.apps.data.install.models.AppInstall +import javax.inject.Inject + +interface UpdatesTracker { + fun addSuccessfullyUpdatedApp(appInstall: AppInstall) + + fun clearSuccessfullyUpdatedApps() + + fun hasSuccessfulUpdatedApps(): Boolean + + fun successfulUpdatedAppsCount(): Int +} + +class UpdatesTrackerImpl @Inject constructor() : UpdatesTracker { + override fun addSuccessfullyUpdatedApp(appInstall: AppInstall) { + UpdatesDao.addSuccessfullyUpdatedApp(appInstall) + } + + override fun clearSuccessfullyUpdatedApps() { + UpdatesDao.clearSuccessfullyUpdatedApps() + } + + override fun hasSuccessfulUpdatedApps(): Boolean { + return UpdatesDao.successfulUpdatedApps.isNotEmpty() + } + + override fun successfulUpdatedAppsCount(): Int { + return UpdatesDao.successfulUpdatedApps.size + } +} 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 84e01ea3c..295d99f92 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -19,9 +19,6 @@ package foundation.e.apps.installProcessor import android.content.Context -import android.net.ConnectivityManager -import android.net.Network -import android.net.NetworkCapabilities import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.work.Operation import com.google.common.util.concurrent.Futures @@ -40,9 +37,14 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager import foundation.e.apps.data.install.workmanager.AppInstallProcessor -import foundation.e.apps.data.preference.PlayStoreAuthStore import foundation.e.apps.data.install.workmanager.InstallWorkManager -import foundation.e.apps.data.system.StorageComputer +import foundation.e.apps.data.install.wrapper.AppEventDispatcher +import foundation.e.apps.data.install.wrapper.NetworkStatusChecker +import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway +import foundation.e.apps.data.install.wrapper.StorageSpaceChecker +import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender +import foundation.e.apps.data.install.wrapper.UpdatesTracker +import foundation.e.apps.data.preference.PlayStoreAuthStore import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.domain.model.ContentRatingValidity import foundation.e.apps.domain.preferences.SessionRepository @@ -110,11 +112,24 @@ class AppInstallProcessorTest { @Mock private lateinit var storageNotificationManager: StorageNotificationManager + private lateinit var appEventDispatcher: AppEventDispatcher + private lateinit var storageSpaceChecker: StorageSpaceChecker + private lateinit var parentalControlAuthGateway: ParentalControlAuthGateway + private lateinit var updatesTracker: UpdatesTracker + private lateinit var updatesNotificationSender: UpdatesNotificationSender + private lateinit var networkStatusChecker: NetworkStatusChecker + private var isInstallWorkManagerMocked = false @Before fun setup() { MockitoAnnotations.openMocks(this) + appEventDispatcher = mockk(relaxed = true) + storageSpaceChecker = mockk(relaxed = true) + parentalControlAuthGateway = mockk(relaxed = true) + updatesTracker = mockk(relaxed = true) + updatesNotificationSender = mockk(relaxed = true) + networkStatusChecker = mockk(relaxed = true) fakeFusedDownloadDAO = FakeAppInstallDAO() appInstallRepository = AppInstallRepository(fakeFusedDownloadDAO) fakeFusedManagerRepository = @@ -129,7 +144,13 @@ class AppInstallProcessorTest { validateAppAgeRatingUseCase, sessionRepository, playStoreAuthStore, - storageNotificationManager + storageNotificationManager, + appEventDispatcher, + storageSpaceChecker, + parentalControlAuthGateway, + updatesTracker, + updatesNotificationSender, + networkStatusChecker ) } @@ -256,21 +277,16 @@ class AppInstallProcessorTest { val appManagerWrapper = mockk(relaxed = true) val processor = createProcessorForCanEnqueue(appManagerWrapper) - mockkObject(StorageComputer) - try { - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - everyNetworkAvailable() - every { StorageComputer.spaceMissing(appInstall) } returns 0 + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0 - val result = processor.canEnqueue(appInstall) + val result = processor.canEnqueue(appInstall) - assertTrue(result) - coVerify { appManagerWrapper.addDownload(appInstall) } - } finally { - unmockkObject(StorageComputer) - } + assertTrue(result) + coVerify { appManagerWrapper.addDownload(appInstall) } } @Test @@ -286,21 +302,16 @@ class AppInstallProcessorTest { val appManagerWrapper = mockk(relaxed = true) val processor = createProcessorForCanEnqueue(appManagerWrapper) - mockkObject(StorageComputer) - try { - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - everyNetworkUnavailable() - every { StorageComputer.spaceMissing(appInstall) } returns 0 + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + every { networkStatusChecker.isNetworkAvailable() } returns false + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0 - val result = processor.canEnqueue(appInstall) + val result = processor.canEnqueue(appInstall) - assertEquals(false, result) - coVerify { appManagerWrapper.installationIssue(appInstall) } - } finally { - unmockkObject(StorageComputer) - } + assertEquals(false, result) + coVerify { appManagerWrapper.installationIssue(appInstall) } } @Test @@ -316,22 +327,17 @@ class AppInstallProcessorTest { val appManagerWrapper = mockk(relaxed = true) val processor = createProcessorForCanEnqueue(appManagerWrapper) - mockkObject(StorageComputer) - try { - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - everyNetworkAvailable() - every { StorageComputer.spaceMissing(appInstall) } returns 100L - - val result = processor.canEnqueue(appInstall) - - assertEquals(false, result) - Mockito.verify(storageNotificationManager).showNotEnoughSpaceNotification(appInstall) - coVerify { appManagerWrapper.installationIssue(appInstall) } - } finally { - unmockkObject(StorageComputer) - } + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 100L + + val result = processor.canEnqueue(appInstall) + + assertEquals(false, result) + Mockito.verify(storageNotificationManager).showNotEnoughSpaceNotification(appInstall) + coVerify { appManagerWrapper.installationIssue(appInstall) } } @Test @@ -347,21 +353,16 @@ class AppInstallProcessorTest { val appManagerWrapper = mockk(relaxed = true) val processor = createProcessorForCanEnqueue(appManagerWrapper) - mockkObject(StorageComputer) - try { - coEvery { appManagerWrapper.addDownload(appInstall) } returns false - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - everyNetworkAvailable() - every { StorageComputer.spaceMissing(appInstall) } returns 0L + coEvery { appManagerWrapper.addDownload(appInstall) } returns false + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - val result = processor.canEnqueue(appInstall) + val result = processor.canEnqueue(appInstall) - assertEquals(false, result) - coVerify(exactly = 0) { appManagerWrapper.installationIssue(appInstall) } - } finally { - unmockkObject(StorageComputer) - } + assertEquals(false, result) + coVerify(exactly = 0) { appManagerWrapper.installationIssue(appInstall) } } @Test @@ -377,21 +378,16 @@ class AppInstallProcessorTest { val appManagerWrapper = mockk(relaxed = true) val processor = createProcessorForCanEnqueue(appManagerWrapper) - mockkObject(StorageComputer) - try { - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.UNKNOWN, ContentRatingValidity(false))) - everyNetworkAvailable() - every { StorageComputer.spaceMissing(appInstall) } returns 0L + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.UNKNOWN, ContentRatingValidity(false))) + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - val result = processor.canEnqueue(appInstall) + val result = processor.canEnqueue(appInstall) - assertEquals(false, result) - coVerify { appManagerWrapper.cancelDownload(appInstall) } - } finally { - unmockkObject(StorageComputer) - } + assertEquals(false, result) + coVerify { appManagerWrapper.cancelDownload(appInstall) } } @Test @@ -401,22 +397,17 @@ class AppInstallProcessorTest { val processor = createProcessorForCanEnqueue(appManagerWrapper) mockInstallWorkManagerSuccess() - mockkObject(StorageComputer) - try { - everyNetworkAvailable() - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { StorageComputer.spaceMissing(appInstall) } returns 0L - - val result = processor.enqueueFusedDownload(appInstall, isAnUpdate = true, isSystemApp = true) - - assertTrue(result) - coVerify { appManagerWrapper.updateAwaiting(appInstall) } - verify(exactly = 1) { InstallWorkManager.enqueueWork(context, appInstall, true) } - } finally { - unmockkObject(StorageComputer) - } + every { networkStatusChecker.isNetworkAvailable() } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = processor.enqueueFusedDownload(appInstall, isAnUpdate = true, isSystemApp = true) + + assertTrue(result) + coVerify { appManagerWrapper.updateAwaiting(appInstall) } + verify(exactly = 1) { InstallWorkManager.enqueueWork(context, appInstall, true) } } @Test @@ -426,22 +417,17 @@ class AppInstallProcessorTest { val processor = createProcessorForCanEnqueue(appManagerWrapper) mockInstallWorkManagerFailure() - mockkObject(StorageComputer) - try { - everyNetworkAvailable() - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { StorageComputer.spaceMissing(appInstall) } returns 0L - - val result = processor.enqueueFusedDownload(appInstall, isAnUpdate = true, isSystemApp = true) - - assertEquals(false, result) - coVerify { appManagerWrapper.updateAwaiting(appInstall) } - coVerify { appManagerWrapper.installationIssue(appInstall) } - } finally { - unmockkObject(StorageComputer) - } + every { networkStatusChecker.isNetworkAvailable() } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = processor.enqueueFusedDownload(appInstall, isAnUpdate = true, isSystemApp = true) + + assertEquals(false, result) + coVerify { appManagerWrapper.updateAwaiting(appInstall) } + coVerify { appManagerWrapper.installationIssue(appInstall) } } @Test @@ -451,22 +437,17 @@ class AppInstallProcessorTest { val processor = createProcessorForCanEnqueue(appManagerWrapper) mockInstallWorkManagerSuccess() - mockkObject(StorageComputer) - try { - everyNetworkAvailable() - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { StorageComputer.spaceMissing(appInstall) } returns 0L - - val result = processor.enqueueFusedDownload(appInstall, isAnUpdate = false, isSystemApp = true) - - assertTrue(result) - coVerify { appManagerWrapper.updateAwaiting(appInstall) } - verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any(), any()) } - } finally { - unmockkObject(StorageComputer) - } + every { networkStatusChecker.isNetworkAvailable() } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = processor.enqueueFusedDownload(appInstall, isAnUpdate = false, isSystemApp = true) + + assertTrue(result) + coVerify { appManagerWrapper.updateAwaiting(appInstall) } + verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any(), any()) } } @Test @@ -476,22 +457,17 @@ class AppInstallProcessorTest { val processor = createProcessorForCanEnqueue(appManagerWrapper) mockInstallWorkManagerFailure() - mockkObject(StorageComputer) - try { - everyNetworkAvailable() - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { StorageComputer.spaceMissing(appInstall) } returns 0L - - val result = processor.enqueueFusedDownload(appInstall, isAnUpdate = false, isSystemApp = true) - - assertTrue(result) - verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any(), any()) } - coVerify(exactly = 0) { appManagerWrapper.installationIssue(appInstall) } - } finally { - unmockkObject(StorageComputer) - } + every { networkStatusChecker.isNetworkAvailable() } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = processor.enqueueFusedDownload(appInstall, isAnUpdate = false, isSystemApp = true) + + assertTrue(result) + verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any(), any()) } + coVerify(exactly = 0) { appManagerWrapper.installationIssue(appInstall) } } @Test @@ -502,22 +478,17 @@ class AppInstallProcessorTest { val processor = createProcessorForCanEnqueue(appManagerWrapper) mockInstallWorkManagerSuccess() - mockkObject(StorageComputer) - try { - everyNetworkAvailable() - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns false - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { StorageComputer.spaceMissing(appInstall) } returns 0L - - val result = processor.initAppInstall(application, isAnUpdate = true) - - assertTrue(result) - verify(exactly = 1) { InstallWorkManager.enqueueWork(context, appInstall, true) } - } finally { - unmockkObject(StorageComputer) - } + every { networkStatusChecker.isNetworkAvailable() } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + every { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns false + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = processor.initAppInstall(application, isAnUpdate = true) + + assertTrue(result) + verify(exactly = 1) { InstallWorkManager.enqueueWork(context, appInstall, true) } } @Test @@ -528,22 +499,17 @@ class AppInstallProcessorTest { val processor = createProcessorForCanEnqueue(appManagerWrapper) mockInstallWorkManagerSuccess() - mockkObject(StorageComputer) - try { - everyNetworkAvailable() - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns false - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { StorageComputer.spaceMissing(appInstall) } returns 0L - - val result = processor.initAppInstall(application, isAnUpdate = false) - - assertTrue(result) - verify(exactly = 1) { InstallWorkManager.enqueueWork(context, appInstall, true) } - } finally { - unmockkObject(StorageComputer) - } + every { networkStatusChecker.isNetworkAvailable() } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + every { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns false + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = processor.initAppInstall(application, isAnUpdate = false) + + assertTrue(result) + verify(exactly = 1) { InstallWorkManager.enqueueWork(context, appInstall, true) } } @Test @@ -554,22 +520,17 @@ class AppInstallProcessorTest { val processor = createProcessorForCanEnqueue(appManagerWrapper) mockInstallWorkManagerSuccess() - mockkObject(StorageComputer) - try { - everyNetworkAvailable() - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns true - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { StorageComputer.spaceMissing(appInstall) } returns 0L - - val result = processor.initAppInstall(application, isAnUpdate = false) - - assertTrue(result) - verify(exactly = 1) { InstallWorkManager.enqueueWork(context, appInstall, true) } - } finally { - unmockkObject(StorageComputer) - } + every { networkStatusChecker.isNetworkAvailable() } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + every { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns true + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = processor.initAppInstall(application, isAnUpdate = false) + + assertTrue(result) + verify(exactly = 1) { InstallWorkManager.enqueueWork(context, appInstall, true) } } @Test @@ -580,22 +541,17 @@ class AppInstallProcessorTest { val processor = createProcessorForCanEnqueue(appManagerWrapper) mockInstallWorkManagerFailure() - mockkObject(StorageComputer) - try { - everyNetworkAvailable() - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns false - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { StorageComputer.spaceMissing(appInstall) } returns 0L - - val result = processor.initAppInstall(application, isAnUpdate = false) - - assertTrue(result) - verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any(), any()) } - } finally { - unmockkObject(StorageComputer) - } + every { networkStatusChecker.isNetworkAvailable() } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + every { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns false + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = processor.initAppInstall(application, isAnUpdate = false) + + assertTrue(result) + verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any(), any()) } } private suspend fun runProcessInstall(appInstall: AppInstall): AppInstall? { @@ -627,7 +583,13 @@ class AppInstallProcessorTest { validateAppAgeRatingUseCase, sessionRepository, playStoreAuthStore, - storageNotificationManager + storageNotificationManager, + appEventDispatcher, + storageSpaceChecker, + parentalControlAuthGateway, + updatesTracker, + updatesNotificationSender, + networkStatusChecker ) } @@ -695,22 +657,4 @@ class AppInstallProcessorTest { return operation } - private fun everyNetworkAvailable() { - val connectivityManager = mock() - val network = mock() - val networkCapabilities = mock() - whenever(context.getSystemService(ConnectivityManager::class.java)).thenReturn(connectivityManager) - whenever(connectivityManager.activeNetwork).thenReturn(network) - whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(networkCapabilities) - whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(true) - whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)).thenReturn(true) - } - - private fun everyNetworkUnavailable() { - val connectivityManager = mock() - val network = mock() - whenever(context.getSystemService(ConnectivityManager::class.java)).thenReturn(connectivityManager) - whenever(connectivityManager.activeNetwork).thenReturn(network) - whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(null) - } } -- GitLab From 0b457fd9bbcdc902c543877ca109af8e12026303 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 12 Mar 2026 21:48:33 +0600 Subject: [PATCH 02/29] test: capture AppInstallProcessor baseline tests for existing behaviour Lock down the current enqueue, worker, and update-completion quirks before extraction so later class splits can be checked against explicit behavior instead of assumptions. --- .../AppInstallProcessorTest.kt | 249 +++++++++++++++++- 1 file changed, 240 insertions(+), 9 deletions(-) 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 295d99f92..ec07912b7 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -21,13 +21,16 @@ package foundation.e.apps.installProcessor import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.work.Operation +import com.aurora.gplayapi.data.models.AuthData import com.google.common.util.concurrent.Futures +import foundation.e.apps.R import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.event.AppEvent import foundation.e.apps.domain.model.install.Status import foundation.e.apps.data.fdroid.FDroidRepository import foundation.e.apps.data.install.AppInstallComponents @@ -44,18 +47,21 @@ import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway import foundation.e.apps.data.install.wrapper.StorageSpaceChecker import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender import foundation.e.apps.data.install.wrapper.UpdatesTracker +import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.PlayStoreAuthStore import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.domain.model.ContentRatingValidity +import foundation.e.apps.domain.model.User import foundation.e.apps.domain.preferences.SessionRepository import foundation.e.apps.util.MainCoroutineRule -import io.mockk.every import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.verify +import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.mockkObject import io.mockk.unmockkObject +import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -69,6 +75,7 @@ import org.mockito.Mockito import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) class AppInstallProcessorTest { @@ -92,16 +99,12 @@ class AppInstallProcessorTest { @Mock private lateinit var fakeFDroidRepository: FDroidRepository - @Mock private lateinit var context: Context - @Mock private lateinit var sessionRepository: SessionRepository - @Mock private lateinit var playStoreAuthStore: PlayStoreAuthStore - @Mock private lateinit var applicationRepository: ApplicationRepository private lateinit var appInstallProcessor: AppInstallProcessor @@ -112,7 +115,7 @@ class AppInstallProcessorTest { @Mock private lateinit var storageNotificationManager: StorageNotificationManager - private lateinit var appEventDispatcher: AppEventDispatcher + private lateinit var appEventDispatcher: RecordingAppEventDispatcher private lateinit var storageSpaceChecker: StorageSpaceChecker private lateinit var parentalControlAuthGateway: ParentalControlAuthGateway private lateinit var updatesTracker: UpdatesTracker @@ -124,7 +127,13 @@ class AppInstallProcessorTest { @Before fun setup() { MockitoAnnotations.openMocks(this) - appEventDispatcher = mockk(relaxed = true) + context = mockk(relaxed = true) + sessionRepository = mockk(relaxed = true) + playStoreAuthStore = mockk(relaxed = true) + applicationRepository = mockk(relaxed = true) + coEvery { sessionRepository.awaitUser() } returns User.NO_GOOGLE + coEvery { playStoreAuthStore.awaitAuthData() } returns null + appEventDispatcher = RecordingAppEventDispatcher() storageSpaceChecker = mockk(relaxed = true) parentalControlAuthGateway = mockk(relaxed = true) updatesTracker = mockk(relaxed = true) @@ -264,6 +273,185 @@ class AppInstallProcessorTest { assertEquals("processInstall", finalFusedDownload, null) } + @Test + fun `enqueueFusedDownload warns anonymous paid users without aborting`() = runTest { + val appInstall = AppInstall( + type = foundation.e.apps.data.enums.Type.PWA, + id = "123", + status = Status.AWAITING, + downloadURLList = mutableListOf("apk"), + packageName = "com.example.paid", + isFree = false + ) + val appManagerWrapper = mockk(relaxed = true) + val processor = createProcessorForCanEnqueue(appManagerWrapper) + + mockkObject(InstallWorkManager) + try { + coEvery { sessionRepository.awaitUser() } returns User.ANONYMOUS + coEvery { + playStoreAuthStore.awaitAuthData() + } returns AuthData(email = "anon@example.com", isAnonymous = true) + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + justRun { InstallWorkManager.enqueueWork(any(), any(), any()) } + + val result = processor.enqueueFusedDownload(appInstall) + + assertTrue(result) + assertTrue(appEventDispatcher.events.any { + it is AppEvent.ErrorMessageEvent && it.data == R.string.paid_app_anonymous_message + }) + coVerify { appManagerWrapper.updateAwaiting(appInstall) } + verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any(), any()) } + } finally { + unmockkObject(InstallWorkManager) + } + } + + @Test + fun `canEnqueue refreshes download urls for non-PWA installs`() = runTest { + val appInstall = AppInstall( + type = foundation.e.apps.data.enums.Type.NATIVE, + source = Source.PLAY_STORE, + id = "123", + status = Status.AWAITING, + packageName = "com.example.app" + ) + + val appManagerWrapper = mockk(relaxed = true) + val processor = createProcessorForCanEnqueue(appManagerWrapper) + + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = processor.canEnqueue(appInstall) + + assertTrue(result) + coVerify { applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) } + coVerify { appManagerWrapper.addDownload(appInstall) } + } + + @Test + fun `canEnqueue returns false when download url refresh throws http error`() = runTest { + val appInstall = AppInstall( + type = foundation.e.apps.data.enums.Type.NATIVE, + source = Source.PLAY_STORE, + id = "123", + status = Status.AWAITING, + packageName = "com.example.app" + ) + + val appManagerWrapper = mockk(relaxed = true) + val processor = createProcessorForCanEnqueue(appManagerWrapper) + + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) + } throws GplayHttpRequestException(403, "forbidden") + + val result = processor.canEnqueue(appInstall) + + assertEquals(false, result) + assertTrue(appEventDispatcher.events.any { it is AppEvent.UpdateEvent }) + coVerify(exactly = 0) { appManagerWrapper.addDownload(appInstall) } + } + + @Test + fun `canEnqueue keeps going when download url refresh throws illegal state`() = runTest { + val appInstall = AppInstall( + type = foundation.e.apps.data.enums.Type.NATIVE, + source = Source.PLAY_STORE, + id = "123", + status = Status.AWAITING, + packageName = "com.example.app" + ) + + val appManagerWrapper = mockk(relaxed = true) + val processor = createProcessorForCanEnqueue(appManagerWrapper) + + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) + } throws IllegalStateException("boom") + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = processor.canEnqueue(appInstall) + + assertTrue(result) + coVerify { appManagerWrapper.addDownload(appInstall) } + } + + @Test + fun `processInstall returns success when internal exception occurs`() = runTest { + val fusedDownload = initTest() + fakeFusedManagerRepository.forceCrash = true + + val result = appInstallProcessor.processInstall(fusedDownload.id, false) { + // _ignored_ + } + + assertTrue(result.isSuccess) + assertEquals(ResultStatus.OK, result.getOrNull()) + } + + @Test + fun `processInstall enters foreground before download starts`() = runTest { + val fusedDownload = initTest() + var statusAtForeground: Status? = null + + appInstallProcessor.processInstall(fusedDownload.id, false) { + statusAtForeground = fusedDownload.status + } + + assertEquals(Status.AWAITING, statusAtForeground) + } + + @Test + fun `processInstall update completion ignores installation issues`() = runTest { + stubUpdateNotificationContext() + every { updatesTracker.hasSuccessfulUpdatedApps() } returns true + every { updatesTracker.successfulUpdatedAppsCount() } returns 1 + + processCompletedUpdate(Status.INSTALLATION_ISSUE) + + verify { updatesTracker.addSuccessfullyUpdatedApp(any()) } + verify { updatesNotificationSender.showNotification("Update", "Updated message") } + verify { updatesTracker.clearSuccessfullyUpdatedApps() } + } + + @Test + fun `processInstall update completion ignores purchase needed`() = runTest { + stubUpdateNotificationContext() + every { updatesTracker.hasSuccessfulUpdatedApps() } returns true + every { updatesTracker.successfulUpdatedAppsCount() } returns 1 + + processCompletedUpdate(Status.PURCHASE_NEEDED) + + verify { updatesTracker.addSuccessfullyUpdatedApp(any()) } + verify { updatesNotificationSender.showNotification("Update", "Updated message") } + verify { updatesTracker.clearSuccessfullyUpdatedApps() } + } + + @Test + fun `processInstall clears tracked updates after final notification`() = runTest { + stubUpdateNotificationContext() + every { updatesTracker.hasSuccessfulUpdatedApps() } returns true + every { updatesTracker.successfulUpdatedAppsCount() } returns 1 + + processCompletedUpdate() + + verify { updatesTracker.clearSuccessfullyUpdatedApps() } + } + @Test fun canEnqueue_returnsTrueWhenAllChecksPass() = runTest { val appInstall = AppInstall( @@ -561,6 +749,41 @@ class AppInstallProcessorTest { return fakeFusedDownloadDAO.getDownloadById(appInstall.id) } + private suspend fun processCompletedUpdate(ignoredStatus: Status? = null) { + val fusedDownload = initTest() + fakeFusedManagerRepository.isAppInstalled = true + + ignoredStatus?.let { + fakeFusedDownloadDAO.addDownload( + AppInstall( + id = "ignored-$it", + status = it, + downloadURLList = mutableListOf("apk"), + packageName = "com.example.$it" + ) + ) + } + + appInstallProcessor.processInstall(fusedDownload.id, true) { + // _ignored_ + } + } + + private suspend fun stubUpdateNotificationContext() { + val authData = AuthData(email = "user@example.com", isAnonymous = false).apply { + locale = Locale.US + } + coEvery { playStoreAuthStore.awaitAuthData() } returns authData + every { context.getString(R.string.update) } returns "Update" + every { + context.getString( + R.string.message_last_update_triggered, + any(), + any() + ) + } returns "Updated message" + } + private fun createFusedDownload( packageName: String? = null, downloadUrlList: MutableList? = null @@ -658,3 +881,11 @@ class AppInstallProcessorTest { } } + +private class RecordingAppEventDispatcher : AppEventDispatcher { + val events = mutableListOf() + + override suspend fun dispatch(event: AppEvent) { + events.add(event) + } +} -- GitLab From a3e77799aeb7deda5d64e6cd9b2ff6c82cbd19d7 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 12 Mar 2026 22:27:54 +0600 Subject: [PATCH 03/29] refactor: extract update completion handling from AppInstallProcessor Move update bookkeeping and final notification logic behind AppUpdateCompletionHandler so the worker flow can keep its current behavior while the update rules gain their own test coverage. --- .../workmanager/AppInstallProcessor.kt | 54 +----- .../workmanager/AppUpdateCompletionHandler.kt | 93 ++++++++++ .../AppInstallProcessorTest.kt | 20 ++- .../AppUpdateCompletionHandlerTest.kt | 166 ++++++++++++++++++ 4 files changed, 275 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/install/workmanager/AppUpdateCompletionHandler.kt create mode 100644 app/src/test/java/foundation/e/apps/installProcessor/AppUpdateCompletionHandlerTest.kt diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt index e65837d73..3457354ca 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt @@ -39,11 +39,8 @@ import foundation.e.apps.data.install.wrapper.AppEventDispatcher import foundation.e.apps.data.install.wrapper.NetworkStatusChecker import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway import foundation.e.apps.data.install.wrapper.StorageSpaceChecker -import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender -import foundation.e.apps.data.install.wrapper.UpdatesTracker import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.PlayStoreAuthStore -import foundation.e.apps.data.utils.getFormattedString import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.domain.model.ContentRatingValidity import foundation.e.apps.domain.model.User @@ -53,8 +50,6 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.flow.transformWhile import timber.log.Timber -import java.text.NumberFormat -import java.util.Date import javax.inject.Inject @Suppress("LongParameterList") @@ -69,9 +64,8 @@ class AppInstallProcessor @Inject constructor( private val appEventDispatcher: AppEventDispatcher, private val storageSpaceChecker: StorageSpaceChecker, private val parentalControlAuthGateway: ParentalControlAuthGateway, - private val updatesTracker: UpdatesTracker, - private val updatesNotificationSender: UpdatesNotificationSender, private val networkStatusChecker: NetworkStatusChecker, + private val appUpdateCompletionHandler: AppUpdateCompletionHandler, ) { @Inject lateinit var downloadManager: DownloadManagerUtils @@ -83,7 +77,6 @@ class AppInstallProcessor @Inject constructor( companion object { private const val TAG = "AppInstallProcessor" - private const val DATE_FORMAT = "dd/MM/yyyy-HH:mm" } /** @@ -369,50 +362,7 @@ class AppInstallProcessor @Inject constructor( private suspend fun checkUpdateWork( appInstall: AppInstall? ) { - if (isItUpdateWork) { - appInstall?.let { - val packageStatus = - appInstallComponents.appManagerWrapper.getFusedDownloadPackageStatus(appInstall) - - if (packageStatus == Status.INSTALLED) { - updatesTracker.addSuccessfullyUpdatedApp(it) - } - - if (isUpdateCompleted()) { // show notification for ended update - showNotificationOnUpdateEnded() - updatesTracker.clearSuccessfullyUpdatedApps() - } - } - } - } - - private suspend fun isUpdateCompleted(): Boolean { - val downloadListWithoutAnyIssue = - appInstallComponents.appInstallRepository.getDownloadList().filter { - !listOf( - Status.INSTALLATION_ISSUE, - Status.PURCHASE_NEEDED - ).contains(it.status) - } - - return updatesTracker.hasSuccessfulUpdatedApps() && downloadListWithoutAnyIssue.isEmpty() - } - - private suspend fun showNotificationOnUpdateEnded() { - val locale = playStoreAuthStore.awaitAuthData()?.locale ?: java.util.Locale.getDefault() - val date = Date().getFormattedString(DATE_FORMAT, locale) - val numberOfUpdatedApps = - NumberFormat.getNumberInstance(locale).format(updatesTracker.successfulUpdatedAppsCount()) - .toString() - - updatesNotificationSender.showNotification( - context.getString(R.string.update), - context.getString( - R.string.message_last_update_triggered, - numberOfUpdatedApps, - date - ) - ) + appUpdateCompletionHandler.onInstallFinished(appInstall, isItUpdateWork) } private suspend fun startAppInstallationProcess(appInstall: AppInstall) { diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppUpdateCompletionHandler.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppUpdateCompletionHandler.kt new file mode 100644 index 000000000..b68636dc6 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppUpdateCompletionHandler.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.workmanager + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.R +import foundation.e.apps.data.install.AppInstallRepository +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender +import foundation.e.apps.data.install.wrapper.UpdatesTracker +import foundation.e.apps.data.preference.PlayStoreAuthStore +import foundation.e.apps.data.utils.getFormattedString +import foundation.e.apps.domain.model.install.Status +import java.text.NumberFormat +import java.util.Date +import java.util.Locale +import javax.inject.Inject + +class AppUpdateCompletionHandler @Inject constructor( + @ApplicationContext private val context: Context, + private val appInstallRepository: AppInstallRepository, + private val appManagerWrapper: AppManagerWrapper, + private val playStoreAuthStore: PlayStoreAuthStore, + private val updatesTracker: UpdatesTracker, + private val updatesNotificationSender: UpdatesNotificationSender, +) { + companion object { + private const val DATE_FORMAT = "dd/MM/yyyy-HH:mm" + } + + suspend fun onInstallFinished(appInstall: AppInstall?, isUpdateWork: Boolean) { + if (!isUpdateWork) { + return + } + + appInstall?.let { + val packageStatus = appManagerWrapper.getFusedDownloadPackageStatus(appInstall) + + if (packageStatus == Status.INSTALLED) { + updatesTracker.addSuccessfullyUpdatedApp(it) + } + + if (isUpdateCompleted()) { + showNotificationOnUpdateEnded() + updatesTracker.clearSuccessfullyUpdatedApps() + } + } + } + + private suspend fun isUpdateCompleted(): Boolean { + val downloadListWithoutAnyIssue = appInstallRepository.getDownloadList().filter { + !listOf(Status.INSTALLATION_ISSUE, Status.PURCHASE_NEEDED).contains(it.status) + } + + return updatesTracker.hasSuccessfulUpdatedApps() && downloadListWithoutAnyIssue.isEmpty() + } + + private suspend fun showNotificationOnUpdateEnded() { + val locale = playStoreAuthStore.awaitAuthData()?.locale ?: Locale.getDefault() + val date = Date().getFormattedString(DATE_FORMAT, locale) + val numberOfUpdatedApps = + NumberFormat.getNumberInstance(locale) + .format(updatesTracker.successfulUpdatedAppsCount()) + .toString() + + updatesNotificationSender.showNotification( + context.getString(R.string.update), + context.getString( + R.string.message_last_update_triggered, + numberOfUpdatedApps, + date + ) + ) + } +} 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 ec07912b7..d073216a6 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -40,6 +40,7 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager import foundation.e.apps.data.install.workmanager.AppInstallProcessor +import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler import foundation.e.apps.data.install.workmanager.InstallWorkManager import foundation.e.apps.data.install.wrapper.AppEventDispatcher import foundation.e.apps.data.install.wrapper.NetworkStatusChecker @@ -121,6 +122,7 @@ class AppInstallProcessorTest { private lateinit var updatesTracker: UpdatesTracker private lateinit var updatesNotificationSender: UpdatesNotificationSender private lateinit var networkStatusChecker: NetworkStatusChecker + private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler private var isInstallWorkManagerMocked = false @@ -145,6 +147,14 @@ class AppInstallProcessorTest { FakeAppManagerWrapper(fakeFusedDownloadDAO, context, fakeFusedManager, fakeFDroidRepository) val appInstallComponents = AppInstallComponents(appInstallRepository, fakeFusedManagerRepository) + appUpdateCompletionHandler = AppUpdateCompletionHandler( + context, + appInstallRepository, + fakeFusedManagerRepository, + playStoreAuthStore, + updatesTracker, + updatesNotificationSender + ) appInstallProcessor = AppInstallProcessor( context, @@ -157,9 +167,8 @@ class AppInstallProcessorTest { appEventDispatcher, storageSpaceChecker, parentalControlAuthGateway, - updatesTracker, - updatesNotificationSender, - networkStatusChecker + networkStatusChecker, + appUpdateCompletionHandler ) } @@ -810,9 +819,8 @@ class AppInstallProcessorTest { appEventDispatcher, storageSpaceChecker, parentalControlAuthGateway, - updatesTracker, - updatesNotificationSender, - networkStatusChecker + networkStatusChecker, + appUpdateCompletionHandler ) } diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppUpdateCompletionHandlerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppUpdateCompletionHandlerTest.kt new file mode 100644 index 000000000..c27314a7f --- /dev/null +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppUpdateCompletionHandlerTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.installProcessor + +import android.content.Context +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.R +import foundation.e.apps.data.install.AppInstallRepository +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler +import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender +import foundation.e.apps.data.install.wrapper.UpdatesTracker +import foundation.e.apps.data.preference.PlayStoreAuthStore +import foundation.e.apps.domain.model.install.Status +import foundation.e.apps.util.MainCoroutineRule +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.Locale + +@OptIn(ExperimentalCoroutinesApi::class) +class AppUpdateCompletionHandlerTest { + + @get:Rule + var mainCoroutineRule = MainCoroutineRule() + + private lateinit var appInstallRepository: AppInstallRepository + private lateinit var appManagerWrapper: AppManagerWrapper + private lateinit var playStoreAuthStore: PlayStoreAuthStore + private lateinit var updatesTracker: UpdatesTracker + private lateinit var updatesNotificationSender: UpdatesNotificationSender + private lateinit var context: Context + private lateinit var handler: AppUpdateCompletionHandler + + @Before + fun setup() { + context = mockk(relaxed = true) + appInstallRepository = AppInstallRepository(FakeAppInstallDAO()) + appManagerWrapper = mockk(relaxed = true) + playStoreAuthStore = mockk(relaxed = true) + updatesTracker = mockk(relaxed = true) + updatesNotificationSender = mockk(relaxed = true) + coEvery { playStoreAuthStore.awaitAuthData() } returns null + handler = AppUpdateCompletionHandler( + context, + appInstallRepository, + appManagerWrapper, + playStoreAuthStore, + updatesTracker, + updatesNotificationSender + ) + } + + @Test + fun onInstallFinished_doesNothingWhenNotUpdateWork() = runTest { + handler.onInstallFinished(AppInstall(id = "123", packageName = "com.example.app"), false) + + verify(exactly = 0) { appManagerWrapper.getFusedDownloadPackageStatus(any()) } + verify(exactly = 0) { updatesTracker.addSuccessfullyUpdatedApp(any()) } + verify(exactly = 0) { updatesNotificationSender.showNotification(any(), any()) } + } + + @Test + fun onInstallFinished_tracksInstalledUpdates() = runTest { + val appInstall = AppInstall(id = "123", packageName = "com.example.app") + every { appManagerWrapper.getFusedDownloadPackageStatus(appInstall) } returns Status.INSTALLED + every { updatesTracker.hasSuccessfulUpdatedApps() } returns false + + handler.onInstallFinished(appInstall, true) + + verify { updatesTracker.addSuccessfullyUpdatedApp(appInstall) } + verify(exactly = 0) { updatesNotificationSender.showNotification(any(), any()) } + } + + @Test + fun onInstallFinished_sendsNotificationWhenUpdateBatchCompletes() = runTest { + val appInstall = AppInstall(id = "123", packageName = "com.example.app") + every { appManagerWrapper.getFusedDownloadPackageStatus(appInstall) } returns Status.INSTALLED + every { updatesTracker.hasSuccessfulUpdatedApps() } returns true + every { updatesTracker.successfulUpdatedAppsCount() } returns 2 + stubUpdateNotificationContext() + + handler.onInstallFinished(appInstall, true) + + verify { updatesNotificationSender.showNotification("Update", "Updated message") } + verify { updatesTracker.clearSuccessfullyUpdatedApps() } + } + + @Test + fun onInstallFinished_ignoresIssueAndPurchaseNeededStatusesForCompletion() = runTest { + val appInstall = AppInstall(id = "123", packageName = "com.example.app") + appInstallRepository.addDownload( + AppInstall( + id = "issue", + status = Status.INSTALLATION_ISSUE, + packageName = "com.example.issue" + ) + ) + appInstallRepository.addDownload( + AppInstall( + id = "purchase", + status = Status.PURCHASE_NEEDED, + packageName = "com.example.purchase" + ) + ) + every { appManagerWrapper.getFusedDownloadPackageStatus(appInstall) } returns Status.INSTALLED + every { updatesTracker.hasSuccessfulUpdatedApps() } returns true + every { updatesTracker.successfulUpdatedAppsCount() } returns 1 + stubUpdateNotificationContext() + + handler.onInstallFinished(appInstall, true) + + verify { updatesNotificationSender.showNotification("Update", "Updated message") } + } + + @Test + fun onInstallFinished_clearsTrackedUpdatesAfterNotification() = runTest { + val appInstall = AppInstall(id = "123", packageName = "com.example.app") + every { appManagerWrapper.getFusedDownloadPackageStatus(appInstall) } returns Status.INSTALLED + every { updatesTracker.hasSuccessfulUpdatedApps() } returns true + every { updatesTracker.successfulUpdatedAppsCount() } returns 1 + stubUpdateNotificationContext() + + handler.onInstallFinished(appInstall, true) + + verify { updatesTracker.clearSuccessfullyUpdatedApps() } + } + + private suspend fun stubUpdateNotificationContext() { + val authData = AuthData(email = "user@example.com", isAnonymous = false).apply { + locale = Locale.US + } + coEvery { playStoreAuthStore.awaitAuthData() } returns authData + every { context.getString(R.string.update) } returns "Update" + every { + context.getString( + R.string.message_last_update_triggered, + any(), + any() + ) + } returns "Updated message" + } +} -- GitLab From 3fda22e1553aadc44f940b51a84c7ecc42b6d4a3 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 12 Mar 2026 22:32:57 +0600 Subject: [PATCH 04/29] refactor: remove shared update state from AppInstallProcessor Pass the update-work flag through the install flow explicitly so the async worker path no longer depends on mutable processor state between callbacks. --- .../workmanager/AppInstallProcessor.kt | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt index 3457354ca..4e45b84bc 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt @@ -73,8 +73,6 @@ class AppInstallProcessor @Inject constructor( @Inject lateinit var appManager: AppManager - private var isItUpdateWork = false - companion object { private const val TAG = "AppInstallProcessor" } @@ -303,7 +301,7 @@ class AppInstallProcessor @Inject constructor( appInstall?.let { checkDownloadingState(appInstall) - this.isItUpdateWork = + val isUpdateWork = isItUpdateWork && appInstallComponents.appManagerWrapper.isFusedDownloadInstalled( appInstall ) @@ -329,7 +327,7 @@ class AppInstallProcessor @Inject constructor( runInForeground.invoke(it.name) - startAppInstallationProcess(it) + startAppInstallationProcess(it, isUpdateWork) } } catch (e: Exception) { Timber.e( @@ -359,15 +357,13 @@ class AppInstallProcessor @Inject constructor( appInstall ) || appInstall.status == Status.INSTALLING) - private suspend fun checkUpdateWork( - appInstall: AppInstall? - ) { - appUpdateCompletionHandler.onInstallFinished(appInstall, isItUpdateWork) + private suspend fun checkUpdateWork(appInstall: AppInstall?, isUpdateWork: Boolean) { + appUpdateCompletionHandler.onInstallFinished(appInstall, isUpdateWork) } - private suspend fun startAppInstallationProcess(appInstall: AppInstall) { + private suspend fun startAppInstallationProcess(appInstall: AppInstall, isUpdateWork: Boolean) { if (appInstall.isAwaiting()) { - appInstallComponents.appManagerWrapper.downloadApp(appInstall, isItUpdateWork) + appInstallComponents.appManagerWrapper.downloadApp(appInstall, isUpdateWork) Timber.i("===> doWork: Download started ${appInstall.name} ${appInstall.status}") } @@ -377,7 +373,7 @@ class AppInstallProcessor @Inject constructor( isInstallRunning(it) } .collect { latestFusedDownload -> - handleFusedDownload(latestFusedDownload, appInstall) + handleFusedDownload(latestFusedDownload, appInstall, isUpdateWork) } } @@ -390,35 +386,37 @@ class AppInstallProcessor @Inject constructor( */ private suspend fun handleFusedDownload( latestAppInstall: AppInstall?, - appInstall: AppInstall + appInstall: AppInstall, + isUpdateWork: Boolean ) { if (latestAppInstall == null) { Timber.d("===> download null: finish installation") - finishInstallation(appInstall) + finishInstallation(appInstall, isUpdateWork) return } - handleFusedDownloadStatusCheckingException(latestAppInstall) + handleFusedDownloadStatusCheckingException(latestAppInstall, isUpdateWork) } private fun isInstallRunning(it: AppInstall?) = it != null && it.status != Status.INSTALLATION_ISSUE private suspend fun handleFusedDownloadStatusCheckingException( - download: AppInstall + download: AppInstall, + isUpdateWork: Boolean ) { try { - handleFusedDownloadStatus(download) + handleFusedDownloadStatus(download, isUpdateWork) } catch (e: Exception) { val message = "Handling install status is failed for ${download.packageName} exception: ${e.localizedMessage}" Timber.e(e, message) appInstallComponents.appManagerWrapper.installationIssue(download) - finishInstallation(download) + finishInstallation(download, isUpdateWork) } } - private suspend fun handleFusedDownloadStatus(appInstall: AppInstall) { + private suspend fun handleFusedDownloadStatus(appInstall: AppInstall, isUpdateWork: Boolean) { when (appInstall.status) { Status.AWAITING, Status.DOWNLOADING -> { } @@ -436,7 +434,7 @@ class AppInstallProcessor @Inject constructor( Status.INSTALLED, Status.INSTALLATION_ISSUE -> { Timber.i("===> doWork: Installed/Failed: ${appInstall.name} ${appInstall.status}") - finishInstallation(appInstall) + finishInstallation(appInstall, isUpdateWork) } else -> { @@ -444,12 +442,12 @@ class AppInstallProcessor @Inject constructor( TAG, "===> ${appInstall.name} is in wrong state ${appInstall.status}" ) - finishInstallation(appInstall) + finishInstallation(appInstall, isUpdateWork) } } } - private suspend fun finishInstallation(appInstall: AppInstall) { - checkUpdateWork(appInstall) + private suspend fun finishInstallation(appInstall: AppInstall, isUpdateWork: Boolean) { + checkUpdateWork(appInstall, isUpdateWork) } } -- GitLab From 5601a5263c8a8c1fb30a6ca0732fb2af093be437 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 12 Mar 2026 23:25:29 +0600 Subject: [PATCH 05/29] refactor: extract AppInstallProcessor age validation flow into separate class Move the age-limit dialog and parental-auth checks behind AppInstallAgeLimitGate so enqueue validation keeps the current order while the restriction flow gains direct unit coverage. --- .../workmanager/AppInstallAgeLimitGate.kt | 70 +++++++++ .../workmanager/AppInstallProcessor.kt | 40 +---- .../AppInstallAgeLimitGateTest.kt | 140 ++++++++++++++++++ .../AppInstallProcessorTest.kt | 32 ++-- .../FakeAppEventDispatcher.kt | 35 +++++ 5 files changed, 265 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallAgeLimitGate.kt create mode 100644 app/src/test/java/foundation/e/apps/installProcessor/AppInstallAgeLimitGateTest.kt create mode 100644 app/src/test/java/foundation/e/apps/installProcessor/FakeAppEventDispatcher.kt diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallAgeLimitGate.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallAgeLimitGate.kt new file mode 100644 index 000000000..56cabf5fe --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallAgeLimitGate.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.workmanager + +import foundation.e.apps.R +import foundation.e.apps.data.event.AppEvent +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.wrapper.AppEventDispatcher +import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway +import foundation.e.apps.domain.ValidateAppAgeLimitUseCase +import foundation.e.apps.domain.model.ContentRatingValidity +import kotlinx.coroutines.CompletableDeferred +import javax.inject.Inject + +@Suppress("ReturnCount") // FIXME: Remove suppression and fix detekt +class AppInstallAgeLimitGate @Inject constructor( + private val validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase, + private val appManagerWrapper: AppManagerWrapper, + private val appEventDispatcher: AppEventDispatcher, + private val parentalControlAuthGateway: ParentalControlAuthGateway, +) { + suspend fun allow(appInstall: AppInstall): Boolean { + val ageLimitValidationResult = validateAppAgeLimitUseCase(appInstall) + if (ageLimitValidationResult.data?.isValid == true) { + return true + } + + if (ageLimitValidationResult.isSuccess()) { + awaitInvokeAgeLimitEvent(appInstall.name) + if (ageLimitValidationResult.data?.requestPin == true) { + val isAuthenticated = parentalControlAuthGateway.awaitAuthentication() + if (isAuthenticated) { + ageLimitValidationResult.setData(ContentRatingValidity(true)) + } + } + } else { + appEventDispatcher.dispatch(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc)) + } + + if (ageLimitValidationResult.data?.isValid == true) { + return true + } + + appManagerWrapper.cancelDownload(appInstall) + return false + } + + private suspend fun awaitInvokeAgeLimitEvent(type: String) { + val deferred = CompletableDeferred() + appEventDispatcher.dispatch(AppEvent.AgeLimitRestrictionEvent(type, deferred)) + deferred.await() + } +} diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt index 4e45b84bc..24fb08667 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt @@ -37,16 +37,12 @@ import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager import foundation.e.apps.data.install.wrapper.AppEventDispatcher import foundation.e.apps.data.install.wrapper.NetworkStatusChecker -import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway import foundation.e.apps.data.install.wrapper.StorageSpaceChecker import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.PlayStoreAuthStore -import foundation.e.apps.domain.ValidateAppAgeLimitUseCase -import foundation.e.apps.domain.model.ContentRatingValidity import foundation.e.apps.domain.model.User import foundation.e.apps.domain.model.install.Status import foundation.e.apps.domain.preferences.SessionRepository -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.flow.transformWhile import timber.log.Timber @@ -57,14 +53,13 @@ class AppInstallProcessor @Inject constructor( @ApplicationContext private val context: Context, private val appInstallComponents: AppInstallComponents, private val applicationRepository: ApplicationRepository, - private val validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase, private val sessionRepository: SessionRepository, private val playStoreAuthStore: PlayStoreAuthStore, private val storageNotificationManager: StorageNotificationManager, private val appEventDispatcher: AppEventDispatcher, private val storageSpaceChecker: StorageSpaceChecker, - private val parentalControlAuthGateway: ParentalControlAuthGateway, private val networkStatusChecker: NetworkStatusChecker, + private val appInstallAgeLimitGate: AppInstallAgeLimitGate, private val appUpdateCompletionHandler: AppUpdateCompletionHandler, ) { @Inject @@ -170,7 +165,7 @@ class AppInstallProcessor @Inject constructor( return false } - if (!validateAgeLimit(appInstall)) { + if (!appInstallAgeLimitGate.allow(appInstall)) { return false } @@ -191,37 +186,6 @@ class AppInstallProcessor @Inject constructor( return true } - private suspend fun validateAgeLimit(appInstall: AppInstall): Boolean { - val ageLimitValidationResult = validateAppAgeLimitUseCase(appInstall) - if (ageLimitValidationResult.data?.isValid == true) { - return true - } - if (ageLimitValidationResult.isSuccess()) { - awaitInvokeAgeLimitEvent(appInstall.name) - if (ageLimitValidationResult.data?.requestPin == true) { - val isAuthenticated = parentalControlAuthGateway.awaitAuthentication() - if (isAuthenticated) { - ageLimitValidationResult.setData(ContentRatingValidity(true)) - } - } - } else { - appEventDispatcher.dispatch(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc)) - } - - var ageIsValid = true - if (ageLimitValidationResult.data?.isValid != true) { - appInstallComponents.appManagerWrapper.cancelDownload(appInstall) - ageIsValid = false - } - return ageIsValid - } - - suspend fun awaitInvokeAgeLimitEvent(type: String) { - val deferred = CompletableDeferred() - appEventDispatcher.dispatch(AppEvent.AgeLimitRestrictionEvent(type, deferred)) - deferred.await() // await closing dialog box - } - // returns TRUE if updating urls is successful, otherwise false. private suspend fun updateDownloadUrls(appInstall: AppInstall): Boolean { try { diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallAgeLimitGateTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallAgeLimitGateTest.kt new file mode 100644 index 000000000..19de4f0f8 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallAgeLimitGateTest.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.installProcessor + +import foundation.e.apps.R +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.event.AppEvent +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate +import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway +import foundation.e.apps.domain.ValidateAppAgeLimitUseCase +import foundation.e.apps.domain.model.ContentRatingValidity +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito + +@OptIn(ExperimentalCoroutinesApi::class) +class AppInstallAgeLimitGateTest { + private lateinit var validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase + private lateinit var appManagerWrapper: AppManagerWrapper + private lateinit var parentalControlAuthGateway: ParentalControlAuthGateway + private lateinit var appEventDispatcher: FakeAppEventDispatcher + private lateinit var gate: AppInstallAgeLimitGate + + @Before + fun setup() { + validateAppAgeLimitUseCase = Mockito.mock(ValidateAppAgeLimitUseCase::class.java) + appManagerWrapper = mockk(relaxed = true) + parentalControlAuthGateway = mockk(relaxed = true) + appEventDispatcher = FakeAppEventDispatcher(autoCompleteDeferred = true) + gate = AppInstallAgeLimitGate( + validateAppAgeLimitUseCase, + appManagerWrapper, + appEventDispatcher, + parentalControlAuthGateway + ) + } + + @Test + fun allow_returnsTrueWhenAgeRatingIsValid() = runTest { + val appInstall = AppInstall(id = "123", packageName = "com.example.app") + Mockito.`when`(validateAppAgeLimitUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) + + val result = gate.allow(appInstall) + + assertTrue(result) + coVerify(exactly = 0) { appManagerWrapper.cancelDownload(any()) } + } + + @Test + fun allow_returnsFalseAndCancelsWhenAgeRatingInvalidWithoutPin() = runTest { + val appInstall = AppInstall(id = "123", name = "App", packageName = "com.example.app") + Mockito.`when`(validateAppAgeLimitUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(false))) + + val result = gate.allow(appInstall) + + assertFalse(result) + assertTrue(appEventDispatcher.events.any { it is AppEvent.AgeLimitRestrictionEvent }) + coVerify { appManagerWrapper.cancelDownload(appInstall) } + } + + @Test + fun allow_returnsTrueWhenPinIsRequiredAndAuthenticationSucceeds() = runTest { + val appInstall = AppInstall(id = "123", name = "App", packageName = "com.example.app") + Mockito.`when`(validateAppAgeLimitUseCase(appInstall)) + .thenReturn( + ResultSupreme.create( + ResultStatus.OK, + ContentRatingValidity(false, requestPin = true) + ) + ) + coEvery { parentalControlAuthGateway.awaitAuthentication() } returns true + + val result = gate.allow(appInstall) + + assertTrue(result) + assertTrue(appEventDispatcher.events.any { it is AppEvent.AgeLimitRestrictionEvent }) + coVerify(exactly = 0) { appManagerWrapper.cancelDownload(any()) } + } + + @Test + fun allow_returnsFalseWhenPinIsRequiredAndAuthenticationFails() = runTest { + val appInstall = AppInstall(id = "123", name = "App", packageName = "com.example.app") + Mockito.`when`(validateAppAgeLimitUseCase(appInstall)) + .thenReturn( + ResultSupreme.create( + ResultStatus.OK, + ContentRatingValidity(false, requestPin = true) + ) + ) + coEvery { parentalControlAuthGateway.awaitAuthentication() } returns false + + val result = gate.allow(appInstall) + + assertFalse(result) + coVerify { appManagerWrapper.cancelDownload(appInstall) } + } + + @Test + fun allow_dispatchesErrorDialogWhenValidationFails() = runTest { + val appInstall = AppInstall(id = "123", packageName = "com.example.app") + Mockito.`when`(validateAppAgeLimitUseCase(appInstall)) + .thenReturn(ResultSupreme.create(ResultStatus.UNKNOWN, ContentRatingValidity(false))) + + val result = gate.allow(appInstall) + + assertFalse(result) + val errorDialogEvent = appEventDispatcher.events.last() as AppEvent.ErrorMessageDialogEvent + assertEquals(R.string.data_load_error_desc, errorDialogEvent.data) + coVerify { appManagerWrapper.cancelDownload(appInstall) } + } +} 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 d073216a6..2b947f98e 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -39,6 +39,7 @@ import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager +import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate import foundation.e.apps.data.install.workmanager.AppInstallProcessor import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler import foundation.e.apps.data.install.workmanager.InstallWorkManager @@ -116,12 +117,13 @@ class AppInstallProcessorTest { @Mock private lateinit var storageNotificationManager: StorageNotificationManager - private lateinit var appEventDispatcher: RecordingAppEventDispatcher + private lateinit var appEventDispatcher: FakeAppEventDispatcher private lateinit var storageSpaceChecker: StorageSpaceChecker private lateinit var parentalControlAuthGateway: ParentalControlAuthGateway private lateinit var updatesTracker: UpdatesTracker private lateinit var updatesNotificationSender: UpdatesNotificationSender private lateinit var networkStatusChecker: NetworkStatusChecker + private lateinit var appInstallAgeLimitGate: AppInstallAgeLimitGate private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler private var isInstallWorkManagerMocked = false @@ -135,7 +137,7 @@ class AppInstallProcessorTest { applicationRepository = mockk(relaxed = true) coEvery { sessionRepository.awaitUser() } returns User.NO_GOOGLE coEvery { playStoreAuthStore.awaitAuthData() } returns null - appEventDispatcher = RecordingAppEventDispatcher() + appEventDispatcher = FakeAppEventDispatcher() storageSpaceChecker = mockk(relaxed = true) parentalControlAuthGateway = mockk(relaxed = true) updatesTracker = mockk(relaxed = true) @@ -147,6 +149,12 @@ class AppInstallProcessorTest { FakeAppManagerWrapper(fakeFusedDownloadDAO, context, fakeFusedManager, fakeFDroidRepository) val appInstallComponents = AppInstallComponents(appInstallRepository, fakeFusedManagerRepository) + appInstallAgeLimitGate = AppInstallAgeLimitGate( + validateAppAgeRatingUseCase, + fakeFusedManagerRepository, + appEventDispatcher, + parentalControlAuthGateway + ) appUpdateCompletionHandler = AppUpdateCompletionHandler( context, appInstallRepository, @@ -160,14 +168,13 @@ class AppInstallProcessorTest { context, appInstallComponents, applicationRepository, - validateAppAgeRatingUseCase, sessionRepository, playStoreAuthStore, storageNotificationManager, appEventDispatcher, storageSpaceChecker, - parentalControlAuthGateway, networkStatusChecker, + appInstallAgeLimitGate, appUpdateCompletionHandler ) } @@ -808,18 +815,23 @@ class AppInstallProcessorTest { ): AppInstallProcessor { val appInstallRepository = AppInstallRepository(FakeAppInstallDAO()) val appInstallComponents = AppInstallComponents(appInstallRepository, appManagerWrapper) + val ageLimitGate = AppInstallAgeLimitGate( + validateAppAgeRatingUseCase, + appManagerWrapper, + appEventDispatcher, + parentalControlAuthGateway + ) return AppInstallProcessor( context, appInstallComponents, applicationRepository, - validateAppAgeRatingUseCase, sessionRepository, playStoreAuthStore, storageNotificationManager, appEventDispatcher, storageSpaceChecker, - parentalControlAuthGateway, networkStatusChecker, + ageLimitGate, appUpdateCompletionHandler ) } @@ -889,11 +901,3 @@ class AppInstallProcessorTest { } } - -private class RecordingAppEventDispatcher : AppEventDispatcher { - val events = mutableListOf() - - override suspend fun dispatch(event: AppEvent) { - events.add(event) - } -} diff --git a/app/src/test/java/foundation/e/apps/installProcessor/FakeAppEventDispatcher.kt b/app/src/test/java/foundation/e/apps/installProcessor/FakeAppEventDispatcher.kt new file mode 100644 index 000000000..b9ff56e85 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/installProcessor/FakeAppEventDispatcher.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.installProcessor + +import foundation.e.apps.data.event.AppEvent +import foundation.e.apps.data.install.wrapper.AppEventDispatcher + +internal class FakeAppEventDispatcher( + private val autoCompleteDeferred: Boolean = false +) : AppEventDispatcher { + val events = mutableListOf() + + override suspend fun dispatch(event: AppEvent) { + events.add(event) + if (autoCompleteDeferred && event is AppEvent.AgeLimitRestrictionEvent) { + event.onClose?.complete(Unit) + } + } +} -- GitLab From 5787b5d077dc94c3ebdf92615b65c02f5a8dff18 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 12 Mar 2026 23:40:59 +0600 Subject: [PATCH 06/29] refactor: move the install-state machine from AppInstallProcessor to AppInstallWorkRunner Move the install-state machine and flow handling into a dedicated runner so worker behavior can be tested directly while the processor keeps the same external contract. --- .../workmanager/AppInstallProcessor.kt | 170 +------------- .../workmanager/AppInstallWorkRunner.kt | 176 +++++++++++++++ .../AppInstallProcessorTest.kt | 20 +- .../AppInstallWorkRunnerTest.kt | 209 ++++++++++++++++++ 4 files changed, 405 insertions(+), 170 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt create mode 100644 app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt index 24fb08667..797dcf951 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt @@ -32,7 +32,6 @@ import foundation.e.apps.data.enums.Type import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.install.AppInstallComponents import foundation.e.apps.data.install.AppManager -import foundation.e.apps.data.install.download.DownloadManagerUtils import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager import foundation.e.apps.data.install.wrapper.AppEventDispatcher @@ -44,7 +43,6 @@ import foundation.e.apps.domain.model.User import foundation.e.apps.domain.model.install.Status import foundation.e.apps.domain.preferences.SessionRepository import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.flow.transformWhile import timber.log.Timber import javax.inject.Inject @@ -60,18 +58,11 @@ class AppInstallProcessor @Inject constructor( private val storageSpaceChecker: StorageSpaceChecker, private val networkStatusChecker: NetworkStatusChecker, private val appInstallAgeLimitGate: AppInstallAgeLimitGate, - private val appUpdateCompletionHandler: AppUpdateCompletionHandler, + private val appInstallWorkRunner: AppInstallWorkRunner, ) { - @Inject - lateinit var downloadManager: DownloadManagerUtils - @Inject lateinit var appManager: AppManager - companion object { - private const val TAG = "AppInstallProcessor" - } - /** * creates [AppInstall] from [Application] and enqueues into WorkManager to run install process. * @param application represents the app info which will be installed @@ -255,163 +246,6 @@ class AppInstallProcessor @Inject constructor( isItUpdateWork: Boolean, runInForeground: (suspend (String) -> Unit) ): Result { - var appInstall: AppInstall? = null - try { - Timber.d("Fused download name $fusedDownloadId") - - appInstall = appInstallComponents.appInstallRepository.getDownloadById(fusedDownloadId) - Timber.i(">>> dowork started for Fused download name " + appInstall?.name + " " + fusedDownloadId) - - appInstall?.let { - checkDownloadingState(appInstall) - - val isUpdateWork = - isItUpdateWork && appInstallComponents.appManagerWrapper.isFusedDownloadInstalled( - appInstall - ) - - if (!appInstall.isAppInstalling()) { - Timber.d("!!! returned") - return@let - } - - if (!appInstallComponents.appManagerWrapper.validateFusedDownload(appInstall)) { - appInstallComponents.appManagerWrapper.installationIssue(it) - Timber.d("!!! installationIssue") - return@let - } - - if (areFilesDownloadedButNotInstalled(appInstall)) { - Timber.i("===> Downloaded But not installed ${appInstall.name}") - appInstallComponents.appManagerWrapper.updateDownloadStatus( - appInstall, - Status.INSTALLING - ) - } - - runInForeground.invoke(it.name) - - startAppInstallationProcess(it, isUpdateWork) - } - } catch (e: Exception) { - Timber.e( - e, - "Install worker is failed for ${appInstall?.packageName} exception: ${e.localizedMessage}" - ) - appInstall?.let { - appInstallComponents.appManagerWrapper.cancelDownload(appInstall) - } - } - - Timber.i("doWork: RESULT SUCCESS: ${appInstall?.name}") - return Result.success(ResultStatus.OK) - } - - @OptIn(DelicateCoroutinesApi::class) - private fun checkDownloadingState(appInstall: AppInstall) { - if (appInstall.status == Status.DOWNLOADING) { - appInstall.downloadIdMap.keys.forEach { downloadId -> - downloadManager.updateDownloadStatus(downloadId) - } - } - } - - private fun areFilesDownloadedButNotInstalled(appInstall: AppInstall) = - appInstall.areFilesDownloaded() && (!appInstallComponents.appManagerWrapper.isFusedDownloadInstalled( - appInstall - ) || appInstall.status == Status.INSTALLING) - - private suspend fun checkUpdateWork(appInstall: AppInstall?, isUpdateWork: Boolean) { - appUpdateCompletionHandler.onInstallFinished(appInstall, isUpdateWork) - } - - private suspend fun startAppInstallationProcess(appInstall: AppInstall, isUpdateWork: Boolean) { - if (appInstall.isAwaiting()) { - appInstallComponents.appManagerWrapper.downloadApp(appInstall, isUpdateWork) - Timber.i("===> doWork: Download started ${appInstall.name} ${appInstall.status}") - } - - appInstallComponents.appInstallRepository.getDownloadFlowById(appInstall.id) - .transformWhile { - emit(it) - isInstallRunning(it) - } - .collect { latestFusedDownload -> - handleFusedDownload(latestFusedDownload, appInstall, isUpdateWork) - } - } - - /** - * Takes actions depending on the status of [AppInstall] - * - * @param latestAppInstall comes from Room database when [Status] is updated - * @param appInstall is the original object when install process isn't started. It's used when [latestAppInstall] - * becomes null, After installation is completed. - */ - private suspend fun handleFusedDownload( - latestAppInstall: AppInstall?, - appInstall: AppInstall, - isUpdateWork: Boolean - ) { - if (latestAppInstall == null) { - Timber.d("===> download null: finish installation") - finishInstallation(appInstall, isUpdateWork) - return - } - - handleFusedDownloadStatusCheckingException(latestAppInstall, isUpdateWork) - } - - private fun isInstallRunning(it: AppInstall?) = - it != null && it.status != Status.INSTALLATION_ISSUE - - private suspend fun handleFusedDownloadStatusCheckingException( - download: AppInstall, - isUpdateWork: Boolean - ) { - try { - handleFusedDownloadStatus(download, isUpdateWork) - } catch (e: Exception) { - val message = - "Handling install status is failed for ${download.packageName} exception: ${e.localizedMessage}" - Timber.e(e, message) - appInstallComponents.appManagerWrapper.installationIssue(download) - finishInstallation(download, isUpdateWork) - } - } - - private suspend fun handleFusedDownloadStatus(appInstall: AppInstall, isUpdateWork: Boolean) { - when (appInstall.status) { - Status.AWAITING, Status.DOWNLOADING -> { - } - - Status.DOWNLOADED -> { - appInstallComponents.appManagerWrapper.updateDownloadStatus( - appInstall, - Status.INSTALLING - ) - } - - Status.INSTALLING -> { - Timber.i("===> doWork: Installing ${appInstall.name} ${appInstall.status}") - } - - Status.INSTALLED, Status.INSTALLATION_ISSUE -> { - Timber.i("===> doWork: Installed/Failed: ${appInstall.name} ${appInstall.status}") - finishInstallation(appInstall, isUpdateWork) - } - - else -> { - Timber.wtf( - TAG, - "===> ${appInstall.name} is in wrong state ${appInstall.status}" - ) - finishInstallation(appInstall, isUpdateWork) - } - } - } - - private suspend fun finishInstallation(appInstall: AppInstall, isUpdateWork: Boolean) { - checkUpdateWork(appInstall, isUpdateWork) + return appInstallWorkRunner.processInstall(fusedDownloadId, isItUpdateWork, runInForeground) } } diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt new file mode 100644 index 000000000..ac9792e65 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.workmanager + +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.install.AppInstallRepository +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.download.DownloadManagerUtils +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.domain.model.install.Status +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.flow.transformWhile +import timber.log.Timber +import javax.inject.Inject + +@Suppress("TooGenericExceptionCaught") // FIXME: Remove suppression and fix detekt +class AppInstallWorkRunner @Inject constructor( + private val appInstallRepository: AppInstallRepository, + private val appManagerWrapper: AppManagerWrapper, + private val downloadManager: DownloadManagerUtils, + private val appUpdateCompletionHandler: AppUpdateCompletionHandler, +) { + @OptIn(DelicateCoroutinesApi::class) + suspend fun processInstall( + fusedDownloadId: String, + isItUpdateWork: Boolean, + runInForeground: suspend (String) -> Unit + ): Result { + var appInstall: AppInstall? = null + try { + Timber.d("Fused download name $fusedDownloadId") + + appInstall = appInstallRepository.getDownloadById(fusedDownloadId) + Timber.i(">>> doWork started for Fused download name ${appInstall?.name} $fusedDownloadId") + + appInstall?.let { + checkDownloadingState(appInstall) + + val isUpdateWork = + isItUpdateWork && appManagerWrapper.isFusedDownloadInstalled(appInstall) + + if (!appInstall.isAppInstalling()) { + Timber.d("!!! returned") + return@let + } + + if (!appManagerWrapper.validateFusedDownload(appInstall)) { + appManagerWrapper.installationIssue(it) + Timber.d("!!! installationIssue") + return@let + } + + if (areFilesDownloadedButNotInstalled(appInstall)) { + Timber.i("===> Downloaded But not installed ${appInstall.name}") + appManagerWrapper.updateDownloadStatus(appInstall, Status.INSTALLING) + } + + runInForeground.invoke(it.name) + + startAppInstallationProcess(it, isUpdateWork) + } + } catch (e: Exception) { + Timber.e( + e, + "Install worker is failed for ${appInstall?.packageName} exception: ${e.localizedMessage}" + ) + appInstall?.let { + appManagerWrapper.cancelDownload(appInstall) + } + } + + Timber.i("doWork: RESULT SUCCESS: ${appInstall?.name}") + return Result.success(ResultStatus.OK) + } + + @OptIn(DelicateCoroutinesApi::class) + private fun checkDownloadingState(appInstall: AppInstall) { + if (appInstall.status == Status.DOWNLOADING) { + appInstall.downloadIdMap.keys.forEach { downloadId -> + downloadManager.updateDownloadStatus(downloadId) + } + } + } + + private fun areFilesDownloadedButNotInstalled(appInstall: AppInstall): Boolean = appInstall.areFilesDownloaded() && + (!appManagerWrapper.isFusedDownloadInstalled(appInstall) || appInstall.status == Status.INSTALLING) + + private suspend fun startAppInstallationProcess(appInstall: AppInstall, isUpdateWork: Boolean) { + if (appInstall.isAwaiting()) { + appManagerWrapper.downloadApp(appInstall, isUpdateWork) + Timber.i("===> doWork: Download started ${appInstall.name} ${appInstall.status}") + } + + appInstallRepository.getDownloadFlowById(appInstall.id) + .transformWhile { + emit(it) + isInstallRunning(it) + } + .collect { latestFusedDownload -> + handleFusedDownload(latestFusedDownload, appInstall, isUpdateWork) + } + } + + private suspend fun handleFusedDownload( + latestAppInstall: AppInstall?, + appInstall: AppInstall, + isUpdateWork: Boolean + ) { + if (latestAppInstall == null) { + Timber.d("===> download null: finish installation") + finishInstallation(appInstall, isUpdateWork) + return + } + + handleFusedDownloadStatusCheckingException(latestAppInstall, isUpdateWork) + } + + private fun isInstallRunning(it: AppInstall?) = + it != null && it.status != Status.INSTALLATION_ISSUE + + private suspend fun handleFusedDownloadStatusCheckingException( + download: AppInstall, + isUpdateWork: Boolean + ) { + try { + handleFusedDownloadStatus(download, isUpdateWork) + } catch (e: Exception) { + val message = + "Handling install status is failed for ${download.packageName} exception: ${e.localizedMessage}" + Timber.e(e, message) + appManagerWrapper.installationIssue(download) + finishInstallation(download, isUpdateWork) + } + } + + private suspend fun handleFusedDownloadStatus(appInstall: AppInstall, isUpdateWork: Boolean) { + when (appInstall.status) { + Status.AWAITING, Status.DOWNLOADING -> Unit + Status.DOWNLOADED -> appManagerWrapper.updateDownloadStatus( + appInstall, + Status.INSTALLING + ) + + Status.INSTALLING -> Timber.i("===> doWork: Installing ${appInstall.name} ${appInstall.status}") + Status.INSTALLED, Status.INSTALLATION_ISSUE -> { + Timber.i("===> doWork: Installed/Failed: ${appInstall.name} ${appInstall.status}") + finishInstallation(appInstall, isUpdateWork) + } + + else -> { + Timber.w("===> ${appInstall.name} is in wrong state ${appInstall.status}") + finishInstallation(appInstall, isUpdateWork) + } + } + } + + private suspend fun finishInstallation(appInstall: AppInstall, isUpdateWork: Boolean) { + appUpdateCompletionHandler.onInstallFinished(appInstall, isUpdateWork) + } +} 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 2b947f98e..0055f81be 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -41,6 +41,7 @@ import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate import foundation.e.apps.data.install.workmanager.AppInstallProcessor +import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler import foundation.e.apps.data.install.workmanager.InstallWorkManager import foundation.e.apps.data.install.wrapper.AppEventDispatcher @@ -125,6 +126,7 @@ class AppInstallProcessorTest { private lateinit var networkStatusChecker: NetworkStatusChecker private lateinit var appInstallAgeLimitGate: AppInstallAgeLimitGate private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler + private lateinit var appInstallWorkRunner: AppInstallWorkRunner private var isInstallWorkManagerMocked = false @@ -163,6 +165,14 @@ class AppInstallProcessorTest { updatesTracker, updatesNotificationSender ) + val downloadManager = + mockk(relaxed = true) + appInstallWorkRunner = AppInstallWorkRunner( + appInstallRepository, + fakeFusedManagerRepository, + downloadManager, + appUpdateCompletionHandler + ) appInstallProcessor = AppInstallProcessor( context, @@ -175,7 +185,7 @@ class AppInstallProcessorTest { storageSpaceChecker, networkStatusChecker, appInstallAgeLimitGate, - appUpdateCompletionHandler + appInstallWorkRunner ) } @@ -821,6 +831,12 @@ class AppInstallProcessorTest { appEventDispatcher, parentalControlAuthGateway ) + val workRunner = AppInstallWorkRunner( + appInstallRepository, + appManagerWrapper, + mockk(relaxed = true), + appUpdateCompletionHandler + ) return AppInstallProcessor( context, appInstallComponents, @@ -832,7 +848,7 @@ class AppInstallProcessorTest { storageSpaceChecker, networkStatusChecker, ageLimitGate, - appUpdateCompletionHandler + workRunner ) } diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt new file mode 100644 index 000000000..7ac00a528 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.installProcessor + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.fdroid.FDroidRepository +import foundation.e.apps.data.install.AppInstallRepository +import foundation.e.apps.data.install.AppManager +import foundation.e.apps.data.install.download.DownloadManagerUtils +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner +import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler +import foundation.e.apps.domain.model.install.Status +import foundation.e.apps.util.MainCoroutineRule +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +class AppInstallWorkRunnerTest { + @Rule + @JvmField + val instantExecutorRule = InstantTaskExecutorRule() + + @get:Rule + var mainCoroutineRule = MainCoroutineRule() + + private lateinit var fakeFusedDownloadDAO: FakeAppInstallDAO + private lateinit var appInstallRepository: AppInstallRepository + private lateinit var fakeFusedManagerRepository: FakeAppManagerWrapper + private lateinit var downloadManagerUtils: DownloadManagerUtils + private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler + private lateinit var workRunner: AppInstallWorkRunner + private lateinit var context: Context + + @Mock + private lateinit var fakeFusedManager: AppManager + + @Mock + private lateinit var fakeFDroidRepository: FDroidRepository + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + context = mockk(relaxed = true) + fakeFusedDownloadDAO = FakeAppInstallDAO() + appInstallRepository = AppInstallRepository(fakeFusedDownloadDAO) + fakeFusedManagerRepository = + FakeAppManagerWrapper(fakeFusedDownloadDAO, context, fakeFusedManager, fakeFDroidRepository) + downloadManagerUtils = mockk(relaxed = true) + appUpdateCompletionHandler = mockk(relaxed = true) + workRunner = AppInstallWorkRunner( + appInstallRepository, + fakeFusedManagerRepository, + downloadManagerUtils, + appUpdateCompletionHandler + ) + } + + @Test + fun processInstall_completesNormalFlow() = runTest { + val fusedDownload = initTest() + + val finalFusedDownload = runProcessInstall(fusedDownload) + + assertTrue(finalFusedDownload == null) + } + + @Test + fun processInstall_keepsBlockedDownloadUntouched() = runTest { + val fusedDownload = initTest() + fusedDownload.status = Status.BLOCKED + + val finalFusedDownload = runProcessInstall(fusedDownload) + + assertEquals(Status.BLOCKED, finalFusedDownload?.status) + } + + @Test + fun processInstall_marksDownloadedFilesAsInstalling() = runTest { + val fusedDownload = initTest() + fusedDownload.downloadIdMap = mutableMapOf(Pair(231, true)) + + val finalFusedDownload = runProcessInstall(fusedDownload) + + assertTrue(finalFusedDownload == null) + } + + @Test + fun processInstall_reportsInvalidPackageAsInstallationIssue() = runTest { + val fusedDownload = initTest(packageName = "") + fusedDownload.downloadIdMap = mutableMapOf(Pair(231, true)) + + val finalFusedDownload = runProcessInstall(fusedDownload) + + assertEquals(Status.INSTALLATION_ISSUE, finalFusedDownload?.status) + } + + @Test + fun processInstall_reportsMissingDownloadUrlsAsInstallationIssue() = runTest { + val fusedDownload = initTest(downloadUrlList = mutableListOf()) + + val finalFusedDownload = runProcessInstall(fusedDownload) + + assertEquals(Status.INSTALLATION_ISSUE, finalFusedDownload?.status) + } + + @Test + fun processInstall_returnsSuccessWhenInternalExceptionOccurs() = runTest { + val fusedDownload = initTest() + fakeFusedManagerRepository.forceCrash = true + + val result = workRunner.processInstall(fusedDownload.id, false) { + // _ignored_ + } + val finalFusedDownload = fakeFusedDownloadDAO.getDownloadById(fusedDownload.id) + + assertTrue(result.isSuccess) + assertEquals(ResultStatus.OK, result.getOrNull()) + assertTrue(finalFusedDownload == null || fusedDownload.status == Status.INSTALLATION_ISSUE) + } + + @Test + fun processInstall_reportsDownloadFailure() = runTest { + val fusedDownload = initTest() + fakeFusedManagerRepository.willDownloadFail = true + + val finalFusedDownload = runProcessInstall(fusedDownload) + + assertEquals(Status.INSTALLATION_ISSUE, finalFusedDownload?.status) + } + + @Test + fun processInstall_reportsInstallFailure() = runTest { + val fusedDownload = initTest() + fakeFusedManagerRepository.willInstallFail = true + + val finalFusedDownload = runProcessInstall(fusedDownload) + + assertEquals(Status.INSTALLATION_ISSUE, finalFusedDownload?.status) + } + + @Test + fun processInstall_updatesDownloadManagerStateForDownloadingItems() = runTest { + val fusedDownload = initTest() + fusedDownload.status = Status.DOWNLOADING + fusedDownload.downloadURLList = mutableListOf() + fusedDownload.downloadIdMap = mutableMapOf(231L to false, 232L to false) + + workRunner.processInstall(fusedDownload.id, false) { + // _ignored_ + } + + verify { downloadManagerUtils.updateDownloadStatus(231L) } + verify { downloadManagerUtils.updateDownloadStatus(232L) } + } + + private suspend fun initTest( + packageName: String? = null, + downloadUrlList: MutableList? = null + ): AppInstall { + val fusedDownload = createFusedDownload(packageName, downloadUrlList) + fakeFusedDownloadDAO.addDownload(fusedDownload) + return fusedDownload + } + + private suspend fun runProcessInstall(appInstall: AppInstall): AppInstall? { + workRunner.processInstall(appInstall.id, false) { + // _ignored_ + } + return fakeFusedDownloadDAO.getDownloadById(appInstall.id) + } + + private fun createFusedDownload( + packageName: String? = null, + downloadUrlList: MutableList? = null + ) = AppInstall( + id = "121", + status = Status.AWAITING, + downloadURLList = downloadUrlList ?: mutableListOf("apk1", "apk2"), + packageName = packageName ?: "com.unit.test" + ) +} -- GitLab From 3973c698ee7a6d7e28c59da6eed14024d5e49873 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 12 Mar 2026 23:49:45 +0600 Subject: [PATCH 07/29] refactor: move Application -> AppInstall conversion logic to AppInstallRequestFactory Move AppInstall construction into a focused factory so request mapping can be tested directly without changing how update detection or enqueueing works. --- .../workmanager/AppInstallProcessor.kt | 27 +---- .../workmanager/AppInstallRequestFactory.kt | 54 +++++++++ .../AppInstallProcessorTest.kt | 9 +- .../AppInstallRequestFactoryTest.kt | 108 ++++++++++++++++++ 4 files changed, 172 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallRequestFactory.kt create mode 100644 app/src/test/java/foundation/e/apps/installProcessor/AppInstallRequestFactoryTest.kt diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt index 797dcf951..c2317d2d5 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt @@ -27,7 +27,6 @@ import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Type import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.install.AppInstallComponents @@ -46,7 +45,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList") // FIXME: Remove suppression and fix detekt class AppInstallProcessor @Inject constructor( @ApplicationContext private val context: Context, private val appInstallComponents: AppInstallComponents, @@ -59,6 +58,7 @@ class AppInstallProcessor @Inject constructor( private val networkStatusChecker: NetworkStatusChecker, private val appInstallAgeLimitGate: AppInstallAgeLimitGate, private val appInstallWorkRunner: AppInstallWorkRunner, + private val appInstallRequestFactory: AppInstallRequestFactory, ) { @Inject lateinit var appManager: AppManager @@ -73,28 +73,7 @@ class AppInstallProcessor @Inject constructor( application: Application, isAnUpdate: Boolean = false ): Boolean { - val appInstall = AppInstall( - application._id, - application.source, - application.status, - application.name, - application.package_name, - mutableListOf(), - mutableMapOf(), - application.status, - application.type, - application.icon_image_path, - application.latest_version_code, - application.offer_type, - application.isFree, - application.originalSize - ).also { - it.contentRating = application.contentRating - } - - if (appInstall.type == Type.PWA || application.source == Source.SYSTEM_APP) { - appInstall.downloadURLList = mutableListOf(application.url) - } + val appInstall = appInstallRequestFactory.create(application) val isUpdate = isAnUpdate || application.status == Status.UPDATABLE || diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallRequestFactory.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallRequestFactory.kt new file mode 100644 index 000000000..2045c39bf --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallRequestFactory.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.workmanager + +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.install.models.AppInstall +import javax.inject.Inject + +class AppInstallRequestFactory @Inject constructor() { + fun create(application: Application): AppInstall { + val appInstall = AppInstall( + application._id, + application.source, + application.status, + application.name, + application.package_name, + mutableListOf(), + mutableMapOf(), + application.status, + application.type, + application.icon_image_path, + application.latest_version_code, + application.offer_type, + application.isFree, + application.originalSize + ).also { + it.contentRating = application.contentRating + } + + if (appInstall.type == Type.PWA || application.source == Source.SYSTEM_APP) { + appInstall.downloadURLList = mutableListOf(application.url) + } + + return appInstall + } +} 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 0055f81be..d117f2edc 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -40,6 +40,7 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate +import foundation.e.apps.data.install.workmanager.AppInstallRequestFactory import foundation.e.apps.data.install.workmanager.AppInstallProcessor import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler @@ -127,6 +128,7 @@ class AppInstallProcessorTest { private lateinit var appInstallAgeLimitGate: AppInstallAgeLimitGate private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler private lateinit var appInstallWorkRunner: AppInstallWorkRunner + private lateinit var appInstallRequestFactory: AppInstallRequestFactory private var isInstallWorkManagerMocked = false @@ -140,6 +142,7 @@ class AppInstallProcessorTest { coEvery { sessionRepository.awaitUser() } returns User.NO_GOOGLE coEvery { playStoreAuthStore.awaitAuthData() } returns null appEventDispatcher = FakeAppEventDispatcher() + appInstallRequestFactory = AppInstallRequestFactory() storageSpaceChecker = mockk(relaxed = true) parentalControlAuthGateway = mockk(relaxed = true) updatesTracker = mockk(relaxed = true) @@ -185,7 +188,8 @@ class AppInstallProcessorTest { storageSpaceChecker, networkStatusChecker, appInstallAgeLimitGate, - appInstallWorkRunner + appInstallWorkRunner, + appInstallRequestFactory ) } @@ -848,7 +852,8 @@ class AppInstallProcessorTest { storageSpaceChecker, networkStatusChecker, ageLimitGate, - workRunner + workRunner, + appInstallRequestFactory ) } diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallRequestFactoryTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallRequestFactoryTest.kt new file mode 100644 index 000000000..53c5a28ef --- /dev/null +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallRequestFactoryTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.installProcessor + +import com.aurora.gplayapi.data.models.ContentRating +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.install.workmanager.AppInstallRequestFactory +import foundation.e.apps.domain.model.install.Status +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class AppInstallRequestFactoryTest { + private lateinit var factory: AppInstallRequestFactory + + @Before + fun setup() { + factory = AppInstallRequestFactory() + } + + @Test + fun create_copiesExpectedFields() { + val application = Application( + _id = "123", + source = Source.PLAY_STORE, + status = Status.AWAITING, + name = "Example", + package_name = "com.example.app", + type = Type.NATIVE, + icon_image_path = "icon.png", + latest_version_code = 42, + offer_type = 1, + isFree = false, + originalSize = 2048L + ) + + val appInstall = factory.create(application) + + assertEquals("123", appInstall.id) + assertEquals(Source.PLAY_STORE, appInstall.source) + assertEquals(Status.AWAITING, appInstall.status) + assertEquals("Example", appInstall.name) + assertEquals("com.example.app", appInstall.packageName) + assertEquals(Type.NATIVE, appInstall.type) + assertEquals("icon.png", appInstall.iconImageUrl) + assertEquals(42, appInstall.versionCode) + assertEquals(1, appInstall.offerType) + assertEquals(false, appInstall.isFree) + assertEquals(2048L, appInstall.appSize) + } + + @Test + fun create_setsContentRating() { + val contentRating = ContentRating() + val application = Application(contentRating = contentRating) + + val appInstall = factory.create(application) + + assertEquals(contentRating, appInstall.contentRating) + } + + @Test + fun create_initializesDirectUrlForPwa() { + val application = Application(type = Type.PWA, url = "https://example.com") + + val appInstall = factory.create(application) + + assertEquals(mutableListOf("https://example.com"), appInstall.downloadURLList) + } + + @Test + fun create_initializesDirectUrlForSystemApp() { + val application = Application(source = Source.SYSTEM_APP, url = "file://app.apk") + + val appInstall = factory.create(application) + + assertEquals(mutableListOf("file://app.apk"), appInstall.downloadURLList) + } + + @Test + fun create_doesNotForceDirectUrlForNativeNonSystemApp() { + val application = + Application(source = Source.PLAY_STORE, type = Type.NATIVE, url = "ignored") + + val appInstall = factory.create(application) + + assertTrue(appInstall.downloadURLList.isEmpty()) + } +} -- GitLab From d6d8b616ff1a6706319f99fed9ca45c5bbca4ce7 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Fri, 13 Mar 2026 00:03:37 +0600 Subject: [PATCH 08/29] refactor: move pre-enqueue and enqueue logic from AppInstallProcessor to a separate class Move enqueue validation and download-link refresh orchestration behind a dedicated coordinator so preflight rules can be exercised directly while the processor stays focused on delegation. --- .../workmanager/AppInstallProcessor.kt | 154 +--------- .../workmanager/AppInstallStartCoordinator.kt | 186 ++++++++++++ .../AppInstallProcessorTest.kt | 50 ++-- .../AppInstallStartCoordinatorTest.kt | 282 ++++++++++++++++++ 4 files changed, 502 insertions(+), 170 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt create mode 100644 app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt index c2317d2d5..23545c09c 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt @@ -18,51 +18,21 @@ package foundation.e.apps.data.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 -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.enums.Type -import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.install.AppInstallComponents -import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.notification.StorageNotificationManager -import foundation.e.apps.data.install.wrapper.AppEventDispatcher -import foundation.e.apps.data.install.wrapper.NetworkStatusChecker -import foundation.e.apps.data.install.wrapper.StorageSpaceChecker -import foundation.e.apps.data.playstore.utils.GplayHttpRequestException -import foundation.e.apps.data.preference.PlayStoreAuthStore -import foundation.e.apps.domain.model.User import foundation.e.apps.domain.model.install.Status -import foundation.e.apps.domain.preferences.SessionRepository -import kotlinx.coroutines.DelicateCoroutinesApi -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") // FIXME: Remove suppression and fix detekt class AppInstallProcessor @Inject constructor( - @ApplicationContext private val context: Context, private val appInstallComponents: AppInstallComponents, - private val applicationRepository: ApplicationRepository, - private val sessionRepository: SessionRepository, - private val playStoreAuthStore: PlayStoreAuthStore, - private val storageNotificationManager: StorageNotificationManager, - private val appEventDispatcher: AppEventDispatcher, - private val storageSpaceChecker: StorageSpaceChecker, - private val networkStatusChecker: NetworkStatusChecker, - private val appInstallAgeLimitGate: AppInstallAgeLimitGate, + private val appInstallStartCoordinator: AppInstallStartCoordinator, private val appInstallWorkRunner: AppInstallWorkRunner, private val appInstallRequestFactory: AppInstallRequestFactory, ) { - @Inject - lateinit var appManager: AppManager - /** * creates [AppInstall] from [Application] and enqueues into WorkManager to run install process. * @param application represents the app info which will be installed @@ -94,132 +64,14 @@ class AppInstallProcessor @Inject constructor( isAnUpdate: Boolean = false, isSystemApp: Boolean = false ): Boolean { - val uniqueWorkName = InstallWorkManager.getUniqueWorkName(appInstall.packageName) - - return try { - val user = sessionRepository.awaitUser() - if (!isSystemApp && (user == User.GOOGLE || user == User.ANONYMOUS)) { - val authData = playStoreAuthStore.awaitAuthData() - if (!appInstall.isFree && authData?.isAnonymous == true) { - appEventDispatcher.dispatch( - AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message) - ) - } - } - - if (!canEnqueue(appInstall)) return false - - appInstallComponents.appManagerWrapper.updateAwaiting(appInstall) - - // Use only for update work for now. For installation work, see InstallOrchestrator#observeDownloads() - if (isAnUpdate) { - InstallWorkManager.enqueueWork(context, appInstall, true) - Timber.d("UPDATE: Successfully enqueued unique work: $uniqueWorkName") - } - true - } catch (e: Exception) { - Timber.e(e, "UPDATE: Failed to enqueue unique work for ${appInstall.packageName}") - appInstallComponents.appManagerWrapper.installationIssue(appInstall) - false - } + return appInstallStartCoordinator.enqueue(appInstall, isAnUpdate, isSystemApp) } @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 (!appInstallAgeLimitGate.allow(appInstall)) { - return false - } - - if (!networkStatusChecker.isNetworkAvailable()) { - appInstallComponents.appManagerWrapper.installationIssue(appInstall) - appEventDispatcher.dispatch(AppEvent.NoInternetEvent(false)) - return false - } - - if (storageSpaceChecker.spaceMissing(appInstall) > 0) { - Timber.d("Storage is not available for: ${appInstall.name} size: ${appInstall.appSize}") - storageNotificationManager.showNotEnoughSpaceNotification(appInstall) - appInstallComponents.appManagerWrapper.installationIssue(appInstall) - appEventDispatcher.dispatch(AppEvent.ErrorMessageEvent(R.string.not_enough_storage)) - return false - } - - return true - } - - // returns TRUE if updating urls is successful, otherwise false. - private suspend fun updateDownloadUrls(appInstall: AppInstall): Boolean { - try { - updateFusedDownloadWithAppDownloadLink(appInstall) - } catch (e: InternalException.AppNotPurchased) { - if (appInstall.isFree) { - handleAppRestricted(appInstall) - return false - } - appInstallComponents.appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) - appEventDispatcher.dispatch(AppEvent.AppPurchaseEvent(appInstall)) - return false - } catch (e: GplayHttpRequestException) { - handleUpdateDownloadError( - appInstall, - "${appInstall.packageName} code: ${e.status} exception: ${e.localizedMessage}", - e - ) - return false - } catch (e: IllegalStateException) { - Timber.e(e) - } catch (e: Exception) { - handleUpdateDownloadError( - appInstall, - "${appInstall.packageName} exception: ${e.localizedMessage}", - e - ) - return false - } - return true - } - - private suspend fun handleAppRestricted(appInstall: AppInstall) { - appEventDispatcher.dispatch(AppEvent.AppRestrictedOrUnavailable(appInstall)) - appManager.addDownload(appInstall) - appManager.updateUnavailable(appInstall) - } - - private suspend fun handleUpdateDownloadError( - appInstall: AppInstall, - message: String, - e: Exception - ) { - Timber.e(e, "Updating download Urls failed for $message") - appEventDispatcher.dispatch( - AppEvent.UpdateEvent( - ResultSupreme.WorkError( - ResultStatus.UNKNOWN, - appInstall - ) - ) - ) - } - - private suspend fun updateFusedDownloadWithAppDownloadLink( - appInstall: AppInstall - ) { - applicationRepository.updateFusedDownloadWithDownloadingInfo( - appInstall.source, - appInstall - ) + return appInstallStartCoordinator.canEnqueue(appInstall) } - @OptIn(DelicateCoroutinesApi::class) suspend fun processInstall( fusedDownloadId: String, isItUpdateWork: Boolean, diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt new file mode 100644 index 000000000..7291aa883 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.workmanager + +import android.content.Context +import com.aurora.gplayapi.exceptions.InternalException +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.R +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.event.AppEvent +import foundation.e.apps.data.install.AppManager +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.notification.StorageNotificationManager +import foundation.e.apps.data.install.wrapper.AppEventDispatcher +import foundation.e.apps.data.install.wrapper.NetworkStatusChecker +import foundation.e.apps.data.install.wrapper.StorageSpaceChecker +import foundation.e.apps.data.playstore.utils.GplayHttpRequestException +import foundation.e.apps.data.preference.PlayStoreAuthStore +import foundation.e.apps.domain.model.User +import foundation.e.apps.domain.preferences.SessionRepository +import timber.log.Timber +import javax.inject.Inject + +@Suppress("LongParameterList", "TooGenericExceptionCaught", "ReturnCount") // FIXME: Remove suppression and fix detekt +class AppInstallStartCoordinator @Inject constructor( + @ApplicationContext private val context: Context, + private val appManagerWrapper: AppManagerWrapper, + private val applicationRepository: ApplicationRepository, + private val sessionRepository: SessionRepository, + private val playStoreAuthStore: PlayStoreAuthStore, + private val storageNotificationManager: StorageNotificationManager, + private val appInstallAgeLimitGate: AppInstallAgeLimitGate, + private val appEventDispatcher: AppEventDispatcher, + private val storageSpaceChecker: StorageSpaceChecker, + private val networkStatusChecker: NetworkStatusChecker, + private val appManager: AppManager, +) { + suspend fun enqueue( + appInstall: AppInstall, + isAnUpdate: Boolean = false, + isSystemApp: Boolean = false + ): Boolean { + val uniqueWorkName = InstallWorkManager.getUniqueWorkName(appInstall.packageName) + + return try { + val user = sessionRepository.awaitUser() + if (!isSystemApp && (user == User.GOOGLE || user == User.ANONYMOUS)) { + val authData = playStoreAuthStore.awaitAuthData() + if (!appInstall.isFree && authData?.isAnonymous == true) { + appEventDispatcher.dispatch( + AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message) + ) + } + } + + if (!canEnqueue(appInstall)) return false + + appManagerWrapper.updateAwaiting(appInstall) + + // Use only for update work for now. For installation work, see InstallOrchestrator#observeDownloads() + if (isAnUpdate) { + InstallWorkManager.enqueueWork(context, appInstall, true) + Timber.d("UPDATE: Successfully enqueued unique work: $uniqueWorkName") + } + true + } catch (e: Exception) { + Timber.e( + e, + "Enqueuing App install work is failed for ${appInstall.packageName} exception: ${e.localizedMessage}" + ) + appManagerWrapper.installationIssue(appInstall) + false + } + } + + suspend fun canEnqueue(appInstall: AppInstall): Boolean { + if (appInstall.type != Type.PWA && !updateDownloadUrls(appInstall)) { + return false + } + + if (!appManagerWrapper.addDownload(appInstall)) { + Timber.i("Update adding ABORTED! status") + return false + } + + if (!appInstallAgeLimitGate.allow(appInstall)) { + return false + } + + if (!networkStatusChecker.isNetworkAvailable()) { + appManagerWrapper.installationIssue(appInstall) + appEventDispatcher.dispatch(AppEvent.NoInternetEvent(false)) + return false + } + + if (storageSpaceChecker.spaceMissing(appInstall) > 0) { + Timber.d("Storage is not available for: ${appInstall.name} size: ${appInstall.appSize}") + storageNotificationManager.showNotEnoughSpaceNotification(appInstall) + appManagerWrapper.installationIssue(appInstall) + appEventDispatcher.dispatch(AppEvent.ErrorMessageEvent(R.string.not_enough_storage)) + return false + } + + return true + } + + private suspend fun updateDownloadUrls(appInstall: AppInstall): Boolean { + try { + updateFusedDownloadWithAppDownloadLink(appInstall) + } catch (_: InternalException.AppNotPurchased) { + if (appInstall.isFree) { + handleAppRestricted(appInstall) + return false + } + appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) + appEventDispatcher.dispatch(AppEvent.AppPurchaseEvent(appInstall)) + return false + } catch (e: GplayHttpRequestException) { + handleUpdateDownloadError( + appInstall, + "${appInstall.packageName} code: ${e.status} exception: ${e.localizedMessage}", + e + ) + return false + } catch (e: IllegalStateException) { + Timber.e(e) + } catch (e: Exception) { + handleUpdateDownloadError( + appInstall, + "${appInstall.packageName} exception: ${e.localizedMessage}", + e + ) + return false + } + return true + } + + private suspend fun handleAppRestricted(appInstall: AppInstall) { + appEventDispatcher.dispatch(AppEvent.AppRestrictedOrUnavailable(appInstall)) + appManager.addDownload(appInstall) + appManager.updateUnavailable(appInstall) + } + + private suspend fun handleUpdateDownloadError( + appInstall: AppInstall, + message: String, + e: Exception + ) { + Timber.e(e, "Updating download Urls failed for $message") + appEventDispatcher.dispatch( + AppEvent.UpdateEvent( + ResultSupreme.WorkError( + ResultStatus.UNKNOWN, + appInstall + ) + ) + ) + } + + private suspend fun updateFusedDownloadWithAppDownloadLink(appInstall: AppInstall) { + applicationRepository.updateFusedDownloadWithDownloadingInfo( + appInstall.source, + appInstall + ) + } +} 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 d117f2edc..190c99107 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -40,8 +40,9 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate -import foundation.e.apps.data.install.workmanager.AppInstallRequestFactory import foundation.e.apps.data.install.workmanager.AppInstallProcessor +import foundation.e.apps.data.install.workmanager.AppInstallRequestFactory +import foundation.e.apps.data.install.workmanager.AppInstallStartCoordinator import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler import foundation.e.apps.data.install.workmanager.InstallWorkManager @@ -126,6 +127,7 @@ class AppInstallProcessorTest { private lateinit var updatesNotificationSender: UpdatesNotificationSender private lateinit var networkStatusChecker: NetworkStatusChecker private lateinit var appInstallAgeLimitGate: AppInstallAgeLimitGate + private lateinit var appInstallStartCoordinator: AppInstallStartCoordinator private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler private lateinit var appInstallWorkRunner: AppInstallWorkRunner private lateinit var appInstallRequestFactory: AppInstallRequestFactory @@ -160,6 +162,19 @@ class AppInstallProcessorTest { appEventDispatcher, parentalControlAuthGateway ) + appInstallStartCoordinator = AppInstallStartCoordinator( + context, + fakeFusedManagerRepository, + applicationRepository, + sessionRepository, + playStoreAuthStore, + storageNotificationManager, + appInstallAgeLimitGate, + appEventDispatcher, + storageSpaceChecker, + networkStatusChecker, + fakeFusedManager + ) appUpdateCompletionHandler = AppUpdateCompletionHandler( context, appInstallRepository, @@ -178,16 +193,8 @@ class AppInstallProcessorTest { ) appInstallProcessor = AppInstallProcessor( - context, appInstallComponents, - applicationRepository, - sessionRepository, - playStoreAuthStore, - storageNotificationManager, - appEventDispatcher, - storageSpaceChecker, - networkStatusChecker, - appInstallAgeLimitGate, + appInstallStartCoordinator, appInstallWorkRunner, appInstallRequestFactory ) @@ -835,23 +842,28 @@ class AppInstallProcessorTest { appEventDispatcher, parentalControlAuthGateway ) - val workRunner = AppInstallWorkRunner( - appInstallRepository, - appManagerWrapper, - mockk(relaxed = true), - appUpdateCompletionHandler - ) - return AppInstallProcessor( + val startCoordinator = AppInstallStartCoordinator( context, - appInstallComponents, + appManagerWrapper, applicationRepository, sessionRepository, playStoreAuthStore, storageNotificationManager, + ageLimitGate, appEventDispatcher, storageSpaceChecker, networkStatusChecker, - ageLimitGate, + fakeFusedManager + ) + val workRunner = AppInstallWorkRunner( + appInstallRepository, + appManagerWrapper, + mockk(relaxed = true), + appUpdateCompletionHandler + ) + return AppInstallProcessor( + appInstallComponents, + startCoordinator, workRunner, appInstallRequestFactory ) diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt new file mode 100644 index 000000000..2385a90a2 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.installProcessor + +import android.content.Context +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.exceptions.InternalException +import foundation.e.apps.R +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.event.AppEvent +import foundation.e.apps.data.install.AppManager +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.notification.StorageNotificationManager +import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate +import foundation.e.apps.data.install.workmanager.AppInstallStartCoordinator +import foundation.e.apps.data.install.workmanager.InstallWorkManager +import foundation.e.apps.data.install.wrapper.NetworkStatusChecker +import foundation.e.apps.data.install.wrapper.StorageSpaceChecker +import foundation.e.apps.data.playstore.utils.GplayHttpRequestException +import foundation.e.apps.data.preference.PlayStoreAuthStore +import foundation.e.apps.domain.model.User +import foundation.e.apps.domain.model.install.Status +import foundation.e.apps.domain.preferences.SessionRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AppInstallStartCoordinatorTest { + private lateinit var context: Context + private lateinit var appManagerWrapper: AppManagerWrapper + private lateinit var applicationRepository: ApplicationRepository + private lateinit var sessionRepository: SessionRepository + private lateinit var playStoreAuthStore: PlayStoreAuthStore + private lateinit var storageNotificationManager: StorageNotificationManager + private lateinit var appInstallAgeLimitGate: AppInstallAgeLimitGate + private lateinit var appEventDispatcher: FakeAppEventDispatcher + private lateinit var storageSpaceChecker: StorageSpaceChecker + private lateinit var networkStatusChecker: NetworkStatusChecker + private lateinit var appManager: AppManager + private lateinit var coordinator: AppInstallStartCoordinator + + @Before + fun setup() { + context = mockk(relaxed = true) + appManagerWrapper = mockk(relaxed = true) + applicationRepository = mockk(relaxed = true) + sessionRepository = mockk(relaxed = true) + playStoreAuthStore = mockk(relaxed = true) + storageNotificationManager = mockk(relaxed = true) + appInstallAgeLimitGate = mockk(relaxed = true) + appEventDispatcher = FakeAppEventDispatcher() + storageSpaceChecker = mockk(relaxed = true) + networkStatusChecker = mockk(relaxed = true) + appManager = mockk(relaxed = true) + coEvery { sessionRepository.awaitUser() } returns User.NO_GOOGLE + coEvery { playStoreAuthStore.awaitAuthData() } returns null + coordinator = AppInstallStartCoordinator( + context, + appManagerWrapper, + applicationRepository, + sessionRepository, + playStoreAuthStore, + storageNotificationManager, + appInstallAgeLimitGate, + appEventDispatcher, + storageSpaceChecker, + networkStatusChecker, + appManager + ) + } + + @Test + fun canEnqueue_returnsTrueWhenAllChecksPass() = runTest { + val appInstall = createPwaInstall() + + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = coordinator.canEnqueue(appInstall) + + assertTrue(result) + } + + @Test + fun canEnqueue_returnsFalseWhenNetworkUnavailable() = runTest { + val appInstall = createPwaInstall() + + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + every { networkStatusChecker.isNetworkAvailable() } returns false + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = coordinator.canEnqueue(appInstall) + + assertFalse(result) + coVerify { appManagerWrapper.installationIssue(appInstall) } + } + + @Test + fun canEnqueue_returnsFalseWhenStorageMissing() = runTest { + val appInstall = createPwaInstall() + + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 100L + + val result = coordinator.canEnqueue(appInstall) + + assertFalse(result) + verify { storageNotificationManager.showNotEnoughSpaceNotification(appInstall) } + coVerify { appManagerWrapper.installationIssue(appInstall) } + } + + @Test + fun canEnqueue_returnsFalseWhenAddDownloadFails() = runTest { + val appInstall = createPwaInstall() + + coEvery { appManagerWrapper.addDownload(appInstall) } returns false + + val result = coordinator.canEnqueue(appInstall) + + assertFalse(result) + coVerify(exactly = 0) { appInstallAgeLimitGate.allow(any()) } + } + + @Test + fun enqueue_warnsAnonymousPaidUsersWithoutAborting() = runTest { + val appInstall = createPwaInstall(isFree = false) + + mockkObject(InstallWorkManager) + try { + coEvery { sessionRepository.awaitUser() } returns User.ANONYMOUS + coEvery { + playStoreAuthStore.awaitAuthData() + } returns AuthData(email = "anon@example.com", isAnonymous = true) + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + justRun { InstallWorkManager.enqueueWork(any(), any(), any()) } + + val result = coordinator.enqueue(appInstall) + + assertTrue(result) + assertTrue(appEventDispatcher.events.any { + it is AppEvent.ErrorMessageEvent && it.data == R.string.paid_app_anonymous_message + }) + coVerify { appManagerWrapper.updateAwaiting(appInstall) } + verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any(), any()) } + } finally { + unmockkObject(InstallWorkManager) + } + } + + @Test + fun canEnqueue_handlesFreeAppNotPurchasedAsRestricted() = runTest { + val appInstall = createNativeInstall(isFree = true) + + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo( + Source.PLAY_STORE, + appInstall + ) + } throws InternalException.AppNotPurchased() + + val result = coordinator.canEnqueue(appInstall) + + assertFalse(result) + assertTrue(appEventDispatcher.events.any { it is AppEvent.AppRestrictedOrUnavailable }) + coVerify { appManager.addDownload(appInstall) } + coVerify { appManager.updateUnavailable(appInstall) } + } + + @Test + fun canEnqueue_handlesPaidAppNotPurchasedAsPurchaseNeeded() = runTest { + val appInstall = createNativeInstall(isFree = false) + + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo( + Source.PLAY_STORE, + appInstall + ) + } throws InternalException.AppNotPurchased() + + val result = coordinator.canEnqueue(appInstall) + + assertFalse(result) + coVerify { appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) } + assertTrue(appEventDispatcher.events.any { it is AppEvent.AppPurchaseEvent }) + } + + @Test + fun canEnqueue_returnsFalseWhenDownloadUrlRefreshThrowsHttpError() = runTest { + val appInstall = createNativeInstall() + + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo( + Source.PLAY_STORE, + appInstall + ) + } throws GplayHttpRequestException(403, "forbidden") + + val result = coordinator.canEnqueue(appInstall) + + assertFalse(result) + assertTrue(appEventDispatcher.events.any { it is AppEvent.UpdateEvent }) + coVerify(exactly = 0) { appManagerWrapper.addDownload(appInstall) } + } + + @Test + fun canEnqueue_keepsGoingWhenDownloadUrlRefreshThrowsIllegalState() = runTest { + val appInstall = createNativeInstall() + + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo( + Source.PLAY_STORE, + appInstall + ) + } throws IllegalStateException("boom") + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = coordinator.canEnqueue(appInstall) + + assertTrue(result) + coVerify { appManagerWrapper.addDownload(appInstall) } + } + + private fun createPwaInstall(isFree: Boolean = true) = AppInstall( + type = Type.PWA, + id = "123", + status = Status.AWAITING, + downloadURLList = mutableListOf("apk"), + packageName = "com.example.app", + isFree = isFree + ) + + private fun createNativeInstall(isFree: Boolean = true) = AppInstall( + type = Type.NATIVE, + source = Source.PLAY_STORE, + id = "123", + status = Status.AWAITING, + packageName = "com.example.app", + isFree = isFree + ) +} -- GitLab From 91be6ed744225e9fa860874ed87ad7b869de6b0e Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Fri, 13 Mar 2026 00:10:36 +0600 Subject: [PATCH 09/29] refactor: make AppInstallProcessor to facade behavior Keep the processor focused on request-to-collaborator delegation so its tests describe the public contract while the extracted components own the detailed behavior coverage. --- .../workmanager/AppInstallProcessor.kt | 6 - .../AppInstallProcessorTest.kt | 885 +----------------- 2 files changed, 40 insertions(+), 851 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt index 23545c09c..3be4b212e 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt @@ -18,7 +18,6 @@ package foundation.e.apps.data.install.workmanager -import androidx.annotation.VisibleForTesting import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.AppInstallComponents @@ -67,11 +66,6 @@ class AppInstallProcessor @Inject constructor( return appInstallStartCoordinator.enqueue(appInstall, isAnUpdate, isSystemApp) } - @VisibleForTesting - suspend fun canEnqueue(appInstall: AppInstall): Boolean { - return appInstallStartCoordinator.canEnqueue(appInstall) - } - suspend fun processInstall( fusedDownloadId: String, isItUpdateWork: Boolean, 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 190c99107..6755c9f32 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright MURENA SAS 2023 + * Copyright MURENA SAS 2026 * Apps Quickly and easily install Android apps onto your device! * * This program is free software: you can redistribute it and/or modify @@ -18,179 +18,55 @@ package foundation.e.apps.installProcessor -import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.work.Operation -import com.aurora.gplayapi.data.models.AuthData -import com.google.common.util.concurrent.Futures -import foundation.e.apps.R -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Type -import foundation.e.apps.data.event.AppEvent import foundation.e.apps.domain.model.install.Status -import foundation.e.apps.data.fdroid.FDroidRepository import foundation.e.apps.data.install.AppInstallComponents import foundation.e.apps.data.install.AppInstallRepository -import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.notification.StorageNotificationManager -import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate import foundation.e.apps.data.install.workmanager.AppInstallProcessor import foundation.e.apps.data.install.workmanager.AppInstallRequestFactory import foundation.e.apps.data.install.workmanager.AppInstallStartCoordinator import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner -import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler -import foundation.e.apps.data.install.workmanager.InstallWorkManager -import foundation.e.apps.data.install.wrapper.AppEventDispatcher -import foundation.e.apps.data.install.wrapper.NetworkStatusChecker -import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway -import foundation.e.apps.data.install.wrapper.StorageSpaceChecker -import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender -import foundation.e.apps.data.install.wrapper.UpdatesTracker -import foundation.e.apps.data.playstore.utils.GplayHttpRequestException -import foundation.e.apps.data.preference.PlayStoreAuthStore -import foundation.e.apps.domain.ValidateAppAgeLimitUseCase -import foundation.e.apps.domain.model.ContentRatingValidity -import foundation.e.apps.domain.model.User -import foundation.e.apps.domain.preferences.SessionRepository import foundation.e.apps.util.MainCoroutineRule import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every -import io.mockk.justRun import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.unmockkObject -import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever -import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) class AppInstallProcessorTest { - // Run tasks synchronously @Rule @JvmField val instantExecutorRule = InstantTaskExecutorRule() - // Sets the main coroutines dispatcher to a TestCoroutineScope for unit testing. - @ExperimentalCoroutinesApi @get:Rule var mainCoroutineRule = MainCoroutineRule() - private lateinit var fakeFusedDownloadDAO: FakeAppInstallDAO - private lateinit var appInstallRepository: AppInstallRepository - private lateinit var fakeFusedManagerRepository: FakeAppManagerWrapper - - @Mock - private lateinit var fakeFusedManager: AppManager - - @Mock - private lateinit var fakeFDroidRepository: FDroidRepository - - private lateinit var context: Context - - private lateinit var sessionRepository: SessionRepository - - private lateinit var playStoreAuthStore: PlayStoreAuthStore - - private lateinit var applicationRepository: ApplicationRepository - + private lateinit var appManagerWrapper: AppManagerWrapper private lateinit var appInstallProcessor: AppInstallProcessor - - @Mock - private lateinit var validateAppAgeRatingUseCase: ValidateAppAgeLimitUseCase - - @Mock - private lateinit var storageNotificationManager: StorageNotificationManager - - private lateinit var appEventDispatcher: FakeAppEventDispatcher - private lateinit var storageSpaceChecker: StorageSpaceChecker - private lateinit var parentalControlAuthGateway: ParentalControlAuthGateway - private lateinit var updatesTracker: UpdatesTracker - private lateinit var updatesNotificationSender: UpdatesNotificationSender - private lateinit var networkStatusChecker: NetworkStatusChecker - private lateinit var appInstallAgeLimitGate: AppInstallAgeLimitGate + private lateinit var appInstallRequestFactory: AppInstallRequestFactory private lateinit var appInstallStartCoordinator: AppInstallStartCoordinator - private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler private lateinit var appInstallWorkRunner: AppInstallWorkRunner - private lateinit var appInstallRequestFactory: AppInstallRequestFactory - - private var isInstallWorkManagerMocked = false @Before fun setup() { - MockitoAnnotations.openMocks(this) - context = mockk(relaxed = true) - sessionRepository = mockk(relaxed = true) - playStoreAuthStore = mockk(relaxed = true) - applicationRepository = mockk(relaxed = true) - coEvery { sessionRepository.awaitUser() } returns User.NO_GOOGLE - coEvery { playStoreAuthStore.awaitAuthData() } returns null - appEventDispatcher = FakeAppEventDispatcher() - appInstallRequestFactory = AppInstallRequestFactory() - storageSpaceChecker = mockk(relaxed = true) - parentalControlAuthGateway = mockk(relaxed = true) - updatesTracker = mockk(relaxed = true) - updatesNotificationSender = mockk(relaxed = true) - networkStatusChecker = mockk(relaxed = true) - fakeFusedDownloadDAO = FakeAppInstallDAO() - appInstallRepository = AppInstallRepository(fakeFusedDownloadDAO) - fakeFusedManagerRepository = - FakeAppManagerWrapper(fakeFusedDownloadDAO, context, fakeFusedManager, fakeFDroidRepository) - val appInstallComponents = - AppInstallComponents(appInstallRepository, fakeFusedManagerRepository) - appInstallAgeLimitGate = AppInstallAgeLimitGate( - validateAppAgeRatingUseCase, - fakeFusedManagerRepository, - appEventDispatcher, - parentalControlAuthGateway - ) - appInstallStartCoordinator = AppInstallStartCoordinator( - context, - fakeFusedManagerRepository, - applicationRepository, - sessionRepository, - playStoreAuthStore, - storageNotificationManager, - appInstallAgeLimitGate, - appEventDispatcher, - storageSpaceChecker, - networkStatusChecker, - fakeFusedManager - ) - appUpdateCompletionHandler = AppUpdateCompletionHandler( - context, - appInstallRepository, - fakeFusedManagerRepository, - playStoreAuthStore, - updatesTracker, - updatesNotificationSender - ) - val downloadManager = - mockk(relaxed = true) - appInstallWorkRunner = AppInstallWorkRunner( - appInstallRepository, - fakeFusedManagerRepository, - downloadManager, - appUpdateCompletionHandler - ) + appManagerWrapper = mockk(relaxed = true) + val appInstallRepository = mockk(relaxed = true) + val appInstallComponents = AppInstallComponents(appInstallRepository, appManagerWrapper) + appInstallRequestFactory = mockk(relaxed = true) + appInstallStartCoordinator = mockk(relaxed = true) + appInstallWorkRunner = mockk(relaxed = true) appInstallProcessor = AppInstallProcessor( appInstallComponents, @@ -200,737 +76,56 @@ class AppInstallProcessorTest { ) } - @After - fun teardown() { - if (isInstallWorkManagerMocked) { - unmockkObject(InstallWorkManager) - isInstallWorkManagerMocked = false - } - } - - @Test - fun processInstallTest() = runTest { - val fusedDownload = initTest() - - val finalFusedDownload = runProcessInstall(fusedDownload) - assertTrue("processInstall", finalFusedDownload == null) - } - - private suspend fun initTest( - packageName: String? = null, - downloadUrlList: MutableList? = null - ): AppInstall { - val fusedDownload = createFusedDownload(packageName, downloadUrlList) - fakeFusedDownloadDAO.addDownload(fusedDownload) - return fusedDownload - } - - @Test - fun `processInstallTest when FusedDownload is already failed`() = runTest { - val fusedDownload = initTest() - fusedDownload.status = Status.BLOCKED - - val finalFusedDownload = runProcessInstall(fusedDownload) - assertEquals("processInstall", Status.BLOCKED, finalFusedDownload?.status) - } - - @Test - fun `processInstallTest when files are downloaded but not installed`() = runTest { - val fusedDownload = initTest() - fusedDownload.downloadIdMap = mutableMapOf(Pair(231, true)) - - val finalFusedDownload = runProcessInstall(fusedDownload) - assertTrue("processInstall", finalFusedDownload == null) - } - - @Test - fun `processInstallTest when packageName is empty and files are downloaded`() = runTest { - val fusedDownload = initTest(packageName = "") - fusedDownload.downloadIdMap = mutableMapOf(Pair(231, true)) - - val finalFusedDownload = runProcessInstall(fusedDownload) - assertEquals("processInstall", Status.INSTALLATION_ISSUE, finalFusedDownload?.status) - } - - @Test - fun `processInstallTest when downloadUrls are not available`() = runTest { - val fusedDownload = initTest(downloadUrlList = mutableListOf()) - - val finalFusedDownload = runProcessInstall(fusedDownload) - assertEquals("processInstall", Status.INSTALLATION_ISSUE, finalFusedDownload?.status) - } - - @Test - fun `processInstallTest when exception is occurred`() = runTest { - val fusedDownload = initTest() - fakeFusedManagerRepository.forceCrash = true - - val finalFusedDownload = runProcessInstall(fusedDownload) - assertTrue( - "processInstall", - finalFusedDownload == null || fusedDownload.status == Status.INSTALLATION_ISSUE - ) - } - - @Test - fun `processInstallTest when download is failed`() = runTest { - val fusedDownload = initTest() - fakeFusedManagerRepository.willDownloadFail = true - - val finalFusedDownload = runProcessInstall(fusedDownload) - assertEquals("processInstall", Status.INSTALLATION_ISSUE, finalFusedDownload?.status) - } - - @Test - fun `processInstallTest when install is failed`() = runTest { - val fusedDownload = initTest() - fakeFusedManagerRepository.willInstallFail = true - - val finalFusedDownload = runProcessInstall(fusedDownload) - assertEquals("processInstall", Status.INSTALLATION_ISSUE, finalFusedDownload?.status) - } - - @Test - fun `processInstallTest when age limit is satisfied`() = runTest { - val fusedDownload = initTest() - Mockito.`when`(validateAppAgeRatingUseCase(fusedDownload)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - - val finalFusedDownload = runProcessInstall(fusedDownload) - assertEquals("processInstall", finalFusedDownload, null) - } - - @Test - fun `processInstallTest when age limit is not satisfied`() = runTest { - val fusedDownload = initTest() - Mockito.`when`(validateAppAgeRatingUseCase(fusedDownload)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(false))) - - val finalFusedDownload = runProcessInstall(fusedDownload) - assertEquals("processInstall", finalFusedDownload, null) - } - - @Test - fun `enqueueFusedDownload warns anonymous paid users without aborting`() = runTest { - val appInstall = AppInstall( - type = foundation.e.apps.data.enums.Type.PWA, - id = "123", - status = Status.AWAITING, - downloadURLList = mutableListOf("apk"), - packageName = "com.example.paid", - isFree = false - ) - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - mockkObject(InstallWorkManager) - try { - coEvery { sessionRepository.awaitUser() } returns User.ANONYMOUS - coEvery { - playStoreAuthStore.awaitAuthData() - } returns AuthData(email = "anon@example.com", isAnonymous = true) - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { networkStatusChecker.isNetworkAvailable() } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - justRun { InstallWorkManager.enqueueWork(any(), any(), any()) } - - val result = processor.enqueueFusedDownload(appInstall) - - assertTrue(result) - assertTrue(appEventDispatcher.events.any { - it is AppEvent.ErrorMessageEvent && it.data == R.string.paid_app_anonymous_message - }) - coVerify { appManagerWrapper.updateAwaiting(appInstall) } - verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any(), any()) } - } finally { - unmockkObject(InstallWorkManager) - } - } - @Test - fun `canEnqueue refreshes download urls for non-PWA installs`() = runTest { - val appInstall = AppInstall( - type = foundation.e.apps.data.enums.Type.NATIVE, + fun initAppInstall_computesUpdateFlagAndDelegates() = runTest { + val application = Application( + _id = "123", source = Source.PLAY_STORE, - id = "123", - status = Status.AWAITING, - packageName = "com.example.app" - ) - - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { networkStatusChecker.isNetworkAvailable() } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + status = Status.UPDATABLE, + name = "Example", + package_name = "com.example.app", + type = Type.NATIVE + ) + val appInstall = AppInstall(id = "123", packageName = "com.example.app") + coEvery { appInstallRequestFactory.create(application) } returns appInstall + coEvery { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns false + coEvery { + appInstallStartCoordinator.enqueue( + appInstall, + true, + application.isSystemApp + ) + } returns true - val result = processor.canEnqueue(appInstall) + val result = appInstallProcessor.initAppInstall(application) assertTrue(result) - coVerify { applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) } - coVerify { appManagerWrapper.addDownload(appInstall) } + coVerify { appInstallRequestFactory.create(application) } + coVerify { appInstallStartCoordinator.enqueue(appInstall, true, application.isSystemApp) } } @Test - fun `canEnqueue returns false when download url refresh throws http error`() = runTest { - val appInstall = AppInstall( - type = foundation.e.apps.data.enums.Type.NATIVE, - source = Source.PLAY_STORE, - id = "123", - status = Status.AWAITING, - packageName = "com.example.app" - ) + fun enqueueFusedDownload_delegatesResult() = runTest { + val appInstall = AppInstall(id = "123", packageName = "com.example.app") + coEvery { appInstallStartCoordinator.enqueue(appInstall, true, true) } returns false - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - coEvery { - applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) - } throws GplayHttpRequestException(403, "forbidden") - - val result = processor.canEnqueue(appInstall) + val result = appInstallProcessor.enqueueFusedDownload(appInstall, true, true) assertEquals(false, result) - assertTrue(appEventDispatcher.events.any { it is AppEvent.UpdateEvent }) - coVerify(exactly = 0) { appManagerWrapper.addDownload(appInstall) } + coVerify { appInstallStartCoordinator.enqueue(appInstall, true, true) } } @Test - fun `canEnqueue keeps going when download url refresh throws illegal state`() = runTest { - val appInstall = AppInstall( - type = foundation.e.apps.data.enums.Type.NATIVE, - source = Source.PLAY_STORE, - id = "123", - status = Status.AWAITING, - packageName = "com.example.app" - ) - - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - + fun processInstall_delegatesResult() = runTest { coEvery { - applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) - } throws IllegalStateException("boom") - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { networkStatusChecker.isNetworkAvailable() } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - - val result = processor.canEnqueue(appInstall) + appInstallWorkRunner.processInstall("123", false, any()) + } returns Result.success(ResultStatus.OK) - assertTrue(result) - coVerify { appManagerWrapper.addDownload(appInstall) } - } - - @Test - fun `processInstall returns success when internal exception occurs`() = runTest { - val fusedDownload = initTest() - fakeFusedManagerRepository.forceCrash = true - - val result = appInstallProcessor.processInstall(fusedDownload.id, false) { + val result = appInstallProcessor.processInstall("123", false) { // _ignored_ } - assertTrue(result.isSuccess) assertEquals(ResultStatus.OK, result.getOrNull()) + coVerify { appInstallWorkRunner.processInstall("123", false, any()) } } - - @Test - fun `processInstall enters foreground before download starts`() = runTest { - val fusedDownload = initTest() - var statusAtForeground: Status? = null - - appInstallProcessor.processInstall(fusedDownload.id, false) { - statusAtForeground = fusedDownload.status - } - - assertEquals(Status.AWAITING, statusAtForeground) - } - - @Test - fun `processInstall update completion ignores installation issues`() = runTest { - stubUpdateNotificationContext() - every { updatesTracker.hasSuccessfulUpdatedApps() } returns true - every { updatesTracker.successfulUpdatedAppsCount() } returns 1 - - processCompletedUpdate(Status.INSTALLATION_ISSUE) - - verify { updatesTracker.addSuccessfullyUpdatedApp(any()) } - verify { updatesNotificationSender.showNotification("Update", "Updated message") } - verify { updatesTracker.clearSuccessfullyUpdatedApps() } - } - - @Test - fun `processInstall update completion ignores purchase needed`() = runTest { - stubUpdateNotificationContext() - every { updatesTracker.hasSuccessfulUpdatedApps() } returns true - every { updatesTracker.successfulUpdatedAppsCount() } returns 1 - - processCompletedUpdate(Status.PURCHASE_NEEDED) - - verify { updatesTracker.addSuccessfullyUpdatedApp(any()) } - verify { updatesNotificationSender.showNotification("Update", "Updated message") } - verify { updatesTracker.clearSuccessfullyUpdatedApps() } - } - - @Test - fun `processInstall clears tracked updates after final notification`() = runTest { - stubUpdateNotificationContext() - every { updatesTracker.hasSuccessfulUpdatedApps() } returns true - every { updatesTracker.successfulUpdatedAppsCount() } returns 1 - - processCompletedUpdate() - - verify { updatesTracker.clearSuccessfullyUpdatedApps() } - } - - @Test - fun canEnqueue_returnsTrueWhenAllChecksPass() = runTest { - val appInstall = AppInstall( - type = Type.PWA, - id = "123", - status = Status.AWAITING, - downloadURLList = mutableListOf("apk"), - packageName = "com.example.app" - ) - - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { networkStatusChecker.isNetworkAvailable() } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0 - - val result = processor.canEnqueue(appInstall) - - assertTrue(result) - coVerify { appManagerWrapper.addDownload(appInstall) } - } - - @Test - fun canEnqueue_returnsFalseWhenNetworkUnavailable() = runTest { - val appInstall = AppInstall( - type = Type.PWA, - id = "123", - status = Status.AWAITING, - downloadURLList = mutableListOf("apk"), - packageName = "com.example.app" - ) - - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { networkStatusChecker.isNetworkAvailable() } returns false - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0 - - val result = processor.canEnqueue(appInstall) - - assertEquals(false, result) - coVerify { appManagerWrapper.installationIssue(appInstall) } - } - - @Test - fun canEnqueue_returnsFalseWhenStorageMissing() = runTest { - val appInstall = AppInstall( - type = Type.PWA, - id = "123", - status = Status.AWAITING, - downloadURLList = mutableListOf("apk"), - packageName = "com.example.app" - ) - - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { networkStatusChecker.isNetworkAvailable() } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 100L - - val result = processor.canEnqueue(appInstall) - - assertEquals(false, result) - Mockito.verify(storageNotificationManager).showNotEnoughSpaceNotification(appInstall) - coVerify { appManagerWrapper.installationIssue(appInstall) } - } - - @Test - fun canEnqueue_returnsFalseWhenAddDownloadFails() = runTest { - val appInstall = AppInstall( - type = Type.PWA, - id = "123", - status = Status.AWAITING, - downloadURLList = mutableListOf("apk"), - packageName = "com.example.app" - ) - - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - coEvery { appManagerWrapper.addDownload(appInstall) } returns false - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { networkStatusChecker.isNetworkAvailable() } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - - val result = processor.canEnqueue(appInstall) - - assertEquals(false, result) - coVerify(exactly = 0) { appManagerWrapper.installationIssue(appInstall) } - } - - @Test - fun canEnqueue_returnsFalseWhenAgeLimitInvalid() = runTest { - val appInstall = AppInstall( - type = Type.PWA, - id = "123", - status = Status.AWAITING, - downloadURLList = mutableListOf("apk"), - packageName = "com.example.app" - ) - - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.UNKNOWN, ContentRatingValidity(false))) - every { networkStatusChecker.isNetworkAvailable() } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - - val result = processor.canEnqueue(appInstall) - - assertEquals(false, result) - coVerify { appManagerWrapper.cancelDownload(appInstall) } - } - - @Test - fun enqueueFusedDownload_returnsTrueAndEnqueuesWorkForUpdate() = runTest { - val appInstall = createEnqueueAppInstall() - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - mockInstallWorkManagerSuccess() - every { networkStatusChecker.isNetworkAvailable() } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - - val result = processor.enqueueFusedDownload(appInstall, isAnUpdate = true, isSystemApp = true) - - assertTrue(result) - coVerify { appManagerWrapper.updateAwaiting(appInstall) } - verify(exactly = 1) { InstallWorkManager.enqueueWork(context, appInstall, true) } - } - - @Test - fun enqueueFusedDownload_returnsFalseAndMarksIssueWhenUpdateEnqueueFails() = runTest { - val appInstall = createEnqueueAppInstall() - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - mockInstallWorkManagerFailure() - every { networkStatusChecker.isNetworkAvailable() } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - - val result = processor.enqueueFusedDownload(appInstall, isAnUpdate = true, isSystemApp = true) - - assertEquals(false, result) - coVerify { appManagerWrapper.updateAwaiting(appInstall) } - coVerify { appManagerWrapper.installationIssue(appInstall) } - } - - @Test - fun enqueueFusedDownload_returnsTrueWithoutEnqueueingWorkForRegularInstall() = runTest { - val appInstall = createEnqueueAppInstall() - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - mockInstallWorkManagerSuccess() - every { networkStatusChecker.isNetworkAvailable() } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - - val result = processor.enqueueFusedDownload(appInstall, isAnUpdate = false, isSystemApp = true) - - assertTrue(result) - coVerify { appManagerWrapper.updateAwaiting(appInstall) } - verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any(), any()) } - } - - @Test - fun enqueueFusedDownload_skipsWorkManagerFailurePathForRegularInstall() = runTest { - val appInstall = createEnqueueAppInstall() - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - mockInstallWorkManagerFailure() - every { networkStatusChecker.isNetworkAvailable() } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - - val result = processor.enqueueFusedDownload(appInstall, isAnUpdate = false, isSystemApp = true) - - assertTrue(result) - verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any(), any()) } - coVerify(exactly = 0) { appManagerWrapper.installationIssue(appInstall) } - } - - @Test - fun initAppInstall_enqueuesUpdateWorkWhenExplicitFlagIsTrue() = runTest { - val application = createApplication(status = Status.INSTALLED) - val appInstall = createExpectedAppInstall(application) - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - mockInstallWorkManagerSuccess() - every { networkStatusChecker.isNetworkAvailable() } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns false - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - - val result = processor.initAppInstall(application, isAnUpdate = true) - - assertTrue(result) - verify(exactly = 1) { InstallWorkManager.enqueueWork(context, appInstall, true) } - } - - @Test - fun initAppInstall_enqueuesUpdateWorkWhenApplicationIsUpdatable() = runTest { - val application = createApplication(status = Status.UPDATABLE) - val appInstall = createExpectedAppInstall(application) - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - mockInstallWorkManagerSuccess() - every { networkStatusChecker.isNetworkAvailable() } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns false - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - - val result = processor.initAppInstall(application, isAnUpdate = false) - - assertTrue(result) - verify(exactly = 1) { InstallWorkManager.enqueueWork(context, appInstall, true) } - } - - @Test - fun initAppInstall_enqueuesUpdateWorkWhenAppIsAlreadyInstalled() = runTest { - val application = createApplication(status = Status.INSTALLED) - val appInstall = createExpectedAppInstall(application) - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - mockInstallWorkManagerSuccess() - every { networkStatusChecker.isNetworkAvailable() } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns true - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - - val result = processor.initAppInstall(application, isAnUpdate = false) - - assertTrue(result) - verify(exactly = 1) { InstallWorkManager.enqueueWork(context, appInstall, true) } - } - - @Test - fun initAppInstall_doesNotEnqueueWorkWhenInstallIsNotAnUpdate() = runTest { - val application = createApplication(status = Status.INSTALLED) - val appInstall = createExpectedAppInstall(application) - val appManagerWrapper = mockk(relaxed = true) - val processor = createProcessorForCanEnqueue(appManagerWrapper) - - mockInstallWorkManagerFailure() - every { networkStatusChecker.isNetworkAvailable() } returns true - Mockito.`when`(validateAppAgeRatingUseCase(appInstall)) - .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true))) - every { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns false - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - - val result = processor.initAppInstall(application, isAnUpdate = false) - - assertTrue(result) - verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any(), any()) } - } - - private suspend fun runProcessInstall(appInstall: AppInstall): AppInstall? { - appInstallProcessor.processInstall(appInstall.id, false) { - // _ignored_ - } - return fakeFusedDownloadDAO.getDownloadById(appInstall.id) - } - - private suspend fun processCompletedUpdate(ignoredStatus: Status? = null) { - val fusedDownload = initTest() - fakeFusedManagerRepository.isAppInstalled = true - - ignoredStatus?.let { - fakeFusedDownloadDAO.addDownload( - AppInstall( - id = "ignored-$it", - status = it, - downloadURLList = mutableListOf("apk"), - packageName = "com.example.$it" - ) - ) - } - - appInstallProcessor.processInstall(fusedDownload.id, true) { - // _ignored_ - } - } - - private suspend fun stubUpdateNotificationContext() { - val authData = AuthData(email = "user@example.com", isAnonymous = false).apply { - locale = Locale.US - } - coEvery { playStoreAuthStore.awaitAuthData() } returns authData - every { context.getString(R.string.update) } returns "Update" - every { - context.getString( - R.string.message_last_update_triggered, - any(), - any() - ) - } returns "Updated message" - } - - private fun createFusedDownload( - packageName: String? = null, - downloadUrlList: MutableList? = null - ) = AppInstall( - id = "121", - status = Status.AWAITING, - downloadURLList = downloadUrlList ?: mutableListOf("apk1", "apk2"), - packageName = packageName ?: "com.unit.test" - ) - - private fun createProcessorForCanEnqueue( - appManagerWrapper: AppManagerWrapper - ): AppInstallProcessor { - val appInstallRepository = AppInstallRepository(FakeAppInstallDAO()) - val appInstallComponents = AppInstallComponents(appInstallRepository, appManagerWrapper) - val ageLimitGate = AppInstallAgeLimitGate( - validateAppAgeRatingUseCase, - appManagerWrapper, - appEventDispatcher, - parentalControlAuthGateway - ) - val startCoordinator = AppInstallStartCoordinator( - context, - appManagerWrapper, - applicationRepository, - sessionRepository, - playStoreAuthStore, - storageNotificationManager, - ageLimitGate, - appEventDispatcher, - storageSpaceChecker, - networkStatusChecker, - fakeFusedManager - ) - val workRunner = AppInstallWorkRunner( - appInstallRepository, - appManagerWrapper, - mockk(relaxed = true), - appUpdateCompletionHandler - ) - return AppInstallProcessor( - appInstallComponents, - startCoordinator, - workRunner, - appInstallRequestFactory - ) - } - - private fun createEnqueueAppInstall() = AppInstall( - id = "123", - status = Status.AWAITING, - downloadURLList = mutableListOf("https://example.org/app.apk"), - packageName = "com.example.app", - type = Type.PWA, - source = Source.PWA - ) - - private fun createApplication(status: Status) = Application( - _id = "123", - name = "Test app", - package_name = "com.example.app", - status = status, - source = Source.PWA, - type = Type.PWA, - latest_version_code = 1L, - isFree = true, - isSystemApp = true, - url = "https://example.org/app.apk" - ) - - private fun createExpectedAppInstall(application: Application) = AppInstall( - application._id, - application.source, - application.status, - application.name, - application.package_name, - mutableListOf(), - mutableMapOf(), - application.status, - application.type, - application.icon_image_path, - application.latest_version_code, - application.offer_type, - application.isFree, - application.originalSize - ).also { - it.contentRating = application.contentRating - if (it.type == Type.PWA || application.source == Source.SYSTEM_APP) { - it.downloadURLList = mutableListOf(application.url) - } - } - - private fun mockInstallWorkManagerSuccess() { - mockkObject(InstallWorkManager) - isInstallWorkManagerMocked = true - every { InstallWorkManager.getUniqueWorkName(any()) } answers { callOriginal() } - every { InstallWorkManager.enqueueWork(any(), any(), any()) } returns successfulOperation() - } - - private fun mockInstallWorkManagerFailure() { - mockkObject(InstallWorkManager) - isInstallWorkManagerMocked = true - every { InstallWorkManager.getUniqueWorkName(any()) } answers { callOriginal() } - every { InstallWorkManager.enqueueWork(any(), any(), any()) } throws RuntimeException("enqueue failed") - } - - private fun successfulOperation(): Operation { - val operation = mock() - whenever(operation.result).thenReturn(Futures.immediateFuture(Operation.SUCCESS)) - return operation - } - } -- GitLab From b6c10bff5571cc8ce81e53aa8518c2ff915218ce Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Fri, 13 Mar 2026 01:43:58 +0600 Subject: [PATCH 10/29] refactor: resolve detekt complaints --- .../workmanager/AppInstallAgeLimitGate.kt | 43 +++-- .../AppInstallDevicePreconditions.kt | 63 +++++++ .../AppInstallDownloadUrlRefresher.kt | 111 ++++++++++++ .../AppInstallPreEnqueueChecker.kt | 51 ++++++ .../workmanager/AppInstallProcessor.kt | 1 - .../workmanager/AppInstallStartCoordinator.kt | 164 ++++-------------- .../workmanager/AppInstallWorkRunner.kt | 62 ++++--- .../AppInstallStartCoordinatorTest.kt | 44 +++-- 8 files changed, 357 insertions(+), 182 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDevicePreconditions.kt create mode 100644 app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDownloadUrlRefresher.kt create mode 100644 app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallPreEnqueueChecker.kt diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallAgeLimitGate.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallAgeLimitGate.kt index 56cabf5fe..c98d5e1e1 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallAgeLimitGate.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallAgeLimitGate.kt @@ -19,6 +19,7 @@ package foundation.e.apps.data.install.workmanager import foundation.e.apps.R +import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall @@ -29,7 +30,6 @@ import foundation.e.apps.domain.model.ContentRatingValidity import kotlinx.coroutines.CompletableDeferred import javax.inject.Inject -@Suppress("ReturnCount") // FIXME: Remove suppression and fix detekt class AppInstallAgeLimitGate @Inject constructor( private val validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase, private val appManagerWrapper: AppManagerWrapper, @@ -38,28 +38,37 @@ class AppInstallAgeLimitGate @Inject constructor( ) { suspend fun allow(appInstall: AppInstall): Boolean { val ageLimitValidationResult = validateAppAgeLimitUseCase(appInstall) - if (ageLimitValidationResult.data?.isValid == true) { - return true - } + val isAllowed = when { + ageLimitValidationResult.data?.isValid == true -> true + ageLimitValidationResult.isSuccess() -> handleSuccessfulValidation( + ageLimitValidationResult, + appInstall.name + ) - if (ageLimitValidationResult.isSuccess()) { - awaitInvokeAgeLimitEvent(appInstall.name) - if (ageLimitValidationResult.data?.requestPin == true) { - val isAuthenticated = parentalControlAuthGateway.awaitAuthentication() - if (isAuthenticated) { - ageLimitValidationResult.setData(ContentRatingValidity(true)) - } + else -> { + appEventDispatcher.dispatch(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc)) + false } - } else { - appEventDispatcher.dispatch(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc)) } - if (ageLimitValidationResult.data?.isValid == true) { - return true + if (!isAllowed) { + appManagerWrapper.cancelDownload(appInstall) } - appManagerWrapper.cancelDownload(appInstall) - return false + return isAllowed + } + + private suspend fun handleSuccessfulValidation( + ageLimitValidationResult: ResultSupreme, + appName: String + ): Boolean { + awaitInvokeAgeLimitEvent(appName) + if (ageLimitValidationResult.data?.requestPin == true && + parentalControlAuthGateway.awaitAuthentication() + ) { + ageLimitValidationResult.setData(ContentRatingValidity(true)) + } + return ageLimitValidationResult.data?.isValid == true } private suspend fun awaitInvokeAgeLimitEvent(type: String) { diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDevicePreconditions.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDevicePreconditions.kt new file mode 100644 index 000000000..e43499ad4 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDevicePreconditions.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.workmanager + +import foundation.e.apps.R +import foundation.e.apps.data.event.AppEvent +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.notification.StorageNotificationManager +import foundation.e.apps.data.install.wrapper.AppEventDispatcher +import foundation.e.apps.data.install.wrapper.NetworkStatusChecker +import foundation.e.apps.data.install.wrapper.StorageSpaceChecker +import timber.log.Timber +import javax.inject.Inject + +class AppInstallDevicePreconditions @Inject constructor( + private val appManagerWrapper: AppManagerWrapper, + private val appEventDispatcher: AppEventDispatcher, + private val storageNotificationManager: StorageNotificationManager, + private val storageSpaceChecker: StorageSpaceChecker, + private val networkStatusChecker: NetworkStatusChecker, +) { + suspend fun canProceed(appInstall: AppInstall): Boolean { + val hasNetwork = hasNetworkConnection(appInstall) + return hasNetwork && hasStorageSpace(appInstall) + } + + private suspend fun hasNetworkConnection(appInstall: AppInstall): Boolean { + val hasNetwork = networkStatusChecker.isNetworkAvailable() + if (!hasNetwork) { + appManagerWrapper.installationIssue(appInstall) + appEventDispatcher.dispatch(AppEvent.NoInternetEvent(false)) + } + return hasNetwork + } + + private suspend fun hasStorageSpace(appInstall: AppInstall): Boolean { + val missingStorage = storageSpaceChecker.spaceMissing(appInstall) + if (missingStorage > 0) { + Timber.d("Storage is not available for: ${appInstall.name} size: ${appInstall.appSize}") + storageNotificationManager.showNotEnoughSpaceNotification(appInstall) + appManagerWrapper.installationIssue(appInstall) + appEventDispatcher.dispatch(AppEvent.ErrorMessageEvent(R.string.not_enough_storage)) + } + return missingStorage <= 0 + } +} diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDownloadUrlRefresher.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDownloadUrlRefresher.kt new file mode 100644 index 000000000..281eb4022 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDownloadUrlRefresher.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.workmanager + +import com.aurora.gplayapi.exceptions.InternalException +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.event.AppEvent +import foundation.e.apps.data.install.AppManager +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.wrapper.AppEventDispatcher +import foundation.e.apps.data.playstore.utils.GplayHttpRequestException +import kotlinx.coroutines.CancellationException +import timber.log.Timber +import javax.inject.Inject + +class AppInstallDownloadUrlRefresher @Inject constructor( + private val applicationRepository: ApplicationRepository, + private val appManagerWrapper: AppManagerWrapper, + private val appEventDispatcher: AppEventDispatcher, + private val appManager: AppManager, +) { + suspend fun updateDownloadUrls(appInstall: AppInstall): Boolean { + return runCatching { + applicationRepository.updateFusedDownloadWithDownloadingInfo( + appInstall.source, + appInstall + ) + }.fold( + onSuccess = { true }, + onFailure = { throwable -> handleUpdateDownloadFailure(appInstall, throwable) } + ) + } + + private suspend fun handleUpdateDownloadFailure(appInstall: AppInstall, throwable: Throwable): Boolean { + return when (throwable) { + is CancellationException -> throw throwable + is InternalException.AppNotPurchased -> handleAppNotPurchased(appInstall) + is GplayHttpRequestException -> { + handleUpdateDownloadError( + appInstall, + "${appInstall.packageName} code: ${throwable.status} exception: ${throwable.localizedMessage}", + throwable + ) + false + } + + is IllegalStateException -> { + Timber.e(throwable) + false + } + + is Exception -> { + handleUpdateDownloadError( + appInstall, + "${appInstall.packageName} exception: ${throwable.localizedMessage}", + throwable + ) + false + } + + else -> throw throwable + } + } + + private suspend fun handleAppNotPurchased(appInstall: AppInstall): Boolean { + if (appInstall.isFree) { + appEventDispatcher.dispatch(AppEvent.AppRestrictedOrUnavailable(appInstall)) + appManager.addDownload(appInstall) + appManager.updateUnavailable(appInstall) + } else { + appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) + appEventDispatcher.dispatch(AppEvent.AppPurchaseEvent(appInstall)) + } + return false + } + + private suspend fun handleUpdateDownloadError( + appInstall: AppInstall, + message: String, + exception: Exception + ) { + Timber.e(exception, "Updating download Urls failed for $message") + appEventDispatcher.dispatch( + AppEvent.UpdateEvent( + ResultSupreme.WorkError( + ResultStatus.UNKNOWN, + appInstall + ) + ) + ) + } +} diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallPreEnqueueChecker.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallPreEnqueueChecker.kt new file mode 100644 index 000000000..e1b380350 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallPreEnqueueChecker.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.data.install.workmanager + +import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.models.AppInstall +import timber.log.Timber +import javax.inject.Inject + +class AppInstallPreEnqueueChecker @Inject constructor( + private val appInstallDownloadUrlRefresher: AppInstallDownloadUrlRefresher, + private val appManagerWrapper: AppManagerWrapper, + private val appInstallAgeLimitGate: AppInstallAgeLimitGate, + private val appInstallDevicePreconditions: AppInstallDevicePreconditions, +) { + suspend fun canEnqueue(appInstall: AppInstall): Boolean { + val hasUpdatedDownloadUrls = appInstall.type == Type.PWA || + appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall) + + val isDownloadAdded = hasUpdatedDownloadUrls && addDownload(appInstall) + val isAgeLimitAllowed = isDownloadAdded && appInstallAgeLimitGate.allow(appInstall) + + return isAgeLimitAllowed && appInstallDevicePreconditions.canProceed(appInstall) + } + + private suspend fun addDownload(appInstall: AppInstall): Boolean { + val isDownloadAdded = appManagerWrapper.addDownload(appInstall) + if (!isDownloadAdded) { + Timber.i("Update adding ABORTED! status") + } + + return isDownloadAdded + } +} diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt index 3be4b212e..1301363d9 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt @@ -25,7 +25,6 @@ import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.domain.model.install.Status import javax.inject.Inject -@Suppress("LongParameterList") // FIXME: Remove suppression and fix detekt class AppInstallProcessor @Inject constructor( private val appInstallComponents: AppInstallComponents, private val appInstallStartCoordinator: AppInstallStartCoordinator, diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt index 7291aa883..1977543cb 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt @@ -19,168 +19,78 @@ package foundation.e.apps.data.install.workmanager import android.content.Context -import com.aurora.gplayapi.exceptions.InternalException import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.enums.Type import foundation.e.apps.data.event.AppEvent -import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.notification.StorageNotificationManager import foundation.e.apps.data.install.wrapper.AppEventDispatcher -import foundation.e.apps.data.install.wrapper.NetworkStatusChecker -import foundation.e.apps.data.install.wrapper.StorageSpaceChecker -import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.PlayStoreAuthStore import foundation.e.apps.domain.model.User import foundation.e.apps.domain.preferences.SessionRepository +import kotlinx.coroutines.CancellationException import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList", "TooGenericExceptionCaught", "ReturnCount") // FIXME: Remove suppression and fix detekt class AppInstallStartCoordinator @Inject constructor( @ApplicationContext private val context: Context, + private val appInstallPreEnqueueChecker: AppInstallPreEnqueueChecker, private val appManagerWrapper: AppManagerWrapper, - private val applicationRepository: ApplicationRepository, private val sessionRepository: SessionRepository, private val playStoreAuthStore: PlayStoreAuthStore, - private val storageNotificationManager: StorageNotificationManager, - private val appInstallAgeLimitGate: AppInstallAgeLimitGate, private val appEventDispatcher: AppEventDispatcher, - private val storageSpaceChecker: StorageSpaceChecker, - private val networkStatusChecker: NetworkStatusChecker, - private val appManager: AppManager, ) { suspend fun enqueue( appInstall: AppInstall, isAnUpdate: Boolean = false, isSystemApp: Boolean = false ): Boolean { - val uniqueWorkName = InstallWorkManager.getUniqueWorkName(appInstall.packageName) - - return try { - val user = sessionRepository.awaitUser() - if (!isSystemApp && (user == User.GOOGLE || user == User.ANONYMOUS)) { - val authData = playStoreAuthStore.awaitAuthData() - if (!appInstall.isFree && authData?.isAnonymous == true) { - appEventDispatcher.dispatch( - AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message) - ) + return runCatching { + dispatchAnonymousPaidAppWarning(appInstall, isSystemApp) + + val canEnqueue = canEnqueue(appInstall) + if (canEnqueue) { + appManagerWrapper.updateAwaiting(appInstall) + + // Use only for update work for now. For installation work, see InstallOrchestrator#observeDownloads() + if (isAnUpdate) { + val uniqueWorkName = InstallWorkManager.getUniqueWorkName(appInstall.packageName) + InstallWorkManager.enqueueWork(context, appInstall, true) + Timber.d("UPDATE: Successfully enqueued unique work: $uniqueWorkName") } } - if (!canEnqueue(appInstall)) return false - - appManagerWrapper.updateAwaiting(appInstall) - - // Use only for update work for now. For installation work, see InstallOrchestrator#observeDownloads() - if (isAnUpdate) { - InstallWorkManager.enqueueWork(context, appInstall, true) - Timber.d("UPDATE: Successfully enqueued unique work: $uniqueWorkName") + canEnqueue + }.getOrElse { throwable -> + when (throwable) { + is CancellationException -> throw throwable + is Exception -> { + Timber.e( + throwable, + "Enqueuing App install work is failed for ${appInstall.packageName} " + + "exception: ${throwable.localizedMessage}" + ) + appManagerWrapper.installationIssue(appInstall) + false + } + else -> throw throwable } - true - } catch (e: Exception) { - Timber.e( - e, - "Enqueuing App install work is failed for ${appInstall.packageName} exception: ${e.localizedMessage}" - ) - appManagerWrapper.installationIssue(appInstall) - false } } suspend fun canEnqueue(appInstall: AppInstall): Boolean { - if (appInstall.type != Type.PWA && !updateDownloadUrls(appInstall)) { - return false - } - - if (!appManagerWrapper.addDownload(appInstall)) { - Timber.i("Update adding ABORTED! status") - return false - } - - if (!appInstallAgeLimitGate.allow(appInstall)) { - return false - } - - if (!networkStatusChecker.isNetworkAvailable()) { - appManagerWrapper.installationIssue(appInstall) - appEventDispatcher.dispatch(AppEvent.NoInternetEvent(false)) - return false - } - - if (storageSpaceChecker.spaceMissing(appInstall) > 0) { - Timber.d("Storage is not available for: ${appInstall.name} size: ${appInstall.appSize}") - storageNotificationManager.showNotEnoughSpaceNotification(appInstall) - appManagerWrapper.installationIssue(appInstall) - appEventDispatcher.dispatch(AppEvent.ErrorMessageEvent(R.string.not_enough_storage)) - return false - } - - return true + return appInstallPreEnqueueChecker.canEnqueue(appInstall) } - private suspend fun updateDownloadUrls(appInstall: AppInstall): Boolean { - try { - updateFusedDownloadWithAppDownloadLink(appInstall) - } catch (_: InternalException.AppNotPurchased) { - if (appInstall.isFree) { - handleAppRestricted(appInstall) - return false + private suspend fun dispatchAnonymousPaidAppWarning(appInstall: AppInstall, isSystemApp: Boolean) { + val user = sessionRepository.awaitUser() + if (!isSystemApp && (user == User.GOOGLE || user == User.ANONYMOUS)) { + val authData = playStoreAuthStore.awaitAuthData() + if (!appInstall.isFree && authData?.isAnonymous == true) { + appEventDispatcher.dispatch( + AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message) + ) } - appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) - appEventDispatcher.dispatch(AppEvent.AppPurchaseEvent(appInstall)) - return false - } catch (e: GplayHttpRequestException) { - handleUpdateDownloadError( - appInstall, - "${appInstall.packageName} code: ${e.status} exception: ${e.localizedMessage}", - e - ) - return false - } catch (e: IllegalStateException) { - Timber.e(e) - } catch (e: Exception) { - handleUpdateDownloadError( - appInstall, - "${appInstall.packageName} exception: ${e.localizedMessage}", - e - ) - return false } - return true - } - - private suspend fun handleAppRestricted(appInstall: AppInstall) { - appEventDispatcher.dispatch(AppEvent.AppRestrictedOrUnavailable(appInstall)) - appManager.addDownload(appInstall) - appManager.updateUnavailable(appInstall) - } - - private suspend fun handleUpdateDownloadError( - appInstall: AppInstall, - message: String, - e: Exception - ) { - Timber.e(e, "Updating download Urls failed for $message") - appEventDispatcher.dispatch( - AppEvent.UpdateEvent( - ResultSupreme.WorkError( - ResultStatus.UNKNOWN, - appInstall - ) - ) - ) - } - - private suspend fun updateFusedDownloadWithAppDownloadLink(appInstall: AppInstall) { - applicationRepository.updateFusedDownloadWithDownloadingInfo( - appInstall.source, - appInstall - ) } } diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt index ac9792e65..04e77985d 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt @@ -24,12 +24,12 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.download.DownloadManagerUtils import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.domain.model.install.Status +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.flow.transformWhile import timber.log.Timber import javax.inject.Inject -@Suppress("TooGenericExceptionCaught") // FIXME: Remove suppression and fix detekt class AppInstallWorkRunner @Inject constructor( private val appInstallRepository: AppInstallRepository, private val appManagerWrapper: AppManagerWrapper, @@ -43,45 +43,53 @@ class AppInstallWorkRunner @Inject constructor( runInForeground: suspend (String) -> Unit ): Result { var appInstall: AppInstall? = null - try { + runCatching { Timber.d("Fused download name $fusedDownloadId") appInstall = appInstallRepository.getDownloadById(fusedDownloadId) Timber.i(">>> doWork started for Fused download name ${appInstall?.name} $fusedDownloadId") appInstall?.let { - checkDownloadingState(appInstall) + checkDownloadingState(it) val isUpdateWork = - isItUpdateWork && appManagerWrapper.isFusedDownloadInstalled(appInstall) + isItUpdateWork && appManagerWrapper.isFusedDownloadInstalled(it) - if (!appInstall.isAppInstalling()) { + if (!it.isAppInstalling()) { Timber.d("!!! returned") return@let } - if (!appManagerWrapper.validateFusedDownload(appInstall)) { + if (!appManagerWrapper.validateFusedDownload(it)) { appManagerWrapper.installationIssue(it) Timber.d("!!! installationIssue") return@let } - if (areFilesDownloadedButNotInstalled(appInstall)) { - Timber.i("===> Downloaded But not installed ${appInstall.name}") - appManagerWrapper.updateDownloadStatus(appInstall, Status.INSTALLING) + if (areFilesDownloadedButNotInstalled(it)) { + Timber.i("===> Downloaded But not installed ${it.name}") + appManagerWrapper.updateDownloadStatus(it, Status.INSTALLING) } runInForeground.invoke(it.name) startAppInstallationProcess(it, isUpdateWork) } - } catch (e: Exception) { - Timber.e( - e, - "Install worker is failed for ${appInstall?.packageName} exception: ${e.localizedMessage}" - ) - appInstall?.let { - appManagerWrapper.cancelDownload(appInstall) + }.onFailure { throwable -> + when (throwable) { + is CancellationException -> throw throwable + is Exception -> { + Timber.e( + throwable, + "Install worker is failed for ${appInstall?.packageName} " + + "exception: ${throwable.localizedMessage}" + ) + appInstall?.let { + appManagerWrapper.cancelDownload(it) + } + } + + else -> throw throwable } } @@ -138,14 +146,22 @@ class AppInstallWorkRunner @Inject constructor( download: AppInstall, isUpdateWork: Boolean ) { - try { + runCatching { handleFusedDownloadStatus(download, isUpdateWork) - } catch (e: Exception) { - val message = - "Handling install status is failed for ${download.packageName} exception: ${e.localizedMessage}" - Timber.e(e, message) - appManagerWrapper.installationIssue(download) - finishInstallation(download, isUpdateWork) + }.onFailure { throwable -> + when (throwable) { + is CancellationException -> throw throwable + is Exception -> { + val message = + "Handling install status is failed for ${download.packageName} " + + "exception: ${throwable.localizedMessage}" + Timber.e(throwable, message) + appManagerWrapper.installationIssue(download) + finishInstallation(download, isUpdateWork) + } + + else -> throw throwable + } } } diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt index 2385a90a2..015f6470a 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt @@ -31,6 +31,9 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate +import foundation.e.apps.data.install.workmanager.AppInstallDevicePreconditions +import foundation.e.apps.data.install.workmanager.AppInstallDownloadUrlRefresher +import foundation.e.apps.data.install.workmanager.AppInstallPreEnqueueChecker import foundation.e.apps.data.install.workmanager.AppInstallStartCoordinator import foundation.e.apps.data.install.workmanager.InstallWorkManager import foundation.e.apps.data.install.wrapper.NetworkStatusChecker @@ -68,6 +71,9 @@ class AppInstallStartCoordinatorTest { private lateinit var storageSpaceChecker: StorageSpaceChecker private lateinit var networkStatusChecker: NetworkStatusChecker private lateinit var appManager: AppManager + private lateinit var devicePreconditions: AppInstallDevicePreconditions + private lateinit var downloadUrlRefresher: AppInstallDownloadUrlRefresher + private lateinit var preflightChecker: AppInstallPreEnqueueChecker private lateinit var coordinator: AppInstallStartCoordinator @Before @@ -85,18 +91,32 @@ class AppInstallStartCoordinatorTest { appManager = mockk(relaxed = true) coEvery { sessionRepository.awaitUser() } returns User.NO_GOOGLE coEvery { playStoreAuthStore.awaitAuthData() } returns null + downloadUrlRefresher = AppInstallDownloadUrlRefresher( + applicationRepository, + appManagerWrapper, + appEventDispatcher, + appManager + ) + devicePreconditions = AppInstallDevicePreconditions( + appManagerWrapper, + appEventDispatcher, + storageNotificationManager, + storageSpaceChecker, + networkStatusChecker + ) + preflightChecker = AppInstallPreEnqueueChecker( + downloadUrlRefresher, + appManagerWrapper, + appInstallAgeLimitGate, + devicePreconditions + ) coordinator = AppInstallStartCoordinator( context, + preflightChecker, appManagerWrapper, - applicationRepository, sessionRepository, playStoreAuthStore, - storageNotificationManager, - appInstallAgeLimitGate, - appEventDispatcher, - storageSpaceChecker, - networkStatusChecker, - appManager + appEventDispatcher ) } @@ -242,7 +262,7 @@ class AppInstallStartCoordinatorTest { } @Test - fun canEnqueue_keepsGoingWhenDownloadUrlRefreshThrowsIllegalState() = runTest { + fun canEnqueue_returnsFalseWhenDownloadUrlRefreshThrowsIllegalState() = runTest { val appInstall = createNativeInstall() coEvery { @@ -251,15 +271,11 @@ class AppInstallStartCoordinatorTest { appInstall ) } throws IllegalStateException("boom") - coEvery { appManagerWrapper.addDownload(appInstall) } returns true - coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true - every { networkStatusChecker.isNetworkAvailable() } returns true - every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L val result = coordinator.canEnqueue(appInstall) - assertTrue(result) - coVerify { appManagerWrapper.addDownload(appInstall) } + assertFalse(result) + coVerify(exactly = 0) { appManagerWrapper.addDownload(appInstall) } } private fun createPwaInstall(isFree: Boolean = true) = AppInstall( -- GitLab From 36b64557eddb61f6982c1359fbc45e1d1d9b21ef Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 00:41:51 +0600 Subject: [PATCH 11/29] refactor: simplify install flow code Flatten processInstall into a guard-clause flow and add a local ReturnCount suppression so the install preconditions and execution path are easier to read. This keeps the existing install behavior and failure handling intact while reducing nesting and temporary state. --- .../AppInstallDownloadUrlRefresher.kt | 72 +++++++++-------- .../AppInstallPreEnqueueChecker.kt | 4 +- .../workmanager/AppInstallStartCoordinator.kt | 8 +- .../workmanager/AppInstallWorkRunner.kt | 80 +++++++++---------- .../install/workmanager/InstallAppWorker.kt | 17 ++-- .../AppInstallStartCoordinatorTest.kt | 31 +++++++ .../AppInstallWorkRunnerTest.kt | 30 ++++++- 7 files changed, 152 insertions(+), 90 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDownloadUrlRefresher.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDownloadUrlRefresher.kt index 281eb4022..bc4ac8c6a 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDownloadUrlRefresher.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDownloadUrlRefresher.kt @@ -23,6 +23,7 @@ import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.event.AppEvent +import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall @@ -34,11 +35,12 @@ import javax.inject.Inject class AppInstallDownloadUrlRefresher @Inject constructor( private val applicationRepository: ApplicationRepository, + private val appInstallRepository: AppInstallRepository, private val appManagerWrapper: AppManagerWrapper, private val appEventDispatcher: AppEventDispatcher, private val appManager: AppManager, ) { - suspend fun updateDownloadUrls(appInstall: AppInstall): Boolean { + suspend fun updateDownloadUrls(appInstall: AppInstall, isAnUpdate: Boolean): Boolean { return runCatching { applicationRepository.updateFusedDownloadWithDownloadingInfo( appInstall.source, @@ -46,34 +48,32 @@ class AppInstallDownloadUrlRefresher @Inject constructor( ) }.fold( onSuccess = { true }, - onFailure = { throwable -> handleUpdateDownloadFailure(appInstall, throwable) } + onFailure = { throwable -> + handleUpdateDownloadFailure( + appInstall, + isAnUpdate, + throwable + ) + } ) } - private suspend fun handleUpdateDownloadFailure(appInstall: AppInstall, throwable: Throwable): Boolean { + private suspend fun handleUpdateDownloadFailure( + appInstall: AppInstall, + isAnUpdate: Boolean, + throwable: Throwable + ): Boolean { return when (throwable) { is CancellationException -> throw throwable is InternalException.AppNotPurchased -> handleAppNotPurchased(appInstall) - is GplayHttpRequestException -> { - handleUpdateDownloadError( - appInstall, - "${appInstall.packageName} code: ${throwable.status} exception: ${throwable.localizedMessage}", - throwable - ) - false - } - - is IllegalStateException -> { - Timber.e(throwable) - false - } - is Exception -> { - handleUpdateDownloadError( - appInstall, - "${appInstall.packageName} exception: ${throwable.localizedMessage}", - throwable - ) + val message = if (throwable is GplayHttpRequestException) { + "${appInstall.packageName} code: ${throwable.status} exception: ${throwable.message}" + } else { + "${appInstall.packageName} exception: ${throwable.message}" + } + Timber.e(throwable, "Updating download URLS failed for $message") + handleUpdateDownloadError(appInstall, isAnUpdate) false } @@ -93,19 +93,23 @@ class AppInstallDownloadUrlRefresher @Inject constructor( return false } - private suspend fun handleUpdateDownloadError( - appInstall: AppInstall, - message: String, - exception: Exception - ) { - Timber.e(exception, "Updating download Urls failed for $message") - appEventDispatcher.dispatch( - AppEvent.UpdateEvent( - ResultSupreme.WorkError( - ResultStatus.UNKNOWN, - appInstall + private suspend fun handleUpdateDownloadError(appInstall: AppInstall, isAnUpdate: Boolean) { + // Insert into DB to reflect error state on UI. + // For example, install button's label will change to Install -> Cancel -> Retry + if (appInstallRepository.getDownloadById(appInstall.id) == null) { + appInstallRepository.addDownload(appInstall) + } + appManagerWrapper.installationIssue(appInstall) + + if (isAnUpdate) { + appEventDispatcher.dispatch( + AppEvent.UpdateEvent( + ResultSupreme.WorkError( + ResultStatus.UNKNOWN, + appInstall + ) ) ) - ) + } } } diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallPreEnqueueChecker.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallPreEnqueueChecker.kt index e1b380350..04684a195 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallPreEnqueueChecker.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallPreEnqueueChecker.kt @@ -30,9 +30,9 @@ class AppInstallPreEnqueueChecker @Inject constructor( private val appInstallAgeLimitGate: AppInstallAgeLimitGate, private val appInstallDevicePreconditions: AppInstallDevicePreconditions, ) { - suspend fun canEnqueue(appInstall: AppInstall): Boolean { + suspend fun canEnqueue(appInstall: AppInstall, isAnUpdate: Boolean = false): Boolean { val hasUpdatedDownloadUrls = appInstall.type == Type.PWA || - appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall) + appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, isAnUpdate) val isDownloadAdded = hasUpdatedDownloadUrls && addDownload(appInstall) val isAgeLimitAllowed = isDownloadAdded && appInstallAgeLimitGate.allow(appInstall) diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt index 1977543cb..c0e11f43e 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt @@ -48,7 +48,7 @@ class AppInstallStartCoordinator @Inject constructor( return runCatching { dispatchAnonymousPaidAppWarning(appInstall, isSystemApp) - val canEnqueue = canEnqueue(appInstall) + val canEnqueue = canEnqueue(appInstall, isAnUpdate) if (canEnqueue) { appManagerWrapper.updateAwaiting(appInstall) @@ -58,6 +58,8 @@ class AppInstallStartCoordinator @Inject constructor( InstallWorkManager.enqueueWork(context, appInstall, true) Timber.d("UPDATE: Successfully enqueued unique work: $uniqueWorkName") } + } else { + Timber.w("Can't enqueue ${appInstall.name}/${appInstall.packageName} for installation.") } canEnqueue @@ -78,8 +80,8 @@ class AppInstallStartCoordinator @Inject constructor( } } - suspend fun canEnqueue(appInstall: AppInstall): Boolean { - return appInstallPreEnqueueChecker.canEnqueue(appInstall) + suspend fun canEnqueue(appInstall: AppInstall, isAnUpdate: Boolean = false): Boolean { + return appInstallPreEnqueueChecker.canEnqueue(appInstall, isAnUpdate) } private suspend fun dispatchAnonymousPaidAppWarning(appInstall: AppInstall, isSystemApp: Boolean) { diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt index 04e77985d..ab59f1e12 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt @@ -36,65 +36,63 @@ class AppInstallWorkRunner @Inject constructor( private val downloadManager: DownloadManagerUtils, private val appUpdateCompletionHandler: AppUpdateCompletionHandler, ) { + @Suppress("ReturnCount") @OptIn(DelicateCoroutinesApi::class) suspend fun processInstall( fusedDownloadId: String, isItUpdateWork: Boolean, runInForeground: suspend (String) -> Unit ): Result { - var appInstall: AppInstall? = null - runCatching { - Timber.d("Fused download name $fusedDownloadId") + val appInstall = + appInstallRepository.getDownloadById(fusedDownloadId) ?: return Result.failure( + IllegalStateException("App can't be null here.") + ) - appInstall = appInstallRepository.getDownloadById(fusedDownloadId) - Timber.i(">>> doWork started for Fused download name ${appInstall?.name} $fusedDownloadId") + Timber.i(">>> doWork() started for ${appInstall.name}/${appInstall.packageName}") - appInstall?.let { - checkDownloadingState(it) + checkDownloadingState(appInstall) - val isUpdateWork = - isItUpdateWork && appManagerWrapper.isFusedDownloadInstalled(it) + if (!appInstall.isAppInstalling()) { + val message = "${appInstall.status} is in invalid state" + Timber.w(message) - if (!it.isAppInstalling()) { - Timber.d("!!! returned") - return@let - } + return Result.failure(IllegalStateException(message)) + } - if (!appManagerWrapper.validateFusedDownload(it)) { - appManagerWrapper.installationIssue(it) - Timber.d("!!! installationIssue") - return@let - } + if (!appManagerWrapper.validateFusedDownload(appInstall)) { + appManagerWrapper.installationIssue(appInstall) + val message = "Installation issue for ${appInstall.name}/${appInstall.packageName}" + Timber.w(message) - if (areFilesDownloadedButNotInstalled(it)) { - Timber.i("===> Downloaded But not installed ${it.name}") - appManagerWrapper.updateDownloadStatus(it, Status.INSTALLING) - } + return Result.failure(IllegalStateException(message)) + } - runInForeground.invoke(it.name) + return runCatching { + val isUpdateWork = + isItUpdateWork && appManagerWrapper.isFusedDownloadInstalled(appInstall) - startAppInstallationProcess(it, isUpdateWork) + if (areFilesDownloadedButNotInstalled(appInstall)) { + Timber.i("===> Downloaded But not installed ${appInstall.name}") + appManagerWrapper.updateDownloadStatus(appInstall, Status.INSTALLING) } - }.onFailure { throwable -> - when (throwable) { - is CancellationException -> throw throwable - is Exception -> { - Timber.e( - throwable, - "Install worker is failed for ${appInstall?.packageName} " + - "exception: ${throwable.localizedMessage}" - ) - appInstall?.let { - appManagerWrapper.cancelDownload(it) - } - } - else -> throw throwable + runInForeground.invoke(appInstall.name) + startAppInstallationProcess(appInstall, isUpdateWork) + Timber.i("doWork: RESULT SUCCESS: ${appInstall.name}") + + ResultStatus.OK + }.onFailure { exception -> + if (exception is CancellationException) { + throw exception } - } - Timber.i("doWork: RESULT SUCCESS: ${appInstall?.name}") - return Result.success(ResultStatus.OK) + Timber.e( + exception, + "Install worker failed for ${appInstall.packageName} exception: ${exception.message}" + ) + + appManagerWrapper.cancelDownload(appInstall) + } } @OptIn(DelicateCoroutinesApi::class) diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallAppWorker.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallAppWorker.kt index c4af7cfbb..6ea50e733 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallAppWorker.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallAppWorker.kt @@ -59,15 +59,20 @@ class InstallAppWorker @AssistedInject constructor( override suspend fun doWork(): Result { val fusedDownloadId = params.inputData.getString(INPUT_DATA_FUSED_DOWNLOAD) ?: "" + if (fusedDownloadId.isEmpty()) { + return Result.failure() + } + val isPackageUpdate = params.inputData.getBoolean(IS_UPDATE_WORK, false) val response = appInstallProcessor.processInstall(fusedDownloadId, isPackageUpdate) { title -> - setForeground( - createForegroundInfo( - "${context.getString(R.string.installing)} $title" - ) - ) + setForeground(createForegroundInfo("${context.getString(R.string.installing)} $title")) + } + + return if (response.isSuccess) { + Result.success() + } else { + Result.failure() } - return if (response.isSuccess) Result.success() else Result.failure() } private fun createForegroundInfo(progress: String): ForegroundInfo { diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt index 015f6470a..9975a676a 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt @@ -27,6 +27,7 @@ import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Type import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.install.AppManager +import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager @@ -63,6 +64,7 @@ class AppInstallStartCoordinatorTest { private lateinit var context: Context private lateinit var appManagerWrapper: AppManagerWrapper private lateinit var applicationRepository: ApplicationRepository + private lateinit var appInstallRepository: AppInstallRepository private lateinit var sessionRepository: SessionRepository private lateinit var playStoreAuthStore: PlayStoreAuthStore private lateinit var storageNotificationManager: StorageNotificationManager @@ -81,6 +83,7 @@ class AppInstallStartCoordinatorTest { context = mockk(relaxed = true) appManagerWrapper = mockk(relaxed = true) applicationRepository = mockk(relaxed = true) + appInstallRepository = mockk(relaxed = true) sessionRepository = mockk(relaxed = true) playStoreAuthStore = mockk(relaxed = true) storageNotificationManager = mockk(relaxed = true) @@ -93,6 +96,7 @@ class AppInstallStartCoordinatorTest { coEvery { playStoreAuthStore.awaitAuthData() } returns null downloadUrlRefresher = AppInstallDownloadUrlRefresher( applicationRepository, + appInstallRepository, appManagerWrapper, appEventDispatcher, appManager @@ -247,6 +251,7 @@ class AppInstallStartCoordinatorTest { fun canEnqueue_returnsFalseWhenDownloadUrlRefreshThrowsHttpError() = runTest { val appInstall = createNativeInstall() + coEvery { appInstallRepository.getDownloadById(appInstall.id) } returns null coEvery { applicationRepository.updateFusedDownloadWithDownloadingInfo( Source.PLAY_STORE, @@ -256,8 +261,31 @@ class AppInstallStartCoordinatorTest { val result = coordinator.canEnqueue(appInstall) + assertFalse(result) + assertTrue(appEventDispatcher.events.none { it is AppEvent.UpdateEvent }) + coVerify { appInstallRepository.addDownload(appInstall) } + coVerify { appManagerWrapper.installationIssue(appInstall) } + coVerify(exactly = 0) { appManagerWrapper.addDownload(appInstall) } + } + + @Test + fun canEnqueue_dispatchesUpdateEventWhenDownloadUrlRefreshFailsForUpdate() = runTest { + val appInstall = createNativeInstall() + + coEvery { appInstallRepository.getDownloadById(appInstall.id) } returns null + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo( + Source.PLAY_STORE, + appInstall + ) + } throws GplayHttpRequestException(403, "forbidden") + + val result = coordinator.canEnqueue(appInstall, true) + assertFalse(result) assertTrue(appEventDispatcher.events.any { it is AppEvent.UpdateEvent }) + coVerify { appInstallRepository.addDownload(appInstall) } + coVerify { appManagerWrapper.installationIssue(appInstall) } coVerify(exactly = 0) { appManagerWrapper.addDownload(appInstall) } } @@ -265,6 +293,7 @@ class AppInstallStartCoordinatorTest { fun canEnqueue_returnsFalseWhenDownloadUrlRefreshThrowsIllegalState() = runTest { val appInstall = createNativeInstall() + coEvery { appInstallRepository.getDownloadById(appInstall.id) } returns null coEvery { applicationRepository.updateFusedDownloadWithDownloadingInfo( Source.PLAY_STORE, @@ -275,6 +304,8 @@ class AppInstallStartCoordinatorTest { val result = coordinator.canEnqueue(appInstall) assertFalse(result) + coVerify { appInstallRepository.addDownload(appInstall) } + coVerify { appManagerWrapper.installationIssue(appInstall) } coVerify(exactly = 0) { appManagerWrapper.addDownload(appInstall) } } diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt index 7ac00a528..3e0e3da90 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt @@ -20,7 +20,6 @@ package foundation.e.apps.installProcessor import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.fdroid.FDroidRepository import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManager @@ -132,7 +131,7 @@ class AppInstallWorkRunnerTest { } @Test - fun processInstall_returnsSuccessWhenInternalExceptionOccurs() = runTest { + fun processInstall_returnsFailureWhenInternalExceptionOccurs() = runTest { val fusedDownload = initTest() fakeFusedManagerRepository.forceCrash = true @@ -141,11 +140,34 @@ class AppInstallWorkRunnerTest { } val finalFusedDownload = fakeFusedDownloadDAO.getDownloadById(fusedDownload.id) - assertTrue(result.isSuccess) - assertEquals(ResultStatus.OK, result.getOrNull()) + assertTrue(result.isFailure) assertTrue(finalFusedDownload == null || fusedDownload.status == Status.INSTALLATION_ISSUE) } + @Test + fun processInstall_returnsFailureWhenStatusIsInvalid() = runTest { + val fusedDownload = initTest() + fusedDownload.status = Status.BLOCKED + + val result = workRunner.processInstall(fusedDownload.id, false) { + // _ignored_ + } + val finalFusedDownload = fakeFusedDownloadDAO.getDownloadById(fusedDownload.id) + + assertTrue(result.isFailure) + assertEquals(Status.BLOCKED, finalFusedDownload?.status) + } + + @Test + fun processInstall_returnsFailureWhenDownloadMissing() = runTest { + val result = workRunner.processInstall("missing", false) { + // _ignored_ + } + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is IllegalStateException) + } + @Test fun processInstall_reportsDownloadFailure() = runTest { val fusedDownload = initTest() -- GitLab From eb3d947a08ad8c7b7e8e249ddc9cbb9e14cbac78 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 01:30:05 +0600 Subject: [PATCH 12/29] test: cover pre-enqueue install gating Pin the new pre-enqueue orchestration logic after the install refactor so short-circuit regressions are caught directly. --- .../AppInstallPreEnqueueCheckerTest.kt | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt new file mode 100644 index 000000000..085af08db --- /dev/null +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.installProcessor + +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate +import foundation.e.apps.data.install.workmanager.AppInstallDevicePreconditions +import foundation.e.apps.data.install.workmanager.AppInstallDownloadUrlRefresher +import foundation.e.apps.data.install.workmanager.AppInstallPreEnqueueChecker +import foundation.e.apps.domain.model.install.Status +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AppInstallPreEnqueueCheckerTest { + private lateinit var appInstallDownloadUrlRefresher: AppInstallDownloadUrlRefresher + private lateinit var appManagerWrapper: AppManagerWrapper + private lateinit var appInstallAgeLimitGate: AppInstallAgeLimitGate + private lateinit var appInstallDevicePreconditions: AppInstallDevicePreconditions + private lateinit var checker: AppInstallPreEnqueueChecker + + @Before + fun setup() { + appInstallDownloadUrlRefresher = mockk(relaxed = true) + appManagerWrapper = mockk(relaxed = true) + appInstallAgeLimitGate = mockk(relaxed = true) + appInstallDevicePreconditions = mockk(relaxed = true) + checker = AppInstallPreEnqueueChecker( + appInstallDownloadUrlRefresher, + appManagerWrapper, + appInstallAgeLimitGate, + appInstallDevicePreconditions + ) + } + + @Test + fun canEnqueue_skipsDownloadUrlRefreshForPwaInstalls() = runTest { + val appInstall = createPwaInstall() + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + coEvery { appInstallDevicePreconditions.canProceed(appInstall) } returns true + + val result = checker.canEnqueue(appInstall) + + assertTrue(result) + coVerify(exactly = 0) { + appInstallDownloadUrlRefresher.updateDownloadUrls(any(), any()) + } + } + + @Test + fun canEnqueue_stopsWhenDownloadRefreshFails() = runTest { + val appInstall = createNativeInstall() + coEvery { appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns false + + val result = checker.canEnqueue(appInstall) + + assertFalse(result) + coVerify(exactly = 0) { appManagerWrapper.addDownload(any()) } + coVerify(exactly = 0) { appInstallAgeLimitGate.allow(any()) } + coVerify(exactly = 0) { appInstallDevicePreconditions.canProceed(any()) } + } + + @Test + fun canEnqueue_stopsWhenAddingDownloadFails() = runTest { + val appInstall = createNativeInstall() + coEvery { appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns true + coEvery { appManagerWrapper.addDownload(appInstall) } returns false + + val result = checker.canEnqueue(appInstall) + + assertFalse(result) + coVerify(exactly = 0) { appInstallAgeLimitGate.allow(any()) } + coVerify(exactly = 0) { appInstallDevicePreconditions.canProceed(any()) } + } + + @Test + fun canEnqueue_stopsWhenAgeLimitRejectsInstall() = runTest { + val appInstall = createNativeInstall() + coEvery { appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns true + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + coEvery { appInstallAgeLimitGate.allow(appInstall) } returns false + + val result = checker.canEnqueue(appInstall) + + assertFalse(result) + coVerify(exactly = 0) { appInstallDevicePreconditions.canProceed(any()) } + } + + @Test + fun canEnqueue_returnsTrueWhenAllChecksPass() = runTest { + val appInstall = createNativeInstall() + coEvery { appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns true + coEvery { appManagerWrapper.addDownload(appInstall) } returns true + coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + coEvery { appInstallDevicePreconditions.canProceed(appInstall) } returns true + + val result = checker.canEnqueue(appInstall) + + assertTrue(result) + } + + private fun createPwaInstall() = AppInstall( + type = Type.PWA, + id = "123", + status = Status.AWAITING, + downloadURLList = mutableListOf("apk"), + packageName = "com.example.app" + ) + + private fun createNativeInstall() = AppInstall( + type = Type.NATIVE, + source = Source.PLAY_STORE, + id = "123", + status = Status.AWAITING, + packageName = "com.example.app" + ) +} -- GitLab From 0bf7b695efc81126318993e1b73f060ab1529bc8 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 01:33:02 +0600 Subject: [PATCH 13/29] test: cover download refresh failure handling Pin the refresh error branches introduced by the install refactor so purchase and retry flows keep their current side effects. --- .../AppInstallDownloadUrlRefresherTest.kt | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 app/src/test/java/foundation/e/apps/installProcessor/AppInstallDownloadUrlRefresherTest.kt diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDownloadUrlRefresherTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDownloadUrlRefresherTest.kt new file mode 100644 index 000000000..6be024abf --- /dev/null +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDownloadUrlRefresherTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.installProcessor + +import com.aurora.gplayapi.exceptions.InternalException +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.event.AppEvent +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.install.AppInstallRepository +import foundation.e.apps.data.install.AppManager +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.workmanager.AppInstallDownloadUrlRefresher +import foundation.e.apps.data.playstore.utils.GplayHttpRequestException +import foundation.e.apps.domain.model.install.Status +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlin.test.assertFailsWith +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AppInstallDownloadUrlRefresherTest { + private lateinit var applicationRepository: ApplicationRepository + private lateinit var appInstallRepository: AppInstallRepository + private lateinit var appManagerWrapper: AppManagerWrapper + private lateinit var appEventDispatcher: FakeAppEventDispatcher + private lateinit var appManager: AppManager + private lateinit var refresher: AppInstallDownloadUrlRefresher + + @Before + fun setup() { + applicationRepository = mockk(relaxed = true) + appInstallRepository = mockk(relaxed = true) + appManagerWrapper = mockk(relaxed = true) + appEventDispatcher = FakeAppEventDispatcher() + appManager = mockk(relaxed = true) + refresher = AppInstallDownloadUrlRefresher( + applicationRepository, + appInstallRepository, + appManagerWrapper, + appEventDispatcher, + appManager + ) + } + + @Test + fun updateDownloadUrls_returnsTrueWhenRefreshSucceeds() = runTest { + val appInstall = createNativeInstall() + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) + } returns Unit + + val result = refresher.updateDownloadUrls(appInstall, false) + + assertTrue(result) + coVerify(exactly = 0) { appManagerWrapper.installationIssue(any()) } + } + + @Test + fun updateDownloadUrls_handlesFreeAppNotPurchasedAsRestricted() = runTest { + val appInstall = createNativeInstall(isFree = true) + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) + } throws InternalException.AppNotPurchased() + + val result = refresher.updateDownloadUrls(appInstall, false) + + assertFalse(result) + assertTrue(appEventDispatcher.events.any { it is AppEvent.AppRestrictedOrUnavailable }) + coVerify { appManager.addDownload(appInstall) } + coVerify { appManager.updateUnavailable(appInstall) } + coVerify(exactly = 0) { appManagerWrapper.addFusedDownloadPurchaseNeeded(any()) } + } + + @Test + fun updateDownloadUrls_handlesPaidAppNotPurchasedAsPurchaseNeeded() = runTest { + val appInstall = createNativeInstall(isFree = false) + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) + } throws InternalException.AppNotPurchased() + + val result = refresher.updateDownloadUrls(appInstall, false) + + assertFalse(result) + coVerify { appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) } + assertTrue(appEventDispatcher.events.any { it is AppEvent.AppPurchaseEvent }) + coVerify(exactly = 0) { appManager.addDownload(any()) } + } + + @Test + fun updateDownloadUrls_recordsIssueWhenHttpRefreshFails() = runTest { + val appInstall = createNativeInstall() + coEvery { appInstallRepository.getDownloadById(appInstall.id) } returns null + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) + } throws GplayHttpRequestException(403, "forbidden") + + val result = refresher.updateDownloadUrls(appInstall, false) + + assertFalse(result) + coVerify { appInstallRepository.addDownload(appInstall) } + coVerify { appManagerWrapper.installationIssue(appInstall) } + assertTrue(appEventDispatcher.events.none { it is AppEvent.UpdateEvent }) + } + + @Test + fun updateDownloadUrls_dispatchesUpdateEventWhenUpdateRefreshFails() = runTest { + val appInstall = createNativeInstall() + coEvery { appInstallRepository.getDownloadById(appInstall.id) } returns null + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) + } throws IllegalStateException("boom") + + val result = refresher.updateDownloadUrls(appInstall, true) + + assertFalse(result) + coVerify { appInstallRepository.addDownload(appInstall) } + coVerify { appManagerWrapper.installationIssue(appInstall) } + assertTrue(appEventDispatcher.events.any { it is AppEvent.UpdateEvent }) + } + + @Test + fun updateDownloadUrls_doesNotDuplicateExistingDownloadOnFailure() = runTest { + val appInstall = createNativeInstall() + coEvery { appInstallRepository.getDownloadById(appInstall.id) } returns appInstall + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) + } throws IllegalStateException("boom") + + val result = refresher.updateDownloadUrls(appInstall, false) + + assertFalse(result) + coVerify(exactly = 0) { appInstallRepository.addDownload(any()) } + coVerify { appManagerWrapper.installationIssue(appInstall) } + } + + @Test + fun updateDownloadUrls_rethrowsCancellation() = runTest { + val appInstall = createNativeInstall() + coEvery { + applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) + } throws CancellationException("cancelled") + + assertFailsWith { + refresher.updateDownloadUrls(appInstall, false) + } + } + + private fun createNativeInstall(isFree: Boolean = true) = AppInstall( + type = Type.NATIVE, + source = Source.PLAY_STORE, + id = "123", + status = Status.AWAITING, + packageName = "com.example.app", + isFree = isFree + ) +} -- GitLab From 281ad165cccb744f880593bd73ae83096668bf85 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 01:35:21 +0600 Subject: [PATCH 14/29] test: cover install device preconditions Pin the network and storage guard rails from the install refactor so user-facing failure signals keep their current behavior. --- .../AppInstallDevicePreconditionsTest.kt | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 app/src/test/java/foundation/e/apps/installProcessor/AppInstallDevicePreconditionsTest.kt diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDevicePreconditionsTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDevicePreconditionsTest.kt new file mode 100644 index 000000000..b67ae8ee2 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDevicePreconditionsTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.installProcessor + +import foundation.e.apps.R +import foundation.e.apps.data.event.AppEvent +import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.notification.StorageNotificationManager +import foundation.e.apps.data.install.workmanager.AppInstallDevicePreconditions +import foundation.e.apps.data.install.wrapper.NetworkStatusChecker +import foundation.e.apps.data.install.wrapper.StorageSpaceChecker +import foundation.e.apps.domain.model.install.Status +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AppInstallDevicePreconditionsTest { + private lateinit var appManagerWrapper: AppManagerWrapper + private lateinit var appEventDispatcher: FakeAppEventDispatcher + private lateinit var storageNotificationManager: StorageNotificationManager + private lateinit var storageSpaceChecker: StorageSpaceChecker + private lateinit var networkStatusChecker: NetworkStatusChecker + private lateinit var preconditions: AppInstallDevicePreconditions + + @Before + fun setup() { + appManagerWrapper = mockk(relaxed = true) + appEventDispatcher = FakeAppEventDispatcher() + storageNotificationManager = mockk(relaxed = true) + storageSpaceChecker = mockk(relaxed = true) + networkStatusChecker = mockk(relaxed = true) + preconditions = AppInstallDevicePreconditions( + appManagerWrapper, + appEventDispatcher, + storageNotificationManager, + storageSpaceChecker, + networkStatusChecker + ) + } + + @Test + fun canProceed_returnsFalseWhenNetworkUnavailable() = runTest { + val appInstall = createInstall() + every { networkStatusChecker.isNetworkAvailable() } returns false + + val result = preconditions.canProceed(appInstall) + + assertFalse(result) + coVerify { appManagerWrapper.installationIssue(appInstall) } + verify(exactly = 0) { storageSpaceChecker.spaceMissing(any()) } + assertTrue(appEventDispatcher.events.any { + it is AppEvent.NoInternetEvent && it.data == false + }) + } + + @Test + fun canProceed_returnsFalseWhenStorageIsMissing() = runTest { + val appInstall = createInstall() + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 512L + + val result = preconditions.canProceed(appInstall) + + assertFalse(result) + verify { storageNotificationManager.showNotEnoughSpaceNotification(appInstall) } + coVerify { appManagerWrapper.installationIssue(appInstall) } + assertTrue(appEventDispatcher.events.any { + it is AppEvent.ErrorMessageEvent && it.data == R.string.not_enough_storage + }) + } + + @Test + fun canProceed_returnsTrueWhenNetworkAndStorageChecksPass() = runTest { + val appInstall = createInstall() + every { networkStatusChecker.isNetworkAvailable() } returns true + every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L + + val result = preconditions.canProceed(appInstall) + + assertTrue(result) + coVerify(exactly = 0) { appManagerWrapper.installationIssue(any()) } + verify(exactly = 0) { storageNotificationManager.showNotEnoughSpaceNotification(any()) } + assertTrue(appEventDispatcher.events.isEmpty()) + } + + private fun createInstall() = AppInstall( + id = "123", + status = Status.AWAITING, + name = "Example App", + packageName = "com.example.app", + appSize = 1024L + ) +} -- GitLab From cc4a3e12dcd770145a85c048abf3ecbc887dca31 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 01:38:00 +0600 Subject: [PATCH 15/29] test: cover install worker result handling Lock down the worker boundary from the install refactor so input validation and processor result mapping stay stable. --- .../installProcessor/InstallAppWorkerTest.kt | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 app/src/test/java/foundation/e/apps/installProcessor/InstallAppWorkerTest.kt diff --git a/app/src/test/java/foundation/e/apps/installProcessor/InstallAppWorkerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/InstallAppWorkerTest.kt new file mode 100644 index 000000000..327f18b6a --- /dev/null +++ b/app/src/test/java/foundation/e/apps/installProcessor/InstallAppWorkerTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package foundation.e.apps.installProcessor + +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import androidx.work.Data +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.install.workmanager.AppInstallProcessor +import foundation.e.apps.data.install.workmanager.InstallAppWorker +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.R]) +@OptIn(ExperimentalCoroutinesApi::class) +class InstallAppWorkerTest { + private lateinit var appInstallProcessor: AppInstallProcessor + + @Before + fun setup() { + appInstallProcessor = mockk(relaxed = true) + } + + @Test + fun doWork_returnsFailureWhenFusedDownloadIdIsMissing() = runTest { + val worker = createWorker(Data.EMPTY) + + val result = worker.doWork() + + assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.failure()) + coVerify(exactly = 0) { appInstallProcessor.processInstall(any(), any(), any()) } + } + + @Test + fun doWork_returnsSuccessWhenProcessorSucceeds() = runTest { + coEvery { + appInstallProcessor.processInstall("123", true, any()) + } returns Result.success(ResultStatus.OK) + val worker = createWorker( + Data.Builder() + .putString(InstallAppWorker.INPUT_DATA_FUSED_DOWNLOAD, "123") + .putBoolean(InstallAppWorker.IS_UPDATE_WORK, true) + .build() + ) + + val result = worker.doWork() + + assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.success()) + coVerify { appInstallProcessor.processInstall("123", true, any()) } + } + + @Test + fun doWork_returnsFailureWhenProcessorFails() = runTest { + coEvery { + appInstallProcessor.processInstall("123", false, any()) + } returns Result.failure(IllegalStateException("boom")) + val worker = createWorker( + Data.Builder() + .putString(InstallAppWorker.INPUT_DATA_FUSED_DOWNLOAD, "123") + .putBoolean(InstallAppWorker.IS_UPDATE_WORK, false) + .build() + ) + + val result = worker.doWork() + + assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.failure()) + coVerify { appInstallProcessor.processInstall("123", false, any()) } + } + + private fun createWorker(inputData: Data): InstallAppWorker { + val params = mockk(relaxed = true) + every { params.inputData } returns inputData + + return InstallAppWorker( + ApplicationProvider.getApplicationContext(), + params, + appInstallProcessor + ) + } +} -- GitLab From 7ff2d42071ef7684e6d74ca8775085c184fcddd4 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 20:35:22 +0600 Subject: [PATCH 16/29] refactor: move AppInstallProcessor to install/core package --- .../{workmanager => core}/AppInstallProcessor.kt | 13 ++++++++----- .../e/apps/data/install/updates/UpdatesWorker.kt | 2 +- .../data/install/workmanager/InstallAppWorker.kt | 1 + .../foundation/e/apps/ui/MainActivityViewModel.kt | 2 +- .../apps/data/install/updates/UpdatesWorkerTest.kt | 2 +- .../installProcessor/AppInstallProcessorTest.kt | 2 +- .../e/apps/installProcessor/InstallAppWorkerTest.kt | 2 +- 7 files changed, 14 insertions(+), 10 deletions(-) rename app/src/main/java/foundation/e/apps/data/install/{workmanager => core}/AppInstallProcessor.kt (81%) diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt similarity index 81% rename from app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt rename to app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt index 1301363d9..63a412c2d 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt @@ -1,6 +1,5 @@ /* - * Copyright MURENA SAS 2023 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,14 +13,18 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * */ -package foundation.e.apps.data.install.workmanager +package foundation.e.apps.data.install.core import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.AppInstallComponents import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.workmanager.AppInstallRequestFactory +import foundation.e.apps.data.install.workmanager.AppInstallStartCoordinator +import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner import foundation.e.apps.domain.model.install.Status import javax.inject.Inject @@ -32,7 +35,7 @@ class AppInstallProcessor @Inject constructor( private val appInstallRequestFactory: AppInstallRequestFactory, ) { /** - * creates [AppInstall] from [Application] and enqueues into WorkManager to run install process. + * creates [foundation.e.apps.data.install.models.AppInstall] from [foundation.e.apps.data.application.data.Application] and enqueues into WorkManager to run install process. * @param application represents the app info which will be installed * @param isAnUpdate indicates the app is requested for update or not * @@ -51,7 +54,7 @@ class AppInstallProcessor @Inject constructor( } /** - * Enqueues [AppInstall] into WorkManager to run app install process. Before enqueuing, + * Enqueues [foundation.e.apps.data.install.models.AppInstall] into WorkManager to run app install process. Before enqueuing, * It validates some corner cases * @param appInstall represents the app downloading and installing related info, example- Installing Status, * Url of the APK,OBB files are needed to be downloaded and installed etc. diff --git a/app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorker.kt index f510a58f2..a4d36ac91 100644 --- a/app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorker.kt @@ -20,7 +20,7 @@ import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.event.EventBus import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository -import foundation.e.apps.data.install.workmanager.AppInstallProcessor +import foundation.e.apps.data.install.core.AppInstallProcessor import foundation.e.apps.data.login.repository.AuthenticatorRepository import foundation.e.apps.data.updates.UpdatesManagerRepository import foundation.e.apps.domain.model.User diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallAppWorker.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallAppWorker.kt index 6ea50e733..97c787556 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallAppWorker.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallAppWorker.kt @@ -32,6 +32,7 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject import foundation.e.apps.R +import foundation.e.apps.data.install.core.AppInstallProcessor import java.util.concurrent.atomic.AtomicInteger @HiltWorker diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt index b5deaa4ee..83c16dc00 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -39,7 +39,7 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.pkg.AppLoungePackageManager import foundation.e.apps.data.install.pkg.PwaManager -import foundation.e.apps.data.install.workmanager.AppInstallProcessor +import foundation.e.apps.data.install.core.AppInstallProcessor import foundation.e.apps.data.login.core.AuthObject import foundation.e.apps.data.parentalcontrol.fdroid.FDroidAntiFeatureRepository import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingRepository diff --git a/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkerTest.kt b/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkerTest.kt index 3e53b5c04..f892ef41c 100644 --- a/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkerTest.kt +++ b/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkerTest.kt @@ -50,7 +50,7 @@ import foundation.e.apps.data.login.core.StoreType import foundation.e.apps.data.login.repository.AuthenticatorRepository import foundation.e.apps.data.preference.SessionDataStore import foundation.e.apps.data.updates.UpdatesManagerRepository -import foundation.e.apps.data.install.workmanager.AppInstallProcessor +import foundation.e.apps.data.install.core.AppInstallProcessor import foundation.e.apps.domain.model.LoginState import foundation.e.apps.domain.model.User import foundation.e.apps.domain.preferences.AppPreferencesRepository 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 6755c9f32..11908eca1 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -28,7 +28,7 @@ import foundation.e.apps.data.install.AppInstallComponents import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.workmanager.AppInstallProcessor +import foundation.e.apps.data.install.core.AppInstallProcessor import foundation.e.apps.data.install.workmanager.AppInstallRequestFactory import foundation.e.apps.data.install.workmanager.AppInstallStartCoordinator import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner diff --git a/app/src/test/java/foundation/e/apps/installProcessor/InstallAppWorkerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/InstallAppWorkerTest.kt index 327f18b6a..8efe9bf3d 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/InstallAppWorkerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/InstallAppWorkerTest.kt @@ -23,7 +23,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.work.Data import com.google.common.truth.Truth.assertThat import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.install.workmanager.AppInstallProcessor +import foundation.e.apps.data.install.core.AppInstallProcessor import foundation.e.apps.data.install.workmanager.InstallAppWorker import io.mockk.coEvery import io.mockk.coVerify -- GitLab From b3a68599ade9f802ce1a6f678e8343c5dc2b3b68 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 20:39:15 +0600 Subject: [PATCH 17/29] refactor: move AppInstallProcessor's collaborator classes into install/core/helper package --- .../e/apps/data/install/core/AppInstallProcessor.kt | 6 +++--- .../helper}/AppInstallAgeLimitGate.kt | 2 +- .../helper}/AppInstallDevicePreconditions.kt | 2 +- .../helper}/AppInstallDownloadUrlRefresher.kt | 2 +- .../helper}/AppInstallPreEnqueueChecker.kt | 2 +- .../helper}/AppInstallRequestFactory.kt | 2 +- .../helper}/AppInstallStartCoordinator.kt | 3 ++- .../helper}/AppInstallWorkRunner.kt | 2 +- .../helper}/AppUpdateCompletionHandler.kt | 2 +- .../installProcessor/AppInstallAgeLimitGateTest.kt | 2 +- .../AppInstallDevicePreconditionsTest.kt | 2 +- .../AppInstallDownloadUrlRefresherTest.kt | 2 +- .../AppInstallPreEnqueueCheckerTest.kt | 8 ++++---- .../e/apps/installProcessor/AppInstallProcessorTest.kt | 6 +++--- .../installProcessor/AppInstallRequestFactoryTest.kt | 2 +- .../installProcessor/AppInstallStartCoordinatorTest.kt | 10 +++++----- .../apps/installProcessor/AppInstallWorkRunnerTest.kt | 4 ++-- .../installProcessor/AppUpdateCompletionHandlerTest.kt | 2 +- 18 files changed, 31 insertions(+), 30 deletions(-) rename app/src/main/java/foundation/e/apps/data/install/{workmanager => core/helper}/AppInstallAgeLimitGate.kt (98%) rename app/src/main/java/foundation/e/apps/data/install/{workmanager => core/helper}/AppInstallDevicePreconditions.kt (98%) rename app/src/main/java/foundation/e/apps/data/install/{workmanager => core/helper}/AppInstallDownloadUrlRefresher.kt (98%) rename app/src/main/java/foundation/e/apps/data/install/{workmanager => core/helper}/AppInstallPreEnqueueChecker.kt (97%) rename app/src/main/java/foundation/e/apps/data/install/{workmanager => core/helper}/AppInstallRequestFactory.kt (97%) rename app/src/main/java/foundation/e/apps/data/install/{workmanager => core/helper}/AppInstallStartCoordinator.kt (97%) rename app/src/main/java/foundation/e/apps/data/install/{workmanager => core/helper}/AppInstallWorkRunner.kt (99%) rename app/src/main/java/foundation/e/apps/data/install/{workmanager => core/helper}/AppUpdateCompletionHandler.kt (98%) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt index 63a412c2d..16bbbad04 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt @@ -22,9 +22,9 @@ import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.AppInstallComponents import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.workmanager.AppInstallRequestFactory -import foundation.e.apps.data.install.workmanager.AppInstallStartCoordinator -import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner +import foundation.e.apps.data.install.core.helper.AppInstallRequestFactory +import foundation.e.apps.data.install.core.helper.AppInstallStartCoordinator +import foundation.e.apps.data.install.core.helper.AppInstallWorkRunner import foundation.e.apps.domain.model.install.Status import javax.inject.Inject diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallAgeLimitGate.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallAgeLimitGate.kt similarity index 98% rename from app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallAgeLimitGate.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallAgeLimitGate.kt index c98d5e1e1..4c4520d82 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallAgeLimitGate.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallAgeLimitGate.kt @@ -16,7 +16,7 @@ * */ -package foundation.e.apps.data.install.workmanager +package foundation.e.apps.data.install.core.helper import foundation.e.apps.R import foundation.e.apps.data.ResultSupreme diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDevicePreconditions.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallDevicePreconditions.kt similarity index 98% rename from app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDevicePreconditions.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallDevicePreconditions.kt index e43499ad4..538ea9ce4 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDevicePreconditions.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallDevicePreconditions.kt @@ -16,7 +16,7 @@ * */ -package foundation.e.apps.data.install.workmanager +package foundation.e.apps.data.install.core.helper import foundation.e.apps.R import foundation.e.apps.data.event.AppEvent diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDownloadUrlRefresher.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallDownloadUrlRefresher.kt similarity index 98% rename from app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDownloadUrlRefresher.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallDownloadUrlRefresher.kt index bc4ac8c6a..dcf84a42b 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallDownloadUrlRefresher.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallDownloadUrlRefresher.kt @@ -16,7 +16,7 @@ * */ -package foundation.e.apps.data.install.workmanager +package foundation.e.apps.data.install.core.helper import com.aurora.gplayapi.exceptions.InternalException import foundation.e.apps.data.ResultSupreme diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallPreEnqueueChecker.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt similarity index 97% rename from app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallPreEnqueueChecker.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt index 04684a195..d307cc1d6 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallPreEnqueueChecker.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt @@ -16,7 +16,7 @@ * */ -package foundation.e.apps.data.install.workmanager +package foundation.e.apps.data.install.core.helper import foundation.e.apps.data.enums.Type import foundation.e.apps.data.install.AppManagerWrapper diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallRequestFactory.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallRequestFactory.kt similarity index 97% rename from app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallRequestFactory.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallRequestFactory.kt index 2045c39bf..17aabba00 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallRequestFactory.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallRequestFactory.kt @@ -16,7 +16,7 @@ * */ -package foundation.e.apps.data.install.workmanager +package foundation.e.apps.data.install.core.helper import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallStartCoordinator.kt similarity index 97% rename from app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallStartCoordinator.kt index c0e11f43e..afb262d49 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallStartCoordinator.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallStartCoordinator.kt @@ -16,7 +16,7 @@ * */ -package foundation.e.apps.data.install.workmanager +package foundation.e.apps.data.install.core.helper import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext @@ -24,6 +24,7 @@ import foundation.e.apps.R import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.install.workmanager.InstallWorkManager import foundation.e.apps.data.install.wrapper.AppEventDispatcher import foundation.e.apps.data.preference.PlayStoreAuthStore import foundation.e.apps.domain.model.User diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallWorkRunner.kt similarity index 99% rename from app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallWorkRunner.kt index ab59f1e12..3cb430e6f 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppInstallWorkRunner.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallWorkRunner.kt @@ -16,7 +16,7 @@ * */ -package foundation.e.apps.data.install.workmanager +package foundation.e.apps.data.install.core.helper import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.AppInstallRepository diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppUpdateCompletionHandler.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppUpdateCompletionHandler.kt similarity index 98% rename from app/src/main/java/foundation/e/apps/data/install/workmanager/AppUpdateCompletionHandler.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/AppUpdateCompletionHandler.kt index b68636dc6..88a8ba843 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/AppUpdateCompletionHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppUpdateCompletionHandler.kt @@ -16,7 +16,7 @@ * */ -package foundation.e.apps.data.install.workmanager +package foundation.e.apps.data.install.core.helper import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallAgeLimitGateTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallAgeLimitGateTest.kt index 19de4f0f8..675ad042d 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallAgeLimitGateTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallAgeLimitGateTest.kt @@ -24,7 +24,7 @@ import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate +import foundation.e.apps.data.install.core.helper.AppInstallAgeLimitGate import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.domain.model.ContentRatingValidity diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDevicePreconditionsTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDevicePreconditionsTest.kt index b67ae8ee2..627c9e3e5 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDevicePreconditionsTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDevicePreconditionsTest.kt @@ -23,7 +23,7 @@ import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager -import foundation.e.apps.data.install.workmanager.AppInstallDevicePreconditions +import foundation.e.apps.data.install.core.helper.AppInstallDevicePreconditions import foundation.e.apps.data.install.wrapper.NetworkStatusChecker import foundation.e.apps.data.install.wrapper.StorageSpaceChecker import foundation.e.apps.domain.model.install.Status diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDownloadUrlRefresherTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDownloadUrlRefresherTest.kt index 6be024abf..2d5afeac6 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDownloadUrlRefresherTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDownloadUrlRefresherTest.kt @@ -27,7 +27,7 @@ import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.workmanager.AppInstallDownloadUrlRefresher +import foundation.e.apps.data.install.core.helper.AppInstallDownloadUrlRefresher import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.domain.model.install.Status import io.mockk.coEvery diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt index 085af08db..58e7efdd4 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt @@ -22,10 +22,10 @@ import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Type import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate -import foundation.e.apps.data.install.workmanager.AppInstallDevicePreconditions -import foundation.e.apps.data.install.workmanager.AppInstallDownloadUrlRefresher -import foundation.e.apps.data.install.workmanager.AppInstallPreEnqueueChecker +import foundation.e.apps.data.install.core.helper.AppInstallAgeLimitGate +import foundation.e.apps.data.install.core.helper.AppInstallDevicePreconditions +import foundation.e.apps.data.install.core.helper.AppInstallDownloadUrlRefresher +import foundation.e.apps.data.install.core.helper.AppInstallPreEnqueueChecker import foundation.e.apps.domain.model.install.Status import io.mockk.coEvery import io.mockk.coVerify 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 11908eca1..f25697c84 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -29,9 +29,9 @@ import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.core.AppInstallProcessor -import foundation.e.apps.data.install.workmanager.AppInstallRequestFactory -import foundation.e.apps.data.install.workmanager.AppInstallStartCoordinator -import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner +import foundation.e.apps.data.install.core.helper.AppInstallRequestFactory +import foundation.e.apps.data.install.core.helper.AppInstallStartCoordinator +import foundation.e.apps.data.install.core.helper.AppInstallWorkRunner import foundation.e.apps.util.MainCoroutineRule import io.mockk.coEvery import io.mockk.coVerify diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallRequestFactoryTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallRequestFactoryTest.kt index 53c5a28ef..068e49d64 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallRequestFactoryTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallRequestFactoryTest.kt @@ -22,7 +22,7 @@ import com.aurora.gplayapi.data.models.ContentRating import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Type -import foundation.e.apps.data.install.workmanager.AppInstallRequestFactory +import foundation.e.apps.data.install.core.helper.AppInstallRequestFactory import foundation.e.apps.domain.model.install.Status import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt index 9975a676a..288977a11 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt @@ -31,11 +31,11 @@ import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager -import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate -import foundation.e.apps.data.install.workmanager.AppInstallDevicePreconditions -import foundation.e.apps.data.install.workmanager.AppInstallDownloadUrlRefresher -import foundation.e.apps.data.install.workmanager.AppInstallPreEnqueueChecker -import foundation.e.apps.data.install.workmanager.AppInstallStartCoordinator +import foundation.e.apps.data.install.core.helper.AppInstallAgeLimitGate +import foundation.e.apps.data.install.core.helper.AppInstallDevicePreconditions +import foundation.e.apps.data.install.core.helper.AppInstallDownloadUrlRefresher +import foundation.e.apps.data.install.core.helper.AppInstallPreEnqueueChecker +import foundation.e.apps.data.install.core.helper.AppInstallStartCoordinator import foundation.e.apps.data.install.workmanager.InstallWorkManager import foundation.e.apps.data.install.wrapper.NetworkStatusChecker import foundation.e.apps.data.install.wrapper.StorageSpaceChecker diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt index 3e0e3da90..fce9d6dc1 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt @@ -25,8 +25,8 @@ import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.download.DownloadManagerUtils import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner -import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler +import foundation.e.apps.data.install.core.helper.AppInstallWorkRunner +import foundation.e.apps.data.install.core.helper.AppUpdateCompletionHandler import foundation.e.apps.domain.model.install.Status import foundation.e.apps.util.MainCoroutineRule import io.mockk.mockk diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppUpdateCompletionHandlerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppUpdateCompletionHandlerTest.kt index c27314a7f..6b8de8e47 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppUpdateCompletionHandlerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppUpdateCompletionHandlerTest.kt @@ -24,7 +24,7 @@ import foundation.e.apps.R import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler +import foundation.e.apps.data.install.core.helper.AppUpdateCompletionHandler import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender import foundation.e.apps.data.install.wrapper.UpdatesTracker import foundation.e.apps.data.preference.PlayStoreAuthStore -- GitLab From 523ad797cd704c593beffbda09f7eeda46fac1f8 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 20:41:21 +0600 Subject: [PATCH 18/29] refactor: rename AppInstallAgeLimitGate to AgeLimitGate --- ...pInstallAgeLimitGate.kt => AgeLimitGate.kt} | 2 +- .../core/helper/AppInstallPreEnqueueChecker.kt | 4 ++-- ...AgeLimitGateTest.kt => AgeLimitGateTest.kt} | 8 ++++---- .../AppInstallPreEnqueueCheckerTest.kt | 18 +++++++++--------- .../AppInstallStartCoordinatorTest.kt | 18 +++++++++--------- 5 files changed, 25 insertions(+), 25 deletions(-) rename app/src/main/java/foundation/e/apps/data/install/core/helper/{AppInstallAgeLimitGate.kt => AgeLimitGate.kt} (98%) rename app/src/test/java/foundation/e/apps/installProcessor/{AppInstallAgeLimitGateTest.kt => AgeLimitGateTest.kt} (96%) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallAgeLimitGate.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AgeLimitGate.kt similarity index 98% rename from app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallAgeLimitGate.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/AgeLimitGate.kt index 4c4520d82..2023908a8 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallAgeLimitGate.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AgeLimitGate.kt @@ -30,7 +30,7 @@ import foundation.e.apps.domain.model.ContentRatingValidity import kotlinx.coroutines.CompletableDeferred import javax.inject.Inject -class AppInstallAgeLimitGate @Inject constructor( +class AgeLimitGate @Inject constructor( private val validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase, private val appManagerWrapper: AppManagerWrapper, private val appEventDispatcher: AppEventDispatcher, diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt index d307cc1d6..a2d40b2cf 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt @@ -27,7 +27,7 @@ import javax.inject.Inject class AppInstallPreEnqueueChecker @Inject constructor( private val appInstallDownloadUrlRefresher: AppInstallDownloadUrlRefresher, private val appManagerWrapper: AppManagerWrapper, - private val appInstallAgeLimitGate: AppInstallAgeLimitGate, + private val ageLimitGate: AgeLimitGate, private val appInstallDevicePreconditions: AppInstallDevicePreconditions, ) { suspend fun canEnqueue(appInstall: AppInstall, isAnUpdate: Boolean = false): Boolean { @@ -35,7 +35,7 @@ class AppInstallPreEnqueueChecker @Inject constructor( appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, isAnUpdate) val isDownloadAdded = hasUpdatedDownloadUrls && addDownload(appInstall) - val isAgeLimitAllowed = isDownloadAdded && appInstallAgeLimitGate.allow(appInstall) + val isAgeLimitAllowed = isDownloadAdded && ageLimitGate.allow(appInstall) return isAgeLimitAllowed && appInstallDevicePreconditions.canProceed(appInstall) } diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallAgeLimitGateTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AgeLimitGateTest.kt similarity index 96% rename from app/src/test/java/foundation/e/apps/installProcessor/AppInstallAgeLimitGateTest.kt rename to app/src/test/java/foundation/e/apps/installProcessor/AgeLimitGateTest.kt index 675ad042d..8eccf4606 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallAgeLimitGateTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AgeLimitGateTest.kt @@ -24,7 +24,7 @@ import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.core.helper.AppInstallAgeLimitGate +import foundation.e.apps.data.install.core.helper.AgeLimitGate import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.domain.model.ContentRatingValidity @@ -41,12 +41,12 @@ import org.junit.Test import org.mockito.Mockito @OptIn(ExperimentalCoroutinesApi::class) -class AppInstallAgeLimitGateTest { +class AgeLimitGateTest { private lateinit var validateAppAgeLimitUseCase: ValidateAppAgeLimitUseCase private lateinit var appManagerWrapper: AppManagerWrapper private lateinit var parentalControlAuthGateway: ParentalControlAuthGateway private lateinit var appEventDispatcher: FakeAppEventDispatcher - private lateinit var gate: AppInstallAgeLimitGate + private lateinit var gate: AgeLimitGate @Before fun setup() { @@ -54,7 +54,7 @@ class AppInstallAgeLimitGateTest { appManagerWrapper = mockk(relaxed = true) parentalControlAuthGateway = mockk(relaxed = true) appEventDispatcher = FakeAppEventDispatcher(autoCompleteDeferred = true) - gate = AppInstallAgeLimitGate( + gate = AgeLimitGate( validateAppAgeLimitUseCase, appManagerWrapper, appEventDispatcher, diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt index 58e7efdd4..38f3bf5dc 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt @@ -22,7 +22,7 @@ import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Type import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.core.helper.AppInstallAgeLimitGate +import foundation.e.apps.data.install.core.helper.AgeLimitGate import foundation.e.apps.data.install.core.helper.AppInstallDevicePreconditions import foundation.e.apps.data.install.core.helper.AppInstallDownloadUrlRefresher import foundation.e.apps.data.install.core.helper.AppInstallPreEnqueueChecker @@ -41,7 +41,7 @@ import org.junit.Test class AppInstallPreEnqueueCheckerTest { private lateinit var appInstallDownloadUrlRefresher: AppInstallDownloadUrlRefresher private lateinit var appManagerWrapper: AppManagerWrapper - private lateinit var appInstallAgeLimitGate: AppInstallAgeLimitGate + private lateinit var ageLimitGate: AgeLimitGate private lateinit var appInstallDevicePreconditions: AppInstallDevicePreconditions private lateinit var checker: AppInstallPreEnqueueChecker @@ -49,12 +49,12 @@ class AppInstallPreEnqueueCheckerTest { fun setup() { appInstallDownloadUrlRefresher = mockk(relaxed = true) appManagerWrapper = mockk(relaxed = true) - appInstallAgeLimitGate = mockk(relaxed = true) + ageLimitGate = mockk(relaxed = true) appInstallDevicePreconditions = mockk(relaxed = true) checker = AppInstallPreEnqueueChecker( appInstallDownloadUrlRefresher, appManagerWrapper, - appInstallAgeLimitGate, + ageLimitGate, appInstallDevicePreconditions ) } @@ -63,7 +63,7 @@ class AppInstallPreEnqueueCheckerTest { fun canEnqueue_skipsDownloadUrlRefreshForPwaInstalls() = runTest { val appInstall = createPwaInstall() coEvery { appManagerWrapper.addDownload(appInstall) } returns true - coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + coEvery { ageLimitGate.allow(appInstall) } returns true coEvery { appInstallDevicePreconditions.canProceed(appInstall) } returns true val result = checker.canEnqueue(appInstall) @@ -83,7 +83,7 @@ class AppInstallPreEnqueueCheckerTest { assertFalse(result) coVerify(exactly = 0) { appManagerWrapper.addDownload(any()) } - coVerify(exactly = 0) { appInstallAgeLimitGate.allow(any()) } + coVerify(exactly = 0) { ageLimitGate.allow(any()) } coVerify(exactly = 0) { appInstallDevicePreconditions.canProceed(any()) } } @@ -96,7 +96,7 @@ class AppInstallPreEnqueueCheckerTest { val result = checker.canEnqueue(appInstall) assertFalse(result) - coVerify(exactly = 0) { appInstallAgeLimitGate.allow(any()) } + coVerify(exactly = 0) { ageLimitGate.allow(any()) } coVerify(exactly = 0) { appInstallDevicePreconditions.canProceed(any()) } } @@ -105,7 +105,7 @@ class AppInstallPreEnqueueCheckerTest { val appInstall = createNativeInstall() coEvery { appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns true coEvery { appManagerWrapper.addDownload(appInstall) } returns true - coEvery { appInstallAgeLimitGate.allow(appInstall) } returns false + coEvery { ageLimitGate.allow(appInstall) } returns false val result = checker.canEnqueue(appInstall) @@ -118,7 +118,7 @@ class AppInstallPreEnqueueCheckerTest { val appInstall = createNativeInstall() coEvery { appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns true coEvery { appManagerWrapper.addDownload(appInstall) } returns true - coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + coEvery { ageLimitGate.allow(appInstall) } returns true coEvery { appInstallDevicePreconditions.canProceed(appInstall) } returns true val result = checker.canEnqueue(appInstall) diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt index 288977a11..5ab790451 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt @@ -31,7 +31,7 @@ import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager -import foundation.e.apps.data.install.core.helper.AppInstallAgeLimitGate +import foundation.e.apps.data.install.core.helper.AgeLimitGate import foundation.e.apps.data.install.core.helper.AppInstallDevicePreconditions import foundation.e.apps.data.install.core.helper.AppInstallDownloadUrlRefresher import foundation.e.apps.data.install.core.helper.AppInstallPreEnqueueChecker @@ -68,7 +68,7 @@ class AppInstallStartCoordinatorTest { private lateinit var sessionRepository: SessionRepository private lateinit var playStoreAuthStore: PlayStoreAuthStore private lateinit var storageNotificationManager: StorageNotificationManager - private lateinit var appInstallAgeLimitGate: AppInstallAgeLimitGate + private lateinit var ageLimitGate: AgeLimitGate private lateinit var appEventDispatcher: FakeAppEventDispatcher private lateinit var storageSpaceChecker: StorageSpaceChecker private lateinit var networkStatusChecker: NetworkStatusChecker @@ -87,7 +87,7 @@ class AppInstallStartCoordinatorTest { sessionRepository = mockk(relaxed = true) playStoreAuthStore = mockk(relaxed = true) storageNotificationManager = mockk(relaxed = true) - appInstallAgeLimitGate = mockk(relaxed = true) + ageLimitGate = mockk(relaxed = true) appEventDispatcher = FakeAppEventDispatcher() storageSpaceChecker = mockk(relaxed = true) networkStatusChecker = mockk(relaxed = true) @@ -111,7 +111,7 @@ class AppInstallStartCoordinatorTest { preflightChecker = AppInstallPreEnqueueChecker( downloadUrlRefresher, appManagerWrapper, - appInstallAgeLimitGate, + ageLimitGate, devicePreconditions ) coordinator = AppInstallStartCoordinator( @@ -129,7 +129,7 @@ class AppInstallStartCoordinatorTest { val appInstall = createPwaInstall() coEvery { appManagerWrapper.addDownload(appInstall) } returns true - coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + coEvery { ageLimitGate.allow(appInstall) } returns true every { networkStatusChecker.isNetworkAvailable() } returns true every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L @@ -143,7 +143,7 @@ class AppInstallStartCoordinatorTest { val appInstall = createPwaInstall() coEvery { appManagerWrapper.addDownload(appInstall) } returns true - coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + coEvery { ageLimitGate.allow(appInstall) } returns true every { networkStatusChecker.isNetworkAvailable() } returns false every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L @@ -158,7 +158,7 @@ class AppInstallStartCoordinatorTest { val appInstall = createPwaInstall() coEvery { appManagerWrapper.addDownload(appInstall) } returns true - coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + coEvery { ageLimitGate.allow(appInstall) } returns true every { networkStatusChecker.isNetworkAvailable() } returns true every { storageSpaceChecker.spaceMissing(appInstall) } returns 100L @@ -178,7 +178,7 @@ class AppInstallStartCoordinatorTest { val result = coordinator.canEnqueue(appInstall) assertFalse(result) - coVerify(exactly = 0) { appInstallAgeLimitGate.allow(any()) } + coVerify(exactly = 0) { ageLimitGate.allow(any()) } } @Test @@ -192,7 +192,7 @@ class AppInstallStartCoordinatorTest { playStoreAuthStore.awaitAuthData() } returns AuthData(email = "anon@example.com", isAnonymous = true) coEvery { appManagerWrapper.addDownload(appInstall) } returns true - coEvery { appInstallAgeLimitGate.allow(appInstall) } returns true + coEvery { ageLimitGate.allow(appInstall) } returns true every { networkStatusChecker.isNetworkAvailable() } returns true every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L justRun { InstallWorkManager.enqueueWork(any(), any(), any()) } -- GitLab From 88ba29f59f71a683d2c762a8b31484f1865e0794 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 20:43:39 +0600 Subject: [PATCH 19/29] refactor: rename AppInstallDevicePreconditions to DevicePreconditions --- .../core/helper/AppInstallPreEnqueueChecker.kt | 4 ++-- ...Preconditions.kt => DevicePreconditions.kt} | 2 +- .../AppInstallPreEnqueueCheckerTest.kt | 18 +++++++++--------- .../AppInstallStartCoordinatorTest.kt | 6 +++--- ...tionsTest.kt => DevicePreconditionsTest.kt} | 8 ++++---- 5 files changed, 19 insertions(+), 19 deletions(-) rename app/src/main/java/foundation/e/apps/data/install/core/helper/{AppInstallDevicePreconditions.kt => DevicePreconditions.kt} (97%) rename app/src/test/java/foundation/e/apps/installProcessor/{AppInstallDevicePreconditionsTest.kt => DevicePreconditionsTest.kt} (94%) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt index a2d40b2cf..6b3674689 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt @@ -28,7 +28,7 @@ class AppInstallPreEnqueueChecker @Inject constructor( private val appInstallDownloadUrlRefresher: AppInstallDownloadUrlRefresher, private val appManagerWrapper: AppManagerWrapper, private val ageLimitGate: AgeLimitGate, - private val appInstallDevicePreconditions: AppInstallDevicePreconditions, + private val devicePreconditions: DevicePreconditions, ) { suspend fun canEnqueue(appInstall: AppInstall, isAnUpdate: Boolean = false): Boolean { val hasUpdatedDownloadUrls = appInstall.type == Type.PWA || @@ -37,7 +37,7 @@ class AppInstallPreEnqueueChecker @Inject constructor( val isDownloadAdded = hasUpdatedDownloadUrls && addDownload(appInstall) val isAgeLimitAllowed = isDownloadAdded && ageLimitGate.allow(appInstall) - return isAgeLimitAllowed && appInstallDevicePreconditions.canProceed(appInstall) + return isAgeLimitAllowed && devicePreconditions.canProceed(appInstall) } private suspend fun addDownload(appInstall: AppInstall): Boolean { diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallDevicePreconditions.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/DevicePreconditions.kt similarity index 97% rename from app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallDevicePreconditions.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/DevicePreconditions.kt index 538ea9ce4..3ba545b68 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallDevicePreconditions.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/DevicePreconditions.kt @@ -29,7 +29,7 @@ import foundation.e.apps.data.install.wrapper.StorageSpaceChecker import timber.log.Timber import javax.inject.Inject -class AppInstallDevicePreconditions @Inject constructor( +class DevicePreconditions @Inject constructor( private val appManagerWrapper: AppManagerWrapper, private val appEventDispatcher: AppEventDispatcher, private val storageNotificationManager: StorageNotificationManager, diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt index 38f3bf5dc..aa3993181 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt @@ -23,7 +23,7 @@ import foundation.e.apps.data.enums.Type import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.core.helper.AgeLimitGate -import foundation.e.apps.data.install.core.helper.AppInstallDevicePreconditions +import foundation.e.apps.data.install.core.helper.DevicePreconditions import foundation.e.apps.data.install.core.helper.AppInstallDownloadUrlRefresher import foundation.e.apps.data.install.core.helper.AppInstallPreEnqueueChecker import foundation.e.apps.domain.model.install.Status @@ -42,7 +42,7 @@ class AppInstallPreEnqueueCheckerTest { private lateinit var appInstallDownloadUrlRefresher: AppInstallDownloadUrlRefresher private lateinit var appManagerWrapper: AppManagerWrapper private lateinit var ageLimitGate: AgeLimitGate - private lateinit var appInstallDevicePreconditions: AppInstallDevicePreconditions + private lateinit var devicePreconditions: DevicePreconditions private lateinit var checker: AppInstallPreEnqueueChecker @Before @@ -50,12 +50,12 @@ class AppInstallPreEnqueueCheckerTest { appInstallDownloadUrlRefresher = mockk(relaxed = true) appManagerWrapper = mockk(relaxed = true) ageLimitGate = mockk(relaxed = true) - appInstallDevicePreconditions = mockk(relaxed = true) + devicePreconditions = mockk(relaxed = true) checker = AppInstallPreEnqueueChecker( appInstallDownloadUrlRefresher, appManagerWrapper, ageLimitGate, - appInstallDevicePreconditions + devicePreconditions ) } @@ -64,7 +64,7 @@ class AppInstallPreEnqueueCheckerTest { val appInstall = createPwaInstall() coEvery { appManagerWrapper.addDownload(appInstall) } returns true coEvery { ageLimitGate.allow(appInstall) } returns true - coEvery { appInstallDevicePreconditions.canProceed(appInstall) } returns true + coEvery { devicePreconditions.canProceed(appInstall) } returns true val result = checker.canEnqueue(appInstall) @@ -84,7 +84,7 @@ class AppInstallPreEnqueueCheckerTest { assertFalse(result) coVerify(exactly = 0) { appManagerWrapper.addDownload(any()) } coVerify(exactly = 0) { ageLimitGate.allow(any()) } - coVerify(exactly = 0) { appInstallDevicePreconditions.canProceed(any()) } + coVerify(exactly = 0) { devicePreconditions.canProceed(any()) } } @Test @@ -97,7 +97,7 @@ class AppInstallPreEnqueueCheckerTest { assertFalse(result) coVerify(exactly = 0) { ageLimitGate.allow(any()) } - coVerify(exactly = 0) { appInstallDevicePreconditions.canProceed(any()) } + coVerify(exactly = 0) { devicePreconditions.canProceed(any()) } } @Test @@ -110,7 +110,7 @@ class AppInstallPreEnqueueCheckerTest { val result = checker.canEnqueue(appInstall) assertFalse(result) - coVerify(exactly = 0) { appInstallDevicePreconditions.canProceed(any()) } + coVerify(exactly = 0) { devicePreconditions.canProceed(any()) } } @Test @@ -119,7 +119,7 @@ class AppInstallPreEnqueueCheckerTest { coEvery { appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns true coEvery { appManagerWrapper.addDownload(appInstall) } returns true coEvery { ageLimitGate.allow(appInstall) } returns true - coEvery { appInstallDevicePreconditions.canProceed(appInstall) } returns true + coEvery { devicePreconditions.canProceed(appInstall) } returns true val result = checker.canEnqueue(appInstall) diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt index 5ab790451..92cee0acb 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt @@ -32,7 +32,7 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager import foundation.e.apps.data.install.core.helper.AgeLimitGate -import foundation.e.apps.data.install.core.helper.AppInstallDevicePreconditions +import foundation.e.apps.data.install.core.helper.DevicePreconditions import foundation.e.apps.data.install.core.helper.AppInstallDownloadUrlRefresher import foundation.e.apps.data.install.core.helper.AppInstallPreEnqueueChecker import foundation.e.apps.data.install.core.helper.AppInstallStartCoordinator @@ -73,7 +73,7 @@ class AppInstallStartCoordinatorTest { private lateinit var storageSpaceChecker: StorageSpaceChecker private lateinit var networkStatusChecker: NetworkStatusChecker private lateinit var appManager: AppManager - private lateinit var devicePreconditions: AppInstallDevicePreconditions + private lateinit var devicePreconditions: DevicePreconditions private lateinit var downloadUrlRefresher: AppInstallDownloadUrlRefresher private lateinit var preflightChecker: AppInstallPreEnqueueChecker private lateinit var coordinator: AppInstallStartCoordinator @@ -101,7 +101,7 @@ class AppInstallStartCoordinatorTest { appEventDispatcher, appManager ) - devicePreconditions = AppInstallDevicePreconditions( + devicePreconditions = DevicePreconditions( appManagerWrapper, appEventDispatcher, storageNotificationManager, diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDevicePreconditionsTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/DevicePreconditionsTest.kt similarity index 94% rename from app/src/test/java/foundation/e/apps/installProcessor/AppInstallDevicePreconditionsTest.kt rename to app/src/test/java/foundation/e/apps/installProcessor/DevicePreconditionsTest.kt index 627c9e3e5..a4dc646e1 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDevicePreconditionsTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/DevicePreconditionsTest.kt @@ -23,7 +23,7 @@ import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager -import foundation.e.apps.data.install.core.helper.AppInstallDevicePreconditions +import foundation.e.apps.data.install.core.helper.DevicePreconditions import foundation.e.apps.data.install.wrapper.NetworkStatusChecker import foundation.e.apps.data.install.wrapper.StorageSpaceChecker import foundation.e.apps.domain.model.install.Status @@ -39,13 +39,13 @@ import org.junit.Before import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) -class AppInstallDevicePreconditionsTest { +class DevicePreconditionsTest { private lateinit var appManagerWrapper: AppManagerWrapper private lateinit var appEventDispatcher: FakeAppEventDispatcher private lateinit var storageNotificationManager: StorageNotificationManager private lateinit var storageSpaceChecker: StorageSpaceChecker private lateinit var networkStatusChecker: NetworkStatusChecker - private lateinit var preconditions: AppInstallDevicePreconditions + private lateinit var preconditions: DevicePreconditions @Before fun setup() { @@ -54,7 +54,7 @@ class AppInstallDevicePreconditionsTest { storageNotificationManager = mockk(relaxed = true) storageSpaceChecker = mockk(relaxed = true) networkStatusChecker = mockk(relaxed = true) - preconditions = AppInstallDevicePreconditions( + preconditions = DevicePreconditions( appManagerWrapper, appEventDispatcher, storageNotificationManager, -- GitLab From 4683e88e78a9ade28b09962458f489492df66a72 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 20:45:04 +0600 Subject: [PATCH 20/29] refactor: rename AppInstallDownloadUrlRefresher to DownloadUrlRefresher --- .../core/helper/AppInstallPreEnqueueChecker.kt | 4 ++-- ...UrlRefresher.kt => DownloadUrlRefresher.kt} | 2 +- .../AppInstallPreEnqueueCheckerTest.kt | 18 +++++++++--------- .../AppInstallStartCoordinatorTest.kt | 6 +++--- ...sherTest.kt => DownloadUrlRefresherTest.kt} | 8 ++++---- 5 files changed, 19 insertions(+), 19 deletions(-) rename app/src/main/java/foundation/e/apps/data/install/core/helper/{AppInstallDownloadUrlRefresher.kt => DownloadUrlRefresher.kt} (98%) rename app/src/test/java/foundation/e/apps/installProcessor/{AppInstallDownloadUrlRefresherTest.kt => DownloadUrlRefresherTest.kt} (96%) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt index 6b3674689..fd7efc0da 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt @@ -25,14 +25,14 @@ import timber.log.Timber import javax.inject.Inject class AppInstallPreEnqueueChecker @Inject constructor( - private val appInstallDownloadUrlRefresher: AppInstallDownloadUrlRefresher, + private val downloadUrlRefresher: DownloadUrlRefresher, private val appManagerWrapper: AppManagerWrapper, private val ageLimitGate: AgeLimitGate, private val devicePreconditions: DevicePreconditions, ) { suspend fun canEnqueue(appInstall: AppInstall, isAnUpdate: Boolean = false): Boolean { val hasUpdatedDownloadUrls = appInstall.type == Type.PWA || - appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, isAnUpdate) + downloadUrlRefresher.updateDownloadUrls(appInstall, isAnUpdate) val isDownloadAdded = hasUpdatedDownloadUrls && addDownload(appInstall) val isAgeLimitAllowed = isDownloadAdded && ageLimitGate.allow(appInstall) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallDownloadUrlRefresher.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/DownloadUrlRefresher.kt similarity index 98% rename from app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallDownloadUrlRefresher.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/DownloadUrlRefresher.kt index dcf84a42b..10bcc0bb5 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallDownloadUrlRefresher.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/DownloadUrlRefresher.kt @@ -33,7 +33,7 @@ import kotlinx.coroutines.CancellationException import timber.log.Timber import javax.inject.Inject -class AppInstallDownloadUrlRefresher @Inject constructor( +class DownloadUrlRefresher @Inject constructor( private val applicationRepository: ApplicationRepository, private val appInstallRepository: AppInstallRepository, private val appManagerWrapper: AppManagerWrapper, diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt index aa3993181..9b6a1b23d 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt @@ -24,7 +24,7 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.core.helper.AgeLimitGate import foundation.e.apps.data.install.core.helper.DevicePreconditions -import foundation.e.apps.data.install.core.helper.AppInstallDownloadUrlRefresher +import foundation.e.apps.data.install.core.helper.DownloadUrlRefresher import foundation.e.apps.data.install.core.helper.AppInstallPreEnqueueChecker import foundation.e.apps.domain.model.install.Status import io.mockk.coEvery @@ -39,7 +39,7 @@ import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class AppInstallPreEnqueueCheckerTest { - private lateinit var appInstallDownloadUrlRefresher: AppInstallDownloadUrlRefresher + private lateinit var downloadUrlRefresher: DownloadUrlRefresher private lateinit var appManagerWrapper: AppManagerWrapper private lateinit var ageLimitGate: AgeLimitGate private lateinit var devicePreconditions: DevicePreconditions @@ -47,12 +47,12 @@ class AppInstallPreEnqueueCheckerTest { @Before fun setup() { - appInstallDownloadUrlRefresher = mockk(relaxed = true) + downloadUrlRefresher = mockk(relaxed = true) appManagerWrapper = mockk(relaxed = true) ageLimitGate = mockk(relaxed = true) devicePreconditions = mockk(relaxed = true) checker = AppInstallPreEnqueueChecker( - appInstallDownloadUrlRefresher, + downloadUrlRefresher, appManagerWrapper, ageLimitGate, devicePreconditions @@ -70,14 +70,14 @@ class AppInstallPreEnqueueCheckerTest { assertTrue(result) coVerify(exactly = 0) { - appInstallDownloadUrlRefresher.updateDownloadUrls(any(), any()) + downloadUrlRefresher.updateDownloadUrls(any(), any()) } } @Test fun canEnqueue_stopsWhenDownloadRefreshFails() = runTest { val appInstall = createNativeInstall() - coEvery { appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns false + coEvery { downloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns false val result = checker.canEnqueue(appInstall) @@ -90,7 +90,7 @@ class AppInstallPreEnqueueCheckerTest { @Test fun canEnqueue_stopsWhenAddingDownloadFails() = runTest { val appInstall = createNativeInstall() - coEvery { appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns true + coEvery { downloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns true coEvery { appManagerWrapper.addDownload(appInstall) } returns false val result = checker.canEnqueue(appInstall) @@ -103,7 +103,7 @@ class AppInstallPreEnqueueCheckerTest { @Test fun canEnqueue_stopsWhenAgeLimitRejectsInstall() = runTest { val appInstall = createNativeInstall() - coEvery { appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns true + coEvery { downloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns true coEvery { appManagerWrapper.addDownload(appInstall) } returns true coEvery { ageLimitGate.allow(appInstall) } returns false @@ -116,7 +116,7 @@ class AppInstallPreEnqueueCheckerTest { @Test fun canEnqueue_returnsTrueWhenAllChecksPass() = runTest { val appInstall = createNativeInstall() - coEvery { appInstallDownloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns true + coEvery { downloadUrlRefresher.updateDownloadUrls(appInstall, false) } returns true coEvery { appManagerWrapper.addDownload(appInstall) } returns true coEvery { ageLimitGate.allow(appInstall) } returns true coEvery { devicePreconditions.canProceed(appInstall) } returns true diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt index 92cee0acb..37a74ab5c 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt @@ -33,7 +33,7 @@ import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.notification.StorageNotificationManager import foundation.e.apps.data.install.core.helper.AgeLimitGate import foundation.e.apps.data.install.core.helper.DevicePreconditions -import foundation.e.apps.data.install.core.helper.AppInstallDownloadUrlRefresher +import foundation.e.apps.data.install.core.helper.DownloadUrlRefresher import foundation.e.apps.data.install.core.helper.AppInstallPreEnqueueChecker import foundation.e.apps.data.install.core.helper.AppInstallStartCoordinator import foundation.e.apps.data.install.workmanager.InstallWorkManager @@ -74,7 +74,7 @@ class AppInstallStartCoordinatorTest { private lateinit var networkStatusChecker: NetworkStatusChecker private lateinit var appManager: AppManager private lateinit var devicePreconditions: DevicePreconditions - private lateinit var downloadUrlRefresher: AppInstallDownloadUrlRefresher + private lateinit var downloadUrlRefresher: DownloadUrlRefresher private lateinit var preflightChecker: AppInstallPreEnqueueChecker private lateinit var coordinator: AppInstallStartCoordinator @@ -94,7 +94,7 @@ class AppInstallStartCoordinatorTest { appManager = mockk(relaxed = true) coEvery { sessionRepository.awaitUser() } returns User.NO_GOOGLE coEvery { playStoreAuthStore.awaitAuthData() } returns null - downloadUrlRefresher = AppInstallDownloadUrlRefresher( + downloadUrlRefresher = DownloadUrlRefresher( applicationRepository, appInstallRepository, appManagerWrapper, diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDownloadUrlRefresherTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/DownloadUrlRefresherTest.kt similarity index 96% rename from app/src/test/java/foundation/e/apps/installProcessor/AppInstallDownloadUrlRefresherTest.kt rename to app/src/test/java/foundation/e/apps/installProcessor/DownloadUrlRefresherTest.kt index 2d5afeac6..5602e02e3 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallDownloadUrlRefresherTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/DownloadUrlRefresherTest.kt @@ -27,7 +27,7 @@ import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.core.helper.AppInstallDownloadUrlRefresher +import foundation.e.apps.data.install.core.helper.DownloadUrlRefresher import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.domain.model.install.Status import io.mockk.coEvery @@ -43,13 +43,13 @@ import org.junit.Before import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) -class AppInstallDownloadUrlRefresherTest { +class DownloadUrlRefresherTest { private lateinit var applicationRepository: ApplicationRepository private lateinit var appInstallRepository: AppInstallRepository private lateinit var appManagerWrapper: AppManagerWrapper private lateinit var appEventDispatcher: FakeAppEventDispatcher private lateinit var appManager: AppManager - private lateinit var refresher: AppInstallDownloadUrlRefresher + private lateinit var refresher: DownloadUrlRefresher @Before fun setup() { @@ -58,7 +58,7 @@ class AppInstallDownloadUrlRefresherTest { appManagerWrapper = mockk(relaxed = true) appEventDispatcher = FakeAppEventDispatcher() appManager = mockk(relaxed = true) - refresher = AppInstallDownloadUrlRefresher( + refresher = DownloadUrlRefresher( applicationRepository, appInstallRepository, appManagerWrapper, -- GitLab From 11169d6e247076b90b7dbca1e55c642fe10503fe Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 20:46:17 +0600 Subject: [PATCH 21/29] refactor: rename AppInstallPreEnqueueChecker to PreEnqueueChecker --- .../install/core/helper/AppInstallStartCoordinator.kt | 4 ++-- ...ppInstallPreEnqueueChecker.kt => PreEnqueueChecker.kt} | 2 +- .../installProcessor/AppInstallStartCoordinatorTest.kt | 6 +++--- ...lPreEnqueueCheckerTest.kt => PreEnqueueCheckerTest.kt} | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) rename app/src/main/java/foundation/e/apps/data/install/core/helper/{AppInstallPreEnqueueChecker.kt => PreEnqueueChecker.kt} (97%) rename app/src/test/java/foundation/e/apps/installProcessor/{AppInstallPreEnqueueCheckerTest.kt => PreEnqueueCheckerTest.kt} (95%) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallStartCoordinator.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallStartCoordinator.kt index afb262d49..b9359f540 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallStartCoordinator.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallStartCoordinator.kt @@ -35,7 +35,7 @@ import javax.inject.Inject class AppInstallStartCoordinator @Inject constructor( @ApplicationContext private val context: Context, - private val appInstallPreEnqueueChecker: AppInstallPreEnqueueChecker, + private val preEnqueueChecker: PreEnqueueChecker, private val appManagerWrapper: AppManagerWrapper, private val sessionRepository: SessionRepository, private val playStoreAuthStore: PlayStoreAuthStore, @@ -82,7 +82,7 @@ class AppInstallStartCoordinator @Inject constructor( } suspend fun canEnqueue(appInstall: AppInstall, isAnUpdate: Boolean = false): Boolean { - return appInstallPreEnqueueChecker.canEnqueue(appInstall, isAnUpdate) + return preEnqueueChecker.canEnqueue(appInstall, isAnUpdate) } private suspend fun dispatchAnonymousPaidAppWarning(appInstall: AppInstall, isSystemApp: Boolean) { diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/PreEnqueueChecker.kt similarity index 97% rename from app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/PreEnqueueChecker.kt index fd7efc0da..7d6cba8de 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallPreEnqueueChecker.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/PreEnqueueChecker.kt @@ -24,7 +24,7 @@ import foundation.e.apps.data.install.models.AppInstall import timber.log.Timber import javax.inject.Inject -class AppInstallPreEnqueueChecker @Inject constructor( +class PreEnqueueChecker @Inject constructor( private val downloadUrlRefresher: DownloadUrlRefresher, private val appManagerWrapper: AppManagerWrapper, private val ageLimitGate: AgeLimitGate, diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt index 37a74ab5c..33409f550 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt @@ -34,7 +34,7 @@ import foundation.e.apps.data.install.notification.StorageNotificationManager import foundation.e.apps.data.install.core.helper.AgeLimitGate import foundation.e.apps.data.install.core.helper.DevicePreconditions import foundation.e.apps.data.install.core.helper.DownloadUrlRefresher -import foundation.e.apps.data.install.core.helper.AppInstallPreEnqueueChecker +import foundation.e.apps.data.install.core.helper.PreEnqueueChecker import foundation.e.apps.data.install.core.helper.AppInstallStartCoordinator import foundation.e.apps.data.install.workmanager.InstallWorkManager import foundation.e.apps.data.install.wrapper.NetworkStatusChecker @@ -75,7 +75,7 @@ class AppInstallStartCoordinatorTest { private lateinit var appManager: AppManager private lateinit var devicePreconditions: DevicePreconditions private lateinit var downloadUrlRefresher: DownloadUrlRefresher - private lateinit var preflightChecker: AppInstallPreEnqueueChecker + private lateinit var preflightChecker: PreEnqueueChecker private lateinit var coordinator: AppInstallStartCoordinator @Before @@ -108,7 +108,7 @@ class AppInstallStartCoordinatorTest { storageSpaceChecker, networkStatusChecker ) - preflightChecker = AppInstallPreEnqueueChecker( + preflightChecker = PreEnqueueChecker( downloadUrlRefresher, appManagerWrapper, ageLimitGate, diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/PreEnqueueCheckerTest.kt similarity index 95% rename from app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt rename to app/src/test/java/foundation/e/apps/installProcessor/PreEnqueueCheckerTest.kt index 9b6a1b23d..bd46bdb07 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallPreEnqueueCheckerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/PreEnqueueCheckerTest.kt @@ -25,7 +25,7 @@ import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.core.helper.AgeLimitGate import foundation.e.apps.data.install.core.helper.DevicePreconditions import foundation.e.apps.data.install.core.helper.DownloadUrlRefresher -import foundation.e.apps.data.install.core.helper.AppInstallPreEnqueueChecker +import foundation.e.apps.data.install.core.helper.PreEnqueueChecker import foundation.e.apps.domain.model.install.Status import io.mockk.coEvery import io.mockk.coVerify @@ -38,12 +38,12 @@ import org.junit.Before import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) -class AppInstallPreEnqueueCheckerTest { +class PreEnqueueCheckerTest { private lateinit var downloadUrlRefresher: DownloadUrlRefresher private lateinit var appManagerWrapper: AppManagerWrapper private lateinit var ageLimitGate: AgeLimitGate private lateinit var devicePreconditions: DevicePreconditions - private lateinit var checker: AppInstallPreEnqueueChecker + private lateinit var checker: PreEnqueueChecker @Before fun setup() { @@ -51,7 +51,7 @@ class AppInstallPreEnqueueCheckerTest { appManagerWrapper = mockk(relaxed = true) ageLimitGate = mockk(relaxed = true) devicePreconditions = mockk(relaxed = true) - checker = AppInstallPreEnqueueChecker( + checker = PreEnqueueChecker( downloadUrlRefresher, appManagerWrapper, ageLimitGate, -- GitLab From 9aed1c571060f7eea8e70e6ba43009118280358a Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 20:52:38 +0600 Subject: [PATCH 22/29] refactor: rename AppInstallRequestFactory to InstallationRequest --- .../data/install/core/AppInstallProcessor.kt | 6 +++--- ...equestFactory.kt => InstallationRequest.kt} | 2 +- .../AppInstallProcessorTest.kt | 12 ++++++------ ...ctoryTest.kt => InstallationRequestTest.kt} | 18 +++++++++--------- 4 files changed, 19 insertions(+), 19 deletions(-) rename app/src/main/java/foundation/e/apps/data/install/core/helper/{AppInstallRequestFactory.kt => InstallationRequest.kt} (97%) rename app/src/test/java/foundation/e/apps/installProcessor/{AppInstallRequestFactoryTest.kt => InstallationRequestTest.kt} (85%) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt index 16bbbad04..17a4f594b 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt @@ -22,7 +22,7 @@ import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.AppInstallComponents import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.core.helper.AppInstallRequestFactory +import foundation.e.apps.data.install.core.helper.InstallationRequest import foundation.e.apps.data.install.core.helper.AppInstallStartCoordinator import foundation.e.apps.data.install.core.helper.AppInstallWorkRunner import foundation.e.apps.domain.model.install.Status @@ -32,7 +32,7 @@ class AppInstallProcessor @Inject constructor( private val appInstallComponents: AppInstallComponents, private val appInstallStartCoordinator: AppInstallStartCoordinator, private val appInstallWorkRunner: AppInstallWorkRunner, - private val appInstallRequestFactory: AppInstallRequestFactory, + private val installationRequest: InstallationRequest, ) { /** * creates [foundation.e.apps.data.install.models.AppInstall] from [foundation.e.apps.data.application.data.Application] and enqueues into WorkManager to run install process. @@ -44,7 +44,7 @@ class AppInstallProcessor @Inject constructor( application: Application, isAnUpdate: Boolean = false ): Boolean { - val appInstall = appInstallRequestFactory.create(application) + val appInstall = installationRequest.create(application) val isUpdate = isAnUpdate || application.status == Status.UPDATABLE || diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallRequestFactory.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationRequest.kt similarity index 97% rename from app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallRequestFactory.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationRequest.kt index 17aabba00..723aef3ff 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallRequestFactory.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationRequest.kt @@ -24,7 +24,7 @@ import foundation.e.apps.data.enums.Type import foundation.e.apps.data.install.models.AppInstall import javax.inject.Inject -class AppInstallRequestFactory @Inject constructor() { +class InstallationRequest @Inject constructor() { fun create(application: Application): AppInstall { val appInstall = AppInstall( application._id, 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 f25697c84..efe1f446d 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -29,7 +29,7 @@ import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.core.AppInstallProcessor -import foundation.e.apps.data.install.core.helper.AppInstallRequestFactory +import foundation.e.apps.data.install.core.helper.InstallationRequest import foundation.e.apps.data.install.core.helper.AppInstallStartCoordinator import foundation.e.apps.data.install.core.helper.AppInstallWorkRunner import foundation.e.apps.util.MainCoroutineRule @@ -55,7 +55,7 @@ class AppInstallProcessorTest { private lateinit var appManagerWrapper: AppManagerWrapper private lateinit var appInstallProcessor: AppInstallProcessor - private lateinit var appInstallRequestFactory: AppInstallRequestFactory + private lateinit var installationRequest: InstallationRequest private lateinit var appInstallStartCoordinator: AppInstallStartCoordinator private lateinit var appInstallWorkRunner: AppInstallWorkRunner @@ -64,7 +64,7 @@ class AppInstallProcessorTest { appManagerWrapper = mockk(relaxed = true) val appInstallRepository = mockk(relaxed = true) val appInstallComponents = AppInstallComponents(appInstallRepository, appManagerWrapper) - appInstallRequestFactory = mockk(relaxed = true) + installationRequest = mockk(relaxed = true) appInstallStartCoordinator = mockk(relaxed = true) appInstallWorkRunner = mockk(relaxed = true) @@ -72,7 +72,7 @@ class AppInstallProcessorTest { appInstallComponents, appInstallStartCoordinator, appInstallWorkRunner, - appInstallRequestFactory + installationRequest ) } @@ -87,7 +87,7 @@ class AppInstallProcessorTest { type = Type.NATIVE ) val appInstall = AppInstall(id = "123", packageName = "com.example.app") - coEvery { appInstallRequestFactory.create(application) } returns appInstall + coEvery { installationRequest.create(application) } returns appInstall coEvery { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns false coEvery { appInstallStartCoordinator.enqueue( @@ -100,7 +100,7 @@ class AppInstallProcessorTest { val result = appInstallProcessor.initAppInstall(application) assertTrue(result) - coVerify { appInstallRequestFactory.create(application) } + coVerify { installationRequest.create(application) } coVerify { appInstallStartCoordinator.enqueue(appInstall, true, application.isSystemApp) } } diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallRequestFactoryTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/InstallationRequestTest.kt similarity index 85% rename from app/src/test/java/foundation/e/apps/installProcessor/AppInstallRequestFactoryTest.kt rename to app/src/test/java/foundation/e/apps/installProcessor/InstallationRequestTest.kt index 068e49d64..5a0d37c3c 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallRequestFactoryTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/InstallationRequestTest.kt @@ -22,19 +22,19 @@ import com.aurora.gplayapi.data.models.ContentRating import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Type -import foundation.e.apps.data.install.core.helper.AppInstallRequestFactory +import foundation.e.apps.data.install.core.helper.InstallationRequest import foundation.e.apps.domain.model.install.Status import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -class AppInstallRequestFactoryTest { - private lateinit var factory: AppInstallRequestFactory +class InstallationRequestTest { + private lateinit var installationRequest: InstallationRequest @Before fun setup() { - factory = AppInstallRequestFactory() + installationRequest = InstallationRequest() } @Test @@ -53,7 +53,7 @@ class AppInstallRequestFactoryTest { originalSize = 2048L ) - val appInstall = factory.create(application) + val appInstall = installationRequest.create(application) assertEquals("123", appInstall.id) assertEquals(Source.PLAY_STORE, appInstall.source) @@ -73,7 +73,7 @@ class AppInstallRequestFactoryTest { val contentRating = ContentRating() val application = Application(contentRating = contentRating) - val appInstall = factory.create(application) + val appInstall = installationRequest.create(application) assertEquals(contentRating, appInstall.contentRating) } @@ -82,7 +82,7 @@ class AppInstallRequestFactoryTest { fun create_initializesDirectUrlForPwa() { val application = Application(type = Type.PWA, url = "https://example.com") - val appInstall = factory.create(application) + val appInstall = installationRequest.create(application) assertEquals(mutableListOf("https://example.com"), appInstall.downloadURLList) } @@ -91,7 +91,7 @@ class AppInstallRequestFactoryTest { fun create_initializesDirectUrlForSystemApp() { val application = Application(source = Source.SYSTEM_APP, url = "file://app.apk") - val appInstall = factory.create(application) + val appInstall = installationRequest.create(application) assertEquals(mutableListOf("file://app.apk"), appInstall.downloadURLList) } @@ -101,7 +101,7 @@ class AppInstallRequestFactoryTest { val application = Application(source = Source.PLAY_STORE, type = Type.NATIVE, url = "ignored") - val appInstall = factory.create(application) + val appInstall = installationRequest.create(application) assertTrue(appInstall.downloadURLList.isEmpty()) } -- GitLab From 9f314420d14de31c82395b24d95e3ba230a2afba Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 20:57:36 +0600 Subject: [PATCH 23/29] refactor: rename AppInstallStartCoordinator to InstallationEnqueuer --- .../data/install/core/AppInstallProcessor.kt | 6 ++-- ...Coordinator.kt => InstallationEnqueuer.kt} | 2 +- .../AppInstallProcessorTest.kt | 16 +++++------ ...torTest.kt => InstallationEnqueuerTest.kt} | 28 +++++++++---------- 4 files changed, 26 insertions(+), 26 deletions(-) rename app/src/main/java/foundation/e/apps/data/install/core/helper/{AppInstallStartCoordinator.kt => InstallationEnqueuer.kt} (98%) rename app/src/test/java/foundation/e/apps/installProcessor/{AppInstallStartCoordinatorTest.kt => InstallationEnqueuerTest.kt} (93%) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt index 17a4f594b..a6b25d110 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt @@ -23,14 +23,14 @@ import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.AppInstallComponents import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.core.helper.InstallationRequest -import foundation.e.apps.data.install.core.helper.AppInstallStartCoordinator +import foundation.e.apps.data.install.core.helper.InstallationEnqueuer import foundation.e.apps.data.install.core.helper.AppInstallWorkRunner import foundation.e.apps.domain.model.install.Status import javax.inject.Inject class AppInstallProcessor @Inject constructor( private val appInstallComponents: AppInstallComponents, - private val appInstallStartCoordinator: AppInstallStartCoordinator, + private val installationEnqueuer: InstallationEnqueuer, private val appInstallWorkRunner: AppInstallWorkRunner, private val installationRequest: InstallationRequest, ) { @@ -65,7 +65,7 @@ class AppInstallProcessor @Inject constructor( isAnUpdate: Boolean = false, isSystemApp: Boolean = false ): Boolean { - return appInstallStartCoordinator.enqueue(appInstall, isAnUpdate, isSystemApp) + return installationEnqueuer.enqueue(appInstall, isAnUpdate, isSystemApp) } suspend fun processInstall( diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallStartCoordinator.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationEnqueuer.kt similarity index 98% rename from app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallStartCoordinator.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationEnqueuer.kt index b9359f540..c5d2340d9 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallStartCoordinator.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationEnqueuer.kt @@ -33,7 +33,7 @@ import kotlinx.coroutines.CancellationException import timber.log.Timber import javax.inject.Inject -class AppInstallStartCoordinator @Inject constructor( +class InstallationEnqueuer @Inject constructor( @ApplicationContext private val context: Context, private val preEnqueueChecker: PreEnqueueChecker, private val appManagerWrapper: AppManagerWrapper, 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 efe1f446d..16a52a9e4 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -30,7 +30,7 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.core.AppInstallProcessor import foundation.e.apps.data.install.core.helper.InstallationRequest -import foundation.e.apps.data.install.core.helper.AppInstallStartCoordinator +import foundation.e.apps.data.install.core.helper.InstallationEnqueuer import foundation.e.apps.data.install.core.helper.AppInstallWorkRunner import foundation.e.apps.util.MainCoroutineRule import io.mockk.coEvery @@ -56,7 +56,7 @@ class AppInstallProcessorTest { private lateinit var appManagerWrapper: AppManagerWrapper private lateinit var appInstallProcessor: AppInstallProcessor private lateinit var installationRequest: InstallationRequest - private lateinit var appInstallStartCoordinator: AppInstallStartCoordinator + private lateinit var installationEnqueuer: InstallationEnqueuer private lateinit var appInstallWorkRunner: AppInstallWorkRunner @Before @@ -65,12 +65,12 @@ class AppInstallProcessorTest { val appInstallRepository = mockk(relaxed = true) val appInstallComponents = AppInstallComponents(appInstallRepository, appManagerWrapper) installationRequest = mockk(relaxed = true) - appInstallStartCoordinator = mockk(relaxed = true) + installationEnqueuer = mockk(relaxed = true) appInstallWorkRunner = mockk(relaxed = true) appInstallProcessor = AppInstallProcessor( appInstallComponents, - appInstallStartCoordinator, + installationEnqueuer, appInstallWorkRunner, installationRequest ) @@ -90,7 +90,7 @@ class AppInstallProcessorTest { coEvery { installationRequest.create(application) } returns appInstall coEvery { appManagerWrapper.isFusedDownloadInstalled(appInstall) } returns false coEvery { - appInstallStartCoordinator.enqueue( + installationEnqueuer.enqueue( appInstall, true, application.isSystemApp @@ -101,18 +101,18 @@ class AppInstallProcessorTest { assertTrue(result) coVerify { installationRequest.create(application) } - coVerify { appInstallStartCoordinator.enqueue(appInstall, true, application.isSystemApp) } + coVerify { installationEnqueuer.enqueue(appInstall, true, application.isSystemApp) } } @Test fun enqueueFusedDownload_delegatesResult() = runTest { val appInstall = AppInstall(id = "123", packageName = "com.example.app") - coEvery { appInstallStartCoordinator.enqueue(appInstall, true, true) } returns false + coEvery { installationEnqueuer.enqueue(appInstall, true, true) } returns false val result = appInstallProcessor.enqueueFusedDownload(appInstall, true, true) assertEquals(false, result) - coVerify { appInstallStartCoordinator.enqueue(appInstall, true, true) } + coVerify { installationEnqueuer.enqueue(appInstall, true, true) } } @Test diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/InstallationEnqueuerTest.kt similarity index 93% rename from app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt rename to app/src/test/java/foundation/e/apps/installProcessor/InstallationEnqueuerTest.kt index 33409f550..7439266b6 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallStartCoordinatorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/InstallationEnqueuerTest.kt @@ -35,7 +35,7 @@ import foundation.e.apps.data.install.core.helper.AgeLimitGate import foundation.e.apps.data.install.core.helper.DevicePreconditions import foundation.e.apps.data.install.core.helper.DownloadUrlRefresher import foundation.e.apps.data.install.core.helper.PreEnqueueChecker -import foundation.e.apps.data.install.core.helper.AppInstallStartCoordinator +import foundation.e.apps.data.install.core.helper.InstallationEnqueuer import foundation.e.apps.data.install.workmanager.InstallWorkManager import foundation.e.apps.data.install.wrapper.NetworkStatusChecker import foundation.e.apps.data.install.wrapper.StorageSpaceChecker @@ -60,7 +60,7 @@ import org.junit.Before import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) -class AppInstallStartCoordinatorTest { +class InstallationEnqueuerTest { private lateinit var context: Context private lateinit var appManagerWrapper: AppManagerWrapper private lateinit var applicationRepository: ApplicationRepository @@ -76,7 +76,7 @@ class AppInstallStartCoordinatorTest { private lateinit var devicePreconditions: DevicePreconditions private lateinit var downloadUrlRefresher: DownloadUrlRefresher private lateinit var preflightChecker: PreEnqueueChecker - private lateinit var coordinator: AppInstallStartCoordinator + private lateinit var enqueuer: InstallationEnqueuer @Before fun setup() { @@ -114,7 +114,7 @@ class AppInstallStartCoordinatorTest { ageLimitGate, devicePreconditions ) - coordinator = AppInstallStartCoordinator( + enqueuer = InstallationEnqueuer( context, preflightChecker, appManagerWrapper, @@ -133,7 +133,7 @@ class AppInstallStartCoordinatorTest { every { networkStatusChecker.isNetworkAvailable() } returns true every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - val result = coordinator.canEnqueue(appInstall) + val result = enqueuer.canEnqueue(appInstall) assertTrue(result) } @@ -147,7 +147,7 @@ class AppInstallStartCoordinatorTest { every { networkStatusChecker.isNetworkAvailable() } returns false every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L - val result = coordinator.canEnqueue(appInstall) + val result = enqueuer.canEnqueue(appInstall) assertFalse(result) coVerify { appManagerWrapper.installationIssue(appInstall) } @@ -162,7 +162,7 @@ class AppInstallStartCoordinatorTest { every { networkStatusChecker.isNetworkAvailable() } returns true every { storageSpaceChecker.spaceMissing(appInstall) } returns 100L - val result = coordinator.canEnqueue(appInstall) + val result = enqueuer.canEnqueue(appInstall) assertFalse(result) verify { storageNotificationManager.showNotEnoughSpaceNotification(appInstall) } @@ -175,7 +175,7 @@ class AppInstallStartCoordinatorTest { coEvery { appManagerWrapper.addDownload(appInstall) } returns false - val result = coordinator.canEnqueue(appInstall) + val result = enqueuer.canEnqueue(appInstall) assertFalse(result) coVerify(exactly = 0) { ageLimitGate.allow(any()) } @@ -197,7 +197,7 @@ class AppInstallStartCoordinatorTest { every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L justRun { InstallWorkManager.enqueueWork(any(), any(), any()) } - val result = coordinator.enqueue(appInstall) + val result = enqueuer.enqueue(appInstall) assertTrue(result) assertTrue(appEventDispatcher.events.any { @@ -221,7 +221,7 @@ class AppInstallStartCoordinatorTest { ) } throws InternalException.AppNotPurchased() - val result = coordinator.canEnqueue(appInstall) + val result = enqueuer.canEnqueue(appInstall) assertFalse(result) assertTrue(appEventDispatcher.events.any { it is AppEvent.AppRestrictedOrUnavailable }) @@ -240,7 +240,7 @@ class AppInstallStartCoordinatorTest { ) } throws InternalException.AppNotPurchased() - val result = coordinator.canEnqueue(appInstall) + val result = enqueuer.canEnqueue(appInstall) assertFalse(result) coVerify { appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) } @@ -259,7 +259,7 @@ class AppInstallStartCoordinatorTest { ) } throws GplayHttpRequestException(403, "forbidden") - val result = coordinator.canEnqueue(appInstall) + val result = enqueuer.canEnqueue(appInstall) assertFalse(result) assertTrue(appEventDispatcher.events.none { it is AppEvent.UpdateEvent }) @@ -280,7 +280,7 @@ class AppInstallStartCoordinatorTest { ) } throws GplayHttpRequestException(403, "forbidden") - val result = coordinator.canEnqueue(appInstall, true) + val result = enqueuer.canEnqueue(appInstall, true) assertFalse(result) assertTrue(appEventDispatcher.events.any { it is AppEvent.UpdateEvent }) @@ -301,7 +301,7 @@ class AppInstallStartCoordinatorTest { ) } throws IllegalStateException("boom") - val result = coordinator.canEnqueue(appInstall) + val result = enqueuer.canEnqueue(appInstall) assertFalse(result) coVerify { appInstallRepository.addDownload(appInstall) } -- GitLab From 07804c830849f283a70674c52a4913511a6fdcf6 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 21:04:44 +0600 Subject: [PATCH 24/29] refactor: rename AppInstallProcessor to AppInstallationFacade --- ...lProcessor.kt => AppInstallationFacade.kt} | 2 +- .../data/install/updates/UpdatesWorker.kt | 6 +- .../install/workmanager/InstallAppWorker.kt | 6 +- .../e/apps/ui/MainActivityViewModel.kt | 8 +- .../data/install/updates/UpdatesWorkerTest.kt | 92 +++++++++---------- ...orTest.kt => AppInstallationFacadeTest.kt} | 14 +-- .../installProcessor/InstallAppWorkerTest.kt | 18 ++-- 7 files changed, 73 insertions(+), 73 deletions(-) rename app/src/main/java/foundation/e/apps/data/install/core/{AppInstallProcessor.kt => AppInstallationFacade.kt} (98%) rename app/src/test/java/foundation/e/apps/installProcessor/{AppInstallProcessorTest.kt => AppInstallationFacadeTest.kt} (90%) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt similarity index 98% rename from app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt rename to app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt index a6b25d110..141ea8a9a 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt @@ -28,7 +28,7 @@ import foundation.e.apps.data.install.core.helper.AppInstallWorkRunner import foundation.e.apps.domain.model.install.Status import javax.inject.Inject -class AppInstallProcessor @Inject constructor( +class AppInstallationFacade @Inject constructor( private val appInstallComponents: AppInstallComponents, private val installationEnqueuer: InstallationEnqueuer, private val appInstallWorkRunner: AppInstallWorkRunner, diff --git a/app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorker.kt index a4d36ac91..58ad52e3d 100644 --- a/app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorker.kt @@ -20,7 +20,7 @@ import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.event.EventBus import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository -import foundation.e.apps.data.install.core.AppInstallProcessor +import foundation.e.apps.data.install.core.AppInstallationFacade import foundation.e.apps.data.login.repository.AuthenticatorRepository import foundation.e.apps.data.updates.UpdatesManagerRepository import foundation.e.apps.domain.model.User @@ -40,7 +40,7 @@ class UpdatesWorker @AssistedInject constructor( private val sessionRepository: SessionRepository, private val appPreferencesRepository: AppPreferencesRepository, private val authenticatorRepository: AuthenticatorRepository, - private val appInstallProcessor: AppInstallProcessor, + private val appInstallationFacade: AppInstallationFacade, private val blockedAppRepository: BlockedAppRepository, private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, ) : CoroutineWorker(context, params) { @@ -235,7 +235,7 @@ class UpdatesWorker @AssistedInject constructor( response.add(Pair(fusedApp, false)) continue } - val status = appInstallProcessor.initAppInstall(fusedApp, true) + val status = appInstallationFacade.initAppInstall(fusedApp, true) response.add(Pair(fusedApp, status)) } return response diff --git a/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallAppWorker.kt b/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallAppWorker.kt index 97c787556..b095fb55f 100644 --- a/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallAppWorker.kt +++ b/app/src/main/java/foundation/e/apps/data/install/workmanager/InstallAppWorker.kt @@ -32,14 +32,14 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject import foundation.e.apps.R -import foundation.e.apps.data.install.core.AppInstallProcessor +import foundation.e.apps.data.install.core.AppInstallationFacade import java.util.concurrent.atomic.AtomicInteger @HiltWorker class InstallAppWorker @AssistedInject constructor( @Assisted private val context: Context, @Assisted private val params: WorkerParameters, - private val appInstallProcessor: AppInstallProcessor + private val appInstallationFacade: AppInstallationFacade ) : CoroutineWorker(context, params) { companion object { @@ -65,7 +65,7 @@ class InstallAppWorker @AssistedInject constructor( } val isPackageUpdate = params.inputData.getBoolean(IS_UPDATE_WORK, false) - val response = appInstallProcessor.processInstall(fusedDownloadId, isPackageUpdate) { title -> + val response = appInstallationFacade.processInstall(fusedDownloadId, isPackageUpdate) { title -> setForeground(createForegroundInfo("${context.getString(R.string.installing)} $title")) } diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt index 83c16dc00..6f08538e4 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -39,7 +39,7 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.pkg.AppLoungePackageManager import foundation.e.apps.data.install.pkg.PwaManager -import foundation.e.apps.data.install.core.AppInstallProcessor +import foundation.e.apps.data.install.core.AppInstallationFacade import foundation.e.apps.data.login.core.AuthObject import foundation.e.apps.data.parentalcontrol.fdroid.FDroidAntiFeatureRepository import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingRepository @@ -64,7 +64,7 @@ class MainActivityViewModel @Inject constructor( private val blockedAppRepository: BlockedAppRepository, private val gPlayContentRatingRepository: GPlayContentRatingRepository, private val fDroidAntiFeatureRepository: FDroidAntiFeatureRepository, - private val appInstallProcessor: AppInstallProcessor, + private val appInstallationFacade: AppInstallationFacade, private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, private val reportFaultyTokenUseCase: ReportFaultyTokenUseCase, ) : ViewModel() { @@ -271,7 +271,7 @@ class MainActivityViewModel @Inject constructor( fun getApplication(app: Application) { viewModelScope.launch(Dispatchers.IO) { - appInstallProcessor.initAppInstall(app) + appInstallationFacade.initAppInstall(app) } } @@ -283,7 +283,7 @@ class MainActivityViewModel @Inject constructor( val fusedDownload = appManagerWrapper.getFusedDownload(packageName = packageName) val authData = sessionRepository.getAuthData() if (!authData.isAnonymous) { - appInstallProcessor.enqueueFusedDownload(fusedDownload) + appInstallationFacade.enqueueFusedDownload(fusedDownload) return fusedDownload } diff --git a/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkerTest.kt b/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkerTest.kt index f892ef41c..9726c1473 100644 --- a/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkerTest.kt +++ b/app/src/test/java/foundation/e/apps/data/install/updates/UpdatesWorkerTest.kt @@ -50,7 +50,7 @@ import foundation.e.apps.data.login.core.StoreType import foundation.e.apps.data.login.repository.AuthenticatorRepository import foundation.e.apps.data.preference.SessionDataStore import foundation.e.apps.data.updates.UpdatesManagerRepository -import foundation.e.apps.data.install.core.AppInstallProcessor +import foundation.e.apps.data.install.core.AppInstallationFacade import foundation.e.apps.domain.model.LoginState import foundation.e.apps.domain.model.User import foundation.e.apps.domain.preferences.AppPreferencesRepository @@ -96,7 +96,7 @@ class UpdatesWorkerTest { val appLoungeDataStore = createDataStore(dataStoreContext) val storeAuthenticator = mockk() val authenticatorRepository = AuthenticatorRepository(listOf(storeAuthenticator), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() val authData = AuthData(email = "user@example.com") @@ -151,7 +151,7 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) @@ -176,7 +176,7 @@ class UpdatesWorkerTest { val storeAuthenticator = mockk() val authenticatorRepository = AuthenticatorRepository(listOf(storeAuthenticator), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() val authData = AuthData(email = "user@example.com") @@ -237,7 +237,7 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) @@ -247,7 +247,7 @@ class UpdatesWorkerTest { assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.failure()) verify(updatesManagerRepository, times(UpdatesWorker.MAX_RETRY_COUNT.plus(1))).getUpdates() verify(updatesManagerRepository, never()).getUpdatesOSS() - verify(appInstallProcessor, never()).initAppInstall(any(), any()) + verify(appInstallationFacade, never()).initAppInstall(any(), any()) } @Test @@ -258,7 +258,7 @@ class UpdatesWorkerTest { val updatesManagerRepository = mock() val appLoungeDataStore = createDataStore(dataStoreContext) val authenticatorRepository = AuthenticatorRepository(emptyList(), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() val notificationManager = mock() @@ -279,7 +279,7 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ), @@ -306,7 +306,7 @@ class UpdatesWorkerTest { val updatesManagerRepository = mock() val appLoungeDataStore = createDataStore(appContext) val authenticatorRepository = AuthenticatorRepository(emptyList(), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() @@ -316,7 +316,7 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) @@ -335,7 +335,7 @@ class UpdatesWorkerTest { val storeAuthenticator = mockk() val authenticatorRepository = AuthenticatorRepository(listOf(storeAuthenticator), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() val worker = createWorker( @@ -344,7 +344,7 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) @@ -366,7 +366,7 @@ class UpdatesWorkerTest { val storeAuthenticator = mockk() val authenticatorRepository = AuthenticatorRepository(listOf(storeAuthenticator), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() val worker = createWorker( @@ -375,7 +375,7 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) @@ -414,7 +414,7 @@ class UpdatesWorkerTest { val updatesManagerRepository = mock() val appLoungeDataStore = createDataStore(appContext) val authenticatorRepository = AuthenticatorRepository(emptyList(), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() @@ -424,7 +424,7 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) @@ -446,7 +446,7 @@ class UpdatesWorkerTest { val updatesManagerRepository = mock() val sessionDataStore = mock() val authenticatorRepository = mock() - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() @@ -464,7 +464,7 @@ class UpdatesWorkerTest { updatesManagerRepository, sessionDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository, createAppPreferencesRepository( @@ -489,7 +489,7 @@ class UpdatesWorkerTest { val updatesManagerRepository = mock() val sessionDataStore = mock() val authenticatorRepository = mock() - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() @@ -508,7 +508,7 @@ class UpdatesWorkerTest { updatesManagerRepository, sessionDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository, createAppPreferencesRepository( @@ -537,7 +537,7 @@ class UpdatesWorkerTest { val updatesManagerRepository = mock() val sessionDataStore = mock() val authenticatorRepository = mock() - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() @@ -582,7 +582,7 @@ class UpdatesWorkerTest { ) whenever(systemAppsUpdatesRepository.fetchUpdatableSystemApps(true)) .thenReturn(ResultSupreme.Success(Unit)) - whenever(appInstallProcessor.initAppInstall(any(), any())).thenReturn(true) + whenever(appInstallationFacade.initAppInstall(any(), any())).thenReturn(true) val worker = createWorker( workerContext, @@ -590,7 +590,7 @@ class UpdatesWorkerTest { updatesManagerRepository, sessionDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository, createAppPreferencesRepository( @@ -603,7 +603,7 @@ class UpdatesWorkerTest { val result = worker.doWork() assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.success()) - verify(appInstallProcessor).initAppInstall(any(), any()) + verify(appInstallationFacade).initAppInstall(any(), any()) } @Test @@ -618,7 +618,7 @@ class UpdatesWorkerTest { val updatesManagerRepository = mock() val sessionDataStore = mock() val authenticatorRepository = mock() - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() @@ -670,7 +670,7 @@ class UpdatesWorkerTest { updatesManagerRepository, sessionDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) @@ -678,7 +678,7 @@ class UpdatesWorkerTest { val result = worker.doWork() assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.failure()) - verify(appInstallProcessor, never()).initAppInstall(any(), any()) + verify(appInstallationFacade, never()).initAppInstall(any(), any()) } @@ -697,7 +697,7 @@ class UpdatesWorkerTest { val storeAuthenticator = mockk() val authenticatorRepository = AuthenticatorRepository(listOf(storeAuthenticator), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() @@ -725,7 +725,7 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) @@ -754,7 +754,7 @@ class UpdatesWorkerTest { val storeAuthenticator = mockk() val authenticatorRepository = AuthenticatorRepository(listOf(storeAuthenticator), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() @@ -782,7 +782,7 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) @@ -806,7 +806,7 @@ class UpdatesWorkerTest { val storeAuthenticator = mockk() val authenticatorRepository = AuthenticatorRepository(listOf(storeAuthenticator), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() @@ -824,14 +824,14 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) val paidApp = Application(name = "Paid", isFree = false) val freeApp = Application(name = "Free", isFree = true) - whenever(appInstallProcessor.initAppInstall(freeApp, true)).thenReturn(true) + whenever(appInstallationFacade.initAppInstall(freeApp, true)).thenReturn(true) val result = worker.startUpdateProcess(listOf(paidApp, freeApp)) @@ -839,8 +839,8 @@ class UpdatesWorkerTest { Pair(paidApp, false), Pair(freeApp, true) ) - verify(appInstallProcessor, times(1)).initAppInstall(freeApp, true) - verify(appInstallProcessor, times(0)).initAppInstall(paidApp, true) + verify(appInstallationFacade, times(1)).initAppInstall(freeApp, true) + verify(appInstallationFacade, times(0)).initAppInstall(paidApp, true) } @Test @@ -853,7 +853,7 @@ class UpdatesWorkerTest { val storeAuthenticator = mockk() val authenticatorRepository = AuthenticatorRepository(listOf(storeAuthenticator), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() val authData = AuthData(email = "user@example.com", isAnonymous = false) @@ -871,18 +871,18 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) val paidApp = Application(name = "Paid", isFree = false) - whenever(appInstallProcessor.initAppInstall(paidApp, true)).thenReturn(false) + whenever(appInstallationFacade.initAppInstall(paidApp, true)).thenReturn(false) val result = worker.startUpdateProcess(listOf(paidApp)) assertThat(result).containsExactly(Pair(paidApp, false)) - verify(appInstallProcessor, times(1)).initAppInstall(paidApp, true) + verify(appInstallationFacade, times(1)).initAppInstall(paidApp, true) } @Test @@ -894,7 +894,7 @@ class UpdatesWorkerTest { val storeAuthenticator = mockk() val authenticatorRepository = AuthenticatorRepository(listOf(storeAuthenticator), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() @@ -909,7 +909,7 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) @@ -944,7 +944,7 @@ class UpdatesWorkerTest { val storeAuthenticator = mockk() val authenticatorRepository = AuthenticatorRepository(listOf(storeAuthenticator), appLoungeDataStore) - val appInstallProcessor = mock() + val appInstallationFacade = mock() val blockedAppRepository = mock() val systemAppsUpdatesRepository = mock() @@ -959,7 +959,7 @@ class UpdatesWorkerTest { updatesManagerRepository, appLoungeDataStore, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) @@ -994,7 +994,7 @@ class UpdatesWorkerTest { updatesManagerRepository: UpdatesManagerRepository, sessionRepository: SessionRepository, authenticatorRepository: AuthenticatorRepository, - appInstallProcessor: AppInstallProcessor, + appInstallationFacade: AppInstallationFacade, blockedAppRepository: BlockedAppRepository, systemAppsUpdatesRepository: SystemAppsUpdatesRepository, appPreferencesRepository: AppPreferencesRepository = createAppPreferencesRepository(), @@ -1006,7 +1006,7 @@ class UpdatesWorkerTest { sessionRepository, appPreferencesRepository, authenticatorRepository, - appInstallProcessor, + appInstallationFacade, blockedAppRepository, systemAppsUpdatesRepository ) diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallationFacadeTest.kt similarity index 90% rename from app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt rename to app/src/test/java/foundation/e/apps/installProcessor/AppInstallationFacadeTest.kt index 16a52a9e4..51e15d619 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallationFacadeTest.kt @@ -28,7 +28,7 @@ import foundation.e.apps.data.install.AppInstallComponents import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.core.AppInstallProcessor +import foundation.e.apps.data.install.core.AppInstallationFacade import foundation.e.apps.data.install.core.helper.InstallationRequest import foundation.e.apps.data.install.core.helper.InstallationEnqueuer import foundation.e.apps.data.install.core.helper.AppInstallWorkRunner @@ -45,7 +45,7 @@ import org.junit.Rule import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) -class AppInstallProcessorTest { +class AppInstallationFacadeTest { @Rule @JvmField val instantExecutorRule = InstantTaskExecutorRule() @@ -54,7 +54,7 @@ class AppInstallProcessorTest { var mainCoroutineRule = MainCoroutineRule() private lateinit var appManagerWrapper: AppManagerWrapper - private lateinit var appInstallProcessor: AppInstallProcessor + private lateinit var appInstallationFacade: AppInstallationFacade private lateinit var installationRequest: InstallationRequest private lateinit var installationEnqueuer: InstallationEnqueuer private lateinit var appInstallWorkRunner: AppInstallWorkRunner @@ -68,7 +68,7 @@ class AppInstallProcessorTest { installationEnqueuer = mockk(relaxed = true) appInstallWorkRunner = mockk(relaxed = true) - appInstallProcessor = AppInstallProcessor( + appInstallationFacade = AppInstallationFacade( appInstallComponents, installationEnqueuer, appInstallWorkRunner, @@ -97,7 +97,7 @@ class AppInstallProcessorTest { ) } returns true - val result = appInstallProcessor.initAppInstall(application) + val result = appInstallationFacade.initAppInstall(application) assertTrue(result) coVerify { installationRequest.create(application) } @@ -109,7 +109,7 @@ class AppInstallProcessorTest { val appInstall = AppInstall(id = "123", packageName = "com.example.app") coEvery { installationEnqueuer.enqueue(appInstall, true, true) } returns false - val result = appInstallProcessor.enqueueFusedDownload(appInstall, true, true) + val result = appInstallationFacade.enqueueFusedDownload(appInstall, true, true) assertEquals(false, result) coVerify { installationEnqueuer.enqueue(appInstall, true, true) } @@ -121,7 +121,7 @@ class AppInstallProcessorTest { appInstallWorkRunner.processInstall("123", false, any()) } returns Result.success(ResultStatus.OK) - val result = appInstallProcessor.processInstall("123", false) { + val result = appInstallationFacade.processInstall("123", false) { // _ignored_ } diff --git a/app/src/test/java/foundation/e/apps/installProcessor/InstallAppWorkerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/InstallAppWorkerTest.kt index 8efe9bf3d..953424eb5 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/InstallAppWorkerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/InstallAppWorkerTest.kt @@ -23,7 +23,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.work.Data import com.google.common.truth.Truth.assertThat import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.install.core.AppInstallProcessor +import foundation.e.apps.data.install.core.AppInstallationFacade import foundation.e.apps.data.install.workmanager.InstallAppWorker import io.mockk.coEvery import io.mockk.coVerify @@ -41,11 +41,11 @@ import org.robolectric.annotation.Config @Config(sdk = [Build.VERSION_CODES.R]) @OptIn(ExperimentalCoroutinesApi::class) class InstallAppWorkerTest { - private lateinit var appInstallProcessor: AppInstallProcessor + private lateinit var appInstallationFacade: AppInstallationFacade @Before fun setup() { - appInstallProcessor = mockk(relaxed = true) + appInstallationFacade = mockk(relaxed = true) } @Test @@ -55,13 +55,13 @@ class InstallAppWorkerTest { val result = worker.doWork() assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.failure()) - coVerify(exactly = 0) { appInstallProcessor.processInstall(any(), any(), any()) } + coVerify(exactly = 0) { appInstallationFacade.processInstall(any(), any(), any()) } } @Test fun doWork_returnsSuccessWhenProcessorSucceeds() = runTest { coEvery { - appInstallProcessor.processInstall("123", true, any()) + appInstallationFacade.processInstall("123", true, any()) } returns Result.success(ResultStatus.OK) val worker = createWorker( Data.Builder() @@ -73,13 +73,13 @@ class InstallAppWorkerTest { val result = worker.doWork() assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.success()) - coVerify { appInstallProcessor.processInstall("123", true, any()) } + coVerify { appInstallationFacade.processInstall("123", true, any()) } } @Test fun doWork_returnsFailureWhenProcessorFails() = runTest { coEvery { - appInstallProcessor.processInstall("123", false, any()) + appInstallationFacade.processInstall("123", false, any()) } returns Result.failure(IllegalStateException("boom")) val worker = createWorker( Data.Builder() @@ -91,7 +91,7 @@ class InstallAppWorkerTest { val result = worker.doWork() assertThat(result).isEqualTo(androidx.work.ListenableWorker.Result.failure()) - coVerify { appInstallProcessor.processInstall("123", false, any()) } + coVerify { appInstallationFacade.processInstall("123", false, any()) } } private fun createWorker(inputData: Data): InstallAppWorker { @@ -101,7 +101,7 @@ class InstallAppWorkerTest { return InstallAppWorker( ApplicationProvider.getApplicationContext(), params, - appInstallProcessor + appInstallationFacade ) } } -- GitLab From ccf4d93a486021e9c5eaa307f21ecd7019b14a74 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 21:07:14 +0600 Subject: [PATCH 25/29] refactor: rename AppInstallWorkRunner to InstallationProcessor --- .../apps/data/install/core/AppInstallationFacade.kt | 6 +++--- ...InstallWorkRunner.kt => InstallationProcessor.kt} | 2 +- .../installProcessor/AppInstallationFacadeTest.kt | 12 ++++++------ ...orkRunnerTest.kt => InstallationProcessorTest.kt} | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) rename app/src/main/java/foundation/e/apps/data/install/core/helper/{AppInstallWorkRunner.kt => InstallationProcessor.kt} (99%) rename app/src/test/java/foundation/e/apps/installProcessor/{AppInstallWorkRunnerTest.kt => InstallationProcessorTest.kt} (97%) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt index 141ea8a9a..b79f1d5f7 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt @@ -24,14 +24,14 @@ import foundation.e.apps.data.install.AppInstallComponents import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.core.helper.InstallationRequest import foundation.e.apps.data.install.core.helper.InstallationEnqueuer -import foundation.e.apps.data.install.core.helper.AppInstallWorkRunner +import foundation.e.apps.data.install.core.helper.InstallationProcessor import foundation.e.apps.domain.model.install.Status import javax.inject.Inject class AppInstallationFacade @Inject constructor( private val appInstallComponents: AppInstallComponents, private val installationEnqueuer: InstallationEnqueuer, - private val appInstallWorkRunner: AppInstallWorkRunner, + private val installationProcessor: InstallationProcessor, private val installationRequest: InstallationRequest, ) { /** @@ -73,6 +73,6 @@ class AppInstallationFacade @Inject constructor( isItUpdateWork: Boolean, runInForeground: (suspend (String) -> Unit) ): Result { - return appInstallWorkRunner.processInstall(fusedDownloadId, isItUpdateWork, runInForeground) + return installationProcessor.processInstall(fusedDownloadId, isItUpdateWork, runInForeground) } } diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallWorkRunner.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationProcessor.kt similarity index 99% rename from app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallWorkRunner.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationProcessor.kt index 3cb430e6f..3eec9ef8c 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppInstallWorkRunner.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationProcessor.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.transformWhile import timber.log.Timber import javax.inject.Inject -class AppInstallWorkRunner @Inject constructor( +class InstallationProcessor @Inject constructor( private val appInstallRepository: AppInstallRepository, private val appManagerWrapper: AppManagerWrapper, private val downloadManager: DownloadManagerUtils, diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallationFacadeTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallationFacadeTest.kt index 51e15d619..ea54d5627 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallationFacadeTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallationFacadeTest.kt @@ -31,7 +31,7 @@ import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.core.AppInstallationFacade import foundation.e.apps.data.install.core.helper.InstallationRequest import foundation.e.apps.data.install.core.helper.InstallationEnqueuer -import foundation.e.apps.data.install.core.helper.AppInstallWorkRunner +import foundation.e.apps.data.install.core.helper.InstallationProcessor import foundation.e.apps.util.MainCoroutineRule import io.mockk.coEvery import io.mockk.coVerify @@ -57,7 +57,7 @@ class AppInstallationFacadeTest { private lateinit var appInstallationFacade: AppInstallationFacade private lateinit var installationRequest: InstallationRequest private lateinit var installationEnqueuer: InstallationEnqueuer - private lateinit var appInstallWorkRunner: AppInstallWorkRunner + private lateinit var installationProcessor: InstallationProcessor @Before fun setup() { @@ -66,12 +66,12 @@ class AppInstallationFacadeTest { val appInstallComponents = AppInstallComponents(appInstallRepository, appManagerWrapper) installationRequest = mockk(relaxed = true) installationEnqueuer = mockk(relaxed = true) - appInstallWorkRunner = mockk(relaxed = true) + installationProcessor = mockk(relaxed = true) appInstallationFacade = AppInstallationFacade( appInstallComponents, installationEnqueuer, - appInstallWorkRunner, + installationProcessor, installationRequest ) } @@ -118,7 +118,7 @@ class AppInstallationFacadeTest { @Test fun processInstall_delegatesResult() = runTest { coEvery { - appInstallWorkRunner.processInstall("123", false, any()) + installationProcessor.processInstall("123", false, any()) } returns Result.success(ResultStatus.OK) val result = appInstallationFacade.processInstall("123", false) { @@ -126,6 +126,6 @@ class AppInstallationFacadeTest { } assertEquals(ResultStatus.OK, result.getOrNull()) - coVerify { appInstallWorkRunner.processInstall("123", false, any()) } + coVerify { installationProcessor.processInstall("123", false, any()) } } } diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/InstallationProcessorTest.kt similarity index 97% rename from app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt rename to app/src/test/java/foundation/e/apps/installProcessor/InstallationProcessorTest.kt index fce9d6dc1..decdaa66a 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallWorkRunnerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/InstallationProcessorTest.kt @@ -25,7 +25,7 @@ import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.download.DownloadManagerUtils import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.core.helper.AppInstallWorkRunner +import foundation.e.apps.data.install.core.helper.InstallationProcessor import foundation.e.apps.data.install.core.helper.AppUpdateCompletionHandler import foundation.e.apps.domain.model.install.Status import foundation.e.apps.util.MainCoroutineRule @@ -42,7 +42,7 @@ import org.mockito.Mock import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) -class AppInstallWorkRunnerTest { +class InstallationProcessorTest { @Rule @JvmField val instantExecutorRule = InstantTaskExecutorRule() @@ -55,7 +55,7 @@ class AppInstallWorkRunnerTest { private lateinit var fakeFusedManagerRepository: FakeAppManagerWrapper private lateinit var downloadManagerUtils: DownloadManagerUtils private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler - private lateinit var workRunner: AppInstallWorkRunner + private lateinit var workRunner: InstallationProcessor private lateinit var context: Context @Mock @@ -74,7 +74,7 @@ class AppInstallWorkRunnerTest { FakeAppManagerWrapper(fakeFusedDownloadDAO, context, fakeFusedManager, fakeFDroidRepository) downloadManagerUtils = mockk(relaxed = true) appUpdateCompletionHandler = mockk(relaxed = true) - workRunner = AppInstallWorkRunner( + workRunner = InstallationProcessor( appInstallRepository, fakeFusedManagerRepository, downloadManagerUtils, -- GitLab From e52d9df4a8625b28d8567af2e1cf64a627478347 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 21:10:22 +0600 Subject: [PATCH 26/29] refactor: rename AppUpdateCompletionHandler to InstallationCompletionHandler --- ...pletionHandler.kt => InstallationCompletionHandler.kt} | 2 +- .../data/install/core/helper/InstallationProcessor.kt | 4 ++-- ...andlerTest.kt => InstallationCompletionHandlerTest.kt} | 8 ++++---- .../e/apps/installProcessor/InstallationProcessorTest.kt | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) rename app/src/main/java/foundation/e/apps/data/install/core/helper/{AppUpdateCompletionHandler.kt => InstallationCompletionHandler.kt} (98%) rename app/src/test/java/foundation/e/apps/installProcessor/{AppUpdateCompletionHandlerTest.kt => InstallationCompletionHandlerTest.kt} (96%) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppUpdateCompletionHandler.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationCompletionHandler.kt similarity index 98% rename from app/src/main/java/foundation/e/apps/data/install/core/helper/AppUpdateCompletionHandler.kt rename to app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationCompletionHandler.kt index 88a8ba843..0fec3c300 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/AppUpdateCompletionHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationCompletionHandler.kt @@ -34,7 +34,7 @@ import java.util.Date import java.util.Locale import javax.inject.Inject -class AppUpdateCompletionHandler @Inject constructor( +class InstallationCompletionHandler @Inject constructor( @ApplicationContext private val context: Context, private val appInstallRepository: AppInstallRepository, private val appManagerWrapper: AppManagerWrapper, diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationProcessor.kt index 3eec9ef8c..e25791fa2 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationProcessor.kt @@ -34,7 +34,7 @@ class InstallationProcessor @Inject constructor( private val appInstallRepository: AppInstallRepository, private val appManagerWrapper: AppManagerWrapper, private val downloadManager: DownloadManagerUtils, - private val appUpdateCompletionHandler: AppUpdateCompletionHandler, + private val installationCompletionHandler: InstallationCompletionHandler, ) { @Suppress("ReturnCount") @OptIn(DelicateCoroutinesApi::class) @@ -185,6 +185,6 @@ class InstallationProcessor @Inject constructor( } private suspend fun finishInstallation(appInstall: AppInstall, isUpdateWork: Boolean) { - appUpdateCompletionHandler.onInstallFinished(appInstall, isUpdateWork) + installationCompletionHandler.onInstallFinished(appInstall, isUpdateWork) } } diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppUpdateCompletionHandlerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/InstallationCompletionHandlerTest.kt similarity index 96% rename from app/src/test/java/foundation/e/apps/installProcessor/AppUpdateCompletionHandlerTest.kt rename to app/src/test/java/foundation/e/apps/installProcessor/InstallationCompletionHandlerTest.kt index 6b8de8e47..43cc6a355 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppUpdateCompletionHandlerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/InstallationCompletionHandlerTest.kt @@ -24,7 +24,7 @@ import foundation.e.apps.R import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.core.helper.AppUpdateCompletionHandler +import foundation.e.apps.data.install.core.helper.InstallationCompletionHandler import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender import foundation.e.apps.data.install.wrapper.UpdatesTracker import foundation.e.apps.data.preference.PlayStoreAuthStore @@ -42,7 +42,7 @@ import org.junit.Test import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) -class AppUpdateCompletionHandlerTest { +class InstallationCompletionHandlerTest { @get:Rule var mainCoroutineRule = MainCoroutineRule() @@ -53,7 +53,7 @@ class AppUpdateCompletionHandlerTest { private lateinit var updatesTracker: UpdatesTracker private lateinit var updatesNotificationSender: UpdatesNotificationSender private lateinit var context: Context - private lateinit var handler: AppUpdateCompletionHandler + private lateinit var handler: InstallationCompletionHandler @Before fun setup() { @@ -64,7 +64,7 @@ class AppUpdateCompletionHandlerTest { updatesTracker = mockk(relaxed = true) updatesNotificationSender = mockk(relaxed = true) coEvery { playStoreAuthStore.awaitAuthData() } returns null - handler = AppUpdateCompletionHandler( + handler = InstallationCompletionHandler( context, appInstallRepository, appManagerWrapper, diff --git a/app/src/test/java/foundation/e/apps/installProcessor/InstallationProcessorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/InstallationProcessorTest.kt index decdaa66a..4bc469b63 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/InstallationProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/InstallationProcessorTest.kt @@ -26,7 +26,7 @@ import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.download.DownloadManagerUtils import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.core.helper.InstallationProcessor -import foundation.e.apps.data.install.core.helper.AppUpdateCompletionHandler +import foundation.e.apps.data.install.core.helper.InstallationCompletionHandler import foundation.e.apps.domain.model.install.Status import foundation.e.apps.util.MainCoroutineRule import io.mockk.mockk @@ -54,7 +54,7 @@ class InstallationProcessorTest { private lateinit var appInstallRepository: AppInstallRepository private lateinit var fakeFusedManagerRepository: FakeAppManagerWrapper private lateinit var downloadManagerUtils: DownloadManagerUtils - private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler + private lateinit var installationCompletionHandler: InstallationCompletionHandler private lateinit var workRunner: InstallationProcessor private lateinit var context: Context @@ -73,12 +73,12 @@ class InstallationProcessorTest { fakeFusedManagerRepository = FakeAppManagerWrapper(fakeFusedDownloadDAO, context, fakeFusedManager, fakeFDroidRepository) downloadManagerUtils = mockk(relaxed = true) - appUpdateCompletionHandler = mockk(relaxed = true) + installationCompletionHandler = mockk(relaxed = true) workRunner = InstallationProcessor( appInstallRepository, fakeFusedManagerRepository, downloadManagerUtils, - appUpdateCompletionHandler + installationCompletionHandler ) } -- GitLab From 4b217b91d7f5025be8da554f42d94c79d4bd4db9 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 21:17:25 +0600 Subject: [PATCH 27/29] refactor: move core classes for app install into data/install/core package --- .../e/apps/data/install/core/AppInstallationFacade.kt | 7 ++----- .../data/install/core/{helper => }/InstallationEnqueuer.kt | 3 ++- .../install/core/{helper => }/InstallationProcessor.kt | 3 ++- .../data/install/core/{helper => }/InstallationRequest.kt | 2 +- .../java/foundation/e/apps/ui/MainActivityViewModel.kt | 2 +- .../e/apps/installProcessor/AppInstallationFacadeTest.kt | 6 +++--- .../e/apps/installProcessor/InstallationEnqueuerTest.kt | 2 +- .../e/apps/installProcessor/InstallationProcessorTest.kt | 2 +- .../e/apps/installProcessor/InstallationRequestTest.kt | 2 +- 9 files changed, 14 insertions(+), 15 deletions(-) rename app/src/main/java/foundation/e/apps/data/install/core/{helper => }/InstallationEnqueuer.kt (97%) rename app/src/main/java/foundation/e/apps/data/install/core/{helper => }/InstallationProcessor.kt (98%) rename app/src/main/java/foundation/e/apps/data/install/core/{helper => }/InstallationRequest.kt (97%) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt index b79f1d5f7..4d4d7cc01 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt @@ -22,9 +22,6 @@ import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.AppInstallComponents import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.core.helper.InstallationRequest -import foundation.e.apps.data.install.core.helper.InstallationEnqueuer -import foundation.e.apps.data.install.core.helper.InstallationProcessor import foundation.e.apps.domain.model.install.Status import javax.inject.Inject @@ -35,7 +32,7 @@ class AppInstallationFacade @Inject constructor( private val installationRequest: InstallationRequest, ) { /** - * creates [foundation.e.apps.data.install.models.AppInstall] from [foundation.e.apps.data.application.data.Application] and enqueues into WorkManager to run install process. + * creates [AppInstall] from [Application] and enqueues into WorkManager to run install process. * @param application represents the app info which will be installed * @param isAnUpdate indicates the app is requested for update or not * @@ -54,7 +51,7 @@ class AppInstallationFacade @Inject constructor( } /** - * Enqueues [foundation.e.apps.data.install.models.AppInstall] into WorkManager to run app install process. Before enqueuing, + * Enqueues [AppInstall] into WorkManager to run app install process. Before enqueuing, * It validates some corner cases * @param appInstall represents the app downloading and installing related info, example- Installing Status, * Url of the APK,OBB files are needed to be downloaded and installed etc. diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationEnqueuer.kt b/app/src/main/java/foundation/e/apps/data/install/core/InstallationEnqueuer.kt similarity index 97% rename from app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationEnqueuer.kt rename to app/src/main/java/foundation/e/apps/data/install/core/InstallationEnqueuer.kt index c5d2340d9..a86aa3697 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationEnqueuer.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/InstallationEnqueuer.kt @@ -16,13 +16,14 @@ * */ -package foundation.e.apps.data.install.core.helper +package foundation.e.apps.data.install.core import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.core.helper.PreEnqueueChecker import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.workmanager.InstallWorkManager import foundation.e.apps.data.install.wrapper.AppEventDispatcher diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationProcessor.kt b/app/src/main/java/foundation/e/apps/data/install/core/InstallationProcessor.kt similarity index 98% rename from app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationProcessor.kt rename to app/src/main/java/foundation/e/apps/data/install/core/InstallationProcessor.kt index e25791fa2..22c2174cb 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationProcessor.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/InstallationProcessor.kt @@ -16,11 +16,12 @@ * */ -package foundation.e.apps.data.install.core.helper +package foundation.e.apps.data.install.core import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.core.helper.InstallationCompletionHandler import foundation.e.apps.data.install.download.DownloadManagerUtils import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.domain.model.install.Status diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationRequest.kt b/app/src/main/java/foundation/e/apps/data/install/core/InstallationRequest.kt similarity index 97% rename from app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationRequest.kt rename to app/src/main/java/foundation/e/apps/data/install/core/InstallationRequest.kt index 723aef3ff..eb351f0b1 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/InstallationRequest.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/InstallationRequest.kt @@ -16,7 +16,7 @@ * */ -package foundation.e.apps.data.install.core.helper +package foundation.e.apps.data.install.core import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt index 6f08538e4..2f7a52a0f 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -36,10 +36,10 @@ import foundation.e.apps.data.enums.isInitialized import foundation.e.apps.data.enums.isUnFiltered import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository import foundation.e.apps.data.install.AppManagerWrapper +import foundation.e.apps.data.install.core.AppInstallationFacade import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.pkg.AppLoungePackageManager import foundation.e.apps.data.install.pkg.PwaManager -import foundation.e.apps.data.install.core.AppInstallationFacade import foundation.e.apps.data.login.core.AuthObject import foundation.e.apps.data.parentalcontrol.fdroid.FDroidAntiFeatureRepository import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingRepository diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallationFacadeTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallationFacadeTest.kt index ea54d5627..747bde6e2 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallationFacadeTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallationFacadeTest.kt @@ -29,9 +29,9 @@ import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.core.AppInstallationFacade -import foundation.e.apps.data.install.core.helper.InstallationRequest -import foundation.e.apps.data.install.core.helper.InstallationEnqueuer -import foundation.e.apps.data.install.core.helper.InstallationProcessor +import foundation.e.apps.data.install.core.InstallationRequest +import foundation.e.apps.data.install.core.InstallationEnqueuer +import foundation.e.apps.data.install.core.InstallationProcessor import foundation.e.apps.util.MainCoroutineRule import io.mockk.coEvery import io.mockk.coVerify diff --git a/app/src/test/java/foundation/e/apps/installProcessor/InstallationEnqueuerTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/InstallationEnqueuerTest.kt index 7439266b6..4e5ba464a 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/InstallationEnqueuerTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/InstallationEnqueuerTest.kt @@ -35,7 +35,7 @@ import foundation.e.apps.data.install.core.helper.AgeLimitGate import foundation.e.apps.data.install.core.helper.DevicePreconditions import foundation.e.apps.data.install.core.helper.DownloadUrlRefresher import foundation.e.apps.data.install.core.helper.PreEnqueueChecker -import foundation.e.apps.data.install.core.helper.InstallationEnqueuer +import foundation.e.apps.data.install.core.InstallationEnqueuer import foundation.e.apps.data.install.workmanager.InstallWorkManager import foundation.e.apps.data.install.wrapper.NetworkStatusChecker import foundation.e.apps.data.install.wrapper.StorageSpaceChecker diff --git a/app/src/test/java/foundation/e/apps/installProcessor/InstallationProcessorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/InstallationProcessorTest.kt index 4bc469b63..8212c636e 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/InstallationProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/InstallationProcessorTest.kt @@ -25,7 +25,7 @@ import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.AppManager import foundation.e.apps.data.install.download.DownloadManagerUtils import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.install.core.helper.InstallationProcessor +import foundation.e.apps.data.install.core.InstallationProcessor import foundation.e.apps.data.install.core.helper.InstallationCompletionHandler import foundation.e.apps.domain.model.install.Status import foundation.e.apps.util.MainCoroutineRule diff --git a/app/src/test/java/foundation/e/apps/installProcessor/InstallationRequestTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/InstallationRequestTest.kt index 5a0d37c3c..38495a31a 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/InstallationRequestTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/InstallationRequestTest.kt @@ -22,7 +22,7 @@ import com.aurora.gplayapi.data.models.ContentRating import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Type -import foundation.e.apps.data.install.core.helper.InstallationRequest +import foundation.e.apps.data.install.core.InstallationRequest import foundation.e.apps.domain.model.install.Status import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -- GitLab From 3eec1a2f92581479eadd13446fa3bbfa8763d0ed Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 21:27:07 +0600 Subject: [PATCH 28/29] refactor: rename AppInstallProcessorBindingsModule to AppInstallationModule --- ...stallProcessorBindingsModule.kt => AppInstallationModule.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/src/main/java/foundation/e/apps/data/di/bindings/{AppInstallProcessorBindingsModule.kt => AppInstallationModule.kt} (98%) diff --git a/app/src/main/java/foundation/e/apps/data/di/bindings/AppInstallProcessorBindingsModule.kt b/app/src/main/java/foundation/e/apps/data/di/bindings/AppInstallationModule.kt similarity index 98% rename from app/src/main/java/foundation/e/apps/data/di/bindings/AppInstallProcessorBindingsModule.kt rename to app/src/main/java/foundation/e/apps/data/di/bindings/AppInstallationModule.kt index ed205c78b..d4e7cbf8a 100644 --- a/app/src/main/java/foundation/e/apps/data/di/bindings/AppInstallProcessorBindingsModule.kt +++ b/app/src/main/java/foundation/e/apps/data/di/bindings/AppInstallationModule.kt @@ -38,7 +38,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -interface AppInstallProcessorBindingsModule { +interface AppInstallationModule { @Binds @Singleton -- GitLab From abf94237a228feaff9de7679864ffe79327eba4b Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 18 Mar 2026 21:32:43 +0600 Subject: [PATCH 29/29] refactor: update DownloadUrlRefresher to explicitly return false within the error handler when an AppNotPurchased exception occurs. Modify handleAppNotPurchased to return Unit instead of a hardcoded Boolean, moving the return logic to the call site for better clarity. --- .../apps/data/install/core/helper/DownloadUrlRefresher.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/install/core/helper/DownloadUrlRefresher.kt b/app/src/main/java/foundation/e/apps/data/install/core/helper/DownloadUrlRefresher.kt index 10bcc0bb5..3b1bc916d 100644 --- a/app/src/main/java/foundation/e/apps/data/install/core/helper/DownloadUrlRefresher.kt +++ b/app/src/main/java/foundation/e/apps/data/install/core/helper/DownloadUrlRefresher.kt @@ -65,7 +65,10 @@ class DownloadUrlRefresher @Inject constructor( ): Boolean { return when (throwable) { is CancellationException -> throw throwable - is InternalException.AppNotPurchased -> handleAppNotPurchased(appInstall) + is InternalException.AppNotPurchased -> { + handleAppNotPurchased(appInstall) + false + } is Exception -> { val message = if (throwable is GplayHttpRequestException) { "${appInstall.packageName} code: ${throwable.status} exception: ${throwable.message}" @@ -81,7 +84,7 @@ class DownloadUrlRefresher @Inject constructor( } } - private suspend fun handleAppNotPurchased(appInstall: AppInstall): Boolean { + private suspend fun handleAppNotPurchased(appInstall: AppInstall) { if (appInstall.isFree) { appEventDispatcher.dispatch(AppEvent.AppRestrictedOrUnavailable(appInstall)) appManager.addDownload(appInstall) @@ -90,7 +93,6 @@ class DownloadUrlRefresher @Inject constructor( appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) appEventDispatcher.dispatch(AppEvent.AppPurchaseEvent(appInstall)) } - return false } private suspend fun handleUpdateDownloadError(appInstall: AppInstall, isAnUpdate: Boolean) { -- GitLab