Loading app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +125 −34 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -205,15 +208,23 @@ 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 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. */ suspend fun getSearchResults(query: String, authData: AuthData): Pair<List<FusedApp>, ResultStatus> { val fusedResponse = ArrayList<FusedApp>() fun getSearchResults( query: String, authData: AuthData ): LiveData<ResultSupreme<Pair<List<FusedApp>, 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<FusedApp>() val status = runCodeBlockWithTimeout({ try { if (preferenceManagerModule.preferredApplicationType() == APP_TYPE_ANY) { try { /* Loading @@ -236,40 +247,95 @@ class FusedAPIImpl @Inject constructor( packageSpecificResults.add(it.data!!) } } } catch (_: Exception) {} }) /* * 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) { emit(ResultSupreme.create(status, Pair(packageSpecificResults, true))) return@liveData } /* * 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. */ fun filterWithKeywordSearch(list: List<FusedApp>): List<FusedApp> { val filteredResults = list.distinctBy { it.package_name } .filter { packageSpecificResults.isEmpty() || it.package_name != query } return packageSpecificResults + filteredResults } val cleanApkResults = mutableListOf<FusedApp>() when (preferenceManagerModule.preferredApplicationType()) { APP_TYPE_ANY -> { fusedResponse.addAll(getCleanAPKSearchResults(query)) fusedResponse.addAll(getGplaySearchResults(query, authData)) val status = runCodeBlockWithTimeout({ cleanApkResults.addAll(getCleanAPKSearchResults(query)) }) if (cleanApkResults.isNotEmpty() || status != ResultStatus.OK) { /* * If cleanapk results are empty, dont emit emit data as it may * briefly show "No apps found..." * If status is timeout, then do emit the value. * Send true in the pair to signal more results (i.e from GPlay) being loaded. */ emit( ResultSupreme.create( status, Pair(filterWithKeywordSearch(cleanApkResults), true) ) ) } emitSource( getGplayAndCleanapkCombinedResults(query, authData, cleanApkResults).map { /* * We are assuming that there will be no timeout here. * If there had to be any timeout, it would already have happened * while fetching package specific results. */ ResultSupreme.Success(Pair(filterWithKeywordSearch(it.first), it.second)) } ) } APP_TYPE_OPEN -> { fusedResponse.addAll(getCleanAPKSearchResults(query)) val status = runCodeBlockWithTimeout({ cleanApkResults.addAll(getCleanAPKSearchResults(query)) }) /* * Send false in pair to signal no more results to load, as only cleanapk * results are fetched, we don't have to wait for GPlay results. */ emit( ResultSupreme.create( status, Pair(filterWithKeywordSearch(cleanApkResults), false) ) ) } APP_TYPE_PWA -> { fusedResponse.addAll( val status = runCodeBlockWithTimeout({ cleanApkResults.addAll( getCleanAPKSearchResults( query, CleanAPKInterface.APP_SOURCE_ANY, CleanAPKInterface.APP_TYPE_PWA ) ) } } }) /* * 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. * Send false in pair to signal no more results to load, as only cleanapk * results are fetched for PWAs. */ val filteredResults = fusedResponse.distinctBy { it.package_name } .filter { packageSpecificResults.isEmpty() || it.package_name != query } return Pair(packageSpecificResults + filteredResults, status) emit(ResultSupreme.create(status, Pair(cleanApkResults, false))) } } } } /* Loading Loading @@ -889,10 +955,35 @@ class FusedAPIImpl @Inject constructor( return list } private suspend fun getGplaySearchResults(query: String, authData: AuthData): List<FusedApp> { /* * 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<FusedApp> ): LiveData<Pair<List<FusedApp>, Boolean>> { val localList = ArrayList<FusedApp>(cleanApkResults) return getGplaySearchResults(query, authData).map { pair -> Pair( localList.apply { addAll(pair.first) }.distinctBy { it.package_name }, pair.second ) } } private fun getGplaySearchResults( query: String, authData: AuthData ): LiveData<Pair<List<FusedApp>, Boolean>> { val searchResults = gPlayAPIRepository.getSearchResults(query, authData) return searchResults.map { app -> app.transformToFusedApp() return searchResults.map { Pair( it.first.map { app -> app.transformToFusedApp() }, it.second ) } } Loading app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +2 −1 Original line number Diff line number Diff line Loading @@ -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.App import com.aurora.gplayapi.data.models.AuthData Loading Loading @@ -111,7 +112,7 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.fetchAuthData(email, aasToken) } suspend fun getSearchResults(query: String, authData: AuthData): Pair<List<FusedApp>, ResultStatus> { fun getSearchResults(query: String, authData: AuthData): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> { return fusedAPIImpl.getSearchResults(query, authData) } Loading app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt +32 −18 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -98,29 +100,41 @@ class GPlayAPIImpl @Inject constructor( return searchData.filter { it.suggestedQuery.isNotBlank() } } suspend fun getSearchResults(query: String, authData: AuthData): List<App> { val searchData = mutableListOf<App>() /** * Sends livedata of list of apps being loaded from search and a boolean * signifying if more data is to be loaded. */ fun getSearchResults(query: String, authData: AuthData): LiveData<Pair<List<App>, Boolean>> { /* * 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 searchResult = searchHelper.searchResults(query) searchData.addAll(searchResult.appList) val searchBundle = searchHelper.searchResults(query) emit(Pair(searchBundle.appList, true)) // Fetch more results in case the given result is a promoted app if (searchData.size == 1) { val bundleSet: MutableSet<SearchBundle.SubBundle> = searchResult.subBundles var nextSubBundleSet: MutableSet<SearchBundle.SubBundle> do { val searchBundle = searchHelper.next(bundleSet) if (searchBundle.appList.isNotEmpty()) { searchData.addAll(searchBundle.appList) 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(Pair(searchBundle.appList, nextSubBundleSet.isNotEmpty())) } bundleSet.apply { clear() addAll(searchBundle.subBundles) } } while (bundleSet.isNotEmpty()) } while (nextSubBundleSet.isNotEmpty()) } } return searchData } suspend fun getDownloadInfo( Loading app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt +2 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -48,7 +49,7 @@ class GPlayAPIRepository @Inject constructor( return gPlayAPIImpl.getSearchSuggestions(query, authData) } suspend fun getSearchResults(query: String, authData: AuthData): List<App> { fun getSearchResults(query: String, authData: AuthData): LiveData<Pair<List<App>, Boolean>> { return gPlayAPIImpl.getSearchResults(query, authData) } Loading app/src/main/java/foundation/e/apps/search/SearchFragment.kt +23 −7 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.view.inputmethod.InputMethodManager import android.widget.ImageView import android.widget.LinearLayout import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.cursoradapter.widget.CursorAdapter import androidx.cursoradapter.widget.SimpleCursorAdapter import androidx.fragment.app.activityViewModels Loading Loading @@ -82,6 +83,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 Loading Loading @@ -162,7 +164,7 @@ class SearchFragment : mainActivityViewModel.downloadList.observe(viewLifecycleOwner) { list -> val searchList = searchViewModel.searchResult.value?.first?.toMutableList() ?: emptyList() searchViewModel.searchResult.value?.data?.first?.toMutableList() ?: emptyList() searchList.let { mainActivityViewModel.updateStatusOfFusedApps(searchList, list) } Loading @@ -171,7 +173,7 @@ class SearchFragment : * Done in one line, so that on Ctrl+click on searchResult, * we can see that it is being updated here. */ searchViewModel.searchResult.apply { value = Pair(searchList, value?.second) } searchViewModel.searchResult.apply { value?.setData(Pair(searchList, value?.data?.second ?: false)) } } /* Loading @@ -191,19 +193,32 @@ class SearchFragment : } searchViewModel.searchResult.observe(viewLifecycleOwner) { if (it.first.isNullOrEmpty()) { if (it.data?.first.isNullOrEmpty()) { noAppsFoundLayout?.visibility = View.VISIBLE } else { listAdapter?.setData(it.first) listAdapter?.setData(it.data!!.first) binding.loadingProgressBar.isVisible = it.data!!.second stopLoadingUI() noAppsFoundLayout?.visibility = View.GONE } 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() } } } }) if (searchText.isNotBlank() && it.second != ResultStatus.OK) { if (searchText.isNotBlank() && !it.isSuccess()) { /* * If blank check is not performed then timeout dialog keeps * popping up whenever search tab is opened. Loading @@ -215,6 +230,7 @@ class SearchFragment : override fun onTimeout() { if (!isTimeoutDialogDisplayed()) { binding.loadingProgressBar.isVisible = false stopLoadingUI() displayTimeoutAlertDialog( timeoutFragment = this, Loading @@ -235,7 +251,7 @@ class SearchFragment : override fun refreshData(authData: AuthData) { showLoadingUI() searchViewModel.getSearchResults(searchText, authData) searchViewModel.getSearchResults(searchText, authData, this) } private fun showLoadingUI() { Loading Loading
app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +125 −34 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -205,15 +208,23 @@ 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 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. */ suspend fun getSearchResults(query: String, authData: AuthData): Pair<List<FusedApp>, ResultStatus> { val fusedResponse = ArrayList<FusedApp>() fun getSearchResults( query: String, authData: AuthData ): LiveData<ResultSupreme<Pair<List<FusedApp>, 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<FusedApp>() val status = runCodeBlockWithTimeout({ try { if (preferenceManagerModule.preferredApplicationType() == APP_TYPE_ANY) { try { /* Loading @@ -236,40 +247,95 @@ class FusedAPIImpl @Inject constructor( packageSpecificResults.add(it.data!!) } } } catch (_: Exception) {} }) /* * 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) { emit(ResultSupreme.create(status, Pair(packageSpecificResults, true))) return@liveData } /* * 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. */ fun filterWithKeywordSearch(list: List<FusedApp>): List<FusedApp> { val filteredResults = list.distinctBy { it.package_name } .filter { packageSpecificResults.isEmpty() || it.package_name != query } return packageSpecificResults + filteredResults } val cleanApkResults = mutableListOf<FusedApp>() when (preferenceManagerModule.preferredApplicationType()) { APP_TYPE_ANY -> { fusedResponse.addAll(getCleanAPKSearchResults(query)) fusedResponse.addAll(getGplaySearchResults(query, authData)) val status = runCodeBlockWithTimeout({ cleanApkResults.addAll(getCleanAPKSearchResults(query)) }) if (cleanApkResults.isNotEmpty() || status != ResultStatus.OK) { /* * If cleanapk results are empty, dont emit emit data as it may * briefly show "No apps found..." * If status is timeout, then do emit the value. * Send true in the pair to signal more results (i.e from GPlay) being loaded. */ emit( ResultSupreme.create( status, Pair(filterWithKeywordSearch(cleanApkResults), true) ) ) } emitSource( getGplayAndCleanapkCombinedResults(query, authData, cleanApkResults).map { /* * We are assuming that there will be no timeout here. * If there had to be any timeout, it would already have happened * while fetching package specific results. */ ResultSupreme.Success(Pair(filterWithKeywordSearch(it.first), it.second)) } ) } APP_TYPE_OPEN -> { fusedResponse.addAll(getCleanAPKSearchResults(query)) val status = runCodeBlockWithTimeout({ cleanApkResults.addAll(getCleanAPKSearchResults(query)) }) /* * Send false in pair to signal no more results to load, as only cleanapk * results are fetched, we don't have to wait for GPlay results. */ emit( ResultSupreme.create( status, Pair(filterWithKeywordSearch(cleanApkResults), false) ) ) } APP_TYPE_PWA -> { fusedResponse.addAll( val status = runCodeBlockWithTimeout({ cleanApkResults.addAll( getCleanAPKSearchResults( query, CleanAPKInterface.APP_SOURCE_ANY, CleanAPKInterface.APP_TYPE_PWA ) ) } } }) /* * 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. * Send false in pair to signal no more results to load, as only cleanapk * results are fetched for PWAs. */ val filteredResults = fusedResponse.distinctBy { it.package_name } .filter { packageSpecificResults.isEmpty() || it.package_name != query } return Pair(packageSpecificResults + filteredResults, status) emit(ResultSupreme.create(status, Pair(cleanApkResults, false))) } } } } /* Loading Loading @@ -889,10 +955,35 @@ class FusedAPIImpl @Inject constructor( return list } private suspend fun getGplaySearchResults(query: String, authData: AuthData): List<FusedApp> { /* * 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<FusedApp> ): LiveData<Pair<List<FusedApp>, Boolean>> { val localList = ArrayList<FusedApp>(cleanApkResults) return getGplaySearchResults(query, authData).map { pair -> Pair( localList.apply { addAll(pair.first) }.distinctBy { it.package_name }, pair.second ) } } private fun getGplaySearchResults( query: String, authData: AuthData ): LiveData<Pair<List<FusedApp>, Boolean>> { val searchResults = gPlayAPIRepository.getSearchResults(query, authData) return searchResults.map { app -> app.transformToFusedApp() return searchResults.map { Pair( it.first.map { app -> app.transformToFusedApp() }, it.second ) } } Loading
app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +2 −1 Original line number Diff line number Diff line Loading @@ -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.App import com.aurora.gplayapi.data.models.AuthData Loading Loading @@ -111,7 +112,7 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.fetchAuthData(email, aasToken) } suspend fun getSearchResults(query: String, authData: AuthData): Pair<List<FusedApp>, ResultStatus> { fun getSearchResults(query: String, authData: AuthData): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> { return fusedAPIImpl.getSearchResults(query, authData) } Loading
app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt +32 −18 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -98,29 +100,41 @@ class GPlayAPIImpl @Inject constructor( return searchData.filter { it.suggestedQuery.isNotBlank() } } suspend fun getSearchResults(query: String, authData: AuthData): List<App> { val searchData = mutableListOf<App>() /** * Sends livedata of list of apps being loaded from search and a boolean * signifying if more data is to be loaded. */ fun getSearchResults(query: String, authData: AuthData): LiveData<Pair<List<App>, Boolean>> { /* * 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 searchResult = searchHelper.searchResults(query) searchData.addAll(searchResult.appList) val searchBundle = searchHelper.searchResults(query) emit(Pair(searchBundle.appList, true)) // Fetch more results in case the given result is a promoted app if (searchData.size == 1) { val bundleSet: MutableSet<SearchBundle.SubBundle> = searchResult.subBundles var nextSubBundleSet: MutableSet<SearchBundle.SubBundle> do { val searchBundle = searchHelper.next(bundleSet) if (searchBundle.appList.isNotEmpty()) { searchData.addAll(searchBundle.appList) 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(Pair(searchBundle.appList, nextSubBundleSet.isNotEmpty())) } bundleSet.apply { clear() addAll(searchBundle.subBundles) } } while (bundleSet.isNotEmpty()) } while (nextSubBundleSet.isNotEmpty()) } } return searchData } suspend fun getDownloadInfo( Loading
app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt +2 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -48,7 +49,7 @@ class GPlayAPIRepository @Inject constructor( return gPlayAPIImpl.getSearchSuggestions(query, authData) } suspend fun getSearchResults(query: String, authData: AuthData): List<App> { fun getSearchResults(query: String, authData: AuthData): LiveData<Pair<List<App>, Boolean>> { return gPlayAPIImpl.getSearchResults(query, authData) } Loading
app/src/main/java/foundation/e/apps/search/SearchFragment.kt +23 −7 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.view.inputmethod.InputMethodManager import android.widget.ImageView import android.widget.LinearLayout import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.cursoradapter.widget.CursorAdapter import androidx.cursoradapter.widget.SimpleCursorAdapter import androidx.fragment.app.activityViewModels Loading Loading @@ -82,6 +83,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 Loading Loading @@ -162,7 +164,7 @@ class SearchFragment : mainActivityViewModel.downloadList.observe(viewLifecycleOwner) { list -> val searchList = searchViewModel.searchResult.value?.first?.toMutableList() ?: emptyList() searchViewModel.searchResult.value?.data?.first?.toMutableList() ?: emptyList() searchList.let { mainActivityViewModel.updateStatusOfFusedApps(searchList, list) } Loading @@ -171,7 +173,7 @@ class SearchFragment : * Done in one line, so that on Ctrl+click on searchResult, * we can see that it is being updated here. */ searchViewModel.searchResult.apply { value = Pair(searchList, value?.second) } searchViewModel.searchResult.apply { value?.setData(Pair(searchList, value?.data?.second ?: false)) } } /* Loading @@ -191,19 +193,32 @@ class SearchFragment : } searchViewModel.searchResult.observe(viewLifecycleOwner) { if (it.first.isNullOrEmpty()) { if (it.data?.first.isNullOrEmpty()) { noAppsFoundLayout?.visibility = View.VISIBLE } else { listAdapter?.setData(it.first) listAdapter?.setData(it.data!!.first) binding.loadingProgressBar.isVisible = it.data!!.second stopLoadingUI() noAppsFoundLayout?.visibility = View.GONE } 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() } } } }) if (searchText.isNotBlank() && it.second != ResultStatus.OK) { if (searchText.isNotBlank() && !it.isSuccess()) { /* * If blank check is not performed then timeout dialog keeps * popping up whenever search tab is opened. Loading @@ -215,6 +230,7 @@ class SearchFragment : override fun onTimeout() { if (!isTimeoutDialogDisplayed()) { binding.loadingProgressBar.isVisible = false stopLoadingUI() displayTimeoutAlertDialog( timeoutFragment = this, Loading @@ -235,7 +251,7 @@ class SearchFragment : override fun refreshData(authData: AuthData) { showLoadingUI() searchViewModel.getSearchResults(searchText, authData) searchViewModel.getSearchResults(searchText, authData, this) } private fun showLoadingUI() { Loading