diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationApi.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationApi.kt index 468ba9190cc27c8b983d18d3dea30cf1f7e8ad1c..3b47a38dc0803b47e8608837a3fe825a246befeb 100644 --- a/app/src/main/java/foundation/e/apps/data/application/ApplicationApi.kt +++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationApi.kt @@ -29,21 +29,6 @@ interface ApplicationApi { fun getApplicationCategoryPreference(): List - /* - * Return three elements from the function. - * - List : List of categories. - * - String : String of application type - By default it is the value in preferences. - * In case there is any failure, for a specific type in handleAllSourcesCategories(), - * the string value is of that type. - * - ResultStatus : ResultStatus - by default is ResultStatus.OK. But in case there is a failure in - * any application category type, then it takes value of that failure. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 - */ - suspend fun getCategoriesList( - type: CategoryType, - ): Triple, String, ResultStatus> - /** * Fetches search results from cleanAPK and GPlay servers and returns them * @param query Query @@ -77,10 +62,6 @@ interface ApplicationApi { suspend fun getOSSDownloadInfo(id: String, version: String?): Response - suspend fun getPWAApps(category: String): ResultSupreme, String>> - - suspend fun getOpenSourceApps(category: String): ResultSupreme, String>> - /* * Function to search cleanapk using package name. * Will be used to handle f-droid deeplink. @@ -148,5 +129,4 @@ interface ApplicationApi { fun isAnyAppInstallStatusChanged(currentList: List): Boolean fun isOpenSourceSelected(): Boolean - suspend fun getGplayAppsByCategory(authData: AuthData, category: String, pageUrl: String?): ResultSupreme, String>> } diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationApiImpl.kt index 6db8ae0678add4809d7285d6519ad5c7ef46e746..60340803d1631cd5da7ed07650b73ea1b1c6da1c 100644 --- a/app/src/main/java/foundation/e/apps/data/application/ApplicationApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationApiImpl.kt @@ -85,9 +85,6 @@ class ApplicationApiImpl @Inject constructor( ) : ApplicationApi { companion object { - private const val CATEGORY_TITLE_REPLACEABLE_CONJUNCTION = "&" - private const val CATEGORY_OPEN_GAMES_ID = "game_open_games" - private const val CATEGORY_OPEN_GAMES_TITLE = "Open games" private const val KEYWORD_TEST_SEARCH = "facebook" } @@ -99,35 +96,6 @@ class ApplicationApiImpl @Inject constructor( return prefs } - /* - * Return three elements from the function. - * - List : List of categories. - * - String : String of application type - By default it is the value in preferences. - * In case there is any failure, for a specific type in handleAllSourcesCategories(), - * the string value is of that type. - * - ResultStatus : ResultStatus - by default is ResultStatus.OK. But in case there is a failure in - * any application category type, then it takes value of that failure. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 - */ - override suspend fun getCategoriesList( - type: CategoryType, - ): Triple, String, ResultStatus> { - val categoriesList = mutableListOf() - val preferredApplicationType = preferenceManagerModule.preferredApplicationType() - var apiStatus: ResultStatus = ResultStatus.OK - var applicationCategoryType = preferredApplicationType - - handleAllSourcesCategories(categoriesList, type).run { - if (first != ResultStatus.OK) { - apiStatus = first - applicationCategoryType = second - } - } - categoriesList.sortBy { item -> item.title.lowercase() } - return Triple(categoriesList, applicationCategoryType, apiStatus) - } - /** * Fetches search results from cleanAPK and GPlay servers and returns them * @param query Query @@ -415,34 +383,6 @@ class ApplicationApiImpl @Inject constructor( override suspend fun getOSSDownloadInfo(id: String, version: String?) = (cleanApkAppsRepository as CleanApkDownloadInfoFetcher).getDownloadInfo(id, version) - override suspend fun getPWAApps(category: String): ResultSupreme, String>> { - val list = mutableListOf() - val result = handleNetworkResult { - val response = getPWAAppsResponse(category) - response?.apps?.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - list.add(it) - } - } - return ResultSupreme.create(result.getResultStatus(), Pair(list, "")) - } - - override suspend fun getOpenSourceApps(category: String): ResultSupreme, String>> { - val list = mutableListOf() - val result = handleNetworkResult { - val response = getOpenSourceAppsResponse(category) - response?.apps?.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - list.add(it) - } - } - return ResultSupreme.create(result.getResultStatus(), Pair(list, "")) - } - /* * Function to search cleanapk using package name. * Will be used to handle f-droid deeplink. @@ -689,208 +629,6 @@ class ApplicationApiImpl @Inject constructor( return Pair(result.data ?: Application(), result.getResultStatus()) } - /* - * Function to populate a given category list, from all GPlay categories, open source categories, - * and PWAs. - * - * Returns: Pair of: - * - ResultStatus - by default ResultStatus.OK, but can be different in case of an error in any category. - * - String - Application category type having error. If no error, then blank string. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 - */ - private suspend fun handleAllSourcesCategories( - categoriesList: MutableList, - type: CategoryType, - ): Pair { - var apiStatus = ResultStatus.OK - var errorApplicationCategory = "" - - if (preferenceManagerModule.isOpenSourceSelected()) { - val openSourceCategoryResult = fetchOpenSourceCategories(type) - categoriesList.addAll(openSourceCategoryResult.second) - apiStatus = openSourceCategoryResult.first - errorApplicationCategory = openSourceCategoryResult.third - } - - if (preferenceManagerModule.isPWASelected()) { - val pwaCategoriesResult = fetchPWACategories(type) - categoriesList.addAll(pwaCategoriesResult.second) - apiStatus = pwaCategoriesResult.first - errorApplicationCategory = pwaCategoriesResult.third - } - - if (preferenceManagerModule.isGplaySelected()) { - val gplayCategoryResult = fetchGplayCategories( - type, - ) - categoriesList.addAll(gplayCategoryResult.data ?: listOf()) - apiStatus = gplayCategoryResult.getResultStatus() - errorApplicationCategory = APP_TYPE_ANY - } - - return Pair(apiStatus, errorApplicationCategory) - } - - private suspend fun fetchGplayCategories( - type: CategoryType, - ): ResultSupreme> { - val categoryList = mutableListOf() - - return handleNetworkResult { - val playResponse = gplayRepository.getCategories(type).map { app -> - val category = app.transformToFusedCategory() - updateCategoryDrawable(category) - category - } - categoryList.addAll(playResponse) - categoryList - } - } - - private suspend fun fetchPWACategories( - type: CategoryType, - ): Triple, String> { - val fusedCategoriesList = mutableListOf() - val result = handleNetworkResult { - getPWAsCategories()?.let { - fusedCategoriesList.addAll( - getFusedCategoryBasedOnCategoryType( - it, type, AppTag.PWA(context.getString(R.string.pwa)) - ) - ) - } - } - - return Triple(result.getResultStatus(), fusedCategoriesList, APP_TYPE_PWA) - } - - private suspend fun fetchOpenSourceCategories( - type: CategoryType, - ): Triple, String> { - val categoryList = mutableListOf() - val result = handleNetworkResult { - getOpenSourceCategories()?.let { - categoryList.addAll( - getFusedCategoryBasedOnCategoryType( - it, - type, - AppTag.OpenSource(context.getString(R.string.open_source)) - ) - ) - } - } - - return Triple(result.getResultStatus(), categoryList, APP_TYPE_OPEN) - } - - private fun updateCategoryDrawable( - category: Category, - ) { - category.drawable = - getCategoryIconResource(getCategoryIconName(category)) - } - - private fun getCategoryIconName(category: Category): String { - var categoryTitle = if (category.tag.getOperationalTag().contentEquals(AppTag.GPlay().getOperationalTag())) - category.id else category.title - - if (categoryTitle.contains(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION)) { - categoryTitle = categoryTitle.replace(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION, "and") - } - categoryTitle = categoryTitle.replace(' ', '_') - return categoryTitle.lowercase() - } - - private fun getFusedCategoryBasedOnCategoryType( - categories: Categories, - categoryType: CategoryType, - tag: AppTag - ): List { - return when (categoryType) { - CategoryType.APPLICATION -> { - getAppsCategoriesAsFusedCategory(categories, tag) - } - - CategoryType.GAMES -> { - getGamesCategoriesAsFusedCategory(categories, tag) - } - } - } - - private fun getAppsCategoriesAsFusedCategory( - categories: Categories, - tag: AppTag - ): List { - return categories.apps.map { category -> - createFusedCategoryFromCategory(category, categories, tag) - } - } - - private fun getGamesCategoriesAsFusedCategory( - categories: Categories, - tag: AppTag - ): List { - return categories.games.map { category -> - createFusedCategoryFromCategory(category, categories, tag) - } - } - - private fun createFusedCategoryFromCategory( - category: String, - categories: Categories, - tag: AppTag - ): Category { - return Category( - id = category, - title = getCategoryTitle(category, categories), - drawable = getCategoryIconResource(category), - tag = tag - ) - } - - private fun getCategoryIconResource(category: String): Int { - return CategoryUtils.provideAppsCategoryIconResource(category) - } - - private fun getCategoryTitle(category: String, categories: Categories): String { - return if (category.contentEquals(CATEGORY_OPEN_GAMES_ID)) { - CATEGORY_OPEN_GAMES_TITLE - } else { - categories.translations.getOrDefault(category, "") - } - } - - private suspend fun getPWAsCategories(): Categories? { - return cleanApkPWARepository.getCategories().body() - } - - private suspend fun getOpenSourceCategories(): Categories? { - return cleanApkAppsRepository.getCategories().body() - } - - private suspend fun getOpenSourceAppsResponse(category: String): Search? { - return cleanApkAppsRepository.getAppsByCategory( - category, - ).body() - } - - private suspend fun getPWAAppsResponse(category: String): Search? { - return cleanApkPWARepository.getAppsByCategory( - category, - ).body() - } - - private fun GplayapiCategory.transformToFusedCategory(): Category { - val id = this.browseUrl.substringAfter("cat=").substringBefore("&c=") - return Category( - id = id.lowercase(), - title = this.title, - browseUrl = this.browseUrl, - imageUrl = this.imageUrl, - ) - } - /* * Search-related internal functions */ @@ -1089,28 +827,4 @@ class ApplicationApiImpl @Inject constructor( } override fun isOpenSourceSelected() = preferenceManagerModule.isOpenSourceSelected() - override suspend fun getGplayAppsByCategory( - authData: AuthData, - category: String, - pageUrl: String? - ): ResultSupreme, String>> { - var applicationList: MutableList = mutableListOf() - var nextPageUrl = "" - - return handleNetworkResult { - val streamCluster = - gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster - - val filteredAppList = filterRestrictedGPlayApps(authData, streamCluster.clusterAppList) - filteredAppList.data?.let { - applicationList = it.toMutableList() - } - - nextPageUrl = streamCluster.clusterNextPageUrl - if (!nextPageUrl.isNullOrEmpty()) { - applicationList.add(Application(isPlaceHolder = true)) - } - Pair(applicationList, nextPageUrl) - } - } } diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt index a537d6bf7c50141c233bc309178bb193609ef18d..0cf2820fefdf3fd60dbe87ae0b542c842cae9822 100644 --- a/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt +++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt @@ -57,7 +57,7 @@ class ApplicationDataManager @Inject constructor( } } - private suspend fun getAppFilterLevel(application: Application, authData: AuthData?): FilterLevel { + suspend fun getAppFilterLevel(application: Application, authData: AuthData?): FilterLevel { return when { application.package_name.isBlank() -> FilterLevel.UNKNOWN !application.isFree && application.price.isBlank() -> FilterLevel.UI @@ -116,5 +116,4 @@ class ApplicationDataManager @Inject constructor( pkgManagerModule.getPackageStatus(application.package_name, application.latest_version_code) } } - } diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt index a7526bb12c27316953cb2fb444a6b18ec3558bc1..81f5d3d8cedb4e4a879749f9ec813cd346987d0b 100644 --- a/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt @@ -39,7 +39,8 @@ import javax.inject.Singleton @Singleton class ApplicationRepository @Inject constructor( private val applicationAPIImpl: ApplicationApi, - private val homeApi: HomeApi + private val homeApi: HomeApi, + private val categoryApi: CategoryApi ) { suspend fun getHomeScreenData(authData: AuthData): LiveData>> { @@ -99,8 +100,8 @@ class ApplicationRepository @Inject constructor( suspend fun getCategoriesList( type: CategoryType, - ): Triple, String, ResultStatus> { - return applicationAPIImpl.getCategoriesList(type) + ): Pair, ResultStatus> { + return categoryApi.getCategoriesList(type) } suspend fun getSearchSuggestions(query: String): List { @@ -128,9 +129,9 @@ class ApplicationRepository @Inject constructor( source: Source ): ResultSupreme, String>> { return when (source) { - Source.OPEN -> applicationAPIImpl.getOpenSourceApps(category) - Source.PWA -> applicationAPIImpl.getPWAApps(category) - else -> applicationAPIImpl.getGplayAppsByCategory(authData, category, pageUrl) + Source.OPEN -> categoryApi.getCleanApkAppsByCategory(category, Source.OPEN) + Source.PWA -> categoryApi.getCleanApkAppsByCategory(category, Source.PWA) + else -> categoryApi.getGplayAppsByCategory(authData, category, pageUrl) } } diff --git a/app/src/main/java/foundation/e/apps/data/application/CategoryApi.kt b/app/src/main/java/foundation/e/apps/data/application/CategoryApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..f6afb796ced8c4a0698a7e5de84b1a5c41d32532 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/application/CategoryApi.kt @@ -0,0 +1,45 @@ +/* + * Copyright MURENA SAS 2023 + * 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.application + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.application.data.Category +import foundation.e.apps.data.application.utils.CategoryType +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.Source + +interface CategoryApi { + + suspend fun getCategoriesList( + type: CategoryType, + ): Pair, ResultStatus> + + suspend fun getGplayAppsByCategory( + authData: AuthData, + category: String, + pageUrl: String? + ): ResultSupreme, String>> + + suspend fun getCleanApkAppsByCategory( + category: String, + source: Source + ): ResultSupreme, String>> +} diff --git a/app/src/main/java/foundation/e/apps/data/application/CategoryApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/CategoryApiImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..e222d724208b86548498a3374c3809bca461690b --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/application/CategoryApiImpl.kt @@ -0,0 +1,269 @@ +/* + * Copyright MURENA SAS 2023 + * 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.application + +import android.content.Context +import com.aurora.gplayapi.data.models.App +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.StreamCluster +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.R +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.application.data.Category +import foundation.e.apps.data.application.utils.CategoryType +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 foundation.e.apps.data.preference.PreferenceManagerModule +import javax.inject.Inject +import javax.inject.Named + +class CategoryApiImpl @Inject constructor( + @ApplicationContext private val context: Context, + private val preferenceManagerModule: PreferenceManagerModule, + @Named("gplayRepository") private val gplayRepository: PlayStoreRepository, + @Named("cleanApkAppsRepository") private val cleanApkAppsRepository: CleanApkRepository, + @Named("cleanApkPWARepository") private val cleanApkPWARepository: CleanApkRepository, + private val applicationDataManager: ApplicationDataManager +) : CategoryApi { + + override suspend fun getCategoriesList(type: CategoryType): Pair, ResultStatus> { + val categoriesList = mutableListOf() + var apiStatus = handleAllSourcesCategories(categoriesList, type) + + categoriesList.sortBy { item -> item.title.lowercase() } + return Pair(categoriesList, apiStatus) + } + + private suspend fun handleAllSourcesCategories( + categoriesList: MutableList, + type: CategoryType, + ): ResultStatus { + val categoryResults: MutableList = mutableListOf() + + if (preferenceManagerModule.isOpenSourceSelected()) { + categoryResults.add(fetchCategoryResult(categoriesList, type, Source.OPEN)) + } + + if (preferenceManagerModule.isPWASelected()) { + categoryResults.add(fetchCategoryResult(categoriesList, type, Source.PWA)) + } + + if (preferenceManagerModule.isGplaySelected()) { + categoryResults.add(fetchCategoryResult(categoriesList, type, Source.GPLAY)) + } + + return categoryResults.find { it != ResultStatus.OK } ?: ResultStatus.OK + } + + private suspend fun fetchCategoryResult( + categoriesList: MutableList, + type: CategoryType, + source: Source + ): ResultStatus { + val categoryResult = when (source) { + Source.OPEN -> { + fetchCleanApkCategories(type, Source.OPEN) + } + + Source.PWA -> { + fetchCleanApkCategories(type, Source.PWA) + } + + else -> { + fetchGplayCategories(type) + } + } + + categoryResult.let { + categoriesList.addAll(it.first) + } + + return categoryResult.second + } + + private suspend fun fetchGplayCategories( + type: CategoryType, + ): Pair, ResultStatus> { + val categoryList = mutableListOf() + val result = handleNetworkResult { + val playResponse = gplayRepository.getCategories(type).map { gplayCategory -> + val category = gplayCategory.toCategory() + category.drawable = + CategoryUtils.provideAppsCategoryIconResource( + CategoryUtils.getCategoryIconName(category) + ) + category + } + + categoryList.addAll(playResponse) + categoryList + } + + return Pair(result.data ?: listOf(), result.getResultStatus()) + } + + private suspend fun fetchCleanApkCategories( + type: CategoryType, + source: Source + ): Pair, ResultStatus> { + val categoryList = mutableListOf() + var tag: AppTag? = null + + val result = handleNetworkResult { + val categories = when (source) { + Source.OPEN -> { + tag = AppTag.OpenSource(context.getString(R.string.open_source)) + cleanApkAppsRepository.getCategories().body() + } + + Source.PWA -> { + tag = AppTag.PWA(context.getString(R.string.pwa)) + cleanApkPWARepository.getCategories().body() + } + + else -> null + } + + categories?.let { + categoryList.addAll(getFusedCategoryBasedOnCategoryType(it, type, tag!!)) + } + } + + return Pair(categoryList, result.getResultStatus()) + } + + private fun getFusedCategoryBasedOnCategoryType( + categories: Categories, + categoryType: CategoryType, + tag: AppTag + ): List { + return when (categoryType) { + CategoryType.APPLICATION -> { + CategoryUtils.getCategories(categories, categories.apps, tag) + } + + CategoryType.GAMES -> { + CategoryUtils.getCategories(categories, categories.games, tag) + } + } + } + + override suspend fun getGplayAppsByCategory( + authData: AuthData, + category: String, + pageUrl: String? + ): ResultSupreme, String>> { + var applicationList: MutableList = mutableListOf() + var nextPageUrl = "" + + return handleNetworkResult { + val streamCluster = + gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster + + val filteredAppList = filterRestrictedGPlayApps(authData, streamCluster.clusterAppList) + filteredAppList.data?.let { + applicationList = it.toMutableList() + } + + nextPageUrl = streamCluster.clusterNextPageUrl + if (nextPageUrl.isNotEmpty()) { + applicationList.add(Application(isPlaceHolder = true)) + } + Pair(applicationList, nextPageUrl) + } + } + + /* + * Filter out apps which are restricted, whose details cannot be fetched. + * If an app is restricted, we do try to fetch the app details inside a + * try-catch block. If that fails, we remove the app, else we keep it even + * if it is restricted. + * + * Popular example: "com.skype.m2" + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5174 + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + */ + private suspend fun filterRestrictedGPlayApps( + authData: AuthData, + appList: List, + ): ResultSupreme> { + val filteredApplications = mutableListOf() + return handleNetworkResult { + appList.forEach { + val filter = applicationDataManager.getAppFilterLevel( + it.toApplication(context), + authData + ) + + if (filter.isUnFiltered()) { + filteredApplications.add( + it.toApplication(context).apply { + this.filterLevel = filter + } + ) + } + } + filteredApplications + } + } + + override suspend fun getCleanApkAppsByCategory( + category: String, + source: Source + ): ResultSupreme, String>> { + val list = mutableListOf() + val result = handleNetworkResult { + val response = getCleanApkAppsResponse(source, category) + + response?.apps?.forEach { + applicationDataManager.updateStatus(it) + it.updateType() + applicationDataManager.updateFilterLevel(null, it) + list.add(it) + } + } + return ResultSupreme.create(result.getResultStatus(), Pair(list, "")) + } + + private suspend fun getCleanApkAppsResponse( + source: Source, + category: String + ) = when (source) { + Source.OPEN -> { + cleanApkAppsRepository.getAppsByCategory(category).body() + } + + Source.PWA -> { + cleanApkPWARepository.getAppsByCategory(category).body() + } + + else -> null + } +} diff --git a/app/src/main/java/foundation/e/apps/data/application/utils/CategoryUtils.kt b/app/src/main/java/foundation/e/apps/data/application/utils/CategoryUtils.kt index 08d3c9816724fd8abd8eac59f23bdd4a63ac6ccc..5908cf2898f9afec3875c28f9306cd46ff405df8 100644 --- a/app/src/main/java/foundation/e/apps/data/application/utils/CategoryUtils.kt +++ b/app/src/main/java/foundation/e/apps/data/application/utils/CategoryUtils.kt @@ -19,9 +19,17 @@ package foundation.e.apps.data.application.utils import foundation.e.apps.R +import foundation.e.apps.data.application.data.Category +import foundation.e.apps.data.cleanapk.data.categories.Categories +import foundation.e.apps.data.enums.AppTag object CategoryUtils { + private const val CATEGORY_OPEN_GAMES_ID = "game_open_games" + private const val CATEGORY_OPEN_GAMES_TITLE = "Open games" + private const val CATEGORY_TITLE_REPLACEABLE_CONJUNCTION = "&" + private const val CATEGORY_TITLE_CONJUNCTION = "and" + private val categoryIconMap = mapOf( "comics" to R.drawable.ic_cat_comics, "connectivity" to R.drawable.ic_cat_connectivity, @@ -102,6 +110,42 @@ object CategoryUtils { fun provideAppsCategoryIconResource(categoryId: String) = categoryIconMap[categoryId] ?: R.drawable.ic_cat_default + fun getCategories( + categories: Categories, + categoryNames: List, + tag: AppTag + ) = categoryNames.map { category -> + Category( + id = category, + title = getCategoryTitle(category, categories), + drawable = provideAppsCategoryIconResource(category), + tag = tag + ) + } + + private fun getCategoryTitle(category: String, categories: Categories): String { + return if (category.contentEquals(CATEGORY_OPEN_GAMES_ID)) { + CATEGORY_OPEN_GAMES_TITLE + } else { + categories.translations.getOrDefault(category, "") + } + } + + fun getCategoryIconName(category: Category): String { + var categoryTitle = + if (category.tag.getOperationalTag().contentEquals(AppTag.GPlay().getOperationalTag())) + category.id else category.title + + if (categoryTitle.contains(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION)) { + categoryTitle = categoryTitle.replace( + CATEGORY_TITLE_REPLACEABLE_CONJUNCTION, + CATEGORY_TITLE_CONJUNCTION + ) + } + + categoryTitle = categoryTitle.replace(' ', '_') + return categoryTitle.lowercase() + } } enum class CategoryType { diff --git a/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt b/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt index 88cdb6451ebf0bfa5e9fd35538bbb04d779151ec..be26c5669b9ad838ef0353104bd290fb94fb07e2 100644 --- a/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt +++ b/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt @@ -22,6 +22,8 @@ import android.content.Context import android.text.format.Formatter import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.Artwork +import com.aurora.gplayapi.data.models.Category +import foundation.e.apps.data.application.data.Category as AppLoungeCategory import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Ratings import foundation.e.apps.data.enums.Origin @@ -60,6 +62,16 @@ fun App.toApplication(context: Context): Application { return app } +fun Category.toCategory(): AppLoungeCategory { + val id = this.browseUrl.substringAfter("cat=").substringBefore("&c=") + return AppLoungeCategory( + id = id.lowercase(), + title = this.title, + browseUrl = this.browseUrl, + imageUrl = this.imageUrl, + ) +} + private fun MutableList.toList(): List { val list = mutableListOf() this.forEach { diff --git a/app/src/main/java/foundation/e/apps/di/DataModule.kt b/app/src/main/java/foundation/e/apps/di/DataModule.kt index f464ef454257050adf9f99d1b1903f6c8d7ba017..0dfccd751157fd2a882f63434d2da2391bbb960f 100644 --- a/app/src/main/java/foundation/e/apps/di/DataModule.kt +++ b/app/src/main/java/foundation/e/apps/di/DataModule.kt @@ -22,6 +22,8 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.application.CategoryApi +import foundation.e.apps.data.application.CategoryApiImpl import foundation.e.apps.data.application.HomeApi import foundation.e.apps.data.application.HomeApiImpl import javax.inject.Singleton @@ -33,4 +35,8 @@ interface DataModule { @Singleton @Binds fun getHomeApi(homeApiImpl: HomeApiImpl): HomeApi + + @Singleton + @Binds + fun getCategoryApi(categoryApiImpl: CategoryApiImpl): CategoryApi } diff --git a/app/src/main/java/foundation/e/apps/ui/categories/CategoriesViewModel.kt b/app/src/main/java/foundation/e/apps/ui/categories/CategoriesViewModel.kt index cb777869c9ffcb919deb5453e025e320302b6e2b..163562d95fefa79ca1f03537dc7d53be5f9eb4b1 100644 --- a/app/src/main/java/foundation/e/apps/ui/categories/CategoriesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/categories/CategoriesViewModel.kt @@ -38,7 +38,7 @@ class CategoriesViewModel @Inject constructor( private val applicationRepository: ApplicationRepository ) : LoadingViewModel() { - val categoriesList: MutableLiveData, String, ResultStatus>> = + val categoriesList: MutableLiveData, ResultStatus>> = MutableLiveData() fun loadData( @@ -65,17 +65,17 @@ class CategoriesViewModel @Inject constructor( val categoriesData = applicationRepository.getCategoriesList(type) categoriesList.postValue(categoriesData) - val status = categoriesData.third + val status = categoriesData.second if (status != ResultStatus.OK) { val exception = if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) GPlayException( - categoriesData.third == ResultStatus.TIMEOUT, + categoriesData.second == ResultStatus.TIMEOUT, status.message.ifBlank { "Data load error" } ) else CleanApkException( - categoriesData.third == ResultStatus.TIMEOUT, + categoriesData.second == ResultStatus.TIMEOUT, status.message.ifBlank { "Data load error" } ) diff --git a/app/src/test/java/foundation/e/apps/category/CategoryApiTest.kt b/app/src/test/java/foundation/e/apps/category/CategoryApiTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..179afb6d4adede4a232da2fe71ca497fccf58239 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/category/CategoryApiTest.kt @@ -0,0 +1,212 @@ +/* + * Copyright MURENA SAS 2023 + * 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.category + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.aurora.gplayapi.data.models.Category +import foundation.e.apps.FakePreferenceModule +import foundation.e.apps.R +import foundation.e.apps.data.application.ApplicationDataManager +import foundation.e.apps.data.application.CategoryApi +import foundation.e.apps.data.application.CategoryApiImpl +import foundation.e.apps.data.application.utils.CategoryType +import foundation.e.apps.data.cleanapk.data.categories.Categories +import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.playstore.PlayStoreRepository +import foundation.e.apps.install.pkg.PWAManagerModule +import foundation.e.apps.install.pkg.PkgManagerModule +import foundation.e.apps.util.MainCoroutineRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +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.eq +import retrofit2.Response + +@OptIn(ExperimentalCoroutinesApi::class) +class CategoryApiTest { + + // 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() + + @Mock + private lateinit var context: Context + + @Mock + private lateinit var pwaManagerModule: PWAManagerModule + + @Mock + private lateinit var pkgManagerModule: PkgManagerModule + + @Mock + private lateinit var cleanApkAppsRepository: CleanApkRepository + + @Mock + private lateinit var cleanApkPWARepository: CleanApkRepository + + @Mock + private lateinit var gPlayAPIRepository: PlayStoreRepository + + private lateinit var preferenceManagerModule: FakePreferenceModule + + private lateinit var categoryApi: CategoryApi + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + preferenceManagerModule = FakePreferenceModule(context) + val applicationDataManager = + ApplicationDataManager(gPlayAPIRepository, pkgManagerModule, pwaManagerModule) + categoryApi = CategoryApiImpl( + context, + preferenceManagerModule, + gPlayAPIRepository, + cleanApkAppsRepository, + cleanApkPWARepository, + applicationDataManager + ) + } + + @Test + fun `getCategory when only pwa is selected`() = runTest { + val categories = + Categories(listOf("app one", "app two", "app three"), listOf("game 1", "game 2"), true) + val response = Response.success(categories) + preferenceManagerModule.isPWASelectedFake = true + preferenceManagerModule.isOpenSourceelectedFake = false + preferenceManagerModule.isGplaySelectedFake = false + + Mockito.`when`( + cleanApkPWARepository.getCategories() + ).thenReturn(response) + + Mockito.`when`(context.getString(eq(R.string.pwa))).thenReturn("PWA") + + val categoryListResponse = + categoryApi.getCategoriesList(CategoryType.APPLICATION) + + Assert.assertEquals("getCategory", 3, categoryListResponse.first.size) + } + + @Test + fun `getCategory when only open source is selected`() = runTest { + val categories = + Categories(listOf("app one", "app two", "app three"), listOf("game 1", "game 2"), true) + val response = Response.success(categories) + + preferenceManagerModule.isPWASelectedFake = false + preferenceManagerModule.isOpenSourceelectedFake = true + preferenceManagerModule.isGplaySelectedFake = false + + Mockito.`when`( + cleanApkAppsRepository.getCategories() + ).thenReturn(response) + Mockito.`when`(context.getString(eq(R.string.open_source))).thenReturn("Open source") + + val categoryListResponse = + categoryApi.getCategoriesList(CategoryType.APPLICATION) + + Assert.assertEquals("getCategory", 3, categoryListResponse.first.size) + } + + @Test + fun `getCategory when gplay source is selected`() = runTest { + val categories = listOf(Category(), Category(), Category(), Category()) + + preferenceManagerModule.isPWASelectedFake = false + preferenceManagerModule.isOpenSourceelectedFake = false + preferenceManagerModule.isGplaySelectedFake = true + + Mockito.`when`( + gPlayAPIRepository.getCategories(CategoryType.APPLICATION) + ).thenReturn(categories) + + val categoryListResponse = + categoryApi.getCategoriesList(CategoryType.APPLICATION) + + Assert.assertEquals("getCategory", 4, categoryListResponse.first.size) + } + + @Test + fun `getCategory when gplay source is selected return error`() = runTest { + preferenceManagerModule.isPWASelectedFake = false + preferenceManagerModule.isOpenSourceelectedFake = false + preferenceManagerModule.isGplaySelectedFake = true + + Mockito.`when`( + gPlayAPIRepository.getCategories(CategoryType.APPLICATION) + ).thenThrow() + + val categoryListResponse = + categoryApi.getCategoriesList(CategoryType.APPLICATION) + + Assert.assertEquals("getCategory", 0, categoryListResponse.first.size) + Assert.assertEquals("getCategory", ResultStatus.UNKNOWN, categoryListResponse.second) + } + + @Test + fun `getCategory when All source is selected`() = runTest { + val gplayCategories = listOf(Category(), Category(), Category(), Category()) + val openSourcecategories = Categories( + listOf("app one", "app two", "app three", "app four"), listOf("game 1", "game 2"), true + ) + val openSourceResponse = Response.success(openSourcecategories) + val pwaCategories = + Categories(listOf("app one", "app two", "app three"), listOf("game 1", "game 2"), true) + val pwaResponse = Response.success(pwaCategories) + + Mockito.`when`( + cleanApkAppsRepository.getCategories() + ).thenReturn(openSourceResponse) + + Mockito.`when`( + cleanApkPWARepository.getCategories() + ).thenReturn(pwaResponse) + + Mockito.`when`( + gPlayAPIRepository.getCategories(CategoryType.APPLICATION) + ).thenReturn(gplayCategories) + + Mockito.`when`(context.getString(eq(R.string.open_source))).thenReturn("Open source") + Mockito.`when`(context.getString(eq(R.string.pwa))).thenReturn("pwa") + + preferenceManagerModule.isPWASelectedFake = true + preferenceManagerModule.isOpenSourceelectedFake = true + preferenceManagerModule.isGplaySelectedFake = true + + val categoryListResponse = + categoryApi.getCategoriesList(CategoryType.APPLICATION) + + Assert.assertEquals("getCategory", 11, categoryListResponse.first.size) + } +} \ No newline at end of file diff --git a/app/src/test/java/foundation/e/apps/fused/ApplicationApiImplTest.kt b/app/src/test/java/foundation/e/apps/fused/ApplicationApiImplTest.kt index 869e8851eb2a5ce24b5efd42e1fabaaa5c9fdca4..39092ecef5963622cd2f62a1b6ee9d040ffd12d6 100644 --- a/app/src/test/java/foundation/e/apps/fused/ApplicationApiImplTest.kt +++ b/app/src/test/java/foundation/e/apps/fused/ApplicationApiImplTest.kt @@ -501,119 +501,6 @@ class ApplicationApiImplTest { assertEquals("getAppFilterLevel", FilterLevel.UI, filterLevel) } - @Test - fun `getCategory when only pwa is selected`() = runTest { - val categories = - Categories(listOf("app one", "app two", "app three"), listOf("game 1", "game 2"), true) - val response = Response.success(categories) - preferenceManagerModule.isPWASelectedFake = true - preferenceManagerModule.isOpenSourceelectedFake = false - preferenceManagerModule.isGplaySelectedFake = false - - Mockito.`when`( - cleanApkPWARepository.getCategories() - ).thenReturn(response) - - Mockito.`when`(context.getString(eq(R.string.pwa))).thenReturn("PWA") - - val categoryListResponse = - fusedAPIImpl.getCategoriesList(CategoryType.APPLICATION) - - assertEquals("getCategory", 3, categoryListResponse.first.size) - } - - @Test - fun `getCategory when only open source is selected`() = runTest { - val categories = - Categories(listOf("app one", "app two", "app three"), listOf("game 1", "game 2"), true) - val response = Response.success(categories) - - preferenceManagerModule.isPWASelectedFake = false - preferenceManagerModule.isOpenSourceelectedFake = true - preferenceManagerModule.isGplaySelectedFake = false - - Mockito.`when`( - cleanApkAppsRepository.getCategories() - ).thenReturn(response) - Mockito.`when`(context.getString(eq(R.string.open_source))).thenReturn("Open source") - - val categoryListResponse = - fusedAPIImpl.getCategoriesList(CategoryType.APPLICATION) - - assertEquals("getCategory", 3, categoryListResponse.first.size) - } - - @Test - fun `getCategory when gplay source is selected`() = runTest { - val categories = listOf(Category(), Category(), Category(), Category()) - - preferenceManagerModule.isPWASelectedFake = false - preferenceManagerModule.isOpenSourceelectedFake = false - preferenceManagerModule.isGplaySelectedFake = true - - Mockito.`when`( - gPlayAPIRepository.getCategories(CategoryType.APPLICATION) - ).thenReturn(categories) - - val categoryListResponse = - fusedAPIImpl.getCategoriesList(CategoryType.APPLICATION) - - assertEquals("getCategory", 4, categoryListResponse.first.size) - } - - @Test - fun `getCategory when gplay source is selected return error`() = runTest { - preferenceManagerModule.isPWASelectedFake = false - preferenceManagerModule.isOpenSourceelectedFake = false - preferenceManagerModule.isGplaySelectedFake = true - - Mockito.`when`( - gPlayAPIRepository.getCategories(CategoryType.APPLICATION) - ).thenThrow() - - val categoryListResponse = - fusedAPIImpl.getCategoriesList(CategoryType.APPLICATION) - - assertEquals("getCategory", 0, categoryListResponse.first.size) - assertEquals("getCategory", ResultStatus.UNKNOWN, categoryListResponse.third) - } - - @Test - fun `getCategory when All source is selected`() = runTest { - val gplayCategories = listOf(Category(), Category(), Category(), Category()) - val openSourcecategories = Categories( - listOf("app one", "app two", "app three", "app four"), listOf("game 1", "game 2"), true - ) - val openSourceResponse = Response.success(openSourcecategories) - val pwaCategories = - Categories(listOf("app one", "app two", "app three"), listOf("game 1", "game 2"), true) - val pwaResponse = Response.success(pwaCategories) - - Mockito.`when`( - cleanApkAppsRepository.getCategories() - ).thenReturn(openSourceResponse) - - Mockito.`when`( - cleanApkPWARepository.getCategories() - ).thenReturn(pwaResponse) - - Mockito.`when`( - gPlayAPIRepository.getCategories(CategoryType.APPLICATION) - ).thenReturn(gplayCategories) - - Mockito.`when`(context.getString(eq(R.string.open_source))).thenReturn("Open source") - Mockito.`when`(context.getString(eq(R.string.pwa))).thenReturn("pwa") - - preferenceManagerModule.isPWASelectedFake = true - preferenceManagerModule.isOpenSourceelectedFake = true - preferenceManagerModule.isGplaySelectedFake = true - - val categoryListResponse = - fusedAPIImpl.getCategoriesList(CategoryType.APPLICATION) - - assertEquals("getCategory", 11, categoryListResponse.first.size) - } - @Ignore("Dependencies are not mockable") @Test fun `getSearchResult When all sources are selected`() = runTest { diff --git a/app/src/test/java/foundation/e/apps/fused/ApplicationApiRepositoryTest.kt b/app/src/test/java/foundation/e/apps/fused/ApplicationApiRepositoryTest.kt index 88e5ff2a97e632fd9626f5c96822699293763108..85fa1333bc5ed4292ca45156d8f733196c505306 100644 --- a/app/src/test/java/foundation/e/apps/fused/ApplicationApiRepositoryTest.kt +++ b/app/src/test/java/foundation/e/apps/fused/ApplicationApiRepositoryTest.kt @@ -19,6 +19,7 @@ package foundation.e.apps.fused import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.ApplicationApiImpl +import foundation.e.apps.data.application.CategoryApi import foundation.e.apps.data.application.HomeApi import org.junit.Assert.assertTrue import org.junit.Before @@ -34,11 +35,13 @@ class ApplicationApiRepositoryTest { private lateinit var fusedAPIImpl: ApplicationApiImpl @Mock private lateinit var homeApi: HomeApi + @Mock + private lateinit var categoryApi: CategoryApi @Before fun setup() { MockitoAnnotations.openMocks(this) - applicationRepository = ApplicationRepository(fusedAPIImpl, homeApi) + applicationRepository = ApplicationRepository(fusedAPIImpl, homeApi, categoryApi) } @Test