Loading app/build.gradle +49 −18 Original line number Diff line number Diff line Loading @@ -66,6 +66,38 @@ def jacocoFileFilter = [ '**/*Hilt*.*' ] def jacocoCoverageProjects = [ project(':app'), project(':data'), project(':domain'), ] def collectJacocoClassDirectories = { Project module, String variantName -> return [ module.fileTree("${module.buildDir}/intermediates/javac/${variantName}/classes") { exclude jacocoFileFilter }, module.fileTree("${module.buildDir}/tmp/kotlin-classes/${variantName}") { exclude jacocoFileFilter } ] } def collectJacocoSourceDirectories = { Project module -> return [ "${module.projectDir}/src/main/java", "${module.projectDir}/src/main/kotlin", ] } def collectJacocoExecutionData = { Project module, String unitTestTaskName -> return module.fileTree(dir: module.buildDir, includes: [ "jacoco/${unitTestTaskName}.exec", "outputs/unit_test_code_coverage/**/${unitTestTaskName}.exec", "outputs/unit_test_code_coverage/**/*.ec", ]) } tasks.withType(Test).configureEach { jacoco { includeNoLocationClasses = true Loading Loading @@ -161,7 +193,9 @@ android.applicationVariants.configureEach { variant -> } tasks.register("jacoco${variantCap}Report", JacocoReport) { dependsOn(unitTestTask) dependsOn(jacocoCoverageProjects.collect { module -> "${module.path}:${unitTestTaskName}" }) group = "verification" description = "Generates Jacoco coverage report for the ${variant.name} build." Loading @@ -170,26 +204,23 @@ android.applicationVariants.configureEach { variant -> html.required = true } def javaClasses = fileTree("${buildDir}/intermediates/javac/${variant.name}/classes") { exclude jacocoFileFilter } def kotlinClasses = fileTree("${buildDir}/tmp/kotlin-classes/${variant.name}") { exclude jacocoFileFilter classDirectories.from = files( jacocoCoverageProjects.collectMany { module -> collectJacocoClassDirectories(module, variant.name) } ) classDirectories.from = files(javaClasses, kotlinClasses) def sourceDirs = variant.sourceSets.collect { sourceSet -> def dirs = [] dirs.addAll(sourceSet.java.srcDirs) if (sourceSet.hasProperty('kotlin')) { dirs.addAll(sourceSet.kotlin.srcDirs) sourceDirectories.from = files( jacocoCoverageProjects.collectMany { module -> collectJacocoSourceDirectories(module) } return dirs }.flatten() ) sourceDirectories.from = files(sourceDirs) executionData.from = file("${buildDir}/jacoco/${unitTestTaskName}.exec") executionData.from = files( jacocoCoverageProjects.collect { module -> collectJacocoExecutionData(module, unitTestTaskName) } ) } } Loading app/src/main/java/foundation/e/apps/AppLoungeApplication.kt +21 −21 Original line number Diff line number Diff line Loading @@ -92,13 +92,6 @@ class AppLoungeApplication : Application(), Configuration.Provider { registerReceiver(pkgManagerBR, appLoungePackageManager.getFilter(), RECEIVER_EXPORTED) coroutineScope.launch { val currentVersion = sessionRepository.awaitTosVersion() if (!currentVersion.contentEquals(TOS_VERSION)) { sessionRepository.saveTocStatus(false, "") } } if (BuildConfig.DEBUG) { plant(Timber.DebugTree()) } else { Loading @@ -118,6 +111,17 @@ class AppLoungeApplication : Application(), Configuration.Provider { }) } // Robolectric eagerly creates the real Application for many unrelated unit tests. // Skip runtime startup jobs there to avoid leaking WorkManager/Room/DataStore work // across otherwise isolated tests. if (!isRunningUnderRobolectric()) { coroutineScope.launch { val currentVersion = sessionRepository.awaitTosVersion() if (!currentVersion.contentEquals(TOS_VERSION)) { sessionRepository.saveTocStatus(false, "") } } coroutineScope.launch { migrateAnonymousUserUpdateIntervalUseCase() val updateInterval = getUpdateIntervalUseCase() Loading @@ -128,14 +132,10 @@ class AppLoungeApplication : Application(), Configuration.Provider { ) } // Robolectric eagerly creates the real Application for many unrelated tests. // Skip install DB cleanup there to avoid background Room work leaking across tests. if (!isRunningUnderRobolectric()) { removeStalledInstallationFromDb() } installOrchestrator.init() } } private fun isRunningUnderRobolectric(): Boolean = Build.FINGERPRINT == "robolectric" Loading app/src/main/java/foundation/e/apps/data/di/bindings/LoginBindingModule.kt 0 → 100644 +60 −0 Original line number Diff line number Diff line /* * 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 <https://www.gnu.org/licenses/>. */ 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.login.microg.MicrogLoginManager import foundation.e.apps.data.login.repository.AuthenticatorRepository import foundation.e.apps.data.login.repository.OkHttpLoginCacheRepository import foundation.e.apps.domain.login.LoginCacheRepository import foundation.e.apps.login.MicrogAccountFetcher import foundation.e.apps.login.PlayStoreAuthManager import foundation.e.apps.login.StoreAuthCoordinator import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) interface LoginBindingModule { @Binds @Singleton fun bindLoginCacheRepository( okHttpLoginCacheRepository: OkHttpLoginCacheRepository, ): LoginCacheRepository @Binds @Singleton fun bindPlayStoreAuthManager( authenticatorRepository: AuthenticatorRepository, ): PlayStoreAuthManager @Binds @Singleton fun bindStoreAuthCoordinator( authenticatorRepository: AuthenticatorRepository, ): StoreAuthCoordinator @Binds @Singleton fun bindMicrogAccountFetcher( microgLoginManager: MicrogLoginManager, ): MicrogAccountFetcher } app/src/main/java/foundation/e/apps/data/di/bindings/PreferenceBindingModule.kt +7 −0 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import foundation.e.apps.data.preference.AppLoungePreference import foundation.e.apps.data.preference.PlayStoreAuthStore import foundation.e.apps.data.preference.SessionDataStore import foundation.e.apps.data.preference.updateinterval.UpdatePreferencesRepositoryImpl import foundation.e.apps.domain.login.PlayStoreCredentialsRepository import foundation.e.apps.domain.preferences.AppPreferencesRepository import foundation.e.apps.domain.preferences.SessionRepository import foundation.e.apps.domain.preferences.updateinterval.UpdatePreferencesRepository Loading Loading @@ -58,4 +59,10 @@ interface PreferenceBindingModule { fun bindPlayStoreAuthStore( sessionDataStore: SessionDataStore ): PlayStoreAuthStore @Binds @Singleton fun bindPlayStoreCredentialsRepository( sessionDataStore: SessionDataStore ): PlayStoreCredentialsRepository } app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorker.kt +20 −35 Original line number Diff line number Diff line Loading @@ -21,28 +21,28 @@ 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.login.repository.AuthenticatorRepository import foundation.e.apps.data.updates.UpdatesManagerRepository import foundation.e.apps.domain.model.User import foundation.e.apps.domain.preferences.AppPreferencesRepository import foundation.e.apps.domain.preferences.SessionRepository import foundation.e.apps.login.PlayStoreAuthManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import timber.log.Timber @Suppress("LongParameterList") @HiltWorker @Suppress("LongParameterList") class UpdatesWorker @AssistedInject constructor( @Assisted private val context: Context, @Assisted private val params: WorkerParameters, private val updatesManagerRepository: UpdatesManagerRepository, private val blockedAppRepository: BlockedAppRepository, private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, private val sessionRepository: SessionRepository, private val appPreferencesRepository: AppPreferencesRepository, private val authenticatorRepository: AuthenticatorRepository, private val playStoreAuthManager: PlayStoreAuthManager, private val appInstallProcessor: AppInstallProcessor, private val blockedAppRepository: BlockedAppRepository, private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, ) : CoroutineWorker(context, params) { companion object { Loading Loading @@ -154,36 +154,19 @@ class UpdatesWorker @AssistedInject constructor( @VisibleForTesting suspend fun getAvailableUpdates(): Pair<List<Application>, ResultStatus> { loadSettings() val appsNeededToUpdate = mutableListOf<Application>() val user = getUser() val authData = authenticatorRepository.getValidatedAuthData().data val resultStatus: ResultStatus if (user in listOf(User.ANONYMOUS, User.GOOGLE) && authData != null) { /* * Signifies valid Google user and valid auth data to update * apps from Google Play store. * The user check will be more useful in No-Google mode. */ val (apps, status) = updatesManagerRepository.getUpdates() appsNeededToUpdate.addAll(apps) resultStatus = status } else if (user == User.NO_GOOGLE) { /* * If authData is null, update apps from cleanapk only. */ val (apps, status) = updatesManagerRepository.getUpdatesOSS() appsNeededToUpdate.addAll(apps) resultStatus = status } else { /* * If user in UNAVAILABLE, don't do anything. */ val authData = playStoreAuthManager.getValidatedAuthData().data val canUsePlayStoreUpdates = (user == User.ANONYMOUS || user == User.GOOGLE) && authData != null return when { canUsePlayStoreUpdates -> updatesManagerRepository.getUpdates() user == User.NO_GOOGLE -> updatesManagerRepository.getUpdatesOSS() else -> { Timber.e("Update is aborted for unavailable user!") resultStatus = ResultStatus.UNKNOWN Pair(emptyList(), ResultStatus.UNKNOWN) } } return Pair(appsNeededToUpdate, resultStatus) } @VisibleForTesting Loading Loading @@ -224,9 +207,11 @@ class UpdatesWorker @AssistedInject constructor( // returns list of Pair(app, status(success|failed)) @VisibleForTesting suspend fun startUpdateProcess(appsNeededToUpdate: List<Application>): List<Pair<Application, Boolean>> { suspend fun startUpdateProcess( appsNeededToUpdate: List<Application> ): List<Pair<Application, Boolean>> { val response = mutableListOf<Pair<Application, Boolean>>() val authData = authenticatorRepository.getValidatedAuthData() val authData = playStoreAuthManager.getValidatedAuthData() val isNotLoggedIntoPersonalAccount = !authData.isValidData() || authData.data?.isAnonymous == true for (fusedApp in appsNeededToUpdate) { Loading Loading
app/build.gradle +49 −18 Original line number Diff line number Diff line Loading @@ -66,6 +66,38 @@ def jacocoFileFilter = [ '**/*Hilt*.*' ] def jacocoCoverageProjects = [ project(':app'), project(':data'), project(':domain'), ] def collectJacocoClassDirectories = { Project module, String variantName -> return [ module.fileTree("${module.buildDir}/intermediates/javac/${variantName}/classes") { exclude jacocoFileFilter }, module.fileTree("${module.buildDir}/tmp/kotlin-classes/${variantName}") { exclude jacocoFileFilter } ] } def collectJacocoSourceDirectories = { Project module -> return [ "${module.projectDir}/src/main/java", "${module.projectDir}/src/main/kotlin", ] } def collectJacocoExecutionData = { Project module, String unitTestTaskName -> return module.fileTree(dir: module.buildDir, includes: [ "jacoco/${unitTestTaskName}.exec", "outputs/unit_test_code_coverage/**/${unitTestTaskName}.exec", "outputs/unit_test_code_coverage/**/*.ec", ]) } tasks.withType(Test).configureEach { jacoco { includeNoLocationClasses = true Loading Loading @@ -161,7 +193,9 @@ android.applicationVariants.configureEach { variant -> } tasks.register("jacoco${variantCap}Report", JacocoReport) { dependsOn(unitTestTask) dependsOn(jacocoCoverageProjects.collect { module -> "${module.path}:${unitTestTaskName}" }) group = "verification" description = "Generates Jacoco coverage report for the ${variant.name} build." Loading @@ -170,26 +204,23 @@ android.applicationVariants.configureEach { variant -> html.required = true } def javaClasses = fileTree("${buildDir}/intermediates/javac/${variant.name}/classes") { exclude jacocoFileFilter } def kotlinClasses = fileTree("${buildDir}/tmp/kotlin-classes/${variant.name}") { exclude jacocoFileFilter classDirectories.from = files( jacocoCoverageProjects.collectMany { module -> collectJacocoClassDirectories(module, variant.name) } ) classDirectories.from = files(javaClasses, kotlinClasses) def sourceDirs = variant.sourceSets.collect { sourceSet -> def dirs = [] dirs.addAll(sourceSet.java.srcDirs) if (sourceSet.hasProperty('kotlin')) { dirs.addAll(sourceSet.kotlin.srcDirs) sourceDirectories.from = files( jacocoCoverageProjects.collectMany { module -> collectJacocoSourceDirectories(module) } return dirs }.flatten() ) sourceDirectories.from = files(sourceDirs) executionData.from = file("${buildDir}/jacoco/${unitTestTaskName}.exec") executionData.from = files( jacocoCoverageProjects.collect { module -> collectJacocoExecutionData(module, unitTestTaskName) } ) } } Loading
app/src/main/java/foundation/e/apps/AppLoungeApplication.kt +21 −21 Original line number Diff line number Diff line Loading @@ -92,13 +92,6 @@ class AppLoungeApplication : Application(), Configuration.Provider { registerReceiver(pkgManagerBR, appLoungePackageManager.getFilter(), RECEIVER_EXPORTED) coroutineScope.launch { val currentVersion = sessionRepository.awaitTosVersion() if (!currentVersion.contentEquals(TOS_VERSION)) { sessionRepository.saveTocStatus(false, "") } } if (BuildConfig.DEBUG) { plant(Timber.DebugTree()) } else { Loading @@ -118,6 +111,17 @@ class AppLoungeApplication : Application(), Configuration.Provider { }) } // Robolectric eagerly creates the real Application for many unrelated unit tests. // Skip runtime startup jobs there to avoid leaking WorkManager/Room/DataStore work // across otherwise isolated tests. if (!isRunningUnderRobolectric()) { coroutineScope.launch { val currentVersion = sessionRepository.awaitTosVersion() if (!currentVersion.contentEquals(TOS_VERSION)) { sessionRepository.saveTocStatus(false, "") } } coroutineScope.launch { migrateAnonymousUserUpdateIntervalUseCase() val updateInterval = getUpdateIntervalUseCase() Loading @@ -128,14 +132,10 @@ class AppLoungeApplication : Application(), Configuration.Provider { ) } // Robolectric eagerly creates the real Application for many unrelated tests. // Skip install DB cleanup there to avoid background Room work leaking across tests. if (!isRunningUnderRobolectric()) { removeStalledInstallationFromDb() } installOrchestrator.init() } } private fun isRunningUnderRobolectric(): Boolean = Build.FINGERPRINT == "robolectric" Loading
app/src/main/java/foundation/e/apps/data/di/bindings/LoginBindingModule.kt 0 → 100644 +60 −0 Original line number Diff line number Diff line /* * 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 <https://www.gnu.org/licenses/>. */ 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.login.microg.MicrogLoginManager import foundation.e.apps.data.login.repository.AuthenticatorRepository import foundation.e.apps.data.login.repository.OkHttpLoginCacheRepository import foundation.e.apps.domain.login.LoginCacheRepository import foundation.e.apps.login.MicrogAccountFetcher import foundation.e.apps.login.PlayStoreAuthManager import foundation.e.apps.login.StoreAuthCoordinator import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) interface LoginBindingModule { @Binds @Singleton fun bindLoginCacheRepository( okHttpLoginCacheRepository: OkHttpLoginCacheRepository, ): LoginCacheRepository @Binds @Singleton fun bindPlayStoreAuthManager( authenticatorRepository: AuthenticatorRepository, ): PlayStoreAuthManager @Binds @Singleton fun bindStoreAuthCoordinator( authenticatorRepository: AuthenticatorRepository, ): StoreAuthCoordinator @Binds @Singleton fun bindMicrogAccountFetcher( microgLoginManager: MicrogLoginManager, ): MicrogAccountFetcher }
app/src/main/java/foundation/e/apps/data/di/bindings/PreferenceBindingModule.kt +7 −0 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import foundation.e.apps.data.preference.AppLoungePreference import foundation.e.apps.data.preference.PlayStoreAuthStore import foundation.e.apps.data.preference.SessionDataStore import foundation.e.apps.data.preference.updateinterval.UpdatePreferencesRepositoryImpl import foundation.e.apps.domain.login.PlayStoreCredentialsRepository import foundation.e.apps.domain.preferences.AppPreferencesRepository import foundation.e.apps.domain.preferences.SessionRepository import foundation.e.apps.domain.preferences.updateinterval.UpdatePreferencesRepository Loading Loading @@ -58,4 +59,10 @@ interface PreferenceBindingModule { fun bindPlayStoreAuthStore( sessionDataStore: SessionDataStore ): PlayStoreAuthStore @Binds @Singleton fun bindPlayStoreCredentialsRepository( sessionDataStore: SessionDataStore ): PlayStoreCredentialsRepository }
app/src/main/java/foundation/e/apps/data/install/updates/UpdatesWorker.kt +20 −35 Original line number Diff line number Diff line Loading @@ -21,28 +21,28 @@ 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.login.repository.AuthenticatorRepository import foundation.e.apps.data.updates.UpdatesManagerRepository import foundation.e.apps.domain.model.User import foundation.e.apps.domain.preferences.AppPreferencesRepository import foundation.e.apps.domain.preferences.SessionRepository import foundation.e.apps.login.PlayStoreAuthManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import timber.log.Timber @Suppress("LongParameterList") @HiltWorker @Suppress("LongParameterList") class UpdatesWorker @AssistedInject constructor( @Assisted private val context: Context, @Assisted private val params: WorkerParameters, private val updatesManagerRepository: UpdatesManagerRepository, private val blockedAppRepository: BlockedAppRepository, private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, private val sessionRepository: SessionRepository, private val appPreferencesRepository: AppPreferencesRepository, private val authenticatorRepository: AuthenticatorRepository, private val playStoreAuthManager: PlayStoreAuthManager, private val appInstallProcessor: AppInstallProcessor, private val blockedAppRepository: BlockedAppRepository, private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, ) : CoroutineWorker(context, params) { companion object { Loading Loading @@ -154,36 +154,19 @@ class UpdatesWorker @AssistedInject constructor( @VisibleForTesting suspend fun getAvailableUpdates(): Pair<List<Application>, ResultStatus> { loadSettings() val appsNeededToUpdate = mutableListOf<Application>() val user = getUser() val authData = authenticatorRepository.getValidatedAuthData().data val resultStatus: ResultStatus if (user in listOf(User.ANONYMOUS, User.GOOGLE) && authData != null) { /* * Signifies valid Google user and valid auth data to update * apps from Google Play store. * The user check will be more useful in No-Google mode. */ val (apps, status) = updatesManagerRepository.getUpdates() appsNeededToUpdate.addAll(apps) resultStatus = status } else if (user == User.NO_GOOGLE) { /* * If authData is null, update apps from cleanapk only. */ val (apps, status) = updatesManagerRepository.getUpdatesOSS() appsNeededToUpdate.addAll(apps) resultStatus = status } else { /* * If user in UNAVAILABLE, don't do anything. */ val authData = playStoreAuthManager.getValidatedAuthData().data val canUsePlayStoreUpdates = (user == User.ANONYMOUS || user == User.GOOGLE) && authData != null return when { canUsePlayStoreUpdates -> updatesManagerRepository.getUpdates() user == User.NO_GOOGLE -> updatesManagerRepository.getUpdatesOSS() else -> { Timber.e("Update is aborted for unavailable user!") resultStatus = ResultStatus.UNKNOWN Pair(emptyList(), ResultStatus.UNKNOWN) } } return Pair(appsNeededToUpdate, resultStatus) } @VisibleForTesting Loading Loading @@ -224,9 +207,11 @@ class UpdatesWorker @AssistedInject constructor( // returns list of Pair(app, status(success|failed)) @VisibleForTesting suspend fun startUpdateProcess(appsNeededToUpdate: List<Application>): List<Pair<Application, Boolean>> { suspend fun startUpdateProcess( appsNeededToUpdate: List<Application> ): List<Pair<Application, Boolean>> { val response = mutableListOf<Pair<Application, Boolean>>() val authData = authenticatorRepository.getValidatedAuthData() val authData = playStoreAuthManager.getValidatedAuthData() val isNotLoggedIntoPersonalAccount = !authData.isValidData() || authData.data?.isAnonymous == true for (fusedApp in appsNeededToUpdate) { Loading