diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt index 8130bf9ae6fc480db5c45f3106073c803a498c07..aaeaab8180b5d058ebcbf4a6216baacae524f311 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt @@ -21,6 +21,9 @@ package foundation.e.apps.api.fused import android.content.Context import android.text.format.Formatter import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.liveData +import androidx.lifecycle.map import com.aurora.gplayapi.Constants import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App @@ -176,30 +179,43 @@ class FusedAPIImpl @Inject constructor( * Fetches search results from cleanAPK and GPlay servers and returns them * @param query Query * @param authData [AuthData] - * @return A list of nullable [FusedApp] + * @return A livedata list of non-nullable [FusedApp]. + * Observe this livedata to display new apps as they are fetched from the network. */ - suspend fun getSearchResults(query: String, authData: AuthData): List { - val fusedResponse = mutableListOf() - - when (preferenceManagerModule.preferredApplicationType()) { - APP_TYPE_ANY -> { - fusedResponse.addAll(getCleanAPKSearchResults(query)) - fusedResponse.addAll(getGplaySearchResults(query, authData)) - } - APP_TYPE_OPEN -> { - fusedResponse.addAll(getCleanAPKSearchResults(query)) - } - APP_TYPE_PWA -> { - fusedResponse.addAll( - getCleanAPKSearchResults( - query, - CleanAPKInterface.APP_SOURCE_ANY, - CleanAPKInterface.APP_TYPE_PWA + fun getSearchResults(query: String, authData: AuthData): LiveData> { + /* + * 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 { + when (preferenceManagerModule.preferredApplicationType()) { + APP_TYPE_ANY -> { + val cleanApkResults = getCleanAPKSearchResults(query) + if (cleanApkResults.isNotEmpty()) { + /* + * If cleanapk results are empty, dont emit emit data as it may + * briefly show "No apps found..." + */ + emit(cleanApkResults) + } + emitSource(getGplayAndCleanapkCombinedResults(query, authData, cleanApkResults)) + } + APP_TYPE_OPEN -> { + emit(getCleanAPKSearchResults(query)) + } + APP_TYPE_PWA -> { + emit( + getCleanAPKSearchResults( + query, + CleanAPKInterface.APP_SOURCE_ANY, + CleanAPKInterface.APP_TYPE_PWA + ) ) - ) + } } } - return fusedResponse.distinctBy { it.package_name } + } suspend fun getSearchSuggestions(query: String, authData: AuthData): List { @@ -589,10 +605,27 @@ class FusedAPIImpl @Inject constructor( return list } - private suspend fun getGplaySearchResults(query: String, authData: AuthData): List { + /* + * Function to return a livedata with value from cleanapk and Google Play store combined. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 + */ + private fun getGplayAndCleanapkCombinedResults( + query: String, + authData: AuthData, + cleanApkResults: List + ): LiveData> { + val localList = ArrayList(cleanApkResults) + return getGplaySearchResults(query, authData).map { list -> + localList.apply { + addAll(list) + }.distinctBy { it.package_name } + } + } + + private fun getGplaySearchResults(query: String, authData: AuthData): LiveData> { val searchResults = gPlayAPIRepository.getSearchResults(query, authData) - return searchResults.map { app -> - app.transformToFusedApp() + return searchResults.map { + it.map { app -> app.transformToFusedApp() } } } diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt index 745dd0773b25bb9ab2ef2ee11be3896266e83ae7..1eada63247193e80034143c3804903917a5b7a79 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt @@ -18,6 +18,7 @@ package foundation.e.apps.api.fused +import androidx.lifecycle.LiveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category @@ -91,7 +92,7 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.fetchAuthData(email, aasToken) } - suspend fun getSearchResults(query: String, authData: AuthData): List { + fun getSearchResults(query: String, authData: AuthData): LiveData> { return fusedAPIImpl.getSearchResults(query, authData) } diff --git a/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt b/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt index f367cdaba8c0bc87a50010fad952193d86458c07..af6ef1c5c7bfaef98e2c945f44ff52b0b75165e9 100644 --- a/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt +++ b/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt @@ -19,6 +19,8 @@ package foundation.e.apps.api.gplay import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.liveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData @@ -82,29 +84,37 @@ class GPlayAPIImpl @Inject constructor( return searchData.filter { it.suggestedQuery.isNotBlank() } } - suspend fun getSearchResults(query: String, authData: AuthData): List { - val searchData = mutableListOf() - withContext(Dispatchers.IO) { - val searchHelper = SearchHelper(authData).using(gPlayHttpClient) - val searchResult = searchHelper.searchResults(query) - searchData.addAll(searchResult.appList) + fun getSearchResults(query: String, authData: AuthData): LiveData> { + /* + * Send livedata to improve UI performance, so we don't have to wait for loading all results. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 + */ + return liveData { + withContext(Dispatchers.IO) { + /* + * Variable names and logic made same as that of Aurora store. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 + */ + val searchHelper = SearchHelper(authData).using(gPlayHttpClient) + val searchBundle = searchHelper.searchResults(query) + + emit(searchBundle.appList) - // Fetch more results in case the given result is a promoted app - if (searchData.size == 1) { - val bundleSet: MutableSet = searchResult.subBundles + var nextSubBundleSet: MutableSet do { - val searchBundle = searchHelper.next(bundleSet) - if (searchBundle.appList.isNotEmpty()) { - searchData.addAll(searchBundle.appList) - } - bundleSet.apply { - clear() - addAll(searchBundle.subBundles) + nextSubBundleSet = searchBundle.subBundles + val newSearchBundle = searchHelper.next(nextSubBundleSet) + if (newSearchBundle.appList.isNotEmpty()) { + searchBundle.apply { + subBundles.clear() + subBundles.addAll(newSearchBundle.subBundles) + appList.addAll(newSearchBundle.appList) + emit(searchBundle.appList) + } } - } while (bundleSet.isNotEmpty()) + } while (nextSubBundleSet.isNotEmpty()) } } - return searchData } suspend fun getDownloadInfo( diff --git a/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt b/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt index 56243a6a50c334d513c0f345cd3f76080b971492..f268a4f9bde255453b448be69e480a2cf675fa00 100644 --- a/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt @@ -18,6 +18,7 @@ package foundation.e.apps.api.gplay +import androidx.lifecycle.LiveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData @@ -46,7 +47,7 @@ class GPlayAPIRepository @Inject constructor( return gPlayAPIImpl.getSearchSuggestions(query, authData) } - suspend fun getSearchResults(query: String, authData: AuthData): List { + fun getSearchResults(query: String, authData: AuthData): LiveData> { return gPlayAPIImpl.getSearchResults(query, authData) } diff --git a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt index 7cf486ab4591862ac24369b1fc72ee4a70a1440e..63bea5d6adc2b114c545eba43f9365bfb55e41bb 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt @@ -79,6 +79,7 @@ class SearchFragment : private val appProgressViewModel: AppProgressViewModel by viewModels() private val SUGGESTION_KEY = "suggestion" + private var lastSearch = "" private var searchView: SearchView? = null private var shimmerLayout: ShimmerFrameLayout? = null @@ -193,7 +194,19 @@ class SearchFragment : } listAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - recyclerView!!.scrollToPosition(0) + searchView?.run { + /* + * Only scroll back to 0 position for a new search. + * + * If we are getting new results from livedata for the old search query, + * do not scroll to top as the user may be scrolling to see already + * populated results. + */ + if (lastSearch != query?.toString()) { + recyclerView?.scrollToPosition(0) + lastSearch = query.toString() + } + } } }) } @@ -218,7 +231,7 @@ class SearchFragment : shimmerLayout?.visibility = View.VISIBLE recyclerView?.visibility = View.GONE noAppsFoundLayout?.visibility = View.GONE - mainActivityViewModel.authData.value?.let { searchViewModel.getSearchResults(text, it) } + mainActivityViewModel.authData.value?.let { searchViewModel.getSearchResults(text, it, this) } } return false } diff --git a/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt index 8749977ad0954339e0e5b549e93c34d50c7907a8..054528562986f3ff86ce9caddb7ba65b1e921620 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt @@ -18,6 +18,7 @@ package foundation.e.apps.search +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -44,9 +45,17 @@ class SearchViewModel @Inject constructor( } } - fun getSearchResults(query: String, authData: AuthData) { - viewModelScope.launch(Dispatchers.IO) { - searchResult.postValue(fusedAPIRepository.getSearchResults(query, authData)) + /* + * Observe data from Fused API and publish the result in searchResult. + * This allows us to show apps as they are being fetched from the network, + * 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) { + fusedAPIRepository.getSearchResults(query, authData).observe(lifecycleOwner) { + searchResult.postValue(it) + } } } }