From c838b7c454befec354b5ed3e5f8da1089dba68fc Mon Sep 17 00:00:00 2001 From: SayantanRC Date: Fri, 20 May 2022 17:36:15 +0530 Subject: [PATCH 1/8] App lounge: (issue_5171) update GPlayAPIImpl.kt getSearchResults() --- .../e/apps/api/gplay/GPlayAPIImpl.kt | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) 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 f367cdaba..c244ca574 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 @@ -85,24 +85,27 @@ class GPlayAPIImpl @Inject constructor( suspend fun getSearchResults(query: String, authData: AuthData): List { val searchData = mutableListOf() 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 searchResult = searchHelper.searchResults(query) - searchData.addAll(searchResult.appList) + val searchBundle = searchHelper.searchResults(query) - // Fetch more results in case the given result is a promoted app - if (searchData.size == 1) { - val bundleSet: MutableSet = searchResult.subBundles - do { - val searchBundle = searchHelper.next(bundleSet) - if (searchBundle.appList.isNotEmpty()) { - searchData.addAll(searchBundle.appList) - } - bundleSet.apply { - clear() - addAll(searchBundle.subBundles) + var nextSubBundleSet: MutableSet + do { + nextSubBundleSet = searchBundle.subBundles + val newSearchBundle = searchHelper.next(nextSubBundleSet) + if (newSearchBundle.appList.isNotEmpty()) { + searchBundle.apply { + subBundles.clear() + subBundles.addAll(newSearchBundle.subBundles) + appList.addAll(newSearchBundle.appList) } - } while (bundleSet.isNotEmpty()) - } + } + } while (nextSubBundleSet.isNotEmpty()) + + searchData.addAll(searchBundle.appList) } return searchData } -- GitLab From a1be203ccb5b42b32e55985c02dd7f762f85e005 Mon Sep 17 00:00:00 2001 From: SayantanRC Date: Wed, 25 May 2022 18:22:49 +0530 Subject: [PATCH 2/8] issue_5171 [WIP]: Pass GPlay search results as LiveData from GPlayAPIImpl. Signatures of all methods need to be changed. --- .../java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt | 11 +++++++---- .../foundation/e/apps/api/gplay/GPlayAPIRepository.kt | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) 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 c244ca574..fc08e89d9 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.MutableLiveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData @@ -82,8 +84,8 @@ class GPlayAPIImpl @Inject constructor( return searchData.filter { it.suggestedQuery.isNotBlank() } } - suspend fun getSearchResults(query: String, authData: AuthData): List { - val searchData = mutableListOf() + suspend fun getSearchResults(query: String, authData: AuthData): LiveData> { + val searchData: MutableLiveData> = MutableLiveData() withContext(Dispatchers.IO) { /* * Variable names and logic made same as that of Aurora store. @@ -92,6 +94,8 @@ class GPlayAPIImpl @Inject constructor( val searchHelper = SearchHelper(authData).using(gPlayHttpClient) val searchBundle = searchHelper.searchResults(query) + searchData.postValue(searchBundle.appList) + var nextSubBundleSet: MutableSet do { nextSubBundleSet = searchBundle.subBundles @@ -101,11 +105,10 @@ class GPlayAPIImpl @Inject constructor( subBundles.clear() subBundles.addAll(newSearchBundle.subBundles) appList.addAll(newSearchBundle.appList) + searchData.postValue(searchBundle.appList) } } } while (nextSubBundleSet.isNotEmpty()) - - searchData.addAll(searchBundle.appList) } return searchData } 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 56243a6a5..9d7a2d40d 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 { + suspend fun getSearchResults(query: String, authData: AuthData): LiveData> { return gPlayAPIImpl.getSearchResults(query, authData) } -- GitLab From e6be0a2ae505ab68ee2134b6db0ea4c5f719f7eb Mon Sep 17 00:00:00 2001 From: SayantanRC Date: Thu, 26 May 2022 00:04:01 +0530 Subject: [PATCH 3/8] issue_5171 [WIP]: return liveData{} from GPlayAPIImpl.getSearchResults() instead of previous MutableLiveData. Make function non-suspend. --- .../e/apps/api/gplay/GPlayAPIImpl.kt | 48 +++++++++---------- .../e/apps/api/gplay/GPlayAPIRepository.kt | 2 +- 2 files changed, 25 insertions(+), 25 deletions(-) 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 fc08e89d9..471d8f9ea 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 @@ -20,7 +20,7 @@ package foundation.e.apps.api.gplay import android.content.Context import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.liveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData @@ -84,33 +84,33 @@ class GPlayAPIImpl @Inject constructor( return searchData.filter { it.suggestedQuery.isNotBlank() } } - suspend fun getSearchResults(query: String, authData: AuthData): LiveData> { - val searchData: MutableLiveData> = MutableLiveData() - 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) + fun getSearchResults(query: String, authData: AuthData): LiveData> { + 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) - searchData.postValue(searchBundle.appList) + emit(searchBundle.appList) - var nextSubBundleSet: MutableSet - do { - nextSubBundleSet = searchBundle.subBundles - val newSearchBundle = searchHelper.next(nextSubBundleSet) - if (newSearchBundle.appList.isNotEmpty()) { - searchBundle.apply { - subBundles.clear() - subBundles.addAll(newSearchBundle.subBundles) - appList.addAll(newSearchBundle.appList) - searchData.postValue(searchBundle.appList) + var nextSubBundleSet: MutableSet + do { + 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 (nextSubBundleSet.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 9d7a2d40d..f268a4f9b 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 @@ -47,7 +47,7 @@ class GPlayAPIRepository @Inject constructor( return gPlayAPIImpl.getSearchSuggestions(query, authData) } - suspend fun getSearchResults(query: String, authData: AuthData): LiveData> { + fun getSearchResults(query: String, authData: AuthData): LiveData> { return gPlayAPIImpl.getSearchResults(query, authData) } -- GitLab From b960b483a7df1d8182b89df5cda895b301c0e1a1 Mon Sep 17 00:00:00 2001 From: SayantanRC Date: Thu, 26 May 2022 00:27:51 +0530 Subject: [PATCH 4/8] issue_5171 [WIP]: return livedata{} from FusedAPIImpl.getSearchResults() instead of just List, update the function signature in FusedAPIRepository --- .../e/apps/api/fused/FusedAPIImpl.kt | 73 +++++++++++++------ .../e/apps/api/fused/FusedAPIRepository.kt | 3 +- 2 files changed, 52 insertions(+), 24 deletions(-) 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 8130bf9ae..56373ec4d 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,37 @@ 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) + 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 +599,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 745dd0773..1eada6324 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) } -- GitLab From dfcec8ad6fe8361575a0642d9cb2803d37ca057b Mon Sep 17 00:00:00 2001 From: SayantanRC Date: Thu, 26 May 2022 00:31:33 +0530 Subject: [PATCH 5/8] issue_5171: update SearchViewModel and SearchFragment to properly observe the data Fused API --- .../foundation/e/apps/search/SearchFragment.kt | 2 +- .../foundation/e/apps/search/SearchViewModel.kt | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) 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 7cf486ab4..348b0c5c2 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt @@ -218,7 +218,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 8749977ad..054528562 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) + } } } } -- GitLab From b30a6b6f79d5b108ff4e3fce0aaba109a089a4fc Mon Sep 17 00:00:00 2001 From: SayantanRC Date: Thu, 26 May 2022 01:01:26 +0530 Subject: [PATCH 6/8] issue_5171: emit cleanapk results before gplay results only cleanapk results are not empty --- .../main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 56373ec4d..aaeaab818 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 @@ -192,7 +192,13 @@ class FusedAPIImpl @Inject constructor( when (preferenceManagerModule.preferredApplicationType()) { APP_TYPE_ANY -> { val cleanApkResults = getCleanAPKSearchResults(query) - emit(cleanApkResults) + 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 -> { -- GitLab From c0c8a56c8cb7a1cbb8f4bca7ff55764153afb942 Mon Sep 17 00:00:00 2001 From: SayantanRC Date: Thu, 26 May 2022 01:04:50 +0530 Subject: [PATCH 7/8] issue_5171: fix scrolling to top for each new livedata results for the same query --- .../foundation/e/apps/search/SearchFragment.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 348b0c5c2..63bea5d6a 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() + } + } } }) } -- GitLab From f707a623386a2758f76c38e1dd5d3dd92b5d61b6 Mon Sep 17 00:00:00 2001 From: SayantanRC Date: Thu, 26 May 2022 01:10:35 +0530 Subject: [PATCH 8/8] issue_5171: minor documentation --- app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 471d8f9ea..af6ef1c5c 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 @@ -85,6 +85,10 @@ class GPlayAPIImpl @Inject constructor( } 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) { /* -- GitLab