From dd340bb92c9fbb5f75da53501f82143bc532fa06 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Mon, 21 Aug 2023 12:20:21 +0600 Subject: [PATCH 1/7] fetch search result api is updated --- app/build.gradle | 4 +- .../e/apps/data/fused/FusedApiImpl.kt | 2 +- .../e/apps/data/gplay/GplayStoreRepository.kt | 3 +- .../data/gplay/GplayStoreRepositoryImpl.kt | 102 +++++++++++------- 4 files changed, 71 insertions(+), 40 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ce11c6234..3414490ee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,8 +90,8 @@ android { buildTypes { debug { - versionNameSuffix ".debug" - applicationIdSuffix ".debug" +// versionNameSuffix ".debug" +// applicationIdSuffix ".debug" signingConfig signingConfigs.debugConfig proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt index 510337d26..2b18e7bd8 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt @@ -1118,7 +1118,7 @@ class FusedApiImpl @Inject constructor( private suspend fun getGplaySearchResult( query: String, ): Flow, Boolean>> { - val searchResults = gplayRepository.getSearchResult(query) + val searchResults = gplayRepository.getSearchResult(query, null) return searchResults.map { val fusedAppList = it.first.map { app -> replaceWithFDroid(app) } Pair( diff --git a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepository.kt b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepository.kt index c1f3d3e1e..2c4df69fa 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepository.kt @@ -22,12 +22,13 @@ import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.File +import com.aurora.gplayapi.data.models.SearchBundle import foundation.e.apps.data.BaseStoreRepository import foundation.e.apps.data.fused.utils.CategoryType import kotlinx.coroutines.flow.Flow interface GplayStoreRepository : BaseStoreRepository { - suspend fun getSearchResult(query: String): Flow, Boolean>> + suspend fun getSearchResult(query: String, subBundle: MutableSet?): Flow, Boolean>> suspend fun getSearchSuggestions(query: String): List suspend fun getAppsByCategory(category: String, pageUrl: String? = null): Any suspend fun getCategories(type: CategoryType? = null): List diff --git a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt index 23d4e8427..fdd1a5e7d 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt @@ -44,6 +44,7 @@ import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext +import timber.log.Timber import javax.inject.Inject class GplayStoreRepositoryImpl @Inject constructor( @@ -76,52 +77,81 @@ class GplayStoreRepositoryImpl @Inject constructor( context.getString(R.string.movers_shakers_games) to mapOf(Chart.MOVERS_SHAKERS to TopChartsHelper.Type.GAME), ) +// override suspend fun getSearchResult( +// query: String, +// ): Flow, Boolean>> { +// return flow { +// +// /* +// * Variable names and logic made same as that of Aurora store. +// * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 +// */ +// var authData = loginSourceRepository.gplayAuth ?: return@flow +// +// val searchHelper = +// SearchHelper(authData).using(gPlayHttpClient) +// val searchBundle = searchHelper.searchResults(query) +// +// val initialReplacedList = mutableListOf() +// val INITIAL_LIMIT = 4 +// +// emitReplacedList( +// this@flow, +// initialReplacedList, +// INITIAL_LIMIT, +// searchBundle, +// true, +// ) +// +// var nextSubBundleSet: MutableSet +// do { +// nextSubBundleSet = fetchNextSubBundle( +// searchBundle, +// searchHelper, +// this@flow, +// initialReplacedList, +// INITIAL_LIMIT +// ) +// } while (nextSubBundleSet.isNotEmpty()) +// +// /* +// * If initialReplacedList size is less than INITIAL_LIMIT, +// * it means the results were very less and nothing has been emitted so far. +// * Hence emit the list. +// */ +// if (initialReplacedList.size < INITIAL_LIMIT) { +// emitInMain(this@flow, initialReplacedList, false) +// } +// }.flowOn(Dispatchers.IO) +// } + override suspend fun getSearchResult( query: String, + subBundle: MutableSet? ): Flow, Boolean>> { return flow { - - /* - * Variable names and logic made same as that of Aurora store. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 - */ var authData = loginSourceRepository.gplayAuth ?: return@flow - val searchHelper = SearchHelper(authData).using(gPlayHttpClient) - val searchBundle = searchHelper.searchResults(query) + Timber.d("Fetching search result for $query, subBundle: $subBundle") - val initialReplacedList = mutableListOf() - val INITIAL_LIMIT = 4 + subBundle?.let { + val searchResult = searchHelper.next(it) + emitSearchResult(searchResult) + return@let + } - emitReplacedList( - this@flow, - initialReplacedList, - INITIAL_LIMIT, - searchBundle, - true, - ) + val searchResult = searchHelper.searchResults(query) + emitSearchResult(searchResult) + } + } - var nextSubBundleSet: MutableSet - do { - nextSubBundleSet = fetchNextSubBundle( - searchBundle, - searchHelper, - this@flow, - initialReplacedList, - INITIAL_LIMIT - ) - } while (nextSubBundleSet.isNotEmpty()) - - /* - * If initialReplacedList size is less than INITIAL_LIMIT, - * it means the results were very less and nothing has been emitted so far. - * Hence emit the list. - */ - if (initialReplacedList.size < INITIAL_LIMIT) { - emitInMain(this@flow, initialReplacedList, false) - } - }.flowOn(Dispatchers.IO) + private suspend fun FlowCollector, Boolean>>.emitSearchResult( + searchBundle: SearchBundle + ) { + val apps = searchBundle.appList + Timber.d("Search result is found: ${apps.size}") + emit(Pair(apps, searchBundle.subBundles.isNotEmpty())) } private suspend fun fetchNextSubBundle( -- GitLab From 9e4fc575ec80ce9dc3e1ffa182a971eb8459ecdd Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Mon, 21 Aug 2023 22:39:29 +0600 Subject: [PATCH 2/7] gpaly search result with pagination --- .../e/apps/data/fused/FusedAPIRepository.kt | 8 ++ .../foundation/e/apps/data/fused/FusedApi.kt | 6 + .../e/apps/data/fused/FusedApiImpl.kt | 103 +++++++++--------- .../e/apps/data/gplay/GplayStoreRepository.kt | 2 +- .../data/gplay/GplayStoreRepositoryImpl.kt | 19 ++-- .../e/apps/ui/search/SearchFragment.kt | 9 ++ .../e/apps/ui/search/SearchViewModel.kt | 47 +++++++- 7 files changed, 129 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt index 0aa269ab6..b12d5def8 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt @@ -21,6 +21,7 @@ package foundation.e.apps.data.fused import androidx.lifecycle.LiveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.SearchBundle import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.FilterLevel import foundation.e.apps.data.enums.Origin @@ -114,6 +115,13 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedApi) return fusedAPIImpl.getSearchResults(query, authData) } + suspend fun getGplaySearchResults( + query: String, + nextPageSubBundle: Set? + ): Pair, Set> { + return fusedAPIImpl.getGplaySearchResult(query, nextPageSubBundle) + } + suspend fun getAppsListBasedOnCategory( authData: AuthData, category: String, diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt index 2c0296373..bd1ffe80a 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.SearchBundle import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.cleanapk.data.download.Download import foundation.e.apps.data.enums.FilterLevel @@ -64,6 +65,11 @@ interface FusedApi { authData: AuthData ): LiveData, Boolean>>> + suspend fun getGplaySearchResult( + query: String, + nextPageSubBundle: Set? + ): Pair, Set> + suspend fun getSearchSuggestions(query: String): List suspend fun getOnDemandModule( diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt index 2b18e7bd8..46973b5f6 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt @@ -21,6 +21,7 @@ package foundation.e.apps.data.fused import android.content.Context import android.text.format.Formatter import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.map import com.aurora.gplayapi.Constants @@ -29,6 +30,7 @@ import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.Artwork import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category +import com.aurora.gplayapi.data.models.SearchBundle import com.aurora.gplayapi.data.models.StreamCluster import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R @@ -80,7 +82,7 @@ import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton -typealias GplaySearchResultLiveData = LiveData, Boolean>>> +typealias GplaySearchResultFlow = Flow, Boolean>>> typealias FusedHomeDeferred = Deferred>> @Singleton @@ -285,15 +287,15 @@ class FusedApiImpl @Inject constructor( ).let { emit(it) } } - if (preferenceManagerModule.isGplaySelected()) { - emitSource( - fetchGplaySearchResults( - query, - searchResult, - packageSpecificResults - ) - ) - } +// if (preferenceManagerModule.isGplaySelected()) { +// emitSource( +// fetchGplaySearchResults( +// query, +// searchResult, +// packageSpecificResults +// ).asLiveData() +// ) +// } } } @@ -331,37 +333,25 @@ class FusedApiImpl @Inject constructor( ) } - private suspend fun fetchGplaySearchResults( - query: String, - searchResult: MutableList, - packageSpecificResults: ArrayList - ): GplaySearchResultLiveData { - return runFlowWithTimeout( - { - getGplaySearchResult(query) - }, { - it.second - }, { - Pair(listOf(), false) // empty data for timeout - } - ).map { - if (it.isSuccess()) { - searchResult.addAll(it.data!!.first) - ResultSupreme.Success( - Pair( - filterWithKeywordSearch( - searchResult, - packageSpecificResults, - query - ), - it.data!!.second - ) - ) - } else { - it - } - } - } +// private suspend fun fetchGplaySearchResults( +// query: String, +// searchResult: MutableList, +// packageSpecificResults: ArrayList +// ): GplaySearchResultFlow = getGplaySearchResult(query).map { +// if (it.first.isNotEmpty()) { +// searchResult.addAll(it.first) +// } +// ResultSupreme.Success( +// Pair( +// filterWithKeywordSearch( +// searchResult, +// packageSpecificResults, +// query +// ), +// it.second +// ) +// ) +// } private suspend fun fetchOpenSourceSearchResult( fusedAPIImpl: FusedApiImpl, @@ -994,7 +984,7 @@ class FusedApiImpl @Inject constructor( private fun getCategoryIconName(category: FusedCategory): String { var categoryTitle = if (category.tag.getOperationalTag() - .contentEquals(AppTag.GPlay().getOperationalTag()) + .contentEquals(AppTag.GPlay().getOperationalTag()) ) category.id else category.title if (categoryTitle.contains(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION)) { @@ -1115,17 +1105,25 @@ class FusedApiImpl @Inject constructor( return list } - private suspend fun getGplaySearchResult( + override suspend fun getGplaySearchResult( query: String, - ): Flow, Boolean>> { - val searchResults = gplayRepository.getSearchResult(query, null) - return searchResults.map { - val fusedAppList = it.first.map { app -> replaceWithFDroid(app) } - Pair( - fusedAppList, - it.second - ) + nextPageSubBundle: Set? + ): Pair, Set> { + val searchResults = + gplayRepository.getSearchResult(query, nextPageSubBundle?.toMutableSet()) + if (!preferenceManagerModule.isGplaySelected()) { + return Pair(emptyList(), emptySet()) + } + + val fusedAppList = searchResults.first.map { app -> replaceWithFDroid(app) }.toMutableList() + if (searchResults.second.isNotEmpty()) { + fusedAppList.add(FusedApp(isPlaceHolder = true)) } + + return Pair( + fusedAppList, + searchResults.second + ) } /* @@ -1431,7 +1429,8 @@ class FusedApiImpl @Inject constructor( var nextPageUrl = "" val status = runCodeWithTimeout({ - val streamCluster = gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster + val streamCluster = + gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster val filteredAppList = filterRestrictedGPlayApps(authData, streamCluster.clusterAppList) filteredAppList.data?.let { fusedAppList = it.toMutableList() diff --git a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepository.kt b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepository.kt index 2c4df69fa..90c1e5b6a 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepository.kt @@ -28,7 +28,7 @@ import foundation.e.apps.data.fused.utils.CategoryType import kotlinx.coroutines.flow.Flow interface GplayStoreRepository : BaseStoreRepository { - suspend fun getSearchResult(query: String, subBundle: MutableSet?): Flow, Boolean>> + suspend fun getSearchResult(query: String, subBundle: MutableSet?): Pair, MutableSet> suspend fun getSearchSuggestions(query: String): List suspend fun getAppsByCategory(category: String, pageUrl: String? = null): Any suspend fun getCategories(type: CategoryType? = null): List diff --git a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt index fdd1a5e7d..ac19e6588 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt @@ -128,30 +128,29 @@ class GplayStoreRepositoryImpl @Inject constructor( override suspend fun getSearchResult( query: String, subBundle: MutableSet? - ): Flow, Boolean>> { - return flow { - var authData = loginSourceRepository.gplayAuth ?: return@flow + ): Pair, MutableSet> { + var authData = loginSourceRepository.gplayAuth ?: return Pair(emptyList(), mutableSetOf()) val searchHelper = SearchHelper(authData).using(gPlayHttpClient) Timber.d("Fetching search result for $query, subBundle: $subBundle") subBundle?.let { val searchResult = searchHelper.next(it) - emitSearchResult(searchResult) - return@let + Timber.d("fetching next page search data...") + return emitSearchResult(searchResult) } val searchResult = searchHelper.searchResults(query) - emitSearchResult(searchResult) - } + return emitSearchResult(searchResult) + } - private suspend fun FlowCollector, Boolean>>.emitSearchResult( + private fun emitSearchResult( searchBundle: SearchBundle - ) { + ): Pair, MutableSet> { val apps = searchBundle.appList Timber.d("Search result is found: ${apps.size}") - emit(Pair(apps, searchBundle.subBundles.isNotEmpty())) + return Pair(apps, searchBundle.subBundles) } private suspend fun fetchNextSubBundle( diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index 983577b5c..5d69a446e 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -132,6 +132,15 @@ class SearchFragment : searchViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { handleExceptionsCommon(it) } + + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (!recyclerView.canScrollVertically(1)) { + searchViewModel.loadMore(searchText) + } + } + }) } private fun shouldIgnore( diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index fc9d6022f..6ddfbe192 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.SearchBundle import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.fused.FusedAPIRepository @@ -34,6 +35,8 @@ import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -42,13 +45,17 @@ class SearchViewModel @Inject constructor( ) : LoadingViewModel() { val searchSuggest: MutableLiveData?> = MutableLiveData() + val searchResult: MutableLiveData, Boolean>>> = MutableLiveData() private var searchResultLiveData: LiveData, Boolean>>> = MutableLiveData() - private var lastAuthObjects: List? = null + private var nextSubBundle: Set? = null + + private var isLoading: Boolean = false + fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { viewModelScope.launch(Dispatchers.IO) { if (gPlayAuth.result.isSuccess()) @@ -95,10 +102,11 @@ class SearchViewModel @Inject constructor( viewModelScope.launch(Dispatchers.Main) { searchResultLiveData.removeObservers(lifecycleOwner) searchResultLiveData = fusedAPIRepository.getSearchResults(query, authData) + searchResultLiveData.observe(lifecycleOwner) { searchResult.postValue(it) - if (!it.isSuccess()) { + if (!it.isSuccess()) { val exception = if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) { GPlayException( @@ -116,9 +124,44 @@ class SearchViewModel @Inject constructor( exceptionsLiveData.postValue(exceptionsList) } } + + withContext(Dispatchers.IO) { + nextSubBundle = null + fetchGplayData(query) + } + } } + fun loadMore(query: String) { + if (isLoading) { + Timber.d("Serach result is loading....") + return + } + viewModelScope.launch(Dispatchers.IO) { + fetchGplayData(query) + } + } + + private suspend fun fetchGplayData(query: String) { + isLoading = true + val gplaySearchResult = fusedAPIRepository.getGplaySearchResults(query, nextSubBundle) + nextSubBundle = gplaySearchResult.second + val searchResult = searchResult.value + val currentAppList = searchResult?.data?.first?.toMutableList() ?: mutableListOf() + currentAppList.removeIf { item -> item.isPlaceHolder } + currentAppList.plus(gplaySearchResult.first) + + val finalResult = if (searchResult is ResultSupreme.Success) { + ResultSupreme.Success(Pair(currentAppList.toList(), gplaySearchResult.second.isNotEmpty())) + } else { + ResultSupreme.Error() + } + + this@SearchViewModel.searchResult.postValue(finalResult) + isLoading = false + } + /** * @return returns true if there is changes in data, otherwise false */ -- GitLab From cf19a70e4e2c856526c48e4ea2aae28385798880 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Tue, 22 Aug 2023 00:04:02 +0600 Subject: [PATCH 3/7] fixed: placeholder item --- .../e/apps/data/fused/FusedAPIRepository.kt | 4 +- .../foundation/e/apps/data/fused/FusedApi.kt | 4 +- .../e/apps/data/fused/FusedApiImpl.kt | 1935 ++++++++--------- .../ApplicationListRVAdapter.kt | 6 + .../e/apps/ui/search/SearchViewModel.kt | 63 +- 5 files changed, 1006 insertions(+), 1006 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt index b12d5def8..b6fd95229 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt @@ -108,10 +108,10 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedApi) return fusedAPIImpl.getSearchSuggestions(query) } - fun getSearchResults( + suspend fun getSearchResults( query: String, authData: AuthData - ): LiveData, Boolean>>> { + ): ResultSupreme, Boolean>> { return fusedAPIImpl.getSearchResults(query, authData) } diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt index bd1ffe80a..4492f2f8d 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt @@ -60,10 +60,10 @@ interface FusedApi { * a Boolean signifying if more search results are being loaded. * Observe this livedata to display new apps as they are fetched from the network. */ - fun getSearchResults( + suspend fun getSearchResults( query: String, authData: AuthData - ): LiveData, Boolean>>> + ): ResultSupreme, Boolean>> suspend fun getGplaySearchResult( query: String, diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt index 46973b5f6..0bb3a2616 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt @@ -246,46 +246,43 @@ class FusedApiImpl @Inject constructor( * a Boolean signifying if more search results are being loaded. * Observe this livedata to display new apps as they are fetched from the network. */ - override fun getSearchResults( + override suspend fun getSearchResults( query: String, authData: AuthData - ): LiveData, Boolean>>> { + ): ResultSupreme, Boolean>> { /* * Returning livedata to improve performance, so that we do not have to wait forever * for all results to be fetched from network before showing them. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ - return liveData { - val packageSpecificResults = ArrayList() + val packageSpecificResults = ArrayList() + var finalSearchResult: ResultSupreme, Boolean>> = ResultSupreme.Error() - fetchPackageSpecificResult(authData, query, packageSpecificResults).let { - if (it.data?.second != true) { // if there are no data to load - emit(it) - return@liveData - } + fetchPackageSpecificResult(authData, query, packageSpecificResults).let { + if (it.data?.second != true) { // if there are no data to load + return it } + } - val searchResult = mutableListOf() - val cleanApkResults = mutableListOf() - - if (preferenceManagerModule.isOpenSourceSelected()) { - fetchOpenSourceSearchResult( - this@FusedApiImpl, - cleanApkResults, - query, - searchResult, - packageSpecificResults - ).let { emit(it) } - } + val searchResult = mutableListOf() + val cleanApkResults = mutableListOf() - if (preferenceManagerModule.isPWASelected()) { - fetchPWASearchResult( - this@FusedApiImpl, - query, - searchResult, - packageSpecificResults - ).let { emit(it) } - } + if (preferenceManagerModule.isOpenSourceSelected()) { + finalSearchResult = fetchOpenSourceSearchResult( + cleanApkResults, + query, + searchResult, + packageSpecificResults + ) + } + + if (preferenceManagerModule.isPWASelected()) { + finalSearchResult = fetchPWASearchResult( + query, + searchResult, + packageSpecificResults + ) + } // if (preferenceManagerModule.isGplaySelected()) { // emitSource( @@ -296,43 +293,42 @@ class FusedApiImpl @Inject constructor( // ).asLiveData() // ) // } - } - } + return finalSearchResult +} - private suspend fun fetchPWASearchResult( - fusedAPIImpl: FusedApiImpl, - query: String, - searchResult: MutableList, - packageSpecificResults: ArrayList - ): ResultSupreme, Boolean>> { - val pwaApps: MutableList = mutableListOf() - val status = fusedAPIImpl.runCodeWithTimeout({ - val apps = - cleanApkPWARepository.getSearchResult(query).body()?.apps - apps?.apply { - if (this.isNotEmpty()) { - pwaApps.addAll(this) - } +private suspend fun fetchPWASearchResult( + query: String, + searchResult: MutableList, + packageSpecificResults: ArrayList +): ResultSupreme, Boolean>> { + val pwaApps: MutableList = mutableListOf() + val status = runCodeWithTimeout({ + val apps = + cleanApkPWARepository.getSearchResult(query).body()?.apps + apps?.apply { + if (this.isNotEmpty()) { + pwaApps.addAll(this) } - }) - - if (pwaApps.isNotEmpty() || status != ResultStatus.OK) { - searchResult.addAll(pwaApps) } + }) - return ResultSupreme.create( - status, - Pair( - filterWithKeywordSearch( - searchResult, - packageSpecificResults, - query - ), - preferenceManagerModule.isGplaySelected() - ) - ) + if (pwaApps.isNotEmpty() || status != ResultStatus.OK) { + searchResult.addAll(pwaApps) } + return ResultSupreme.create( + status, + Pair( + filterWithKeywordSearch( + searchResult, + packageSpecificResults, + query + ), + preferenceManagerModule.isGplaySelected() + ) + ) +} + // private suspend fun fetchGplaySearchResults( // query: String, // searchResult: MutableList, @@ -353,1095 +349,1094 @@ class FusedApiImpl @Inject constructor( // ) // } - private suspend fun fetchOpenSourceSearchResult( - fusedAPIImpl: FusedApiImpl, - cleanApkResults: MutableList, - query: String, - searchResult: MutableList, - packageSpecificResults: ArrayList - ): ResultSupreme, Boolean>> { - val status = fusedAPIImpl.runCodeWithTimeout({ - cleanApkResults.addAll(getCleanAPKSearchResults(query)) - }) - - if (cleanApkResults.isNotEmpty()) { - searchResult.addAll(cleanApkResults) - } - - return ResultSupreme.create( - status, - Pair( - filterWithKeywordSearch( - searchResult, - packageSpecificResults, - query - ), - preferenceManagerModule.isGplaySelected() || preferenceManagerModule.isPWASelected() - ) - ) +private suspend fun fetchOpenSourceSearchResult( + cleanApkResults: MutableList, + query: String, + searchResult: MutableList, + packageSpecificResults: ArrayList +): ResultSupreme, Boolean>> { + val status = runCodeWithTimeout({ + cleanApkResults.addAll(getCleanAPKSearchResults(query)) + }) + + if (cleanApkResults.isNotEmpty()) { + searchResult.addAll(cleanApkResults) } - private suspend fun fetchPackageSpecificResult( - authData: AuthData, - query: String, - packageSpecificResults: MutableList - ): ResultSupreme, Boolean>> { - var gplayPackageResult: FusedApp? = null - var cleanapkPackageResult: FusedApp? = null + return ResultSupreme.create( + status, + Pair( + filterWithKeywordSearch( + searchResult, + packageSpecificResults, + query + ), + preferenceManagerModule.isGplaySelected() || preferenceManagerModule.isPWASelected() + ) + ) +} - val status = runCodeWithTimeout({ - if (preferenceManagerModule.isGplaySelected()) { - gplayPackageResult = getGplayPackagResult(query, authData) - } +private suspend fun fetchPackageSpecificResult( + authData: AuthData, + query: String, + packageSpecificResults: MutableList +): ResultSupreme, Boolean>> { + var gplayPackageResult: FusedApp? = null + var cleanapkPackageResult: FusedApp? = null - if (preferenceManagerModule.isOpenSourceSelected()) { - cleanapkPackageResult = getCleanApkPackageResult(query) - } - }) - - /* - * Currently only show open source package result if exists in both fdroid and gplay. - * This is temporary. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5783 - */ - cleanapkPackageResult?.let { packageSpecificResults.add(it) } ?: run { - gplayPackageResult?.let { packageSpecificResults.add(it) } + val status = runCodeWithTimeout({ + if (preferenceManagerModule.isGplaySelected()) { + gplayPackageResult = getGplayPackagResult(query, authData) } - /* - * If there was a timeout, return it and don't try to fetch anything else. - * Also send true in the pair to signal more results being loaded. - */ - if (status != ResultStatus.OK) { - return ResultSupreme.create(status, Pair(packageSpecificResults, false)) + if (preferenceManagerModule.isOpenSourceSelected()) { + cleanapkPackageResult = getCleanApkPackageResult(query) } - return ResultSupreme.create(status, Pair(packageSpecificResults, true)) - } + }) /* - * The list packageSpecificResults may contain apps with duplicate package names. - * Example, "org.telegram.messenger" will result in "Telegram" app from Play Store - * and "Telegram FOSS" from F-droid. We show both of them at the top. - * - * But for the other keyword related search results, we do not allow duplicate package names. - * We also filter out apps which are already present in packageSpecificResults list. - */ - private fun filterWithKeywordSearch( - list: List, - packageSpecificResults: List, - query: String - ): List { - val filteredResults = list.distinctBy { it.package_name } - .filter { packageSpecificResults.isEmpty() || it.package_name != query } - return packageSpecificResults + filteredResults - } - - private suspend fun getCleanApkPackageResult( - query: String, - ): FusedApp? { - getCleanapkSearchResult(query).let { - if (it.isSuccess() && it.data!!.package_name.isNotBlank()) { - return it.data!! - } - } - return null - } - - private suspend fun getGplayPackagResult( - query: String, - authData: AuthData, - ): FusedApp? { - try { - getApplicationDetails(query, query, authData, Origin.GPLAY).let { - if (it.second == ResultStatus.OK) { - return it.first - } - } - } catch (e: Exception) { - Timber.e(e) - } - return null + * Currently only show open source package result if exists in both fdroid and gplay. + * This is temporary. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5783 + */ + cleanapkPackageResult?.let { packageSpecificResults.add(it) } ?: run { + gplayPackageResult?.let { packageSpecificResults.add(it) } } /* - * Method to search cleanapk based on package name. - * This is to be only used for showing an entry in search results list. - * DO NOT use this to show info on ApplicationFragment as it will not have all the required - * information to show for an app. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/2629 + * If there was a timeout, return it and don't try to fetch anything else. + * Also send true in the pair to signal more results being loaded. */ - private suspend fun getCleanapkSearchResult(packageName: String): ResultSupreme { - var fusedApp = FusedApp() - val status = runCodeWithTimeout({ - val result = cleanApkAppsRepository.getSearchResult( - packageName, - "package_name" - ).body() - - if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { - fusedApp = result.apps[0] - } - }) - return ResultSupreme.create(status, fusedApp) + if (status != ResultStatus.OK) { + return ResultSupreme.create(status, Pair(packageSpecificResults, false)) } + return ResultSupreme.create(status, Pair(packageSpecificResults, true)) +} - override suspend fun getSearchSuggestions(query: String): List { - var searchSuggesions = listOf() - runCodeWithTimeout({ - searchSuggesions = gplayRepository.getSearchSuggestions(query) - }) +/* + * The list packageSpecificResults may contain apps with duplicate package names. + * Example, "org.telegram.messenger" will result in "Telegram" app from Play Store + * and "Telegram FOSS" from F-droid. We show both of them at the top. + * + * But for the other keyword related search results, we do not allow duplicate package names. + * We also filter out apps which are already present in packageSpecificResults list. + */ +private fun filterWithKeywordSearch( + list: List, + packageSpecificResults: List, + query: String +): List { + val filteredResults = list.distinctBy { it.package_name } + .filter { packageSpecificResults.isEmpty() || it.package_name != query } + return packageSpecificResults + filteredResults +} - return searchSuggesions +private suspend fun getCleanApkPackageResult( + query: String, +): FusedApp? { + getCleanapkSearchResult(query).let { + if (it.isSuccess() && it.data!!.package_name.isNotBlank()) { + return it.data!! + } } + return null +} - override suspend fun getOnDemandModule( - packageName: String, - moduleName: String, - versionCode: Int, - offerType: Int - ): String? { - val list = gplayRepository.getOnDemandModule( - packageName, - moduleName, - versionCode, - offerType, - ) - for (element in list) { - if (element.name == "$moduleName.apk") { - return element.url +private suspend fun getGplayPackagResult( + query: String, + authData: AuthData, +): FusedApp? { + try { + getApplicationDetails(query, query, authData, Origin.GPLAY).let { + if (it.second == ResultStatus.OK) { + return it.first } } - return null + } catch (e: Exception) { + Timber.e(e) } + return null +} - override suspend fun updateFusedDownloadWithDownloadingInfo( - origin: Origin, - fusedDownload: FusedDownload - ) { - val list = mutableListOf() - when (origin) { - Origin.CLEANAPK -> { - val downloadInfo = - (cleanApkAppsRepository as CleanApkDownloadInfoFetcher).getDownloadInfo( - fusedDownload.id - ) - .body() - downloadInfo?.download_data?.download_link?.let { list.add(it) } - fusedDownload.signature = downloadInfo?.download_data?.signature ?: "" - } +/* + * Method to search cleanapk based on package name. + * This is to be only used for showing an entry in search results list. + * DO NOT use this to show info on ApplicationFragment as it will not have all the required + * information to show for an app. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/2629 + */ +private suspend fun getCleanapkSearchResult(packageName: String): ResultSupreme { + var fusedApp = FusedApp() + val status = runCodeWithTimeout({ + val result = cleanApkAppsRepository.getSearchResult( + packageName, + "package_name" + ).body() - Origin.GPLAY -> { - val downloadList = - gplayRepository.getDownloadInfo( - fusedDownload.packageName, - fusedDownload.versionCode, - fusedDownload.offerType - ) - fusedDownload.files = downloadList - list.addAll(downloadList.map { it.url }) - } + if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { + fusedApp = result.apps[0] } - fusedDownload.downloadURLList = list - } + }) + return ResultSupreme.create(status, fusedApp) +} - 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 status = runCodeWithTimeout({ - val response = getPWAAppsResponse(category) - response?.apps?.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - list.add(it) - } - }) - return ResultSupreme.create(status, Pair(list, "")) - } +override suspend fun getSearchSuggestions(query: String): List { + var searchSuggesions = listOf() + runCodeWithTimeout({ + searchSuggesions = gplayRepository.getSearchSuggestions(query) + }) - override suspend fun getOpenSourceApps(category: String): ResultSupreme, String>> { - val list = mutableListOf() - val status = runCodeWithTimeout({ - val response = getOpenSourceAppsResponse(category) - response?.apps?.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - list.add(it) - } - }) - return ResultSupreme.create(status, Pair(list, "")) + return searchSuggesions +} + +override suspend fun getOnDemandModule( + packageName: String, + moduleName: String, + versionCode: Int, + offerType: Int +): String? { + val list = gplayRepository.getOnDemandModule( + packageName, + moduleName, + versionCode, + offerType, + ) + for (element in list) { + if (element.name == "$moduleName.apk") { + return element.url + } } + return null +} - /* - * Function to search cleanapk using package name. - * Will be used to handle f-droid deeplink. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509 - */ - override suspend fun getCleanapkAppDetails(packageName: String): Pair { - var fusedApp = FusedApp() - val status = runCodeWithTimeout({ - val result = cleanApkAppsRepository.getSearchResult( - packageName, - "package_name" - ).body() +override suspend fun updateFusedDownloadWithDownloadingInfo( + origin: Origin, + fusedDownload: FusedDownload +) { + val list = mutableListOf() + when (origin) { + Origin.CLEANAPK -> { + val downloadInfo = + (cleanApkAppsRepository as CleanApkDownloadInfoFetcher).getDownloadInfo( + fusedDownload.id + ) + .body() + downloadInfo?.download_data?.download_link?.let { list.add(it) } + fusedDownload.signature = downloadInfo?.download_data?.signature ?: "" + } - if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { - fusedApp = - (cleanApkAppsRepository.getAppDetails(result.apps[0]._id) as Response).body()?.app - ?: FusedApp() - } - fusedApp.updateFilterLevel(null) - }) - return Pair(fusedApp, status) + Origin.GPLAY -> { + val downloadList = + gplayRepository.getDownloadInfo( + fusedDownload.packageName, + fusedDownload.versionCode, + fusedDownload.offerType + ) + fusedDownload.files = downloadList + list.addAll(downloadList.map { it.url }) + } } + fusedDownload.downloadURLList = list +} - override suspend fun getApplicationDetails( - packageNameList: List, - authData: AuthData, - origin: Origin - ): Pair, ResultStatus> { - val list = mutableListOf() - - val response: Pair, ResultStatus> = - if (origin == Origin.CLEANAPK) { - getAppDetailsListFromCleanapk(packageNameList) - } else { - getAppDetailsListFromGPlay(packageNameList, authData) - } +override suspend fun getOSSDownloadInfo(id: String, version: String?) = + (cleanApkAppsRepository as CleanApkDownloadInfoFetcher).getDownloadInfo(id, version) - response.first.forEach { - if (it.package_name.isNotBlank()) { - it.updateStatus() - it.updateType() - list.add(it) - } +override suspend fun getPWAApps(category: String): ResultSupreme, String>> { + val list = mutableListOf() + val status = runCodeWithTimeout({ + val response = getPWAAppsResponse(category) + response?.apps?.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) + list.add(it) } + }) + return ResultSupreme.create(status, Pair(list, "")) +} - return Pair(list, response.second) - } +override suspend fun getOpenSourceApps(category: String): ResultSupreme, String>> { + val list = mutableListOf() + val status = runCodeWithTimeout({ + val response = getOpenSourceAppsResponse(category) + response?.apps?.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) + list.add(it) + } + }) + return ResultSupreme.create(status, Pair(list, "")) +} - /* - * Get app details of a list of apps from cleanapk. - * Returns list of FusedApp and ResultStatus - which will reflect timeout if even one app fails. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 - */ - private suspend fun getAppDetailsListFromCleanapk( - packageNameList: List, - ): Pair, ResultStatus> { - var status = ResultStatus.OK - val fusedAppList = mutableListOf() +/* + * Function to search cleanapk using package name. + * Will be used to handle f-droid deeplink. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509 + */ +override suspend fun getCleanapkAppDetails(packageName: String): Pair { + var fusedApp = FusedApp() + val status = runCodeWithTimeout({ + val result = cleanApkAppsRepository.getSearchResult( + packageName, + "package_name" + ).body() - /* - * Fetch result of each cleanapk search with separate timeout, - * i.e. check timeout for individual package query. - */ - for (packageName in packageNameList) { - status = runCodeWithTimeout({ - cleanApkAppsRepository.getSearchResult( - packageName, - "package_name" - ).body()?.run { - if (apps.isNotEmpty() && numberOfResults == 1) { - fusedAppList.add( - apps[0].apply { - updateFilterLevel(null) - } - ) - } - } - }) + if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { + fusedApp = + (cleanApkAppsRepository.getAppDetails(result.apps[0]._id) as Response).body()?.app + ?: FusedApp() + } + fusedApp.updateFilterLevel(null) + }) + return Pair(fusedApp, status) +} - /* - * If status is not ok, immediately return. - */ - if (status != ResultStatus.OK) { - return Pair(fusedAppList, status) - } +override suspend fun getApplicationDetails( + packageNameList: List, + authData: AuthData, + origin: Origin +): Pair, ResultStatus> { + val list = mutableListOf() + + val response: Pair, ResultStatus> = + if (origin == Origin.CLEANAPK) { + getAppDetailsListFromCleanapk(packageNameList) + } else { + getAppDetailsListFromGPlay(packageNameList, authData) } - return Pair(fusedAppList, status) + response.first.forEach { + if (it.package_name.isNotBlank()) { + it.updateStatus() + it.updateType() + list.add(it) + } } + return Pair(list, response.second) +} + +/* + * Get app details of a list of apps from cleanapk. + * Returns list of FusedApp and ResultStatus - which will reflect timeout if even one app fails. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 + */ +private suspend fun getAppDetailsListFromCleanapk( + packageNameList: List, +): Pair, ResultStatus> { + var status = ResultStatus.OK + val fusedAppList = mutableListOf() + /* - * Get app details of a list of apps from Google Play store. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 + * Fetch result of each cleanapk search with separate timeout, + * i.e. check timeout for individual package query. */ - private suspend fun getAppDetailsListFromGPlay( - packageNameList: List, - authData: AuthData, - ): Pair, ResultStatus> { - val fusedAppList = mutableListOf() - - /* - * Old code moved from getApplicationDetails() - */ - val status = runCodeWithTimeout({ - gplayRepository.getAppsDetails(packageNameList).forEach { app -> - /* - * Some apps are restricted to locations. Example "com.skype.m2". - * For restricted apps, check if it is possible to get their specific app info. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5174 - */ - val filter = getAppFilterLevel(app, authData) - if (filter.isUnFiltered()) { + for (packageName in packageNameList) { + status = runCodeWithTimeout({ + cleanApkAppsRepository.getSearchResult( + packageName, + "package_name" + ).body()?.run { + if (apps.isNotEmpty() && numberOfResults == 1) { fusedAppList.add( - app.transformToFusedApp().apply { - filterLevel = filter + apps[0].apply { + updateFilterLevel(null) } ) } } }) - return Pair(fusedAppList, status) + /* + * If status is not ok, immediately return. + */ + if (status != ResultStatus.OK) { + return Pair(fusedAppList, status) + } } - /** - * 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] - */ - override suspend fun filterRestrictedGPlayApps( - authData: AuthData, - appList: List, - ): ResultSupreme> { - val filteredFusedApps = mutableListOf() - val status = runCodeWithTimeout({ - appList.forEach { - val filter = getAppFilterLevel(it, authData) - if (filter.isUnFiltered()) { - filteredFusedApps.add( - it.transformToFusedApp().apply { - this.filterLevel = filter - } - ) - } - } - }) + return Pair(fusedAppList, status) +} - return ResultSupreme.create(status, filteredFusedApps) - } +/* + * Get app details of a list of apps from Google Play store. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 + */ +private suspend fun getAppDetailsListFromGPlay( + packageNameList: List, + authData: AuthData, +): Pair, ResultStatus> { + val fusedAppList = mutableListOf() - /** - * Get different filter levels. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5720 + /* + * Old code moved from getApplicationDetails() */ - override suspend fun getAppFilterLevel(fusedApp: FusedApp, authData: AuthData?): FilterLevel { - if (fusedApp.package_name.isBlank()) { - return FilterLevel.UNKNOWN - } - if (fusedApp.origin == Origin.CLEANAPK) { + val status = runCodeWithTimeout({ + gplayRepository.getAppsDetails(packageNameList).forEach { app -> /* - * Whitelist all open source apps. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5785 + * Some apps are restricted to locations. Example "com.skype.m2". + * For restricted apps, check if it is possible to get their specific app info. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5174 */ - return FilterLevel.NONE - } - if (authData == null) { - return if (fusedApp.origin == Origin.GPLAY) FilterLevel.UNKNOWN - else FilterLevel.NONE - } - - if (!fusedApp.isFree && fusedApp.price.isBlank()) { - return FilterLevel.UI + val filter = getAppFilterLevel(app, authData) + if (filter.isUnFiltered()) { + fusedAppList.add( + app.transformToFusedApp().apply { + filterLevel = filter + } + ) + } } + }) - if (fusedApp.restriction != Constants.Restriction.NOT_RESTRICTED) { - /* - * Check if app details can be shown. If not then remove the app from lists. - */ - try { - gplayRepository.getAppDetails(fusedApp.package_name) - } catch (e: Exception) { - return FilterLevel.DATA - } + return Pair(fusedAppList, status) +} - /* - * If the app can be shown, check if the app is downloadable. - * If not then change "Install" button to "N/A" - */ - try { - gplayRepository.getDownloadInfo( - fusedApp.package_name, - fusedApp.latest_version_code, - fusedApp.offer_type, +/** + * 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] + */ +override suspend fun filterRestrictedGPlayApps( + authData: AuthData, + appList: List, +): ResultSupreme> { + val filteredFusedApps = mutableListOf() + val status = runCodeWithTimeout({ + appList.forEach { + val filter = getAppFilterLevel(it, authData) + if (filter.isUnFiltered()) { + filteredFusedApps.add( + it.transformToFusedApp().apply { + this.filterLevel = filter + } ) - } catch (e: Exception) { - return FilterLevel.UI } - } else if (fusedApp.originalSize == 0L) { - return FilterLevel.UI } + }) + + return ResultSupreme.create(status, filteredFusedApps) +} + +/** + * Get different filter levels. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5720 + */ +override suspend fun getAppFilterLevel(fusedApp: FusedApp, authData: AuthData?): FilterLevel { + if (fusedApp.package_name.isBlank()) { + return FilterLevel.UNKNOWN + } + if (fusedApp.origin == Origin.CLEANAPK) { + /* + * Whitelist all open source apps. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5785 + */ return FilterLevel.NONE } - - /* - * Similar to above method but uses Aurora OSS data class "App". - */ - override suspend fun getAppFilterLevel(app: App, authData: AuthData): FilterLevel { - return getAppFilterLevel(app.transformToFusedApp(), authData) + if (authData == null) { + return if (fusedApp.origin == Origin.GPLAY) FilterLevel.UNKNOWN + else FilterLevel.NONE } - /* - * Handy method to run on an instance of FusedApp to update its filter level. - */ - private suspend fun FusedApp.updateFilterLevel(authData: AuthData?) { - this.filterLevel = getAppFilterLevel(this, authData) + if (!fusedApp.isFree && fusedApp.price.isBlank()) { + return FilterLevel.UI } - override suspend fun getApplicationDetails( - id: String, - packageName: String, - authData: AuthData, - origin: Origin - ): Pair { + if (fusedApp.restriction != Constants.Restriction.NOT_RESTRICTED) { + /* + * Check if app details can be shown. If not then remove the app from lists. + */ + try { + gplayRepository.getAppDetails(fusedApp.package_name) + } catch (e: Exception) { + return FilterLevel.DATA + } - var response: FusedApp? = null + /* + * If the app can be shown, check if the app is downloadable. + * If not then change "Install" button to "N/A" + */ + try { + gplayRepository.getDownloadInfo( + fusedApp.package_name, + fusedApp.latest_version_code, + fusedApp.offer_type, + ) + } catch (e: Exception) { + return FilterLevel.UI + } + } else if (fusedApp.originalSize == 0L) { + return FilterLevel.UI + } + return FilterLevel.NONE +} - val status = runCodeWithTimeout({ - response = if (origin == Origin.CLEANAPK) { - (cleanApkAppsRepository.getAppDetails(id) as Response).body()?.app - } else { - val app = gplayRepository.getAppDetails(packageName) as App? - app?.transformToFusedApp() - } - response?.let { - it.updateStatus() - it.updateType() - it.updateSource() - it.updateFilterLevel(authData) - } - }) +/* + * Similar to above method but uses Aurora OSS data class "App". + */ +override suspend fun getAppFilterLevel(app: App, authData: AuthData): FilterLevel { + return getAppFilterLevel(app.transformToFusedApp(), authData) +} - return Pair(response ?: FusedApp(), status) - } +/* + * Handy method to run on an instance of FusedApp to update its filter level. + */ +private suspend fun FusedApp.updateFilterLevel(authData: AuthData?) { + this.filterLevel = getAppFilterLevel(this, authData) +} - /* - * 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 = "" +override suspend fun getApplicationDetails( + id: String, + packageName: String, + authData: AuthData, + origin: Origin +): Pair { - if (preferenceManagerModule.isOpenSourceSelected()) { - val openSourceCategoryResult = fetchOpenSourceCategories(type) - categoriesList.addAll(openSourceCategoryResult.second) - apiStatus = openSourceCategoryResult.first - errorApplicationCategory = openSourceCategoryResult.third - } + var response: FusedApp? = null - if (preferenceManagerModule.isPWASelected()) { - val pwaCategoriesResult = fetchPWACategories(type) - categoriesList.addAll(pwaCategoriesResult.second) - apiStatus = pwaCategoriesResult.first - errorApplicationCategory = pwaCategoriesResult.third + val status = runCodeWithTimeout({ + response = if (origin == Origin.CLEANAPK) { + (cleanApkAppsRepository.getAppDetails(id) as Response).body()?.app + } else { + val app = gplayRepository.getAppDetails(packageName) as App? + app?.transformToFusedApp() } - - if (preferenceManagerModule.isGplaySelected()) { - val gplayCategoryResult = fetchGplayCategories( - type, - ) - categoriesList.addAll(gplayCategoryResult.second) - apiStatus = gplayCategoryResult.first - errorApplicationCategory = gplayCategoryResult.third + response?.let { + it.updateStatus() + it.updateType() + it.updateSource() + it.updateFilterLevel(authData) } + }) - return Pair(apiStatus, errorApplicationCategory) - } + return Pair(response ?: FusedApp(), status) +} - private suspend fun fetchGplayCategories( - type: CategoryType, - ): Triple, String> { - var errorApplicationCategory = "" - var apiStatus = ResultStatus.OK - val categoryList = mutableListOf() - runCodeWithTimeout({ - val playResponse = gplayRepository.getCategories(type).map { app -> - val category = app.transformToFusedCategory() - updateCategoryDrawable(category) - category - } - categoryList.addAll(playResponse) - }, { - errorApplicationCategory = APP_TYPE_ANY - apiStatus = ResultStatus.TIMEOUT - }, { - errorApplicationCategory = APP_TYPE_ANY - apiStatus = ResultStatus.UNKNOWN - }) - return Triple(apiStatus, categoryList, errorApplicationCategory) +/* + * 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 } - private suspend fun fetchPWACategories( - type: CategoryType, - ): Triple, String> { - var errorApplicationCategory = "" - var apiStatus: ResultStatus = ResultStatus.OK - val fusedCategoriesList = mutableListOf() - runCodeWithTimeout({ - getPWAsCategories()?.let { - fusedCategoriesList.addAll( - getFusedCategoryBasedOnCategoryType( - it, type, AppTag.PWA(context.getString(R.string.pwa)) - ) - ) - } - }, { - errorApplicationCategory = APP_TYPE_PWA - apiStatus = ResultStatus.TIMEOUT - }, { - errorApplicationCategory = APP_TYPE_PWA - apiStatus = ResultStatus.UNKNOWN - }) - return Triple(apiStatus, fusedCategoriesList, errorApplicationCategory) + if (preferenceManagerModule.isPWASelected()) { + val pwaCategoriesResult = fetchPWACategories(type) + categoriesList.addAll(pwaCategoriesResult.second) + apiStatus = pwaCategoriesResult.first + errorApplicationCategory = pwaCategoriesResult.third } - private suspend fun fetchOpenSourceCategories( - type: CategoryType, - ): Triple, String> { - var errorApplicationCategory = "" - var apiStatus: ResultStatus = ResultStatus.OK - val fusedCategoryList = mutableListOf() - runCodeWithTimeout({ - getOpenSourceCategories()?.let { - fusedCategoryList.addAll( - getFusedCategoryBasedOnCategoryType( - it, - type, - AppTag.OpenSource(context.getString(R.string.open_source)) - ) - ) - } - }, { - errorApplicationCategory = APP_TYPE_OPEN - apiStatus = ResultStatus.TIMEOUT - }, { - errorApplicationCategory = APP_TYPE_OPEN - apiStatus = ResultStatus.UNKNOWN - }) - return Triple(apiStatus, fusedCategoryList, errorApplicationCategory) + if (preferenceManagerModule.isGplaySelected()) { + val gplayCategoryResult = fetchGplayCategories( + type, + ) + categoriesList.addAll(gplayCategoryResult.second) + apiStatus = gplayCategoryResult.first + errorApplicationCategory = gplayCategoryResult.third } - /** - * Run a block of code with timeout. Returns status. - * - * @param block Main block to execute within [timeoutDurationInMillis] limit. - * @param timeoutBlock Optional code to execute in case of timeout. - * @param exceptionBlock Optional code to execute in case of an exception other than timeout. - * - * @return Instance of [ResultStatus] based on whether [block] was executed within timeout limit. - */ - private suspend fun runCodeWithTimeout( - block: suspend () -> Unit, - timeoutBlock: (() -> Unit)? = null, - exceptionBlock: ((e: Exception) -> Unit)? = null, - ): ResultStatus { - return try { - withTimeout(timeoutDurationInMillis) { - block() - } - ResultStatus.OK - } catch (e: TimeoutCancellationException) { - timeoutBlock?.invoke() - ResultStatus.TIMEOUT - } catch (e: Exception) { - e.printStackTrace() - exceptionBlock?.invoke(e) - ResultStatus.UNKNOWN.apply { - message = e.stackTraceToString() - } + return Pair(apiStatus, errorApplicationCategory) +} + +private suspend fun fetchGplayCategories( + type: CategoryType, +): Triple, String> { + var errorApplicationCategory = "" + var apiStatus = ResultStatus.OK + val categoryList = mutableListOf() + runCodeWithTimeout({ + val playResponse = gplayRepository.getCategories(type).map { app -> + val category = app.transformToFusedCategory() + updateCategoryDrawable(category) + category } - } + categoryList.addAll(playResponse) + }, { + errorApplicationCategory = APP_TYPE_ANY + apiStatus = ResultStatus.TIMEOUT + }, { + errorApplicationCategory = APP_TYPE_ANY + apiStatus = ResultStatus.UNKNOWN + }) + return Triple(apiStatus, categoryList, errorApplicationCategory) +} - private fun updateCategoryDrawable( - category: FusedCategory, - ) { - category.drawable = - getCategoryIconResource(getCategoryIconName(category)) - } +private suspend fun fetchPWACategories( + type: CategoryType, +): Triple, String> { + var errorApplicationCategory = "" + var apiStatus: ResultStatus = ResultStatus.OK + val fusedCategoriesList = mutableListOf() + runCodeWithTimeout({ + getPWAsCategories()?.let { + fusedCategoriesList.addAll( + getFusedCategoryBasedOnCategoryType( + it, type, AppTag.PWA(context.getString(R.string.pwa)) + ) + ) + } + }, { + errorApplicationCategory = APP_TYPE_PWA + apiStatus = ResultStatus.TIMEOUT + }, { + errorApplicationCategory = APP_TYPE_PWA + apiStatus = ResultStatus.UNKNOWN + }) + return Triple(apiStatus, fusedCategoriesList, errorApplicationCategory) +} - private fun getCategoryIconName(category: FusedCategory): String { - var categoryTitle = if (category.tag.getOperationalTag() - .contentEquals(AppTag.GPlay().getOperationalTag()) - ) category.id else category.title +private suspend fun fetchOpenSourceCategories( + type: CategoryType, +): Triple, String> { + var errorApplicationCategory = "" + var apiStatus: ResultStatus = ResultStatus.OK + val fusedCategoryList = mutableListOf() + runCodeWithTimeout({ + getOpenSourceCategories()?.let { + fusedCategoryList.addAll( + getFusedCategoryBasedOnCategoryType( + it, + type, + AppTag.OpenSource(context.getString(R.string.open_source)) + ) + ) + } + }, { + errorApplicationCategory = APP_TYPE_OPEN + apiStatus = ResultStatus.TIMEOUT + }, { + errorApplicationCategory = APP_TYPE_OPEN + apiStatus = ResultStatus.UNKNOWN + }) + return Triple(apiStatus, fusedCategoryList, errorApplicationCategory) +} - if (categoryTitle.contains(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION)) { - categoryTitle = categoryTitle.replace(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION, "and") +/** + * Run a block of code with timeout. Returns status. + * + * @param block Main block to execute within [timeoutDurationInMillis] limit. + * @param timeoutBlock Optional code to execute in case of timeout. + * @param exceptionBlock Optional code to execute in case of an exception other than timeout. + * + * @return Instance of [ResultStatus] based on whether [block] was executed within timeout limit. + */ +private suspend fun runCodeWithTimeout( + block: suspend () -> Unit, + timeoutBlock: (() -> Unit)? = null, + exceptionBlock: ((e: Exception) -> Unit)? = null, +): ResultStatus { + return try { + withTimeout(timeoutDurationInMillis) { + block() + } + ResultStatus.OK + } catch (e: TimeoutCancellationException) { + timeoutBlock?.invoke() + ResultStatus.TIMEOUT + } catch (e: Exception) { + e.printStackTrace() + exceptionBlock?.invoke(e) + ResultStatus.UNKNOWN.apply { + message = e.stackTraceToString() } - categoryTitle = categoryTitle.replace(' ', '_') - return categoryTitle.lowercase() } +} - private fun getFusedCategoryBasedOnCategoryType( - categories: Categories, - categoryType: CategoryType, - tag: AppTag - ): List { - return when (categoryType) { - CategoryType.APPLICATION -> { - getAppsCategoriesAsFusedCategory(categories, tag) - } +private fun updateCategoryDrawable( + category: FusedCategory, +) { + category.drawable = + getCategoryIconResource(getCategoryIconName(category)) +} - CategoryType.GAMES -> { - getGamesCategoriesAsFusedCategory(categories, tag) - } - } +private fun getCategoryIconName(category: FusedCategory): 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 getAppsCategoriesAsFusedCategory( - categories: Categories, - tag: AppTag - ): List { - return categories.apps.map { category -> - createFusedCategoryFromCategory(category, categories, tag) +private fun getFusedCategoryBasedOnCategoryType( + categories: Categories, + categoryType: CategoryType, + tag: AppTag +): List { + return when (categoryType) { + CategoryType.APPLICATION -> { + getAppsCategoriesAsFusedCategory(categories, tag) } - } - private fun getGamesCategoriesAsFusedCategory( - categories: Categories, - tag: AppTag - ): List { - return categories.games.map { category -> - createFusedCategoryFromCategory(category, categories, tag) + CategoryType.GAMES -> { + getGamesCategoriesAsFusedCategory(categories, tag) } } +} - private fun createFusedCategoryFromCategory( - category: String, - categories: Categories, - tag: AppTag - ): FusedCategory { - return FusedCategory( - id = category, - title = getCategoryTitle(category, categories), - drawable = getCategoryIconResource(category), - tag = tag - ) +private fun getAppsCategoriesAsFusedCategory( + categories: Categories, + tag: AppTag +): List { + return categories.apps.map { category -> + createFusedCategoryFromCategory(category, categories, tag) } +} - private fun getCategoryIconResource(category: String): Int { - return CategoryUtils.provideAppsCategoryIconResource(category) +private fun getGamesCategoriesAsFusedCategory( + categories: Categories, + tag: AppTag +): List { + return categories.games.map { category -> + createFusedCategoryFromCategory(category, categories, 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, "") - } - } +private fun createFusedCategoryFromCategory( + category: String, + categories: Categories, + tag: AppTag +): FusedCategory { + return FusedCategory( + id = category, + title = getCategoryTitle(category, categories), + drawable = getCategoryIconResource(category), + tag = tag + ) +} - private suspend fun getPWAsCategories(): Categories? { - return cleanApkPWARepository.getCategories().body() - } +private fun getCategoryIconResource(category: String): Int { + return CategoryUtils.provideAppsCategoryIconResource(category) +} - private suspend fun getOpenSourceCategories(): Categories? { - return cleanApkAppsRepository.getCategories().body() +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 getOpenSourceAppsResponse(category: String): Search? { - return cleanApkAppsRepository.getAppsByCategory( - category, - ).body() - } +private suspend fun getPWAsCategories(): Categories? { + return cleanApkPWARepository.getCategories().body() +} - private suspend fun getPWAAppsResponse(category: String): Search? { - return cleanApkPWARepository.getAppsByCategory( - category, - ).body() - } +private suspend fun getOpenSourceCategories(): Categories? { + return cleanApkAppsRepository.getCategories().body() +} - private fun Category.transformToFusedCategory(): FusedCategory { - val id = this.browseUrl.substringAfter("cat=").substringBefore("&c=") - return FusedCategory( - id = id.lowercase(), - title = this.title, - browseUrl = this.browseUrl, - imageUrl = this.imageUrl, - ) - } +private suspend fun getOpenSourceAppsResponse(category: String): Search? { + return cleanApkAppsRepository.getAppsByCategory( + category, + ).body() +} - /* - * Search-related internal functions - */ +private suspend fun getPWAAppsResponse(category: String): Search? { + return cleanApkPWARepository.getAppsByCategory( + category, + ).body() +} - private suspend fun getCleanAPKSearchResults( - keyword: String, - source: String = CleanApkRetrofit.APP_SOURCE_FOSS, - ): List { - val list = mutableListOf() - val response = - cleanApkAppsRepository.getSearchResult(keyword).body()?.apps +private fun Category.transformToFusedCategory(): FusedCategory { + val id = this.browseUrl.substringAfter("cat=").substringBefore("&c=") + return FusedCategory( + id = id.lowercase(), + title = this.title, + browseUrl = this.browseUrl, + imageUrl = this.imageUrl, + ) +} - response?.forEach { - it.updateStatus() - it.updateType() - it.source = - if (source.contentEquals(CleanApkRetrofit.APP_SOURCE_FOSS)) "Open Source" else "PWA" - list.add(it) - } - return list - } +/* + * Search-related internal functions + */ - override suspend fun getGplaySearchResult( - query: String, - nextPageSubBundle: Set? - ): Pair, Set> { - val searchResults = - gplayRepository.getSearchResult(query, nextPageSubBundle?.toMutableSet()) - if (!preferenceManagerModule.isGplaySelected()) { - return Pair(emptyList(), emptySet()) - } +private suspend fun getCleanAPKSearchResults( + keyword: String, + source: String = CleanApkRetrofit.APP_SOURCE_FOSS, +): List { + val list = mutableListOf() + val response = + cleanApkAppsRepository.getSearchResult(keyword).body()?.apps + + response?.forEach { + it.updateStatus() + it.updateType() + it.source = + if (source.contentEquals(CleanApkRetrofit.APP_SOURCE_FOSS)) "Open Source" else "PWA" + list.add(it) + } + return list +} - val fusedAppList = searchResults.first.map { app -> replaceWithFDroid(app) }.toMutableList() - if (searchResults.second.isNotEmpty()) { - fusedAppList.add(FusedApp(isPlaceHolder = true)) - } +override suspend fun getGplaySearchResult( + query: String, + nextPageSubBundle: Set? +): Pair, Set> { + val searchResults = + gplayRepository.getSearchResult(query, nextPageSubBundle?.toMutableSet()) + if (!preferenceManagerModule.isGplaySelected()) { + return Pair(emptyList(), emptySet()) + } - return Pair( - fusedAppList, - searchResults.second - ) + val fusedAppList = searchResults.first.map { app -> replaceWithFDroid(app) }.toMutableList() + if (searchResults.second.isNotEmpty()) { + fusedAppList.add(FusedApp(isPlaceHolder = true)) } - /* - * This function will replace a GPlay app with F-Droid app if exists, - * else will show the GPlay app itself. - */ - private suspend fun replaceWithFDroid(gPlayApp: App): FusedApp { - val gPlayFusedApp = gPlayApp.transformToFusedApp() - val response = fdroidWebInterface.getFdroidApp(gPlayFusedApp.package_name) - if (response.isSuccessful) { - val fdroidApp = getCleanApkPackageResult(gPlayFusedApp.package_name)?.apply { - updateSource() - isGplayReplaced = true - } - return fdroidApp ?: gPlayFusedApp - } + return Pair( + fusedAppList, + searchResults.second + ) +} - return gPlayFusedApp +/* + * This function will replace a GPlay app with F-Droid app if exists, + * else will show the GPlay app itself. + */ +private suspend fun replaceWithFDroid(gPlayApp: App): FusedApp { + val gPlayFusedApp = gPlayApp.transformToFusedApp() + val response = fdroidWebInterface.getFdroidApp(gPlayFusedApp.package_name) + if (response.isSuccessful) { + val fdroidApp = getCleanApkPackageResult(gPlayFusedApp.package_name)?.apply { + updateSource() + isGplayReplaced = true + } + return fdroidApp ?: gPlayFusedApp } - /* - * Home screen-related internal functions - */ + return gPlayFusedApp +} - private suspend fun generateCleanAPKHome(home: Home, appType: String): List { - val list = mutableListOf() - val headings = if (appType == APP_TYPE_OPEN) { - mapOf( - "top_updated_apps" to context.getString(R.string.top_updated_apps), - "top_updated_games" to context.getString(R.string.top_updated_games), - "popular_apps_in_last_24_hours" to context.getString(R.string.popular_apps_in_last_24_hours), - "popular_games_in_last_24_hours" to context.getString(R.string.popular_games_in_last_24_hours), - "discover" to context.getString(R.string.discover) - ) - } else { - mapOf( - "popular_apps" to context.getString(R.string.popular_apps), - "popular_games" to context.getString(R.string.popular_games), - "discover" to context.getString(R.string.discover_pwa) - ) - } - headings.forEach { (key, value) -> - when (key) { - "top_updated_apps" -> { - if (home.top_updated_apps.isNotEmpty()) { - home.top_updated_apps.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - } - list.add(FusedHome(value, home.top_updated_apps)) +/* + * Home screen-related internal functions + */ + +private suspend fun generateCleanAPKHome(home: Home, appType: String): List { + val list = mutableListOf() + val headings = if (appType == APP_TYPE_OPEN) { + mapOf( + "top_updated_apps" to context.getString(R.string.top_updated_apps), + "top_updated_games" to context.getString(R.string.top_updated_games), + "popular_apps_in_last_24_hours" to context.getString(R.string.popular_apps_in_last_24_hours), + "popular_games_in_last_24_hours" to context.getString(R.string.popular_games_in_last_24_hours), + "discover" to context.getString(R.string.discover) + ) + } else { + mapOf( + "popular_apps" to context.getString(R.string.popular_apps), + "popular_games" to context.getString(R.string.popular_games), + "discover" to context.getString(R.string.discover_pwa) + ) + } + headings.forEach { (key, value) -> + when (key) { + "top_updated_apps" -> { + if (home.top_updated_apps.isNotEmpty()) { + home.top_updated_apps.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) } + list.add(FusedHome(value, home.top_updated_apps)) } + } - "top_updated_games" -> { - if (home.top_updated_games.isNotEmpty()) { - home.top_updated_games.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - } - list.add(FusedHome(value, home.top_updated_games)) + "top_updated_games" -> { + if (home.top_updated_games.isNotEmpty()) { + home.top_updated_games.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) } + list.add(FusedHome(value, home.top_updated_games)) } + } - "popular_apps" -> { - if (home.popular_apps.isNotEmpty()) { - home.popular_apps.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - } - list.add(FusedHome(value, home.popular_apps)) + "popular_apps" -> { + if (home.popular_apps.isNotEmpty()) { + home.popular_apps.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) } + list.add(FusedHome(value, home.popular_apps)) } + } - "popular_games" -> { - if (home.popular_games.isNotEmpty()) { - home.popular_games.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - } - list.add(FusedHome(value, home.popular_games)) + "popular_games" -> { + if (home.popular_games.isNotEmpty()) { + home.popular_games.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) } + list.add(FusedHome(value, home.popular_games)) } + } - "popular_apps_in_last_24_hours" -> { - if (home.popular_apps_in_last_24_hours.isNotEmpty()) { - home.popular_apps_in_last_24_hours.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - } - list.add(FusedHome(value, home.popular_apps_in_last_24_hours)) + "popular_apps_in_last_24_hours" -> { + if (home.popular_apps_in_last_24_hours.isNotEmpty()) { + home.popular_apps_in_last_24_hours.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) } + list.add(FusedHome(value, home.popular_apps_in_last_24_hours)) } + } - "popular_games_in_last_24_hours" -> { - if (home.popular_games_in_last_24_hours.isNotEmpty()) { - home.popular_games_in_last_24_hours.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - } - list.add(FusedHome(value, home.popular_games_in_last_24_hours)) + "popular_games_in_last_24_hours" -> { + if (home.popular_games_in_last_24_hours.isNotEmpty()) { + home.popular_games_in_last_24_hours.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) } + list.add(FusedHome(value, home.popular_games_in_last_24_hours)) } + } - "discover" -> { - if (home.discover.isNotEmpty()) { - home.discover.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - } - list.add(FusedHome(value, home.discover)) + "discover" -> { + if (home.discover.isNotEmpty()) { + home.discover.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) } + list.add(FusedHome(value, home.discover)) } } } - return list.map { - it.source = appType - it - } } + return list.map { + it.source = appType + it + } +} - private suspend fun fetchGPlayHome(authData: AuthData): List { - val list = mutableListOf() - val gplayHomeData = gplayRepository.getHomeScreenData() as Map> - gplayHomeData.map { - val fusedApps = it.value.map { app -> - app.transformToFusedApp().apply { - updateFilterLevel(authData) - } +private suspend fun fetchGPlayHome(authData: AuthData): List { + val list = mutableListOf() + val gplayHomeData = gplayRepository.getHomeScreenData() as Map> + gplayHomeData.map { + val fusedApps = it.value.map { app -> + app.transformToFusedApp().apply { + updateFilterLevel(authData) } - list.add(FusedHome(it.key, fusedApps)) } - Timber.d("===> $list") - return list + list.add(FusedHome(it.key, fusedApps)) } + Timber.d("===> $list") + return list +} - /* - * FusedApp-related internal extensions and functions - */ +/* + * FusedApp-related internal extensions and functions + */ - private fun App.transformToFusedApp(): FusedApp { - val app = FusedApp( - _id = this.id.toString(), - author = this.developerName, - category = this.categoryName, - description = this.description, - perms = this.permissions, - icon_image_path = this.iconArtwork.url, - last_modified = this.updatedOn, - latest_version_code = this.versionCode, - latest_version_number = this.versionName, - name = this.displayName, - other_images_path = this.screenshots.transformToList(), - package_name = this.packageName, - ratings = Ratings( - usageQualityScore = - this.labeledRating.run { - if (isNotEmpty()) { - this.replace(",", ".").toDoubleOrNull() ?: -1.0 - } else -1.0 - } - ), - offer_type = this.offerType, - origin = Origin.GPLAY, - shareUrl = this.shareUrl, - originalSize = this.size, - appSize = Formatter.formatFileSize(context, this.size), - isFree = this.isFree, - price = this.price, - restriction = this.restriction, - ) - app.updateStatus() - return app - } +private fun App.transformToFusedApp(): FusedApp { + val app = FusedApp( + _id = this.id.toString(), + author = this.developerName, + category = this.categoryName, + description = this.description, + perms = this.permissions, + icon_image_path = this.iconArtwork.url, + last_modified = this.updatedOn, + latest_version_code = this.versionCode, + latest_version_number = this.versionName, + name = this.displayName, + other_images_path = this.screenshots.transformToList(), + package_name = this.packageName, + ratings = Ratings( + usageQualityScore = + this.labeledRating.run { + if (isNotEmpty()) { + this.replace(",", ".").toDoubleOrNull() ?: -1.0 + } else -1.0 + } + ), + offer_type = this.offerType, + origin = Origin.GPLAY, + shareUrl = this.shareUrl, + originalSize = this.size, + appSize = Formatter.formatFileSize(context, this.size), + isFree = this.isFree, + price = this.price, + restriction = this.restriction, + ) + app.updateStatus() + return app +} - /** - * Get fused app installation status. - * Applicable for both native apps and PWAs. - * - * Recommended to use this instead of [PkgManagerModule.getPackageStatus]. - */ - override fun getFusedAppInstallationStatus(fusedApp: FusedApp): Status { - return if (fusedApp.is_pwa) { - pwaManagerModule.getPwaStatus(fusedApp) - } else { - pkgManagerModule.getPackageStatus(fusedApp.package_name, fusedApp.latest_version_code) - } +/** + * Get fused app installation status. + * Applicable for both native apps and PWAs. + * + * Recommended to use this instead of [PkgManagerModule.getPackageStatus]. + */ +override fun getFusedAppInstallationStatus(fusedApp: FusedApp): Status { + return if (fusedApp.is_pwa) { + pwaManagerModule.getPwaStatus(fusedApp) + } else { + pkgManagerModule.getPackageStatus(fusedApp.package_name, fusedApp.latest_version_code) } +} - private fun FusedApp.updateStatus() { - if (this.status != Status.INSTALLATION_ISSUE) { - this.status = getFusedAppInstallationStatus(this) - } +private fun FusedApp.updateStatus() { + if (this.status != Status.INSTALLATION_ISSUE) { + this.status = getFusedAppInstallationStatus(this) } +} + +private fun FusedApp.updateType() { + this.type = if (this.is_pwa) Type.PWA else Type.NATIVE +} - private fun FusedApp.updateType() { - this.type = if (this.is_pwa) Type.PWA else Type.NATIVE +private fun FusedApp.updateSource() { + this.apply { + source = if (origin == Origin.CLEANAPK && is_pwa) context.getString(R.string.pwa) + else if (origin == Origin.CLEANAPK) context.getString(R.string.open_source) + else "" } +} - private fun FusedApp.updateSource() { - this.apply { - source = if (origin == Origin.CLEANAPK && is_pwa) context.getString(R.string.pwa) - else if (origin == Origin.CLEANAPK) context.getString(R.string.open_source) - else "" - } +private fun MutableList.transformToList(): List { + val list = mutableListOf() + this.forEach { + list.add(it.url) } + return list +} - private fun MutableList.transformToList(): List { - val list = mutableListOf() - this.forEach { - list.add(it.url) - } - return list +/** + * @return true, if any change is found, otherwise false + */ +override fun isHomeDataUpdated( + newHomeData: List, + oldHomeData: List +): Boolean { + if (newHomeData.size != oldHomeData.size) { + return true } - /** - * @return true, if any change is found, otherwise false - */ - override fun isHomeDataUpdated( - newHomeData: List, - oldHomeData: List - ): Boolean { - if (newHomeData.size != oldHomeData.size) { + oldHomeData.forEach { + val fusedHome = newHomeData[oldHomeData.indexOf(it)] + if (!it.title.contentEquals(fusedHome.title) || areFusedAppsUpdated(it, fusedHome)) { return true } + } + return false +} - oldHomeData.forEach { - val fusedHome = newHomeData[oldHomeData.indexOf(it)] - if (!it.title.contentEquals(fusedHome.title) || areFusedAppsUpdated(it, fusedHome)) { - return true - } - } - return false +private fun areFusedAppsUpdated( + oldFusedHome: FusedHome, + newFusedHome: FusedHome, +): Boolean { + val fusedAppDiffUtil = HomeChildFusedAppDiffUtil() + if (oldFusedHome.list.size != newFusedHome.list.size) { + return true } - private fun areFusedAppsUpdated( - oldFusedHome: FusedHome, - newFusedHome: FusedHome, - ): Boolean { - val fusedAppDiffUtil = HomeChildFusedAppDiffUtil() - if (oldFusedHome.list.size != newFusedHome.list.size) { + oldFusedHome.list.forEach { oldFusedApp -> + val indexOfOldFusedApp = oldFusedHome.list.indexOf(oldFusedApp) + val fusedApp = newFusedHome.list[indexOfOldFusedApp] + if (!fusedAppDiffUtil.areContentsTheSame(oldFusedApp, fusedApp)) { return true } + } + return false +} - oldFusedHome.list.forEach { oldFusedApp -> - val indexOfOldFusedApp = oldFusedHome.list.indexOf(oldFusedApp) - val fusedApp = newFusedHome.list[indexOfOldFusedApp] - if (!fusedAppDiffUtil.areContentsTheSame(oldFusedApp, fusedApp)) { - return true - } - } - return false +/** + * @return returns true if there is changes in data, otherwise false + */ +override fun isAnyFusedAppUpdated( + newFusedApps: List, + oldFusedApps: List +): Boolean { + val fusedAppDiffUtil = HomeChildFusedAppDiffUtil() + if (newFusedApps.size != oldFusedApps.size) { + return true } - /** - * @return returns true if there is changes in data, otherwise false - */ - override fun isAnyFusedAppUpdated( - newFusedApps: List, - oldFusedApps: List - ): Boolean { - val fusedAppDiffUtil = HomeChildFusedAppDiffUtil() - if (newFusedApps.size != oldFusedApps.size) { + newFusedApps.forEach { + val indexOfNewFusedApp = newFusedApps.indexOf(it) + if (!fusedAppDiffUtil.areContentsTheSame(it, oldFusedApps[indexOfNewFusedApp])) { return true } - - newFusedApps.forEach { - val indexOfNewFusedApp = newFusedApps.indexOf(it) - if (!fusedAppDiffUtil.areContentsTheSame(it, oldFusedApps[indexOfNewFusedApp])) { - return true - } - } - return false } + return false +} - override fun isAnyAppInstallStatusChanged(currentList: List): Boolean { - currentList.forEach { - if (it.status == Status.INSTALLATION_ISSUE) { - return@forEach - } - val currentAppStatus = - pkgManagerModule.getPackageStatus(it.package_name, it.latest_version_code) - if (it.status != currentAppStatus) { - return true - } +override fun isAnyAppInstallStatusChanged(currentList: List): Boolean { + currentList.forEach { + if (it.status == Status.INSTALLATION_ISSUE) { + return@forEach + } + val currentAppStatus = + pkgManagerModule.getPackageStatus(it.package_name, it.latest_version_code) + if (it.status != currentAppStatus) { + return true } - return false } + return false +} - override fun isOpenSourceSelected() = preferenceManagerModule.isOpenSourceSelected() - override suspend fun getGplayAppsByCategory( - authData: AuthData, - category: String, - pageUrl: String? - ): ResultSupreme, String>> { - var fusedAppList: MutableList = mutableListOf() - var nextPageUrl = "" - - val status = runCodeWithTimeout({ - val streamCluster = - gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster - val filteredAppList = filterRestrictedGPlayApps(authData, streamCluster.clusterAppList) - filteredAppList.data?.let { - fusedAppList = it.toMutableList() - } +override fun isOpenSourceSelected() = preferenceManagerModule.isOpenSourceSelected() +override suspend fun getGplayAppsByCategory( + authData: AuthData, + category: String, + pageUrl: String? +): ResultSupreme, String>> { + var fusedAppList: MutableList = mutableListOf() + var nextPageUrl = "" + + val status = runCodeWithTimeout({ + val streamCluster = + gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster + val filteredAppList = filterRestrictedGPlayApps(authData, streamCluster.clusterAppList) + filteredAppList.data?.let { + fusedAppList = it.toMutableList() + } - nextPageUrl = streamCluster.clusterNextPageUrl - if (!nextPageUrl.isNullOrEmpty()) { - fusedAppList.add(FusedApp(isPlaceHolder = true)) - } - }) + nextPageUrl = streamCluster.clusterNextPageUrl + if (!nextPageUrl.isNullOrEmpty()) { + fusedAppList.add(FusedApp(isPlaceHolder = true)) + } + }) - return ResultSupreme.create(status, Pair(fusedAppList, nextPageUrl)) - } + return ResultSupreme.create(status, Pair(fusedAppList, nextPageUrl)) +} } diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt index 1799be248..d28e864b0 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt @@ -115,6 +115,12 @@ class ApplicationListRVAdapter( onPlaceHolderShow?.invoke() // Do not process anything else for this entry return + } else { + val progressBar = holder.binding.placeholderProgressBar + holder.binding.root.children.forEach { + it.visibility = if (it != progressBar) View.VISIBLE + else View.INVISIBLE + } } holder.binding.apply { diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 6ddfbe192..6eb448377 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -98,38 +98,32 @@ class SearchViewModel @Inject constructor( * without having to wait for all of the apps. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ - fun getSearchResults(query: String, authData: AuthData, lifecycleOwner: LifecycleOwner) { - viewModelScope.launch(Dispatchers.Main) { - searchResultLiveData.removeObservers(lifecycleOwner) - searchResultLiveData = fusedAPIRepository.getSearchResults(query, authData) - - searchResultLiveData.observe(lifecycleOwner) { - searchResult.postValue(it) - - if (!it.isSuccess()) { - val exception = - if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) { - GPlayException( - it.isTimeout(), - it.message.ifBlank { "Data load error" } - ) - } else { - CleanApkException( - it.isTimeout(), - it.message.ifBlank { "Data load error" } - ) - } - - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) - } - } - - withContext(Dispatchers.IO) { - nextSubBundle = null - fetchGplayData(query) + private fun getSearchResults(query: String, authData: AuthData, lifecycleOwner: LifecycleOwner) { + viewModelScope.launch(Dispatchers.IO) { + val searchResultSupreme = fusedAPIRepository.getSearchResults(query, authData) + + searchResult.postValue(searchResultSupreme) + + if (!searchResultSupreme.isSuccess()) { + val exception = + if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) { + GPlayException( + searchResultSupreme.isTimeout(), + searchResultSupreme.message.ifBlank { "Data load error" } + ) + } else { + CleanApkException( + searchResultSupreme.isTimeout(), + searchResultSupreme.message.ifBlank { "Data load error" } + ) + } + + exceptionsList.add(exception) + exceptionsLiveData.postValue(exceptionsList) } + nextSubBundle = null + fetchGplayData(query) } } @@ -150,10 +144,15 @@ class SearchViewModel @Inject constructor( val searchResult = searchResult.value val currentAppList = searchResult?.data?.first?.toMutableList() ?: mutableListOf() currentAppList.removeIf { item -> item.isPlaceHolder } - currentAppList.plus(gplaySearchResult.first) + currentAppList.addAll(gplaySearchResult.first) val finalResult = if (searchResult is ResultSupreme.Success) { - ResultSupreme.Success(Pair(currentAppList.toList(), gplaySearchResult.second.isNotEmpty())) + ResultSupreme.Success( + Pair( + currentAppList.toList(), + gplaySearchResult.second.isNotEmpty() + ) + ) } else { ResultSupreme.Error() } -- GitLab From adfc07b32e7dd4a868bd760663c4cf0a87078839 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Tue, 22 Aug 2023 09:23:01 +0600 Subject: [PATCH 4/7] Handled gplay api errors --- .../e/apps/data/fused/FusedAPIRepository.kt | 2 +- .../foundation/e/apps/data/fused/FusedApi.kt | 4 +- .../e/apps/data/fused/FusedApiImpl.kt | 1893 +++++++++-------- .../apps/data/gplay/utils/GPlayHttpClient.kt | 19 + .../workmanager/AppInstallProcessor.kt | 19 +- .../e/apps/ui/search/SearchFragment.kt | 4 + .../e/apps/ui/search/SearchViewModel.kt | 51 +- .../foundation/e/apps/utils/Extensions.kt | 18 + 8 files changed, 1032 insertions(+), 978 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt index b6fd95229..7c45bb5cf 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt @@ -118,7 +118,7 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedApi) suspend fun getGplaySearchResults( query: String, nextPageSubBundle: Set? - ): Pair, Set> { + ): GplaySearchResult { return fusedAPIImpl.getGplaySearchResult(query, nextPageSubBundle) } diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt index 4492f2f8d..d0a11c20e 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt @@ -18,6 +18,8 @@ import foundation.e.apps.data.fused.utils.CategoryType import foundation.e.apps.data.fusedDownload.models.FusedDownload import retrofit2.Response +typealias GplaySearchResult = ResultSupreme, Set>> + interface FusedApi { companion object { const val APP_TYPE_ANY = "any" @@ -68,7 +70,7 @@ interface FusedApi { suspend fun getGplaySearchResult( query: String, nextPageSubBundle: Set? - ): Pair, Set> + ): GplaySearchResult suspend fun getSearchSuggestions(query: String): List diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt index 0bb3a2616..06d09d36c 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt @@ -64,11 +64,15 @@ import foundation.e.apps.data.fused.utils.CategoryType import foundation.e.apps.data.fused.utils.CategoryUtils import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.gplay.GplayStoreRepository +import foundation.e.apps.data.gplay.utils.GplayHttpRequestException import foundation.e.apps.data.gplay.utils.runFlowWithTimeout +import foundation.e.apps.data.login.exceptions.GPlayException +import foundation.e.apps.data.login.exceptions.UnknownSourceException import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule import foundation.e.apps.ui.home.model.HomeChildFusedAppDiffUtil +import foundation.e.apps.ui.search.SearchViewModel import kotlinx.coroutines.Deferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async @@ -78,11 +82,11 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.withTimeout import retrofit2.Response import timber.log.Timber +import java.net.SocketTimeoutException import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton -typealias GplaySearchResultFlow = Flow, Boolean>>> typealias FusedHomeDeferred = Deferred>> @Singleton @@ -101,6 +105,8 @@ class FusedApiImpl @Inject constructor( 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 ERROR_GPLAY_SEARCH = "Gplay search is failed!" + private const val ERROR_GPLAY_SOURCE_NOT_SELECTED = "Gplay apps are not selected!" } /** @@ -293,41 +299,41 @@ class FusedApiImpl @Inject constructor( // ).asLiveData() // ) // } - return finalSearchResult -} + return finalSearchResult + } -private suspend fun fetchPWASearchResult( - query: String, - searchResult: MutableList, - packageSpecificResults: ArrayList -): ResultSupreme, Boolean>> { - val pwaApps: MutableList = mutableListOf() - val status = runCodeWithTimeout({ - val apps = - cleanApkPWARepository.getSearchResult(query).body()?.apps - apps?.apply { - if (this.isNotEmpty()) { - pwaApps.addAll(this) + private suspend fun fetchPWASearchResult( + query: String, + searchResult: MutableList, + packageSpecificResults: ArrayList + ): ResultSupreme, Boolean>> { + val pwaApps: MutableList = mutableListOf() + val status = runCodeWithTimeout({ + val apps = + cleanApkPWARepository.getSearchResult(query).body()?.apps + apps?.apply { + if (this.isNotEmpty()) { + pwaApps.addAll(this) + } } - } - }) + }) - if (pwaApps.isNotEmpty() || status != ResultStatus.OK) { - searchResult.addAll(pwaApps) - } + if (pwaApps.isNotEmpty() || status != ResultStatus.OK) { + searchResult.addAll(pwaApps) + } - return ResultSupreme.create( - status, - Pair( - filterWithKeywordSearch( - searchResult, - packageSpecificResults, - query - ), - preferenceManagerModule.isGplaySelected() + return ResultSupreme.create( + status, + Pair( + filterWithKeywordSearch( + searchResult, + packageSpecificResults, + query + ), + preferenceManagerModule.isGplaySelected() + ) ) - ) -} + } // private suspend fun fetchGplaySearchResults( // query: String, @@ -349,1094 +355,1103 @@ private suspend fun fetchPWASearchResult( // ) // } -private suspend fun fetchOpenSourceSearchResult( - cleanApkResults: MutableList, - query: String, - searchResult: MutableList, - packageSpecificResults: ArrayList -): ResultSupreme, Boolean>> { - val status = runCodeWithTimeout({ - cleanApkResults.addAll(getCleanAPKSearchResults(query)) - }) - - if (cleanApkResults.isNotEmpty()) { - searchResult.addAll(cleanApkResults) - } + private suspend fun fetchOpenSourceSearchResult( + cleanApkResults: MutableList, + query: String, + searchResult: MutableList, + packageSpecificResults: ArrayList + ): ResultSupreme, Boolean>> { + val status = runCodeWithTimeout({ + cleanApkResults.addAll(getCleanAPKSearchResults(query)) + }) - return ResultSupreme.create( - status, - Pair( - filterWithKeywordSearch( - searchResult, - packageSpecificResults, - query - ), - preferenceManagerModule.isGplaySelected() || preferenceManagerModule.isPWASelected() + if (cleanApkResults.isNotEmpty()) { + searchResult.addAll(cleanApkResults) + } + + return ResultSupreme.create( + status, + Pair( + filterWithKeywordSearch( + searchResult, + packageSpecificResults, + query + ), + preferenceManagerModule.isGplaySelected() || preferenceManagerModule.isPWASelected() + ) ) - ) -} + } + + private suspend fun fetchPackageSpecificResult( + authData: AuthData, + query: String, + packageSpecificResults: MutableList + ): ResultSupreme, Boolean>> { + var gplayPackageResult: FusedApp? = null + var cleanapkPackageResult: FusedApp? = null -private suspend fun fetchPackageSpecificResult( - authData: AuthData, - query: String, - packageSpecificResults: MutableList -): ResultSupreme, Boolean>> { - var gplayPackageResult: FusedApp? = null - var cleanapkPackageResult: FusedApp? = null + val status = runCodeWithTimeout({ + if (preferenceManagerModule.isGplaySelected()) { + gplayPackageResult = getGplayPackagResult(query, authData) + } - val status = runCodeWithTimeout({ - if (preferenceManagerModule.isGplaySelected()) { - gplayPackageResult = getGplayPackagResult(query, authData) - } + if (preferenceManagerModule.isOpenSourceSelected()) { + cleanapkPackageResult = getCleanApkPackageResult(query) + } + }) - if (preferenceManagerModule.isOpenSourceSelected()) { - cleanapkPackageResult = getCleanApkPackageResult(query) + /* + * Currently only show open source package result if exists in both fdroid and gplay. + * This is temporary. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5783 + */ + cleanapkPackageResult?.let { packageSpecificResults.add(it) } ?: run { + gplayPackageResult?.let { packageSpecificResults.add(it) } } - }) - /* - * Currently only show open source package result if exists in both fdroid and gplay. - * This is temporary. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5783 - */ - cleanapkPackageResult?.let { packageSpecificResults.add(it) } ?: run { - gplayPackageResult?.let { packageSpecificResults.add(it) } + /* + * If there was a timeout, return it and don't try to fetch anything else. + * Also send true in the pair to signal more results being loaded. + */ + if (status != ResultStatus.OK) { + return ResultSupreme.create(status, Pair(packageSpecificResults, false)) + } + return ResultSupreme.create(status, Pair(packageSpecificResults, true)) } /* - * If there was a timeout, return it and don't try to fetch anything else. - * Also send true in the pair to signal more results being loaded. - */ - if (status != ResultStatus.OK) { - return ResultSupreme.create(status, Pair(packageSpecificResults, false)) + * The list packageSpecificResults may contain apps with duplicate package names. + * Example, "org.telegram.messenger" will result in "Telegram" app from Play Store + * and "Telegram FOSS" from F-droid. We show both of them at the top. + * + * But for the other keyword related search results, we do not allow duplicate package names. + * We also filter out apps which are already present in packageSpecificResults list. + */ + private fun filterWithKeywordSearch( + list: List, + packageSpecificResults: List, + query: String + ): List { + val filteredResults = list.distinctBy { it.package_name } + .filter { packageSpecificResults.isEmpty() || it.package_name != query } + return packageSpecificResults + filteredResults } - return ResultSupreme.create(status, Pair(packageSpecificResults, true)) -} - -/* - * The list packageSpecificResults may contain apps with duplicate package names. - * Example, "org.telegram.messenger" will result in "Telegram" app from Play Store - * and "Telegram FOSS" from F-droid. We show both of them at the top. - * - * But for the other keyword related search results, we do not allow duplicate package names. - * We also filter out apps which are already present in packageSpecificResults list. - */ -private fun filterWithKeywordSearch( - list: List, - packageSpecificResults: List, - query: String -): List { - val filteredResults = list.distinctBy { it.package_name } - .filter { packageSpecificResults.isEmpty() || it.package_name != query } - return packageSpecificResults + filteredResults -} -private suspend fun getCleanApkPackageResult( - query: String, -): FusedApp? { - getCleanapkSearchResult(query).let { - if (it.isSuccess() && it.data!!.package_name.isNotBlank()) { - return it.data!! + private suspend fun getCleanApkPackageResult( + query: String, + ): FusedApp? { + getCleanapkSearchResult(query).let { + if (it.isSuccess() && it.data!!.package_name.isNotBlank()) { + return it.data!! + } } + return null } - return null -} -private suspend fun getGplayPackagResult( - query: String, - authData: AuthData, -): FusedApp? { - try { - getApplicationDetails(query, query, authData, Origin.GPLAY).let { - if (it.second == ResultStatus.OK) { - return it.first + private suspend fun getGplayPackagResult( + query: String, + authData: AuthData, + ): FusedApp? { + try { + getApplicationDetails(query, query, authData, Origin.GPLAY).let { + if (it.second == ResultStatus.OK) { + return it.first + } } + } catch (e: Exception) { + Timber.e(e) } - } catch (e: Exception) { - Timber.e(e) + return null } - return null -} -/* - * Method to search cleanapk based on package name. - * This is to be only used for showing an entry in search results list. - * DO NOT use this to show info on ApplicationFragment as it will not have all the required - * information to show for an app. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/2629 - */ -private suspend fun getCleanapkSearchResult(packageName: String): ResultSupreme { - var fusedApp = FusedApp() - val status = runCodeWithTimeout({ - val result = cleanApkAppsRepository.getSearchResult( - packageName, - "package_name" - ).body() + /* + * Method to search cleanapk based on package name. + * This is to be only used for showing an entry in search results list. + * DO NOT use this to show info on ApplicationFragment as it will not have all the required + * information to show for an app. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/2629 + */ + private suspend fun getCleanapkSearchResult(packageName: String): ResultSupreme { + var fusedApp = FusedApp() + val status = runCodeWithTimeout({ + val result = cleanApkAppsRepository.getSearchResult( + packageName, + "package_name" + ).body() - if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { - fusedApp = result.apps[0] - } - }) - return ResultSupreme.create(status, fusedApp) -} + if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { + fusedApp = result.apps[0] + } + }) + return ResultSupreme.create(status, fusedApp) + } -override suspend fun getSearchSuggestions(query: String): List { - var searchSuggesions = listOf() - runCodeWithTimeout({ - searchSuggesions = gplayRepository.getSearchSuggestions(query) - }) + override suspend fun getSearchSuggestions(query: String): List { + var searchSuggesions = listOf() + runCodeWithTimeout({ + searchSuggesions = gplayRepository.getSearchSuggestions(query) + }) - return searchSuggesions -} + return searchSuggesions + } -override suspend fun getOnDemandModule( - packageName: String, - moduleName: String, - versionCode: Int, - offerType: Int -): String? { - val list = gplayRepository.getOnDemandModule( - packageName, - moduleName, - versionCode, - offerType, - ) - for (element in list) { - if (element.name == "$moduleName.apk") { - return element.url + override suspend fun getOnDemandModule( + packageName: String, + moduleName: String, + versionCode: Int, + offerType: Int + ): String? { + val list = gplayRepository.getOnDemandModule( + packageName, + moduleName, + versionCode, + offerType, + ) + for (element in list) { + if (element.name == "$moduleName.apk") { + return element.url + } } + return null } - return null -} -override suspend fun updateFusedDownloadWithDownloadingInfo( - origin: Origin, - fusedDownload: FusedDownload -) { - val list = mutableListOf() - when (origin) { - Origin.CLEANAPK -> { - val downloadInfo = - (cleanApkAppsRepository as CleanApkDownloadInfoFetcher).getDownloadInfo( - fusedDownload.id - ) - .body() - downloadInfo?.download_data?.download_link?.let { list.add(it) } - fusedDownload.signature = downloadInfo?.download_data?.signature ?: "" - } + override suspend fun updateFusedDownloadWithDownloadingInfo( + origin: Origin, + fusedDownload: FusedDownload + ) { + val list = mutableListOf() + when (origin) { + Origin.CLEANAPK -> { + val downloadInfo = + (cleanApkAppsRepository as CleanApkDownloadInfoFetcher).getDownloadInfo( + fusedDownload.id + ) + .body() + downloadInfo?.download_data?.download_link?.let { list.add(it) } + fusedDownload.signature = downloadInfo?.download_data?.signature ?: "" + } - Origin.GPLAY -> { - val downloadList = - gplayRepository.getDownloadInfo( - fusedDownload.packageName, - fusedDownload.versionCode, - fusedDownload.offerType - ) - fusedDownload.files = downloadList - list.addAll(downloadList.map { it.url }) + Origin.GPLAY -> { + val downloadList = + gplayRepository.getDownloadInfo( + fusedDownload.packageName, + fusedDownload.versionCode, + fusedDownload.offerType + ) + fusedDownload.files = downloadList + list.addAll(downloadList.map { it.url }) + } } + fusedDownload.downloadURLList = list } - fusedDownload.downloadURLList = list -} -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 status = runCodeWithTimeout({ - val response = getPWAAppsResponse(category) - response?.apps?.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - list.add(it) - } - }) - return ResultSupreme.create(status, Pair(list, "")) -} + 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 status = runCodeWithTimeout({ + val response = getPWAAppsResponse(category) + response?.apps?.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) + list.add(it) + } + }) + return ResultSupreme.create(status, Pair(list, "")) + } -override suspend fun getOpenSourceApps(category: String): ResultSupreme, String>> { - val list = mutableListOf() - val status = runCodeWithTimeout({ - val response = getOpenSourceAppsResponse(category) - response?.apps?.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) - list.add(it) - } - }) - return ResultSupreme.create(status, Pair(list, "")) -} + override suspend fun getOpenSourceApps(category: String): ResultSupreme, String>> { + val list = mutableListOf() + val status = runCodeWithTimeout({ + val response = getOpenSourceAppsResponse(category) + response?.apps?.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) + list.add(it) + } + }) + return ResultSupreme.create(status, Pair(list, "")) + } -/* - * Function to search cleanapk using package name. - * Will be used to handle f-droid deeplink. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509 - */ -override suspend fun getCleanapkAppDetails(packageName: String): Pair { - var fusedApp = FusedApp() - val status = runCodeWithTimeout({ - val result = cleanApkAppsRepository.getSearchResult( - packageName, - "package_name" - ).body() + /* + * Function to search cleanapk using package name. + * Will be used to handle f-droid deeplink. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509 + */ + override suspend fun getCleanapkAppDetails(packageName: String): Pair { + var fusedApp = FusedApp() + val status = runCodeWithTimeout({ + val result = cleanApkAppsRepository.getSearchResult( + packageName, + "package_name" + ).body() - if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { - fusedApp = - (cleanApkAppsRepository.getAppDetails(result.apps[0]._id) as Response).body()?.app - ?: FusedApp() - } - fusedApp.updateFilterLevel(null) - }) - return Pair(fusedApp, status) -} + if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { + fusedApp = + (cleanApkAppsRepository.getAppDetails(result.apps[0]._id) as Response).body()?.app + ?: FusedApp() + } + fusedApp.updateFilterLevel(null) + }) + return Pair(fusedApp, status) + } -override suspend fun getApplicationDetails( - packageNameList: List, - authData: AuthData, - origin: Origin -): Pair, ResultStatus> { - val list = mutableListOf() + override suspend fun getApplicationDetails( + packageNameList: List, + authData: AuthData, + origin: Origin + ): Pair, ResultStatus> { + val list = mutableListOf() + + val response: Pair, ResultStatus> = + if (origin == Origin.CLEANAPK) { + getAppDetailsListFromCleanapk(packageNameList) + } else { + getAppDetailsListFromGPlay(packageNameList, authData) + } - val response: Pair, ResultStatus> = - if (origin == Origin.CLEANAPK) { - getAppDetailsListFromCleanapk(packageNameList) - } else { - getAppDetailsListFromGPlay(packageNameList, authData) + response.first.forEach { + if (it.package_name.isNotBlank()) { + it.updateStatus() + it.updateType() + list.add(it) + } } - response.first.forEach { - if (it.package_name.isNotBlank()) { - it.updateStatus() - it.updateType() - list.add(it) - } + return Pair(list, response.second) } - return Pair(list, response.second) -} + /* + * Get app details of a list of apps from cleanapk. + * Returns list of FusedApp and ResultStatus - which will reflect timeout if even one app fails. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 + */ + private suspend fun getAppDetailsListFromCleanapk( + packageNameList: List, + ): Pair, ResultStatus> { + var status = ResultStatus.OK + val fusedAppList = mutableListOf() -/* - * Get app details of a list of apps from cleanapk. - * Returns list of FusedApp and ResultStatus - which will reflect timeout if even one app fails. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 - */ -private suspend fun getAppDetailsListFromCleanapk( - packageNameList: List, -): Pair, ResultStatus> { - var status = ResultStatus.OK - val fusedAppList = mutableListOf() + /* + * Fetch result of each cleanapk search with separate timeout, + * i.e. check timeout for individual package query. + */ + for (packageName in packageNameList) { + status = runCodeWithTimeout({ + cleanApkAppsRepository.getSearchResult( + packageName, + "package_name" + ).body()?.run { + if (apps.isNotEmpty() && numberOfResults == 1) { + fusedAppList.add( + apps[0].apply { + updateFilterLevel(null) + } + ) + } + } + }) + + /* + * If status is not ok, immediately return. + */ + if (status != ResultStatus.OK) { + return Pair(fusedAppList, status) + } + } + + return Pair(fusedAppList, status) + } /* - * Fetch result of each cleanapk search with separate timeout, - * i.e. check timeout for individual package query. + * Get app details of a list of apps from Google Play store. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 */ - for (packageName in packageNameList) { - status = runCodeWithTimeout({ - cleanApkAppsRepository.getSearchResult( - packageName, - "package_name" - ).body()?.run { - if (apps.isNotEmpty() && numberOfResults == 1) { + private suspend fun getAppDetailsListFromGPlay( + packageNameList: List, + authData: AuthData, + ): Pair, ResultStatus> { + val fusedAppList = mutableListOf() + + /* + * Old code moved from getApplicationDetails() + */ + val status = runCodeWithTimeout({ + gplayRepository.getAppsDetails(packageNameList).forEach { app -> + /* + * Some apps are restricted to locations. Example "com.skype.m2". + * For restricted apps, check if it is possible to get their specific app info. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5174 + */ + val filter = getAppFilterLevel(app, authData) + if (filter.isUnFiltered()) { fusedAppList.add( - apps[0].apply { - updateFilterLevel(null) + app.transformToFusedApp().apply { + filterLevel = filter } ) } } }) - /* - * If status is not ok, immediately return. - */ - if (status != ResultStatus.OK) { - return Pair(fusedAppList, status) - } + return Pair(fusedAppList, status) } - return Pair(fusedAppList, status) -} + /** + * 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] + */ + override suspend fun filterRestrictedGPlayApps( + authData: AuthData, + appList: List, + ): ResultSupreme> { + val filteredFusedApps = mutableListOf() + val status = runCodeWithTimeout({ + appList.forEach { + val filter = getAppFilterLevel(it, authData) + if (filter.isUnFiltered()) { + filteredFusedApps.add( + it.transformToFusedApp().apply { + this.filterLevel = filter + } + ) + } + } + }) -/* - * Get app details of a list of apps from Google Play store. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 - */ -private suspend fun getAppDetailsListFromGPlay( - packageNameList: List, - authData: AuthData, -): Pair, ResultStatus> { - val fusedAppList = mutableListOf() + return ResultSupreme.create(status, filteredFusedApps) + } - /* - * Old code moved from getApplicationDetails() + /** + * Get different filter levels. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5720 */ - val status = runCodeWithTimeout({ - gplayRepository.getAppsDetails(packageNameList).forEach { app -> + override suspend fun getAppFilterLevel(fusedApp: FusedApp, authData: AuthData?): FilterLevel { + if (fusedApp.package_name.isBlank()) { + return FilterLevel.UNKNOWN + } + if (fusedApp.origin == Origin.CLEANAPK) { /* - * Some apps are restricted to locations. Example "com.skype.m2". - * For restricted apps, check if it is possible to get their specific app info. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5174 + * Whitelist all open source apps. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5785 */ - val filter = getAppFilterLevel(app, authData) - if (filter.isUnFiltered()) { - fusedAppList.add( - app.transformToFusedApp().apply { - filterLevel = filter - } - ) - } + return FilterLevel.NONE + } + if (authData == null) { + return if (fusedApp.origin == Origin.GPLAY) FilterLevel.UNKNOWN + else FilterLevel.NONE } - }) - return Pair(fusedAppList, status) -} + if (!fusedApp.isFree && fusedApp.price.isBlank()) { + return FilterLevel.UI + } -/** - * 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] - */ -override suspend fun filterRestrictedGPlayApps( - authData: AuthData, - appList: List, -): ResultSupreme> { - val filteredFusedApps = mutableListOf() - val status = runCodeWithTimeout({ - appList.forEach { - val filter = getAppFilterLevel(it, authData) - if (filter.isUnFiltered()) { - filteredFusedApps.add( - it.transformToFusedApp().apply { - this.filterLevel = filter - } + if (fusedApp.restriction != Constants.Restriction.NOT_RESTRICTED) { + /* + * Check if app details can be shown. If not then remove the app from lists. + */ + try { + gplayRepository.getAppDetails(fusedApp.package_name) + } catch (e: Exception) { + return FilterLevel.DATA + } + + /* + * If the app can be shown, check if the app is downloadable. + * If not then change "Install" button to "N/A" + */ + try { + gplayRepository.getDownloadInfo( + fusedApp.package_name, + fusedApp.latest_version_code, + fusedApp.offer_type, ) + } catch (e: Exception) { + return FilterLevel.UI } + } else if (fusedApp.originalSize == 0L) { + return FilterLevel.UI } - }) - - return ResultSupreme.create(status, filteredFusedApps) -} - -/** - * Get different filter levels. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5720 - */ -override suspend fun getAppFilterLevel(fusedApp: FusedApp, authData: AuthData?): FilterLevel { - if (fusedApp.package_name.isBlank()) { - return FilterLevel.UNKNOWN - } - if (fusedApp.origin == Origin.CLEANAPK) { - /* - * Whitelist all open source apps. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5785 - */ return FilterLevel.NONE } - if (authData == null) { - return if (fusedApp.origin == Origin.GPLAY) FilterLevel.UNKNOWN - else FilterLevel.NONE - } - if (!fusedApp.isFree && fusedApp.price.isBlank()) { - return FilterLevel.UI + /* + * Similar to above method but uses Aurora OSS data class "App". + */ + override suspend fun getAppFilterLevel(app: App, authData: AuthData): FilterLevel { + return getAppFilterLevel(app.transformToFusedApp(), authData) } - if (fusedApp.restriction != Constants.Restriction.NOT_RESTRICTED) { - /* - * Check if app details can be shown. If not then remove the app from lists. - */ - try { - gplayRepository.getAppDetails(fusedApp.package_name) - } catch (e: Exception) { - return FilterLevel.DATA - } - - /* - * If the app can be shown, check if the app is downloadable. - * If not then change "Install" button to "N/A" - */ - try { - gplayRepository.getDownloadInfo( - fusedApp.package_name, - fusedApp.latest_version_code, - fusedApp.offer_type, - ) - } catch (e: Exception) { - return FilterLevel.UI - } - } else if (fusedApp.originalSize == 0L) { - return FilterLevel.UI + /* + * Handy method to run on an instance of FusedApp to update its filter level. + */ + private suspend fun FusedApp.updateFilterLevel(authData: AuthData?) { + this.filterLevel = getAppFilterLevel(this, authData) } - return FilterLevel.NONE -} -/* - * Similar to above method but uses Aurora OSS data class "App". - */ -override suspend fun getAppFilterLevel(app: App, authData: AuthData): FilterLevel { - return getAppFilterLevel(app.transformToFusedApp(), authData) -} + override suspend fun getApplicationDetails( + id: String, + packageName: String, + authData: AuthData, + origin: Origin + ): Pair { -/* - * Handy method to run on an instance of FusedApp to update its filter level. - */ -private suspend fun FusedApp.updateFilterLevel(authData: AuthData?) { - this.filterLevel = getAppFilterLevel(this, authData) -} + var response: FusedApp? = null -override suspend fun getApplicationDetails( - id: String, - packageName: String, - authData: AuthData, - origin: Origin -): Pair { + val status = runCodeWithTimeout({ + response = if (origin == Origin.CLEANAPK) { + (cleanApkAppsRepository.getAppDetails(id) as Response).body()?.app + } else { + val app = gplayRepository.getAppDetails(packageName) as App? + app?.transformToFusedApp() + } + response?.let { + it.updateStatus() + it.updateType() + it.updateSource() + it.updateFilterLevel(authData) + } + }) - var response: FusedApp? = null + return Pair(response ?: FusedApp(), status) + } - val status = runCodeWithTimeout({ - response = if (origin == Origin.CLEANAPK) { - (cleanApkAppsRepository.getAppDetails(id) as Response).body()?.app - } else { - val app = gplayRepository.getAppDetails(packageName) as App? - app?.transformToFusedApp() + /* + * 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 } - response?.let { - it.updateStatus() - it.updateType() - it.updateSource() - it.updateFilterLevel(authData) + + if (preferenceManagerModule.isPWASelected()) { + val pwaCategoriesResult = fetchPWACategories(type) + categoriesList.addAll(pwaCategoriesResult.second) + apiStatus = pwaCategoriesResult.first + errorApplicationCategory = pwaCategoriesResult.third } - }) - return Pair(response ?: FusedApp(), status) -} + if (preferenceManagerModule.isGplaySelected()) { + val gplayCategoryResult = fetchGplayCategories( + type, + ) + categoriesList.addAll(gplayCategoryResult.second) + apiStatus = gplayCategoryResult.first + errorApplicationCategory = gplayCategoryResult.third + } -/* - * 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 + return Pair(apiStatus, errorApplicationCategory) } - if (preferenceManagerModule.isPWASelected()) { - val pwaCategoriesResult = fetchPWACategories(type) - categoriesList.addAll(pwaCategoriesResult.second) - apiStatus = pwaCategoriesResult.first - errorApplicationCategory = pwaCategoriesResult.third + private suspend fun fetchGplayCategories( + type: CategoryType, + ): Triple, String> { + var errorApplicationCategory = "" + var apiStatus = ResultStatus.OK + val categoryList = mutableListOf() + runCodeWithTimeout({ + val playResponse = gplayRepository.getCategories(type).map { app -> + val category = app.transformToFusedCategory() + updateCategoryDrawable(category) + category + } + categoryList.addAll(playResponse) + }, { + errorApplicationCategory = APP_TYPE_ANY + apiStatus = ResultStatus.TIMEOUT + }, { + errorApplicationCategory = APP_TYPE_ANY + apiStatus = ResultStatus.UNKNOWN + }) + return Triple(apiStatus, categoryList, errorApplicationCategory) } - if (preferenceManagerModule.isGplaySelected()) { - val gplayCategoryResult = fetchGplayCategories( - type, - ) - categoriesList.addAll(gplayCategoryResult.second) - apiStatus = gplayCategoryResult.first - errorApplicationCategory = gplayCategoryResult.third + private suspend fun fetchPWACategories( + type: CategoryType, + ): Triple, String> { + var errorApplicationCategory = "" + var apiStatus: ResultStatus = ResultStatus.OK + val fusedCategoriesList = mutableListOf() + runCodeWithTimeout({ + getPWAsCategories()?.let { + fusedCategoriesList.addAll( + getFusedCategoryBasedOnCategoryType( + it, type, AppTag.PWA(context.getString(R.string.pwa)) + ) + ) + } + }, { + errorApplicationCategory = APP_TYPE_PWA + apiStatus = ResultStatus.TIMEOUT + }, { + errorApplicationCategory = APP_TYPE_PWA + apiStatus = ResultStatus.UNKNOWN + }) + return Triple(apiStatus, fusedCategoriesList, errorApplicationCategory) } - return Pair(apiStatus, errorApplicationCategory) -} + private suspend fun fetchOpenSourceCategories( + type: CategoryType, + ): Triple, String> { + var errorApplicationCategory = "" + var apiStatus: ResultStatus = ResultStatus.OK + val fusedCategoryList = mutableListOf() + runCodeWithTimeout({ + getOpenSourceCategories()?.let { + fusedCategoryList.addAll( + getFusedCategoryBasedOnCategoryType( + it, + type, + AppTag.OpenSource(context.getString(R.string.open_source)) + ) + ) + } + }, { + errorApplicationCategory = APP_TYPE_OPEN + apiStatus = ResultStatus.TIMEOUT + }, { + errorApplicationCategory = APP_TYPE_OPEN + apiStatus = ResultStatus.UNKNOWN + }) + return Triple(apiStatus, fusedCategoryList, errorApplicationCategory) + } -private suspend fun fetchGplayCategories( - type: CategoryType, -): Triple, String> { - var errorApplicationCategory = "" - var apiStatus = ResultStatus.OK - val categoryList = mutableListOf() - runCodeWithTimeout({ - val playResponse = gplayRepository.getCategories(type).map { app -> - val category = app.transformToFusedCategory() - updateCategoryDrawable(category) - category + /** + * Run a block of code with timeout. Returns status. + * + * @param block Main block to execute within [timeoutDurationInMillis] limit. + * @param timeoutBlock Optional code to execute in case of timeout. + * @param exceptionBlock Optional code to execute in case of an exception other than timeout. + * + * @return Instance of [ResultStatus] based on whether [block] was executed within timeout limit. + */ + private suspend fun runCodeWithTimeout( + block: suspend () -> Unit, + timeoutBlock: (() -> Unit)? = null, + exceptionBlock: ((e: Exception) -> Unit)? = null, + ): ResultStatus { + return try { + withTimeout(timeoutDurationInMillis) { + block() + } + ResultStatus.OK + } catch (e: TimeoutCancellationException) { + timeoutBlock?.invoke() + ResultStatus.TIMEOUT + } catch (e: Exception) { + e.printStackTrace() + exceptionBlock?.invoke(e) + ResultStatus.UNKNOWN.apply { + message = e.stackTraceToString() + } } - categoryList.addAll(playResponse) - }, { - errorApplicationCategory = APP_TYPE_ANY - apiStatus = ResultStatus.TIMEOUT - }, { - errorApplicationCategory = APP_TYPE_ANY - apiStatus = ResultStatus.UNKNOWN - }) - return Triple(apiStatus, categoryList, errorApplicationCategory) -} + } -private suspend fun fetchPWACategories( - type: CategoryType, -): Triple, String> { - var errorApplicationCategory = "" - var apiStatus: ResultStatus = ResultStatus.OK - val fusedCategoriesList = mutableListOf() - runCodeWithTimeout({ - getPWAsCategories()?.let { - fusedCategoriesList.addAll( - getFusedCategoryBasedOnCategoryType( - it, type, AppTag.PWA(context.getString(R.string.pwa)) - ) - ) - } - }, { - errorApplicationCategory = APP_TYPE_PWA - apiStatus = ResultStatus.TIMEOUT - }, { - errorApplicationCategory = APP_TYPE_PWA - apiStatus = ResultStatus.UNKNOWN - }) - return Triple(apiStatus, fusedCategoriesList, errorApplicationCategory) -} + private fun updateCategoryDrawable( + category: FusedCategory, + ) { + category.drawable = + getCategoryIconResource(getCategoryIconName(category)) + } -private suspend fun fetchOpenSourceCategories( - type: CategoryType, -): Triple, String> { - var errorApplicationCategory = "" - var apiStatus: ResultStatus = ResultStatus.OK - val fusedCategoryList = mutableListOf() - runCodeWithTimeout({ - getOpenSourceCategories()?.let { - fusedCategoryList.addAll( - getFusedCategoryBasedOnCategoryType( - it, - type, - AppTag.OpenSource(context.getString(R.string.open_source)) - ) - ) - } - }, { - errorApplicationCategory = APP_TYPE_OPEN - apiStatus = ResultStatus.TIMEOUT - }, { - errorApplicationCategory = APP_TYPE_OPEN - apiStatus = ResultStatus.UNKNOWN - }) - return Triple(apiStatus, fusedCategoryList, errorApplicationCategory) -} + private fun getCategoryIconName(category: FusedCategory): String { + var categoryTitle = if (category.tag.getOperationalTag() + .contentEquals(AppTag.GPlay().getOperationalTag()) + ) category.id else category.title -/** - * Run a block of code with timeout. Returns status. - * - * @param block Main block to execute within [timeoutDurationInMillis] limit. - * @param timeoutBlock Optional code to execute in case of timeout. - * @param exceptionBlock Optional code to execute in case of an exception other than timeout. - * - * @return Instance of [ResultStatus] based on whether [block] was executed within timeout limit. - */ -private suspend fun runCodeWithTimeout( - block: suspend () -> Unit, - timeoutBlock: (() -> Unit)? = null, - exceptionBlock: ((e: Exception) -> Unit)? = null, -): ResultStatus { - return try { - withTimeout(timeoutDurationInMillis) { - block() - } - ResultStatus.OK - } catch (e: TimeoutCancellationException) { - timeoutBlock?.invoke() - ResultStatus.TIMEOUT - } catch (e: Exception) { - e.printStackTrace() - exceptionBlock?.invoke(e) - ResultStatus.UNKNOWN.apply { - message = e.stackTraceToString() + if (categoryTitle.contains(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION)) { + categoryTitle = categoryTitle.replace(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION, "and") } + categoryTitle = categoryTitle.replace(' ', '_') + return categoryTitle.lowercase() } -} -private fun updateCategoryDrawable( - category: FusedCategory, -) { - category.drawable = - getCategoryIconResource(getCategoryIconName(category)) -} - -private fun getCategoryIconName(category: FusedCategory): String { - var categoryTitle = if (category.tag.getOperationalTag() - .contentEquals(AppTag.GPlay().getOperationalTag()) - ) category.id else category.title + private fun getFusedCategoryBasedOnCategoryType( + categories: Categories, + categoryType: CategoryType, + tag: AppTag + ): List { + return when (categoryType) { + CategoryType.APPLICATION -> { + getAppsCategoriesAsFusedCategory(categories, tag) + } - if (categoryTitle.contains(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION)) { - categoryTitle = categoryTitle.replace(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION, "and") + CategoryType.GAMES -> { + getGamesCategoriesAsFusedCategory(categories, tag) + } + } } - categoryTitle = categoryTitle.replace(' ', '_') - return categoryTitle.lowercase() -} -private fun getFusedCategoryBasedOnCategoryType( - categories: Categories, - categoryType: CategoryType, - tag: AppTag -): List { - return when (categoryType) { - CategoryType.APPLICATION -> { - getAppsCategoriesAsFusedCategory(categories, tag) + private fun getAppsCategoriesAsFusedCategory( + categories: Categories, + tag: AppTag + ): List { + return categories.apps.map { category -> + createFusedCategoryFromCategory(category, categories, tag) } + } - CategoryType.GAMES -> { - getGamesCategoriesAsFusedCategory(categories, tag) + private fun getGamesCategoriesAsFusedCategory( + categories: Categories, + tag: AppTag + ): List { + return categories.games.map { category -> + createFusedCategoryFromCategory(category, categories, tag) } } -} -private fun getAppsCategoriesAsFusedCategory( - categories: Categories, - tag: AppTag -): List { - return categories.apps.map { category -> - createFusedCategoryFromCategory(category, categories, tag) + private fun createFusedCategoryFromCategory( + category: String, + categories: Categories, + tag: AppTag + ): FusedCategory { + return FusedCategory( + id = category, + title = getCategoryTitle(category, categories), + drawable = getCategoryIconResource(category), + tag = tag + ) } -} -private fun getGamesCategoriesAsFusedCategory( - categories: Categories, - tag: AppTag -): List { - return categories.games.map { category -> - createFusedCategoryFromCategory(category, categories, tag) + private fun getCategoryIconResource(category: String): Int { + return CategoryUtils.provideAppsCategoryIconResource(category) } -} - -private fun createFusedCategoryFromCategory( - category: String, - categories: Categories, - tag: AppTag -): FusedCategory { - return FusedCategory( - 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 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 getPWAsCategories(): Categories? { - return cleanApkPWARepository.getCategories().body() -} + private suspend fun getOpenSourceCategories(): Categories? { + return cleanApkAppsRepository.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 getOpenSourceAppsResponse(category: String): Search? { - return cleanApkAppsRepository.getAppsByCategory( - category, - ).body() -} + private suspend fun getPWAAppsResponse(category: String): Search? { + return cleanApkPWARepository.getAppsByCategory( + category, + ).body() + } -private suspend fun getPWAAppsResponse(category: String): Search? { - return cleanApkPWARepository.getAppsByCategory( - category, - ).body() -} + private fun Category.transformToFusedCategory(): FusedCategory { + val id = this.browseUrl.substringAfter("cat=").substringBefore("&c=") + return FusedCategory( + id = id.lowercase(), + title = this.title, + browseUrl = this.browseUrl, + imageUrl = this.imageUrl, + ) + } -private fun Category.transformToFusedCategory(): FusedCategory { - val id = this.browseUrl.substringAfter("cat=").substringBefore("&c=") - return FusedCategory( - id = id.lowercase(), - title = this.title, - browseUrl = this.browseUrl, - imageUrl = this.imageUrl, - ) -} + /* + * Search-related internal functions + */ -/* - * Search-related internal functions - */ + private suspend fun getCleanAPKSearchResults( + keyword: String, + source: String = CleanApkRetrofit.APP_SOURCE_FOSS, + ): List { + val list = mutableListOf() + val response = + cleanApkAppsRepository.getSearchResult(keyword).body()?.apps -private suspend fun getCleanAPKSearchResults( - keyword: String, - source: String = CleanApkRetrofit.APP_SOURCE_FOSS, -): List { - val list = mutableListOf() - val response = - cleanApkAppsRepository.getSearchResult(keyword).body()?.apps - - response?.forEach { - it.updateStatus() - it.updateType() - it.source = - if (source.contentEquals(CleanApkRetrofit.APP_SOURCE_FOSS)) "Open Source" else "PWA" - list.add(it) + response?.forEach { + it.updateStatus() + it.updateType() + it.source = + if (source.contentEquals(CleanApkRetrofit.APP_SOURCE_FOSS)) "Open Source" else "PWA" + list.add(it) + } + return list } - return list -} -override suspend fun getGplaySearchResult( - query: String, - nextPageSubBundle: Set? -): Pair, Set> { - val searchResults = - gplayRepository.getSearchResult(query, nextPageSubBundle?.toMutableSet()) - if (!preferenceManagerModule.isGplaySelected()) { - return Pair(emptyList(), emptySet()) - } + override suspend fun getGplaySearchResult( + query: String, + nextPageSubBundle: Set? + ): GplaySearchResult { + try { + val searchResults = + gplayRepository.getSearchResult(query, nextPageSubBundle?.toMutableSet()) - val fusedAppList = searchResults.first.map { app -> replaceWithFDroid(app) }.toMutableList() - if (searchResults.second.isNotEmpty()) { - fusedAppList.add(FusedApp(isPlaceHolder = true)) - } + if (!preferenceManagerModule.isGplaySelected()) { + return ResultSupreme.Error(ERROR_GPLAY_SOURCE_NOT_SELECTED) + } - return Pair( - fusedAppList, - searchResults.second - ) -} + val fusedAppList = + searchResults.first.map { app -> replaceWithFDroid(app) }.toMutableList() + if (searchResults.second.isNotEmpty()) { + fusedAppList.add(FusedApp(isPlaceHolder = true)) + } -/* - * This function will replace a GPlay app with F-Droid app if exists, - * else will show the GPlay app itself. - */ -private suspend fun replaceWithFDroid(gPlayApp: App): FusedApp { - val gPlayFusedApp = gPlayApp.transformToFusedApp() - val response = fdroidWebInterface.getFdroidApp(gPlayFusedApp.package_name) - if (response.isSuccessful) { - val fdroidApp = getCleanApkPackageResult(gPlayFusedApp.package_name)?.apply { - updateSource() - isGplayReplaced = true + return ResultSupreme.Success(Pair(fusedAppList.toList(), searchResults.second.toSet())) + } catch (e: GplayHttpRequestException) { + val message = e.localizedMessage?.ifBlank { ERROR_GPLAY_SEARCH } ?: ERROR_GPLAY_SEARCH + val exception = GPlayException(e.status == 408, message) + return ResultSupreme.Error(message, exception) + } catch (e: Exception) { + val exception = + GPlayException(e is SocketTimeoutException, e.localizedMessage) + return ResultSupreme.Error(e.localizedMessage, exception) } - return fdroidApp ?: gPlayFusedApp } - return gPlayFusedApp -} - -/* - * Home screen-related internal functions - */ + /* + * This function will replace a GPlay app with F-Droid app if exists, + * else will show the GPlay app itself. + */ + private suspend fun replaceWithFDroid(gPlayApp: App): FusedApp { + val gPlayFusedApp = gPlayApp.transformToFusedApp() + val response = fdroidWebInterface.getFdroidApp(gPlayFusedApp.package_name) + if (response.isSuccessful) { + val fdroidApp = getCleanApkPackageResult(gPlayFusedApp.package_name)?.apply { + updateSource() + isGplayReplaced = true + } + return fdroidApp ?: gPlayFusedApp + } -private suspend fun generateCleanAPKHome(home: Home, appType: String): List { - val list = mutableListOf() - val headings = if (appType == APP_TYPE_OPEN) { - mapOf( - "top_updated_apps" to context.getString(R.string.top_updated_apps), - "top_updated_games" to context.getString(R.string.top_updated_games), - "popular_apps_in_last_24_hours" to context.getString(R.string.popular_apps_in_last_24_hours), - "popular_games_in_last_24_hours" to context.getString(R.string.popular_games_in_last_24_hours), - "discover" to context.getString(R.string.discover) - ) - } else { - mapOf( - "popular_apps" to context.getString(R.string.popular_apps), - "popular_games" to context.getString(R.string.popular_games), - "discover" to context.getString(R.string.discover_pwa) - ) + return gPlayFusedApp } - headings.forEach { (key, value) -> - when (key) { - "top_updated_apps" -> { - if (home.top_updated_apps.isNotEmpty()) { - home.top_updated_apps.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) + + /* + * Home screen-related internal functions + */ + + private suspend fun generateCleanAPKHome(home: Home, appType: String): List { + val list = mutableListOf() + val headings = if (appType == APP_TYPE_OPEN) { + mapOf( + "top_updated_apps" to context.getString(R.string.top_updated_apps), + "top_updated_games" to context.getString(R.string.top_updated_games), + "popular_apps_in_last_24_hours" to context.getString(R.string.popular_apps_in_last_24_hours), + "popular_games_in_last_24_hours" to context.getString(R.string.popular_games_in_last_24_hours), + "discover" to context.getString(R.string.discover) + ) + } else { + mapOf( + "popular_apps" to context.getString(R.string.popular_apps), + "popular_games" to context.getString(R.string.popular_games), + "discover" to context.getString(R.string.discover_pwa) + ) + } + headings.forEach { (key, value) -> + when (key) { + "top_updated_apps" -> { + if (home.top_updated_apps.isNotEmpty()) { + home.top_updated_apps.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) + } + list.add(FusedHome(value, home.top_updated_apps)) } - list.add(FusedHome(value, home.top_updated_apps)) } - } - "top_updated_games" -> { - if (home.top_updated_games.isNotEmpty()) { - home.top_updated_games.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) + "top_updated_games" -> { + if (home.top_updated_games.isNotEmpty()) { + home.top_updated_games.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) + } + list.add(FusedHome(value, home.top_updated_games)) } - list.add(FusedHome(value, home.top_updated_games)) } - } - "popular_apps" -> { - if (home.popular_apps.isNotEmpty()) { - home.popular_apps.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) + "popular_apps" -> { + if (home.popular_apps.isNotEmpty()) { + home.popular_apps.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) + } + list.add(FusedHome(value, home.popular_apps)) } - list.add(FusedHome(value, home.popular_apps)) } - } - "popular_games" -> { - if (home.popular_games.isNotEmpty()) { - home.popular_games.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) + "popular_games" -> { + if (home.popular_games.isNotEmpty()) { + home.popular_games.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) + } + list.add(FusedHome(value, home.popular_games)) } - list.add(FusedHome(value, home.popular_games)) } - } - "popular_apps_in_last_24_hours" -> { - if (home.popular_apps_in_last_24_hours.isNotEmpty()) { - home.popular_apps_in_last_24_hours.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) + "popular_apps_in_last_24_hours" -> { + if (home.popular_apps_in_last_24_hours.isNotEmpty()) { + home.popular_apps_in_last_24_hours.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) + } + list.add(FusedHome(value, home.popular_apps_in_last_24_hours)) } - list.add(FusedHome(value, home.popular_apps_in_last_24_hours)) } - } - "popular_games_in_last_24_hours" -> { - if (home.popular_games_in_last_24_hours.isNotEmpty()) { - home.popular_games_in_last_24_hours.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) + "popular_games_in_last_24_hours" -> { + if (home.popular_games_in_last_24_hours.isNotEmpty()) { + home.popular_games_in_last_24_hours.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) + } + list.add(FusedHome(value, home.popular_games_in_last_24_hours)) } - list.add(FusedHome(value, home.popular_games_in_last_24_hours)) } - } - "discover" -> { - if (home.discover.isNotEmpty()) { - home.discover.forEach { - it.updateStatus() - it.updateType() - it.updateFilterLevel(null) + "discover" -> { + if (home.discover.isNotEmpty()) { + home.discover.forEach { + it.updateStatus() + it.updateType() + it.updateFilterLevel(null) + } + list.add(FusedHome(value, home.discover)) } - list.add(FusedHome(value, home.discover)) } } } + return list.map { + it.source = appType + it + } } - return list.map { - it.source = appType - it - } -} -private suspend fun fetchGPlayHome(authData: AuthData): List { - val list = mutableListOf() - val gplayHomeData = gplayRepository.getHomeScreenData() as Map> - gplayHomeData.map { - val fusedApps = it.value.map { app -> - app.transformToFusedApp().apply { - updateFilterLevel(authData) + private suspend fun fetchGPlayHome(authData: AuthData): List { + val list = mutableListOf() + val gplayHomeData = gplayRepository.getHomeScreenData() as Map> + gplayHomeData.map { + val fusedApps = it.value.map { app -> + app.transformToFusedApp().apply { + updateFilterLevel(authData) + } } + list.add(FusedHome(it.key, fusedApps)) } - list.add(FusedHome(it.key, fusedApps)) + Timber.d("===> $list") + return list } - Timber.d("===> $list") - return list -} - -/* - * FusedApp-related internal extensions and functions - */ -private fun App.transformToFusedApp(): FusedApp { - val app = FusedApp( - _id = this.id.toString(), - author = this.developerName, - category = this.categoryName, - description = this.description, - perms = this.permissions, - icon_image_path = this.iconArtwork.url, - last_modified = this.updatedOn, - latest_version_code = this.versionCode, - latest_version_number = this.versionName, - name = this.displayName, - other_images_path = this.screenshots.transformToList(), - package_name = this.packageName, - ratings = Ratings( - usageQualityScore = - this.labeledRating.run { - if (isNotEmpty()) { - this.replace(",", ".").toDoubleOrNull() ?: -1.0 - } else -1.0 - } - ), - offer_type = this.offerType, - origin = Origin.GPLAY, - shareUrl = this.shareUrl, - originalSize = this.size, - appSize = Formatter.formatFileSize(context, this.size), - isFree = this.isFree, - price = this.price, - restriction = this.restriction, - ) - app.updateStatus() - return app -} + /* + * FusedApp-related internal extensions and functions + */ -/** - * Get fused app installation status. - * Applicable for both native apps and PWAs. - * - * Recommended to use this instead of [PkgManagerModule.getPackageStatus]. - */ -override fun getFusedAppInstallationStatus(fusedApp: FusedApp): Status { - return if (fusedApp.is_pwa) { - pwaManagerModule.getPwaStatus(fusedApp) - } else { - pkgManagerModule.getPackageStatus(fusedApp.package_name, fusedApp.latest_version_code) + private fun App.transformToFusedApp(): FusedApp { + val app = FusedApp( + _id = this.id.toString(), + author = this.developerName, + category = this.categoryName, + description = this.description, + perms = this.permissions, + icon_image_path = this.iconArtwork.url, + last_modified = this.updatedOn, + latest_version_code = this.versionCode, + latest_version_number = this.versionName, + name = this.displayName, + other_images_path = this.screenshots.transformToList(), + package_name = this.packageName, + ratings = Ratings( + usageQualityScore = + this.labeledRating.run { + if (isNotEmpty()) { + this.replace(",", ".").toDoubleOrNull() ?: -1.0 + } else -1.0 + } + ), + offer_type = this.offerType, + origin = Origin.GPLAY, + shareUrl = this.shareUrl, + originalSize = this.size, + appSize = Formatter.formatFileSize(context, this.size), + isFree = this.isFree, + price = this.price, + restriction = this.restriction, + ) + app.updateStatus() + return app } -} -private fun FusedApp.updateStatus() { - if (this.status != Status.INSTALLATION_ISSUE) { - this.status = getFusedAppInstallationStatus(this) + /** + * Get fused app installation status. + * Applicable for both native apps and PWAs. + * + * Recommended to use this instead of [PkgManagerModule.getPackageStatus]. + */ + override fun getFusedAppInstallationStatus(fusedApp: FusedApp): Status { + return if (fusedApp.is_pwa) { + pwaManagerModule.getPwaStatus(fusedApp) + } else { + pkgManagerModule.getPackageStatus(fusedApp.package_name, fusedApp.latest_version_code) + } } -} -private fun FusedApp.updateType() { - this.type = if (this.is_pwa) Type.PWA else Type.NATIVE -} + private fun FusedApp.updateStatus() { + if (this.status != Status.INSTALLATION_ISSUE) { + this.status = getFusedAppInstallationStatus(this) + } + } -private fun FusedApp.updateSource() { - this.apply { - source = if (origin == Origin.CLEANAPK && is_pwa) context.getString(R.string.pwa) - else if (origin == Origin.CLEANAPK) context.getString(R.string.open_source) - else "" + private fun FusedApp.updateType() { + this.type = if (this.is_pwa) Type.PWA else Type.NATIVE } -} -private fun MutableList.transformToList(): List { - val list = mutableListOf() - this.forEach { - list.add(it.url) + private fun FusedApp.updateSource() { + this.apply { + source = if (origin == Origin.CLEANAPK && is_pwa) context.getString(R.string.pwa) + else if (origin == Origin.CLEANAPK) context.getString(R.string.open_source) + else "" + } } - return list -} -/** - * @return true, if any change is found, otherwise false - */ -override fun isHomeDataUpdated( - newHomeData: List, - oldHomeData: List -): Boolean { - if (newHomeData.size != oldHomeData.size) { - return true + private fun MutableList.transformToList(): List { + val list = mutableListOf() + this.forEach { + list.add(it.url) + } + return list } - oldHomeData.forEach { - val fusedHome = newHomeData[oldHomeData.indexOf(it)] - if (!it.title.contentEquals(fusedHome.title) || areFusedAppsUpdated(it, fusedHome)) { + /** + * @return true, if any change is found, otherwise false + */ + override fun isHomeDataUpdated( + newHomeData: List, + oldHomeData: List + ): Boolean { + if (newHomeData.size != oldHomeData.size) { return true } - } - return false -} -private fun areFusedAppsUpdated( - oldFusedHome: FusedHome, - newFusedHome: FusedHome, -): Boolean { - val fusedAppDiffUtil = HomeChildFusedAppDiffUtil() - if (oldFusedHome.list.size != newFusedHome.list.size) { - return true + oldHomeData.forEach { + val fusedHome = newHomeData[oldHomeData.indexOf(it)] + if (!it.title.contentEquals(fusedHome.title) || areFusedAppsUpdated(it, fusedHome)) { + return true + } + } + return false } - oldFusedHome.list.forEach { oldFusedApp -> - val indexOfOldFusedApp = oldFusedHome.list.indexOf(oldFusedApp) - val fusedApp = newFusedHome.list[indexOfOldFusedApp] - if (!fusedAppDiffUtil.areContentsTheSame(oldFusedApp, fusedApp)) { + private fun areFusedAppsUpdated( + oldFusedHome: FusedHome, + newFusedHome: FusedHome, + ): Boolean { + val fusedAppDiffUtil = HomeChildFusedAppDiffUtil() + if (oldFusedHome.list.size != newFusedHome.list.size) { return true } - } - return false -} -/** - * @return returns true if there is changes in data, otherwise false - */ -override fun isAnyFusedAppUpdated( - newFusedApps: List, - oldFusedApps: List -): Boolean { - val fusedAppDiffUtil = HomeChildFusedAppDiffUtil() - if (newFusedApps.size != oldFusedApps.size) { - return true + oldFusedHome.list.forEach { oldFusedApp -> + val indexOfOldFusedApp = oldFusedHome.list.indexOf(oldFusedApp) + val fusedApp = newFusedHome.list[indexOfOldFusedApp] + if (!fusedAppDiffUtil.areContentsTheSame(oldFusedApp, fusedApp)) { + return true + } + } + return false } - newFusedApps.forEach { - val indexOfNewFusedApp = newFusedApps.indexOf(it) - if (!fusedAppDiffUtil.areContentsTheSame(it, oldFusedApps[indexOfNewFusedApp])) { + /** + * @return returns true if there is changes in data, otherwise false + */ + override fun isAnyFusedAppUpdated( + newFusedApps: List, + oldFusedApps: List + ): Boolean { + val fusedAppDiffUtil = HomeChildFusedAppDiffUtil() + if (newFusedApps.size != oldFusedApps.size) { return true } - } - return false -} -override fun isAnyAppInstallStatusChanged(currentList: List): Boolean { - currentList.forEach { - if (it.status == Status.INSTALLATION_ISSUE) { - return@forEach - } - val currentAppStatus = - pkgManagerModule.getPackageStatus(it.package_name, it.latest_version_code) - if (it.status != currentAppStatus) { - return true + newFusedApps.forEach { + val indexOfNewFusedApp = newFusedApps.indexOf(it) + if (!fusedAppDiffUtil.areContentsTheSame(it, oldFusedApps[indexOfNewFusedApp])) { + return true + } } + return false } - return false -} -override fun isOpenSourceSelected() = preferenceManagerModule.isOpenSourceSelected() -override suspend fun getGplayAppsByCategory( - authData: AuthData, - category: String, - pageUrl: String? -): ResultSupreme, String>> { - var fusedAppList: MutableList = mutableListOf() - var nextPageUrl = "" - - val status = runCodeWithTimeout({ - val streamCluster = - gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster - val filteredAppList = filterRestrictedGPlayApps(authData, streamCluster.clusterAppList) - filteredAppList.data?.let { - fusedAppList = it.toMutableList() + override fun isAnyAppInstallStatusChanged(currentList: List): Boolean { + currentList.forEach { + if (it.status == Status.INSTALLATION_ISSUE) { + return@forEach + } + val currentAppStatus = + pkgManagerModule.getPackageStatus(it.package_name, it.latest_version_code) + if (it.status != currentAppStatus) { + return true + } } + return false + } - nextPageUrl = streamCluster.clusterNextPageUrl - if (!nextPageUrl.isNullOrEmpty()) { - fusedAppList.add(FusedApp(isPlaceHolder = true)) - } - }) + override fun isOpenSourceSelected() = preferenceManagerModule.isOpenSourceSelected() + override suspend fun getGplayAppsByCategory( + authData: AuthData, + category: String, + pageUrl: String? + ): ResultSupreme, String>> { + var fusedAppList: MutableList = mutableListOf() + var nextPageUrl = "" + + val status = runCodeWithTimeout({ + val streamCluster = + gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster + val filteredAppList = filterRestrictedGPlayApps(authData, streamCluster.clusterAppList) + filteredAppList.data?.let { + fusedAppList = it.toMutableList() + } - return ResultSupreme.create(status, Pair(fusedAppList, nextPageUrl)) -} + nextPageUrl = streamCluster.clusterNextPageUrl + if (!nextPageUrl.isNullOrEmpty()) { + fusedAppList.add(FusedApp(isPlaceHolder = true)) + } + }) + + return ResultSupreme.create(status, Pair(fusedAppList, nextPageUrl)) + } } diff --git a/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt index 33a3b8370..28d82ade3 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt @@ -41,6 +41,7 @@ import timber.log.Timber import java.io.IOException import java.net.SocketTimeoutException import java.net.UnknownHostException +import java.util.concurrent.TimeUnit import javax.inject.Inject class GPlayHttpClient @Inject constructor( @@ -52,10 +53,12 @@ class GPlayHttpClient @Inject constructor( companion object { private const val TAG = "GPlayHttpClient" + private const val HTTP_TIMEOUT_IN_SECOND = 10L } private val okHttpClient = OkHttpClient().newBuilder() .retryOnConnectionFailure(false) + .callTimeout(HTTP_TIMEOUT_IN_SECOND, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) .cache(cache) @@ -155,6 +158,13 @@ class GPlayHttpClient @Inject constructor( val call = okHttpClient.newCall(request) buildPlayResponse(call.execute()) } catch (e: Exception) { + //TODO: exception will be thrown for all apis when all gplay api implementation + // will handle the exceptions. this will be done in following issue. + // Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/1483 + if (request.url.toString().contains("search")) { + throw e + } + when (e) { is UnknownHostException, is SocketTimeoutException -> handleExceptionOnGooglePlayRequest(e) @@ -185,6 +195,13 @@ class GPlayHttpClient @Inject constructor( Timber.d("$TAG: Url: ${response.request.url}\nStatus: $code") + //TODO: exception will be thrown for all apis when all gplay api implementation + // will handle the exceptions. this will be done in following issue. + // Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/1483 + if (response.request.url.toString().contains("search") && code != 200) { + throw GplayHttpRequestException(code, response.message) + } + if (code == 401) { MainScope().launch { EventBus.invokeEvent( @@ -203,3 +220,5 @@ class GPlayHttpClient @Inject constructor( } } } + +class GplayHttpRequestException (val status: Int, message: String) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt index 604365df4..ab9c35e14 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt @@ -41,6 +41,7 @@ import foundation.e.apps.install.updates.UpdatesNotifier import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import foundation.e.apps.utils.getFormattedString +import foundation.e.apps.utils.isNetworkAvailable import kotlinx.coroutines.flow.transformWhile import timber.log.Timber import java.text.NumberFormat @@ -122,7 +123,7 @@ class AppInstallProcessor @Inject constructor( return } - if (!isNetworkAvailable()) { + if (!context.isNetworkAvailable()) { fusedManagerRepository.installationIssue(fusedDownload) EventBus.invokeEvent(AppEvent.NoInternetEvent(false)) return @@ -185,22 +186,6 @@ class AppInstallProcessor @Inject constructor( return statFs.availableBytes } - private fun isNetworkAvailable(): Boolean { - val connectivityManager = - context.getSystemService(ConnectivityManager::class.java) - val capabilities = - connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) - ?: return false - - if (capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - ) { - return true - } - - return false - } - suspend fun processInstall( fusedDownloadId: String, isItUpdateWork: Boolean, diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index 5d69a446e..7ad25fba7 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -59,6 +59,7 @@ import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.applicationlist.ApplicationListRVAdapter import foundation.e.apps.ui.parentFragment.TimeoutFragment +import foundation.e.apps.utils.isNetworkAvailable import kotlinx.coroutines.launch import javax.inject.Inject @@ -137,6 +138,9 @@ class SearchFragment : override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) if (!recyclerView.canScrollVertically(1)) { + if (!requireContext().isNetworkAvailable()) { + return + } searchViewModel.loadMore(searchText) } } diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 6eb448377..2b8bd4aea 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -32,10 +32,10 @@ import foundation.e.apps.data.fused.data.FusedApp import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.exceptions.CleanApkException import foundation.e.apps.data.login.exceptions.GPlayException +import foundation.e.apps.data.login.exceptions.UnknownSourceException import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject @@ -56,6 +56,10 @@ class SearchViewModel @Inject constructor( private var isLoading: Boolean = false + companion object { + private const val DATA_LOAD_ERROR = "Data load error" + } + fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { viewModelScope.launch(Dispatchers.IO) { if (gPlayAuth.result.isSuccess()) @@ -98,7 +102,11 @@ class SearchViewModel @Inject constructor( * without having to wait for all of the apps. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ - private fun getSearchResults(query: String, authData: AuthData, lifecycleOwner: LifecycleOwner) { + private fun getSearchResults( + query: String, + authData: AuthData, + lifecycleOwner: LifecycleOwner + ) { viewModelScope.launch(Dispatchers.IO) { val searchResultSupreme = fusedAPIRepository.getSearchResults(query, authData) @@ -109,17 +117,16 @@ class SearchViewModel @Inject constructor( if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) { GPlayException( searchResultSupreme.isTimeout(), - searchResultSupreme.message.ifBlank { "Data load error" } + searchResultSupreme.message.ifBlank { DATA_LOAD_ERROR } ) } else { CleanApkException( searchResultSupreme.isTimeout(), - searchResultSupreme.message.ifBlank { "Data load error" } + searchResultSupreme.message.ifBlank { DATA_LOAD_ERROR } ) } - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) + handleException(exception) } nextSubBundle = null @@ -132,6 +139,7 @@ class SearchViewModel @Inject constructor( Timber.d("Serach result is loading....") return } + viewModelScope.launch(Dispatchers.IO) { fetchGplayData(query) } @@ -140,27 +148,30 @@ class SearchViewModel @Inject constructor( private suspend fun fetchGplayData(query: String) { isLoading = true val gplaySearchResult = fusedAPIRepository.getGplaySearchResults(query, nextSubBundle) - nextSubBundle = gplaySearchResult.second - val searchResult = searchResult.value - val currentAppList = searchResult?.data?.first?.toMutableList() ?: mutableListOf() - currentAppList.removeIf { item -> item.isPlaceHolder } - currentAppList.addAll(gplaySearchResult.first) - val finalResult = if (searchResult is ResultSupreme.Success) { - ResultSupreme.Success( - Pair( - currentAppList.toList(), - gplaySearchResult.second.isNotEmpty() - ) - ) - } else { - ResultSupreme.Error() + if (!gplaySearchResult.isSuccess()) { + handleException(gplaySearchResult.exception ?: UnknownSourceException()) } + nextSubBundle = gplaySearchResult.data?.second + + val currentSearchResult = searchResult.value?.data + val currentAppList = currentSearchResult?.first?.toMutableList() ?: mutableListOf() + currentAppList.removeIf { item -> item.isPlaceHolder } + currentAppList.addAll(gplaySearchResult.data?.first ?: emptyList()) + + val finalResult = ResultSupreme.Success( + Pair(currentAppList.toList(), nextSubBundle?.isNotEmpty() ?: false) + ) this@SearchViewModel.searchResult.postValue(finalResult) isLoading = false } + private fun handleException(exception: Exception) { + exceptionsList.add(exception) + exceptionsLiveData.postValue(exceptionsList) + } + /** * @return returns true if there is changes in data, otherwise false */ diff --git a/app/src/main/java/foundation/e/apps/utils/Extensions.kt b/app/src/main/java/foundation/e/apps/utils/Extensions.kt index 047cd03a6..32fa5f85e 100644 --- a/app/src/main/java/foundation/e/apps/utils/Extensions.kt +++ b/app/src/main/java/foundation/e/apps/utils/Extensions.kt @@ -1,6 +1,8 @@ package foundation.e.apps.utils import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import androidx.appcompat.app.AlertDialog import foundation.e.apps.R import java.text.SimpleDateFormat @@ -23,3 +25,19 @@ fun Context.showGoogleSignInAlertDialog( .setNegativeButton(R.string.cancel) { _, _ -> onCancelClickListener() } .show() } + +fun Context.isNetworkAvailable(): Boolean { + val connectivityManager = + this.getSystemService(ConnectivityManager::class.java) + val capabilities = + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + ?: return false + + if (capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + ) { + return true + } + + return false +} -- GitLab From d6cf164e8daff3c2fe62ba93c7d3b5bbe4f0c5ab Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Tue, 22 Aug 2023 10:45:47 +0600 Subject: [PATCH 5/7] refactor: code cleanup --- app/build.gradle | 4 +- .../e/apps/data/fused/FusedApiImpl.kt | 36 +---- .../e/apps/data/gplay/GplayStoreRepository.kt | 1 - .../data/gplay/GplayStoreRepositoryImpl.kt | 150 ++---------------- .../apps/data/gplay/utils/GPlayHttpClient.kt | 6 +- .../workmanager/AppInstallProcessor.kt | 2 - .../e/apps/ui/search/SearchFragment.kt | 2 +- .../e/apps/ui/search/SearchViewModel.kt | 16 +- 8 files changed, 31 insertions(+), 186 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3414490ee..ce11c6234 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,8 +90,8 @@ android { buildTypes { debug { -// versionNameSuffix ".debug" -// applicationIdSuffix ".debug" + versionNameSuffix ".debug" + applicationIdSuffix ".debug" signingConfig signingConfigs.debugConfig proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt index 06d09d36c..4c74996bd 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt @@ -21,7 +21,6 @@ package foundation.e.apps.data.fused import android.content.Context import android.text.format.Formatter import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.map import com.aurora.gplayapi.Constants @@ -65,19 +64,15 @@ import foundation.e.apps.data.fused.utils.CategoryUtils import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.gplay.GplayStoreRepository import foundation.e.apps.data.gplay.utils.GplayHttpRequestException -import foundation.e.apps.data.gplay.utils.runFlowWithTimeout import foundation.e.apps.data.login.exceptions.GPlayException -import foundation.e.apps.data.login.exceptions.UnknownSourceException import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule import foundation.e.apps.ui.home.model.HomeChildFusedAppDiffUtil -import foundation.e.apps.ui.search.SearchViewModel import kotlinx.coroutines.Deferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withTimeout import retrofit2.Response @@ -290,15 +285,6 @@ class FusedApiImpl @Inject constructor( ) } -// if (preferenceManagerModule.isGplaySelected()) { -// emitSource( -// fetchGplaySearchResults( -// query, -// searchResult, -// packageSpecificResults -// ).asLiveData() -// ) -// } return finalSearchResult } @@ -335,26 +321,6 @@ class FusedApiImpl @Inject constructor( ) } -// private suspend fun fetchGplaySearchResults( -// query: String, -// searchResult: MutableList, -// packageSpecificResults: ArrayList -// ): GplaySearchResultFlow = getGplaySearchResult(query).map { -// if (it.first.isNotEmpty()) { -// searchResult.addAll(it.first) -// } -// ResultSupreme.Success( -// Pair( -// filterWithKeywordSearch( -// searchResult, -// packageSpecificResults, -// query -// ), -// it.second -// ) -// ) -// } - private suspend fun fetchOpenSourceSearchResult( cleanApkResults: MutableList, query: String, @@ -985,7 +951,7 @@ class FusedApiImpl @Inject constructor( private fun getCategoryIconName(category: FusedCategory): String { var categoryTitle = if (category.tag.getOperationalTag() - .contentEquals(AppTag.GPlay().getOperationalTag()) + .contentEquals(AppTag.GPlay().getOperationalTag()) ) category.id else category.title if (categoryTitle.contains(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION)) { diff --git a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepository.kt b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepository.kt index 90c1e5b6a..5ad24dc43 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepository.kt @@ -25,7 +25,6 @@ import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.SearchBundle import foundation.e.apps.data.BaseStoreRepository import foundation.e.apps.data.fused.utils.CategoryType -import kotlinx.coroutines.flow.Flow interface GplayStoreRepository : BaseStoreRepository { suspend fun getSearchResult(query: String, subBundle: MutableSet?): Pair, MutableSet> diff --git a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt index ac19e6588..c6097eec3 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt @@ -39,10 +39,6 @@ import foundation.e.apps.data.fused.utils.CategoryType import foundation.e.apps.data.gplay.utils.GPlayHttpClient import foundation.e.apps.data.login.LoginSourceRepository import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject @@ -77,75 +73,26 @@ class GplayStoreRepositoryImpl @Inject constructor( context.getString(R.string.movers_shakers_games) to mapOf(Chart.MOVERS_SHAKERS to TopChartsHelper.Type.GAME), ) -// override suspend fun getSearchResult( -// query: String, -// ): Flow, Boolean>> { -// return flow { -// -// /* -// * Variable names and logic made same as that of Aurora store. -// * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 -// */ -// var authData = loginSourceRepository.gplayAuth ?: return@flow -// -// val searchHelper = -// SearchHelper(authData).using(gPlayHttpClient) -// val searchBundle = searchHelper.searchResults(query) -// -// val initialReplacedList = mutableListOf() -// val INITIAL_LIMIT = 4 -// -// emitReplacedList( -// this@flow, -// initialReplacedList, -// INITIAL_LIMIT, -// searchBundle, -// true, -// ) -// -// var nextSubBundleSet: MutableSet -// do { -// nextSubBundleSet = fetchNextSubBundle( -// searchBundle, -// searchHelper, -// this@flow, -// initialReplacedList, -// INITIAL_LIMIT -// ) -// } while (nextSubBundleSet.isNotEmpty()) -// -// /* -// * If initialReplacedList size is less than INITIAL_LIMIT, -// * it means the results were very less and nothing has been emitted so far. -// * Hence emit the list. -// */ -// if (initialReplacedList.size < INITIAL_LIMIT) { -// emitInMain(this@flow, initialReplacedList, false) -// } -// }.flowOn(Dispatchers.IO) -// } - override suspend fun getSearchResult( query: String, subBundle: MutableSet? ): Pair, MutableSet> { - var authData = loginSourceRepository.gplayAuth ?: return Pair(emptyList(), mutableSetOf()) - val searchHelper = - SearchHelper(authData).using(gPlayHttpClient) - Timber.d("Fetching search result for $query, subBundle: $subBundle") - - subBundle?.let { - val searchResult = searchHelper.next(it) - Timber.d("fetching next page search data...") - return emitSearchResult(searchResult) - } - - val searchResult = searchHelper.searchResults(query) - return emitSearchResult(searchResult) + var authData = loginSourceRepository.gplayAuth ?: return Pair(emptyList(), mutableSetOf()) + val searchHelper = + SearchHelper(authData).using(gPlayHttpClient) + Timber.d("Fetching search result for $query, subBundle: $subBundle") + + subBundle?.let { + val searchResult = searchHelper.next(it) + Timber.d("fetching next page search data...") + return getSearchResultPair(searchResult) + } + val searchResult = searchHelper.searchResults(query) + return getSearchResultPair(searchResult) } - private fun emitSearchResult( + private fun getSearchResultPair( searchBundle: SearchBundle ): Pair, MutableSet> { val apps = searchBundle.appList @@ -153,31 +100,6 @@ class GplayStoreRepositoryImpl @Inject constructor( return Pair(apps, searchBundle.subBundles) } - private suspend fun fetchNextSubBundle( - searchBundle: SearchBundle, - searchHelper: SearchHelper, - scope: FlowCollector, Boolean>>, - accumulationList: MutableList, - accumulationLimit: Int, - ): MutableSet { - val nextSubBundleSet = searchBundle.subBundles - val newSearchBundle = searchHelper.next(nextSubBundleSet) - if (newSearchBundle.appList.isNotEmpty()) { - searchBundle.apply { - subBundles.clear() - subBundles.addAll(newSearchBundle.subBundles) - emitReplacedList( - scope, - accumulationList, - accumulationLimit, - newSearchBundle, - nextSubBundleSet.isNotEmpty(), - ) - } - } - return nextSubBundleSet - } - override suspend fun getSearchSuggestions(query: String): List { val authData = loginSourceRepository.gplayAuth ?: return listOf() @@ -243,52 +165,6 @@ class GplayStoreRepositoryImpl @Inject constructor( return if (type == CategoryType.APPLICATION) Category.Type.APPLICATION else Category.Type.GAME } - private suspend fun emitReplacedList( - scope: FlowCollector, Boolean>>, - accumulationList: MutableList, - accumulationLimit: Int, - searchBundle: SearchBundle, - moreToEmit: Boolean, - ) { - searchBundle.appList.forEach { - when { - accumulationList.size < accumulationLimit - 1 -> { - /* - * If initial limit is 4, add apps to list (without emitting) - * till 2 apps. - */ - accumulationList.add(it) - } - - accumulationList.size == accumulationLimit - 1 -> { - /* - * If initial limit is 4, and we have reached till 3 apps, - * add the 4th app and emit the list. - */ - accumulationList.add(it) - scope.emit(Pair(accumulationList, moreToEmit)) - emitInMain(scope, accumulationList, moreToEmit) - } - - accumulationList.size == accumulationLimit -> { - /* - * If initial limit is 4, and we have emitted 4 apps, - * for all rest of the apps, emit each app one by one. - */ - emitInMain(scope, listOf(it), moreToEmit) - } - } - } - } - - private suspend fun emitInMain( - scope: FlowCollector, Boolean>>, - it: List, - moreToEmit: Boolean - ) { - scope.emit(Pair(it, moreToEmit)) - } - private suspend fun getTopApps( type: TopChartsHelper.Type, chart: Chart, diff --git a/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt index 28d82ade3..ffaff547c 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt @@ -158,7 +158,7 @@ class GPlayHttpClient @Inject constructor( val call = okHttpClient.newCall(request) buildPlayResponse(call.execute()) } catch (e: Exception) { - //TODO: exception will be thrown for all apis when all gplay api implementation + // TODO: exception will be thrown for all apis when all gplay api implementation // will handle the exceptions. this will be done in following issue. // Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/1483 if (request.url.toString().contains("search")) { @@ -195,7 +195,7 @@ class GPlayHttpClient @Inject constructor( Timber.d("$TAG: Url: ${response.request.url}\nStatus: $code") - //TODO: exception will be thrown for all apis when all gplay api implementation + // TODO: exception will be thrown for all apis when all gplay api implementation // will handle the exceptions. this will be done in following issue. // Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/1483 if (response.request.url.toString().contains("search") && code != 200) { @@ -221,4 +221,4 @@ class GPlayHttpClient @Inject constructor( } } -class GplayHttpRequestException (val status: Int, message: String) : Exception(message) \ No newline at end of file +class GplayHttpRequestException(val status: Int, message: String) : Exception(message) diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt index ab9c35e14..8abb41398 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt @@ -19,8 +19,6 @@ package foundation.e.apps.install.workmanager import android.content.Context -import android.net.ConnectivityManager -import android.net.NetworkCapabilities import android.os.Environment import android.os.StatFs import com.aurora.gplayapi.exceptions.ApiException diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index 7ad25fba7..191ab1340 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -76,7 +76,7 @@ class SearchFragment : private var _binding: FragmentSearchBinding? = null private val binding get() = _binding!! - private val searchViewModel: SearchViewModel by viewModels() + protected val searchViewModel: SearchViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() override val mainActivityViewModel: MainActivityViewModel by activityViewModels() diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 2b8bd4aea..d4920ce6d 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -28,6 +28,7 @@ import com.aurora.gplayapi.data.models.SearchBundle import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.fused.FusedAPIRepository +import foundation.e.apps.data.fused.GplaySearchResult import foundation.e.apps.data.fused.data.FusedApp import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.exceptions.CleanApkException @@ -155,18 +156,23 @@ class SearchViewModel @Inject constructor( nextSubBundle = gplaySearchResult.data?.second - val currentSearchResult = searchResult.value?.data - val currentAppList = currentSearchResult?.first?.toMutableList() ?: mutableListOf() - currentAppList.removeIf { item -> item.isPlaceHolder } - currentAppList.addAll(gplaySearchResult.data?.first ?: emptyList()) - + val currentAppList = updateCurrentAppList(gplaySearchResult) val finalResult = ResultSupreme.Success( Pair(currentAppList.toList(), nextSubBundle?.isNotEmpty() ?: false) ) + this@SearchViewModel.searchResult.postValue(finalResult) isLoading = false } + private fun updateCurrentAppList(gplaySearchResult: GplaySearchResult): MutableList { + val currentSearchResult = searchResult.value?.data + val currentAppList = currentSearchResult?.first?.toMutableList() ?: mutableListOf() + currentAppList.removeIf { item -> item.isPlaceHolder } + currentAppList.addAll(gplaySearchResult.data?.first ?: emptyList()) + return currentAppList + } + private fun handleException(exception: Exception) { exceptionsList.add(exception) exceptionsLiveData.postValue(exceptionsList) -- GitLab From 1980f59fa742397a10ba09cb2ac1543157a96a75 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Tue, 22 Aug 2023 11:51:24 +0600 Subject: [PATCH 6/7] fixed: unit test --- .../foundation/e/apps/FusedApiImplTest.kt | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt index 9858c4900..f03472d7d 100644 --- a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt +++ b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt @@ -24,6 +24,7 @@ import com.aurora.gplayapi.Constants import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category +import com.aurora.gplayapi.data.models.SearchBundle import foundation.e.apps.data.cleanapk.data.categories.Categories import foundation.e.apps.data.cleanapk.data.search.Search import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository @@ -40,10 +41,7 @@ import foundation.e.apps.data.gplay.GplayStoreRepository import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule import foundation.e.apps.util.MainCoroutineRule -import foundation.e.apps.util.getOrAwaitValue import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.After @@ -763,10 +761,8 @@ class FusedApiImplTest { preferenceManagerModule.isPWASelectedFake = true preferenceManagerModule.isOpenSourceelectedFake = true preferenceManagerModule.isGplaySelectedFake = true - val gplayFlow: Flow, Boolean>> = flowOf( - Pair( - listOf(App("a.b.c"), App("c.d.e"), App("d.e.f"), App("d.e.g")), false - ) + val gplayFlow: Pair, MutableSet> = Pair( + listOf(App("a.b.c"), App("c.d.e"), App("d.e.f"), App("d.e.g")), mutableSetOf() ) setupMockingSearchApp( @@ -774,7 +770,7 @@ class FusedApiImplTest { ) val searchResultLiveData = - fusedAPIImpl.getSearchResults("com.search.package", AUTH_DATA).getOrAwaitValue() + fusedAPIImpl.getSearchResults("com.search.package", AUTH_DATA) val size = searchResultLiveData.data?.first?.size ?: -2 assertEquals("getSearchResult", 8, size) @@ -783,7 +779,7 @@ class FusedApiImplTest { private suspend fun setupMockingSearchApp( packageNameSearchResponse: Response?, gplayPackageResult: App, - gplayLivedata: Flow, Boolean>>, + gplayLivedata: Pair, MutableSet>, willThrowException: Boolean = false ) { Mockito.`when`(pwaManagerModule.getPwaStatus(any())).thenReturn(Status.UNAVAILABLE) @@ -816,9 +812,11 @@ class FusedApiImplTest { ) ).thenReturn(packageNameSearchResponse) - Mockito.`when`(fdroidWebInterface.getFdroidApp(any())).thenReturn(Response.error(404, "".toResponseBody(null))) + Mockito.`when`(fdroidWebInterface.getFdroidApp(any())) + .thenReturn(Response.error(404, "".toResponseBody(null))) - Mockito.`when`(gPlayAPIRepository.getSearchResult(eq("com.search.package"),)).thenReturn(gplayLivedata) + Mockito.`when`(gPlayAPIRepository.getSearchResult(eq("com.search.package"), null)) + .thenReturn(gplayLivedata) } @Ignore("Dependencies are not mockable") @@ -852,10 +850,8 @@ class FusedApiImplTest { val packageNameSearchResponse = Response.success(searchResult) val gplayPackageResult = App("com.search.package") - val gplayFlow: Flow, Boolean>> = flowOf( - Pair( - listOf(App("a.b.c"), App("c.d.e"), App("d.e.f"), App("d.e.g")), false - ) + val gplayFlow: Pair, MutableSet> = Pair( + listOf(App("a.b.c"), App("c.d.e"), App("d.e.f"), App("d.e.g")), mutableSetOf() ) setupMockingSearchApp( @@ -867,7 +863,7 @@ class FusedApiImplTest { preferenceManagerModule.isGplaySelectedFake = true val searchResultLiveData = - fusedAPIImpl.getSearchResults("com.search.package", AUTH_DATA).getOrAwaitValue() + fusedAPIImpl.getSearchResults("com.search.package", AUTH_DATA) val size = searchResultLiveData.data?.first?.size ?: -2 assertEquals("getSearchResult", 4, size) -- GitLab From ae359801d5d56f356da48e800c940d0caebffbaf Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Tue, 22 Aug 2023 12:47:19 +0600 Subject: [PATCH 7/7] refactor: applied coding conventions --- .../e/apps/data/fused/FusedAPIRepository.kt | 4 ++-- .../java/foundation/e/apps/data/fused/FusedApi.kt | 7 +++---- .../foundation/e/apps/data/fused/FusedApiImpl.kt | 15 +++++++++++---- .../e/apps/data/gplay/GplayStoreRepositoryImpl.kt | 1 + .../e/apps/data/gplay/utils/GPlayHttpClient.kt | 5 +++-- .../e/apps/ui/search/SearchViewModel.kt | 4 ++-- .../java/foundation/e/apps/utils/Extensions.kt | 1 + .../java/foundation/e/apps/FusedApiImplTest.kt | 4 ++-- 8 files changed, 25 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt index 7c45bb5cf..00dd50683 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt @@ -108,11 +108,11 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedApi) return fusedAPIImpl.getSearchSuggestions(query) } - suspend fun getSearchResults( + suspend fun getCleanApkSearchResults( query: String, authData: AuthData ): ResultSupreme, Boolean>> { - return fusedAPIImpl.getSearchResults(query, authData) + return fusedAPIImpl.getCleanApkSearchResults(query, authData) } suspend fun getGplaySearchResults( diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt index d0a11c20e..7267f634b 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt @@ -58,11 +58,10 @@ interface FusedApi { * Fetches search results from cleanAPK and GPlay servers and returns them * @param query Query * @param authData [AuthData] - * @return A livedata Pair of list of non-nullable [FusedApp] and - * a Boolean signifying if more search results are being loaded. - * Observe this livedata to display new apps as they are fetched from the network. + * @return ResultSupreme which contains a Pair, Boolean> where List + * is the app list and [Boolean] indicates more data to load or not. */ - suspend fun getSearchResults( + suspend fun getCleanApkSearchResults( query: String, authData: AuthData ): ResultSupreme, Boolean>> diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt index 4c74996bd..16b3d2cef 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt @@ -100,7 +100,7 @@ class FusedApiImpl @Inject constructor( 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 ERROR_GPLAY_SEARCH = "Gplay search is failed!" + private const val ERROR_GPLAY_SEARCH = "Gplay search has failed!" private const val ERROR_GPLAY_SOURCE_NOT_SELECTED = "Gplay apps are not selected!" } @@ -247,7 +247,7 @@ class FusedApiImpl @Inject constructor( * a Boolean signifying if more search results are being loaded. * Observe this livedata to display new apps as they are fetched from the network. */ - override suspend fun getSearchResults( + override suspend fun getCleanApkSearchResults( query: String, authData: AuthData ): ResultSupreme, Boolean>> { @@ -1086,19 +1086,25 @@ class FusedApiImpl @Inject constructor( val fusedAppList = searchResults.first.map { app -> replaceWithFDroid(app) }.toMutableList() + if (searchResults.second.isNotEmpty()) { fusedAppList.add(FusedApp(isPlaceHolder = true)) } return ResultSupreme.Success(Pair(fusedAppList.toList(), searchResults.second.toSet())) } catch (e: GplayHttpRequestException) { - val message = e.localizedMessage?.ifBlank { ERROR_GPLAY_SEARCH } ?: ERROR_GPLAY_SEARCH + val message = ( + e.localizedMessage?.ifBlank { ERROR_GPLAY_SEARCH } + ?: ERROR_GPLAY_SEARCH + ) + "Status: ${e.status}" + val exception = GPlayException(e.status == 408, message) return ResultSupreme.Error(message, exception) } catch (e: Exception) { val exception = GPlayException(e is SocketTimeoutException, e.localizedMessage) - return ResultSupreme.Error(e.localizedMessage, exception) + + return ResultSupreme.Error(e.localizedMessage ?: "", exception) } } @@ -1407,6 +1413,7 @@ class FusedApiImpl @Inject constructor( val status = runCodeWithTimeout({ val streamCluster = gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster + val filteredAppList = filterRestrictedGPlayApps(authData, streamCluster.clusterAppList) filteredAppList.data?.let { fusedAppList = it.toMutableList() diff --git a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt index c6097eec3..19461db72 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt @@ -80,6 +80,7 @@ class GplayStoreRepositoryImpl @Inject constructor( var authData = loginSourceRepository.gplayAuth ?: return Pair(emptyList(), mutableSetOf()) val searchHelper = SearchHelper(authData).using(gPlayHttpClient) + Timber.d("Fetching search result for $query, subBundle: $subBundle") subBundle?.let { diff --git a/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt index ffaff547c..296b15d23 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt @@ -54,6 +54,7 @@ class GPlayHttpClient @Inject constructor( companion object { private const val TAG = "GPlayHttpClient" private const val HTTP_TIMEOUT_IN_SECOND = 10L + private const val SEARCH = "search" } private val okHttpClient = OkHttpClient().newBuilder() @@ -161,7 +162,7 @@ class GPlayHttpClient @Inject constructor( // TODO: exception will be thrown for all apis when all gplay api implementation // will handle the exceptions. this will be done in following issue. // Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/1483 - if (request.url.toString().contains("search")) { + if (request.url.toString().contains(SEARCH)) { throw e } @@ -198,7 +199,7 @@ class GPlayHttpClient @Inject constructor( // TODO: exception will be thrown for all apis when all gplay api implementation // will handle the exceptions. this will be done in following issue. // Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/1483 - if (response.request.url.toString().contains("search") && code != 200) { + if (response.request.url.toString().contains(SEARCH) && code != 200) { throw GplayHttpRequestException(code, response.message) } diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index d4920ce6d..e1c740ccc 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -109,7 +109,7 @@ class SearchViewModel @Inject constructor( lifecycleOwner: LifecycleOwner ) { viewModelScope.launch(Dispatchers.IO) { - val searchResultSupreme = fusedAPIRepository.getSearchResults(query, authData) + val searchResultSupreme = fusedAPIRepository.getCleanApkSearchResults(query, authData) searchResult.postValue(searchResultSupreme) @@ -137,7 +137,7 @@ class SearchViewModel @Inject constructor( fun loadMore(query: String) { if (isLoading) { - Timber.d("Serach result is loading....") + Timber.d("Search result is loading....") return } diff --git a/app/src/main/java/foundation/e/apps/utils/Extensions.kt b/app/src/main/java/foundation/e/apps/utils/Extensions.kt index 32fa5f85e..aa763f6a8 100644 --- a/app/src/main/java/foundation/e/apps/utils/Extensions.kt +++ b/app/src/main/java/foundation/e/apps/utils/Extensions.kt @@ -29,6 +29,7 @@ fun Context.showGoogleSignInAlertDialog( fun Context.isNetworkAvailable(): Boolean { val connectivityManager = this.getSystemService(ConnectivityManager::class.java) + val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) ?: return false diff --git a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt index f03472d7d..3096c444c 100644 --- a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt +++ b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt @@ -770,7 +770,7 @@ class FusedApiImplTest { ) val searchResultLiveData = - fusedAPIImpl.getSearchResults("com.search.package", AUTH_DATA) + fusedAPIImpl.getCleanApkSearchResults("com.search.package", AUTH_DATA) val size = searchResultLiveData.data?.first?.size ?: -2 assertEquals("getSearchResult", 8, size) @@ -863,7 +863,7 @@ class FusedApiImplTest { preferenceManagerModule.isGplaySelectedFake = true val searchResultLiveData = - fusedAPIImpl.getSearchResults("com.search.package", AUTH_DATA) + fusedAPIImpl.getCleanApkSearchResults("com.search.package", AUTH_DATA) val size = searchResultLiveData.data?.first?.size ?: -2 assertEquals("getSearchResult", 4, size) -- GitLab