diff --git a/app/src/main/java/foundation/e/apps/data/AppSourcesContainer.kt b/app/src/main/java/foundation/e/apps/data/AppSourcesContainer.kt deleted file mode 100644 index c32a2fe17a5fb8a1388eaf63df0e5434b45a2924..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/data/AppSourcesContainer.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright MURENA SAS 2024 - * Apps Quickly and easily install Android apps onto your device! - * - * 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 - -import foundation.e.apps.data.cleanapk.repositories.CleanApkAppsRepository -import foundation.e.apps.data.cleanapk.repositories.CleanApkPwaRepository -import foundation.e.apps.data.playstore.PlayStoreRepository -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class AppSourcesContainer @Inject constructor( - val gplayRepo: PlayStoreRepository, - val cleanApkAppsRepo: CleanApkAppsRepository, - val cleanApkPWARepo: CleanApkPwaRepository -) diff --git a/app/src/main/java/foundation/e/apps/data/StoreDescriptor.kt b/app/src/main/java/foundation/e/apps/data/StoreDescriptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..847b155bbf39974df08bf1aca1299f2cc25a5293 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/StoreDescriptor.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 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 + +import foundation.e.apps.data.enums.Source + +/** + * Describes a store integration so that it can be discovered and toggled dynamically. + * + * Adding a new store only requires contributing another [StoreDescriptor] via DI. + */ +data class StoreDescriptor( + val source: Source, + val repository: StoreRepository, + val isEnabled: () -> Boolean, + val enable: () -> Unit, + val disable: () -> Unit, +) diff --git a/app/src/main/java/foundation/e/apps/data/Stores.kt b/app/src/main/java/foundation/e/apps/data/Stores.kt index 2437dbc87b4c7b7858f2b6018fb4a0c4b23046ba..3b81ef49d292be63fc51cafd41aa0b28410e4b74 100644 --- a/app/src/main/java/foundation/e/apps/data/Stores.kt +++ b/app/src/main/java/foundation/e/apps/data/Stores.kt @@ -19,64 +19,50 @@ package foundation.e.apps.data -import foundation.e.apps.data.cleanapk.repositories.CleanApkAppsRepository -import foundation.e.apps.data.cleanapk.repositories.CleanApkPwaRepository import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.enums.Source.OPEN_SOURCE -import foundation.e.apps.data.enums.Source.PLAY_STORE -import foundation.e.apps.data.enums.Source.PWA -import foundation.e.apps.data.playstore.PlayStoreRepository -import foundation.e.apps.data.preference.AppLoungePreference +import foundation.e.apps.data.enums.Source.SYSTEM_APP import javax.inject.Inject import javax.inject.Singleton @Singleton class Stores @Inject constructor( - private val playStoreRepository: PlayStoreRepository, - private val cleanApkAppsRepository: CleanApkAppsRepository, - private val cleanApkPwaRepository: CleanApkPwaRepository, - private val appLoungePreference: AppLoungePreference + storeDescriptors: Set ) { + private val descriptorsBySource = storeDescriptors.associateBy { it.source } + /** - * Retrieves a map of enabled store repositories based on user preferences. + * Retrieves a map of enabled store repositories. * * @return A map where the keys are the selected [Source] types and * the values are their corresponding [StoreRepository] instances. */ - fun getStores(): Map { - val showOpenSourceApps = appLoungePreference.isOpenSourceSelected() - val showPwaApps = appLoungePreference.isPWASelected() - val showPlayStoreApps = appLoungePreference.isPlayStoreSelected() - - return buildMap { - if (showPlayStoreApps) { - put(PLAY_STORE, playStoreRepository) - } - if (showOpenSourceApps) { - put(OPEN_SOURCE, cleanApkAppsRepository) - } - if (showPwaApps) { - put(PWA, cleanApkPwaRepository) - } - } - } + fun getStores(): Map = + descriptorsBySource + .filterValues { it.isEnabled() } + .mapValues { (_, descriptor) -> descriptor.repository } - fun getStore(source: Source): StoreRepository? = getStores()[source] + fun getStore(source: Source): StoreRepository? = + descriptorsBySource[source]?.takeIf { it.isEnabled() }?.repository - fun enableStore(source: Source) = when (source) { - OPEN_SOURCE -> appLoungePreference.enableOpenSource() - PWA -> appLoungePreference.enablePwa() - PLAY_STORE -> appLoungePreference.enablePlayStore() - else -> error("No matching Store found for $source.") + fun enableStore(source: Source) { + validateToggleable(source) + descriptorsBySource[source]?.enable?.invoke() + ?: error("No matching Store found for $source.") } - fun disableStore(source: Source) = when (source) { - OPEN_SOURCE -> appLoungePreference.disableOpenSource() - PWA -> appLoungePreference.disablePwa() - PLAY_STORE -> appLoungePreference.disablePlayStore() - else -> error("No matching Store found for $source.") + fun disableStore(source: Source) { + validateToggleable(source) + descriptorsBySource[source]?.disable?.invoke() + ?: error("No matching Store found for $source.") } - fun isStoreEnabled(source: Source): Boolean = getStores().containsKey(source) + fun isStoreEnabled(source: Source): Boolean = + descriptorsBySource[source]?.isEnabled?.invoke() ?: false + + private fun validateToggleable(source: Source) { + if (source == SYSTEM_APP) { + error("System apps cannot be enabled or disabled.") + } + } } diff --git a/app/src/main/java/foundation/e/apps/data/application/category/CategoryApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/category/CategoryApiImpl.kt index f6b2eb40e2a0260ef327e5ae56134f5814f62016..90f33eb324e50a65b2e9d56d7ed56d0fbe97a1b8 100644 --- a/app/src/main/java/foundation/e/apps/data/application/category/CategoryApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/category/CategoryApiImpl.kt @@ -22,8 +22,8 @@ import android.content.Context import com.aurora.gplayapi.data.models.App import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R -import foundation.e.apps.data.AppSourcesContainer import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.StoreRepository import foundation.e.apps.data.Stores import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.data.Application @@ -33,16 +33,17 @@ import foundation.e.apps.data.application.utils.CategoryUtils import foundation.e.apps.data.application.utils.toApplication import foundation.e.apps.data.application.utils.toCategory import foundation.e.apps.data.cleanapk.data.categories.Categories +import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository import foundation.e.apps.data.enums.AppTag import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.isUnFiltered import foundation.e.apps.data.handleNetworkResult +import foundation.e.apps.data.playstore.PlayStoreRepository import javax.inject.Inject class CategoryApiImpl @Inject constructor( @ApplicationContext private val context: Context, - private val appSources: AppSourcesContainer, private val stores: Stores, private val applicationDataManager: ApplicationDataManager ) : CategoryApi { @@ -50,9 +51,9 @@ class CategoryApiImpl @Inject constructor( override suspend fun getCategoriesList(type: CategoryType) : List { val categoryResponses = mutableListOf() - for ((source, _) in stores.getStores()) { + for ((source, repository) in stores.getStores()) { val categories = mutableListOf() - val status = fetchCategoryResult(categories, type, source) + val status = fetchCategoryResult(categories, type, source, repository) categoryResponses.add(CategoriesResponse(categories, type, source, status)) } return categoryResponses @@ -61,20 +62,27 @@ class CategoryApiImpl @Inject constructor( private suspend fun fetchCategoryResult( categoriesList: MutableList, type: CategoryType, - source: Source + source: Source, + repository: StoreRepository ): ResultStatus { - val categoryResult = when (source) { - Source.OPEN_SOURCE -> { - fetchCleanApkCategories(type, Source.OPEN_SOURCE) - } - - Source.PWA -> { - fetchCleanApkCategories(type, Source.PWA) + val categoryResult: Pair, ResultStatus> = when (source) { + Source.OPEN_SOURCE, Source.PWA -> { + val cleanApkRepository = repository as? CleanApkRepository + if (cleanApkRepository != null) { + fetchCleanApkCategories(type, source, cleanApkRepository) + } else { + Pair(emptyList(), ResultStatus.UNKNOWN) + } } - - else -> { - fetchGplayCategories(type) + Source.PLAY_STORE -> { + val playStoreRepository = repository as? PlayStoreRepository + if (playStoreRepository != null) { + fetchGplayCategories(type, playStoreRepository) + } else { + Pair(emptyList(), ResultStatus.UNKNOWN) + } } + else -> Pair(emptyList(), ResultStatus.OK) } categoryResult.let { @@ -86,10 +94,11 @@ class CategoryApiImpl @Inject constructor( private suspend fun fetchGplayCategories( type: CategoryType, + playStoreRepository: PlayStoreRepository, ): Pair, ResultStatus> { val categoryList = mutableListOf() val result = handleNetworkResult { - val playResponse = appSources.gplayRepo.getCategories(type).map { gplayCategory -> + val playResponse = playStoreRepository.getCategories(type).map { gplayCategory -> val category = gplayCategory.toCategory() category.drawable = CategoryUtils.provideAppsCategoryIconResource( @@ -107,28 +116,21 @@ class CategoryApiImpl @Inject constructor( private suspend fun fetchCleanApkCategories( type: CategoryType, - source: Source + source: Source, + repository: CleanApkRepository ): Pair, ResultStatus> { val categoryList = mutableListOf() - var tag: AppTag? = null - val result = handleNetworkResult { - val categories = when (source) { - Source.OPEN_SOURCE -> { - tag = AppTag.OpenSource(context.getString(R.string.open_source)) - appSources.cleanApkAppsRepo.getCategories().body() - } - - Source.PWA -> { - tag = AppTag.PWA(context.getString(R.string.pwa)) - appSources.cleanApkPWARepo.getCategories().body() - } - + val tag = when (source) { + Source.OPEN_SOURCE -> AppTag.OpenSource(context.getString(R.string.open_source)) + Source.PWA -> AppTag.PWA(context.getString(R.string.pwa)) else -> null } - categories?.let { - categoryList.addAll(getFusedCategoryBasedOnCategoryType(it, type, tag!!)) + val categories = repository.getCategories().body() + + if (categories != null && tag != null) { + categoryList.addAll(getFusedCategoryBasedOnCategoryType(categories, type, tag)) } } diff --git a/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt index 1c36aac865d47ffbced95167d958515ef4ffeae1..379346ae385c0ff74d8160c9dc8b3a84c25248c2 100644 --- a/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt @@ -18,15 +18,16 @@ package foundation.e.apps.data.application.downloadInfo -import foundation.e.apps.data.AppSourcesContainer +import foundation.e.apps.data.Stores import foundation.e.apps.data.cleanapk.CleanApkDownloadInfoFetcher import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.handleNetworkResult +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.playstore.PlayStoreRepository import javax.inject.Inject class DownloadInfoApiImpl @Inject constructor( - private val appSources: AppSourcesContainer + private val stores: Stores ) : DownloadInfoApi { override suspend fun getOnDemandModule( @@ -35,8 +36,11 @@ class DownloadInfoApiImpl @Inject constructor( versionCode: Long, offerType: Int ): String? { + val playStoreRepository = stores.getStore(Source.PLAY_STORE) as? PlayStoreRepository + ?: return null + val result = handleNetworkResult { - appSources.gplayRepo.getOnDemandModule( + playStoreRepository.getOnDemandModule( packageName, moduleName, versionCode, @@ -44,15 +48,8 @@ class DownloadInfoApiImpl @Inject constructor( ) } - if (result.isSuccess()) { - for (element in result.data!!) { // isSuccess() checks ensures null safety of data - if (element.name == "$moduleName.apk") { - return element.url - } - } - } - - return null + val moduleFile = result.data?.firstOrNull { it.name == "$moduleName.apk" } + return moduleFile?.url } override suspend fun updateFusedDownloadWithDownloadingInfo( @@ -64,7 +61,7 @@ class DownloadInfoApiImpl @Inject constructor( Source.OPEN_SOURCE -> { updateDownloadInfoFromCleanApk(appInstall, list) } - + Source.PWA -> { updateDownloadInfoFromCleanApk(appInstall, list) } @@ -86,7 +83,7 @@ class DownloadInfoApiImpl @Inject constructor( list: MutableList ) { val downloadList = - appSources.gplayRepo.getDownloadInfo( + getPlayStoreRepositoryOrThrow().getDownloadInfo( appInstall.packageName, appInstall.versionCode, appInstall.offerType @@ -99,14 +96,30 @@ class DownloadInfoApiImpl @Inject constructor( appInstall: AppInstall, list: MutableList ) { - val downloadInfo = - (appSources.cleanApkAppsRepo as CleanApkDownloadInfoFetcher) - .getDownloadInfo(appInstall.id).body() + val downloadInfoFetcher = getCleanApkDownloadInfoFetcherOrNull() ?: return + val downloadInfo = downloadInfoFetcher.getDownloadInfo(appInstall.id).body() downloadInfo?.download_data?.download_link?.let { list.add(it) } appInstall.signature = downloadInfo?.download_data?.signature ?: "" } override suspend fun getOSSDownloadInfo(id: String, version: String?) = - (appSources.cleanApkAppsRepo as CleanApkDownloadInfoFetcher) + getCleanApkDownloadInfoFetcherOrThrow() .getDownloadInfo(id, version) + + private fun getPlayStoreRepositoryOrThrow(): PlayStoreRepository = + stores.getStore(Source.PLAY_STORE) as? PlayStoreRepository + ?: error("Play Store repository is not available.") + + private fun getCleanApkDownloadInfoFetcherOrNull(): CleanApkDownloadInfoFetcher? { + val openSourceFetcher = + stores.getStore(Source.OPEN_SOURCE) as? CleanApkDownloadInfoFetcher + if (openSourceFetcher != null) { + return openSourceFetcher + } + return stores.getStore(Source.PWA) as? CleanApkDownloadInfoFetcher + } + + private fun getCleanApkDownloadInfoFetcherOrThrow(): CleanApkDownloadInfoFetcher = + getCleanApkDownloadInfoFetcherOrNull() + ?: error("CleanApk download repository is not available.") } diff --git a/app/src/main/java/foundation/e/apps/di/StoreModule.kt b/app/src/main/java/foundation/e/apps/di/StoreModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..d238baf855c15fbd619cbe3b6b8fa317fc8a66a5 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/di/StoreModule.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2025 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.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import foundation.e.apps.data.StoreDescriptor +import foundation.e.apps.data.cleanapk.repositories.CleanApkAppsRepository +import foundation.e.apps.data.cleanapk.repositories.CleanApkPwaRepository +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.playstore.PlayStoreRepository +import foundation.e.apps.data.preference.AppLoungePreference +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object StoreModule { + + @Provides + @IntoSet + @Singleton + fun providePlayStoreDescriptor( + playStoreRepository: PlayStoreRepository, + appLoungePreference: AppLoungePreference, + ): StoreDescriptor = StoreDescriptor( + source = Source.PLAY_STORE, + repository = playStoreRepository, + isEnabled = { appLoungePreference.isPlayStoreSelected() }, + enable = { appLoungePreference.enablePlayStore() }, + disable = { appLoungePreference.disablePlayStore() }, + ) + + @Provides + @IntoSet + @Singleton + fun provideOpenSourceDescriptor( + cleanApkAppsRepository: CleanApkAppsRepository, + appLoungePreference: AppLoungePreference, + ): StoreDescriptor = StoreDescriptor( + source = Source.OPEN_SOURCE, + repository = cleanApkAppsRepository, + isEnabled = { appLoungePreference.isOpenSourceSelected() }, + enable = { appLoungePreference.enableOpenSource() }, + disable = { appLoungePreference.disableOpenSource() }, + ) + + @Provides + @IntoSet + @Singleton + fun providePwaDescriptor( + cleanApkPwaRepository: CleanApkPwaRepository, + appLoungePreference: AppLoungePreference, + ): StoreDescriptor = StoreDescriptor( + source = Source.PWA, + repository = cleanApkPwaRepository, + isEnabled = { appLoungePreference.isPWASelected() }, + enable = { appLoungePreference.enablePwa() }, + disable = { appLoungePreference.disablePwa() }, + ) +} diff --git a/app/src/test/java/foundation/e/apps/category/CategoryApiTest.kt b/app/src/test/java/foundation/e/apps/category/CategoryApiTest.kt index 6631cc1e48c05e5bf9a57ee2da74aeead9cb42cb..ce1a88392138a4344e3aa17401ab02f8f96cf782 100644 --- a/app/src/test/java/foundation/e/apps/category/CategoryApiTest.kt +++ b/app/src/test/java/foundation/e/apps/category/CategoryApiTest.kt @@ -23,7 +23,8 @@ import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.aurora.gplayapi.data.models.Category import foundation.e.apps.R -import foundation.e.apps.data.AppSourcesContainer +import foundation.e.apps.data.StoreDescriptor +import foundation.e.apps.data.StoreRepository import foundation.e.apps.data.Stores import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.category.CategoryApi @@ -99,18 +100,36 @@ class CategoryApiTest { val applicationDataManager = ApplicationDataManager(appLoungePackageManager, pwaManager, visibilityFetcher) - fakeStores = Stores(playStoreRepository, cleanApkAppsRepository, cleanApkPWARepository, appLoungePreference) + fakeStores = Stores( + setOf( + createDescriptor(Source.PLAY_STORE, playStoreRepository, enabled = true), + createDescriptor(Source.OPEN_SOURCE, cleanApkAppsRepository, enabled = true), + createDescriptor(Source.PWA, cleanApkPWARepository, enabled = true), + ) + ) - val appSourcesContainer = - AppSourcesContainer(playStoreRepository, cleanApkAppsRepository, cleanApkPWARepository) categoryApi = CategoryApiImpl( context, - appSourcesContainer, fakeStores, applicationDataManager ) } + private fun createDescriptor( + source: Source, + repository: StoreRepository, + enabled: Boolean, + ): StoreDescriptor { + var currentState = enabled + return StoreDescriptor( + source = source, + repository = repository, + isEnabled = { currentState }, + enable = { currentState = true }, + disable = { currentState = false }, + ) + } + @Test fun `getCategory when only pwa is selected`() = runTest { val categories = @@ -126,8 +145,6 @@ class CategoryApiTest { Mockito.`when`(context.getString(eq(R.string.pwa))).thenReturn("PWA") - Mockito.`when`(appLoungePreference.isPWASelected()).thenReturn(true) - val categoryListResponse = categoryApi.getCategoriesList(CategoryType.APPLICATION).map { it.categories }.flatten() @@ -146,8 +163,6 @@ class CategoryApiTest { Mockito.`when`(context.getString(eq(R.string.open_source))).thenReturn("Open source") - Mockito.`when`(appLoungePreference.isOpenSourceSelected()).thenReturn(true) - fakeStores.disableStore(Source.PWA) fakeStores.disableStore(Source.PLAY_STORE) diff --git a/app/src/test/java/foundation/e/apps/fused/SearchRepositoryImplTest.kt b/app/src/test/java/foundation/e/apps/fused/SearchRepositoryImplTest.kt index 13cc3ce506bf081296cdb748337d8afb77bbf1b2..d891b9c1ae8b13a15e92c96999b998ce6e57c559 100644 --- a/app/src/test/java/foundation/e/apps/fused/SearchRepositoryImplTest.kt +++ b/app/src/test/java/foundation/e/apps/fused/SearchRepositoryImplTest.kt @@ -22,7 +22,6 @@ import android.text.format.Formatter import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.aurora.gplayapi.data.models.App import foundation.e.apps.FakeAppLoungePreference -import foundation.e.apps.data.AppSourcesContainer import foundation.e.apps.data.Stores import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.apps.AppsApi @@ -113,8 +112,6 @@ class SearchRepositoryImplTest { preferenceManagerModule = FakeAppLoungePreference(context, appLoungeDataStore) applicationDataManager = ApplicationDataManager(appLoungePackageManager, pwaManager, visibilityFetcher) - val appSourcesContainer = - AppSourcesContainer(playStoreRepository, cleanApkAppsRepository, cleanApkPWARepository) appsApi = AppsApiImpl( stores, applicationDataManager,