Loading app/src/main/java/foundation/e/apps/search/SearchFragment.kt +47 −41 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.ImageView import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.cursoradapter.widget.CursorAdapter Loading @@ -38,7 +39,6 @@ import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData import com.facebook.shimmer.ShimmerFrameLayout import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.AppInfoFetchViewModel Loading @@ -52,17 +52,19 @@ import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.applicationlist.ApplicationListRVAdapter import foundation.e.apps.databinding.FragmentSearchBinding import foundation.e.apps.login.AuthObject import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.exceptions.GPlayValidationException import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.parentFragment.TimeoutFragment import foundation.e.apps.utils.parentFragment.TimeoutFragment2 import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint class SearchFragment : TimeoutFragment(R.layout.fragment_search), TimeoutFragment2(R.layout.fragment_search), SearchView.OnQueryTextListener, SearchView.OnSuggestionListener, FusedAPIInterface { Loading Loading @@ -92,7 +94,7 @@ class SearchFragment : private var noAppsFoundLayout: LinearLayout? = null /* * Store the string from onQueryTextSubmit() and access it from refreshData() * Store the string from onQueryTextSubmit() and access it from loadData() * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 */ private var searchText = "" Loading @@ -114,6 +116,17 @@ class SearchFragment : val listAdapter = setupSearchResult(view) observeSearchResult(listAdapter) setupListening() authObjects.observe(viewLifecycleOwner) { if (it == null) return@observe loadData(it) } searchViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { handleExceptionsCommon(it) } } private fun observeSearchResult(listAdapter: ApplicationListRVAdapter?) { Loading @@ -129,13 +142,6 @@ class SearchFragment : } observeScrollOfSearchResult(listAdapter) if (searchText.isNotBlank() && !it.isSuccess()) { /* * If blank check is not performed then timeout dialog keeps * popping up whenever search tab is opened. */ onTimeout() } } } Loading Loading @@ -253,39 +259,42 @@ class SearchFragment : } } override fun onTimeout() { if (!isTimeoutDialogDisplayed()) { binding.loadingProgressBar.isVisible = false stopLoadingUI() displayTimeoutAlertDialog( timeoutFragment = this, activity = requireActivity(), message = getString(R.string.timeout_desc_cleanapk), positiveButtonText = getString(R.string.retry), positiveButtonBlock = { showLoadingUI() resetTimeoutDialogLock() mainActivityViewModel.retryFetchingTokenAfterTimeout() }, negativeButtonText = getString(android.R.string.ok), negativeButtonBlock = {}, allowCancel = true, ) override fun onTimeout( exception: Exception, predefinedDialog: AlertDialog.Builder ): AlertDialog.Builder? { return predefinedDialog } override fun onDataLoadError( exception: Exception, predefinedDialog: AlertDialog.Builder ): AlertDialog.Builder? { return predefinedDialog } override fun onSignInError( exception: GPlayValidationException, predefinedDialog: AlertDialog.Builder ): AlertDialog.Builder? { return predefinedDialog } override fun refreshData(authData: AuthData) { override fun loadData(authObjectList: List<AuthObject>) { showLoadingUI() searchViewModel.getSearchResults(searchText, authData, viewLifecycleOwner) searchViewModel.loadData(searchText, viewLifecycleOwner, authObjectList) { clearAndRestartGPlayLogin() true } } private fun showLoadingUI() { override fun showLoadingUI() { binding.shimmerLayout.startShimmer() binding.shimmerLayout.visibility = View.VISIBLE binding.recyclerView.visibility = View.GONE } private fun stopLoadingUI() { override fun stopLoadingUI() { binding.shimmerLayout.stopShimmer() binding.shimmerLayout.visibility = View.GONE binding.recyclerView.visibility = View.VISIBLE Loading Loading @@ -315,16 +324,13 @@ class SearchFragment : override fun onResume() { super.onResume() resetTimeoutDialogLock() binding.shimmerLayout.startShimmer() appProgressViewModel.downloadProgress.observe(viewLifecycleOwner) { updateProgressOfInstallingApps(it) } if (shouldRefreshData()) { mainActivityViewModel.authData.value?.let { refreshData(it) } repostAuthObjects() } if (searchText.isEmpty() && (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty()) { Loading Loading @@ -358,15 +364,15 @@ class SearchFragment : * Set the search text and call for network result. */ searchText = text refreshDataOrRefreshToken(mainActivityViewModel) repostAuthObjects() } return false } override fun onQueryTextChange(newText: String?): Boolean { newText?.let { text -> mainActivityViewModel.authData.value?.let { searchViewModel.getSearchSuggestions(text, it) authObjects.value?.find { it is AuthObject.GPlayAuth }?.run { searchViewModel.getSearchSuggestions(text, this as AuthObject.GPlayAuth) } } return true Loading app/src/main/java/foundation/e/apps/search/SearchViewModel.kt +43 −4 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ package foundation.e.apps.search import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData Loading @@ -28,6 +27,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.login.AuthObject import foundation.e.apps.utils.exceptions.CleanApkException import foundation.e.apps.utils.exceptions.GPlayException import foundation.e.apps.utils.parentFragment.LoadingViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject Loading @@ -35,18 +38,43 @@ import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( private val fusedAPIRepository: FusedAPIRepository, ) : ViewModel() { ) : LoadingViewModel() { val searchSuggest: MutableLiveData<List<SearchSuggestEntry>?> = MutableLiveData() val searchResult: MutableLiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> = MutableLiveData() fun getSearchSuggestions(query: String, authData: AuthData) { fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { viewModelScope.launch(Dispatchers.IO) { searchSuggest.postValue(fusedAPIRepository.getSearchSuggestions(query, authData)) if (gPlayAuth.result.isSuccess()) searchSuggest.postValue(fusedAPIRepository.getSearchSuggestions(query, gPlayAuth.result.data!!)) } } fun loadData( query: String, lifecycleOwner: LifecycleOwner, authObjectList: List<AuthObject>, retryBlock: (failedObjects: List<AuthObject>) -> Boolean ) { if (query.isBlank()) return super.onLoadData(authObjectList, { successAuthList, _ -> successAuthList.find { it is AuthObject.GPlayAuth }?.run { getSearchResults(query, result.data!! as AuthData, lifecycleOwner) return@onLoadData } successAuthList.find { it is AuthObject.CleanApk }?.run { getSearchResults(query, AuthData("", ""), lifecycleOwner) return@onLoadData } }, retryBlock) } /* * 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, Loading @@ -57,6 +85,17 @@ class SearchViewModel @Inject constructor( viewModelScope.launch(Dispatchers.Main) { fusedAPIRepository.getSearchResults(query, authData).observe(lifecycleOwner) { searchResult.postValue(it) if (!it.isSuccess()) { val exception = if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) { GPlayException(it.isTimeout(), "Data load error") } else CleanApkException(it.isTimeout(), "Data load error") exceptionsList.add(exception) exceptionsLiveData.postValue(exceptionsList) } } } } Loading Loading
app/src/main/java/foundation/e/apps/search/SearchFragment.kt +47 −41 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.ImageView import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.cursoradapter.widget.CursorAdapter Loading @@ -38,7 +39,6 @@ import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData import com.facebook.shimmer.ShimmerFrameLayout import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.AppInfoFetchViewModel Loading @@ -52,17 +52,19 @@ import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.applicationlist.ApplicationListRVAdapter import foundation.e.apps.databinding.FragmentSearchBinding import foundation.e.apps.login.AuthObject import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.exceptions.GPlayValidationException import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.parentFragment.TimeoutFragment import foundation.e.apps.utils.parentFragment.TimeoutFragment2 import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint class SearchFragment : TimeoutFragment(R.layout.fragment_search), TimeoutFragment2(R.layout.fragment_search), SearchView.OnQueryTextListener, SearchView.OnSuggestionListener, FusedAPIInterface { Loading Loading @@ -92,7 +94,7 @@ class SearchFragment : private var noAppsFoundLayout: LinearLayout? = null /* * Store the string from onQueryTextSubmit() and access it from refreshData() * Store the string from onQueryTextSubmit() and access it from loadData() * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 */ private var searchText = "" Loading @@ -114,6 +116,17 @@ class SearchFragment : val listAdapter = setupSearchResult(view) observeSearchResult(listAdapter) setupListening() authObjects.observe(viewLifecycleOwner) { if (it == null) return@observe loadData(it) } searchViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { handleExceptionsCommon(it) } } private fun observeSearchResult(listAdapter: ApplicationListRVAdapter?) { Loading @@ -129,13 +142,6 @@ class SearchFragment : } observeScrollOfSearchResult(listAdapter) if (searchText.isNotBlank() && !it.isSuccess()) { /* * If blank check is not performed then timeout dialog keeps * popping up whenever search tab is opened. */ onTimeout() } } } Loading Loading @@ -253,39 +259,42 @@ class SearchFragment : } } override fun onTimeout() { if (!isTimeoutDialogDisplayed()) { binding.loadingProgressBar.isVisible = false stopLoadingUI() displayTimeoutAlertDialog( timeoutFragment = this, activity = requireActivity(), message = getString(R.string.timeout_desc_cleanapk), positiveButtonText = getString(R.string.retry), positiveButtonBlock = { showLoadingUI() resetTimeoutDialogLock() mainActivityViewModel.retryFetchingTokenAfterTimeout() }, negativeButtonText = getString(android.R.string.ok), negativeButtonBlock = {}, allowCancel = true, ) override fun onTimeout( exception: Exception, predefinedDialog: AlertDialog.Builder ): AlertDialog.Builder? { return predefinedDialog } override fun onDataLoadError( exception: Exception, predefinedDialog: AlertDialog.Builder ): AlertDialog.Builder? { return predefinedDialog } override fun onSignInError( exception: GPlayValidationException, predefinedDialog: AlertDialog.Builder ): AlertDialog.Builder? { return predefinedDialog } override fun refreshData(authData: AuthData) { override fun loadData(authObjectList: List<AuthObject>) { showLoadingUI() searchViewModel.getSearchResults(searchText, authData, viewLifecycleOwner) searchViewModel.loadData(searchText, viewLifecycleOwner, authObjectList) { clearAndRestartGPlayLogin() true } } private fun showLoadingUI() { override fun showLoadingUI() { binding.shimmerLayout.startShimmer() binding.shimmerLayout.visibility = View.VISIBLE binding.recyclerView.visibility = View.GONE } private fun stopLoadingUI() { override fun stopLoadingUI() { binding.shimmerLayout.stopShimmer() binding.shimmerLayout.visibility = View.GONE binding.recyclerView.visibility = View.VISIBLE Loading Loading @@ -315,16 +324,13 @@ class SearchFragment : override fun onResume() { super.onResume() resetTimeoutDialogLock() binding.shimmerLayout.startShimmer() appProgressViewModel.downloadProgress.observe(viewLifecycleOwner) { updateProgressOfInstallingApps(it) } if (shouldRefreshData()) { mainActivityViewModel.authData.value?.let { refreshData(it) } repostAuthObjects() } if (searchText.isEmpty() && (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty()) { Loading Loading @@ -358,15 +364,15 @@ class SearchFragment : * Set the search text and call for network result. */ searchText = text refreshDataOrRefreshToken(mainActivityViewModel) repostAuthObjects() } return false } override fun onQueryTextChange(newText: String?): Boolean { newText?.let { text -> mainActivityViewModel.authData.value?.let { searchViewModel.getSearchSuggestions(text, it) authObjects.value?.find { it is AuthObject.GPlayAuth }?.run { searchViewModel.getSearchSuggestions(text, this as AuthObject.GPlayAuth) } } return true Loading
app/src/main/java/foundation/e/apps/search/SearchViewModel.kt +43 −4 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ package foundation.e.apps.search import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData Loading @@ -28,6 +27,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.login.AuthObject import foundation.e.apps.utils.exceptions.CleanApkException import foundation.e.apps.utils.exceptions.GPlayException import foundation.e.apps.utils.parentFragment.LoadingViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject Loading @@ -35,18 +38,43 @@ import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( private val fusedAPIRepository: FusedAPIRepository, ) : ViewModel() { ) : LoadingViewModel() { val searchSuggest: MutableLiveData<List<SearchSuggestEntry>?> = MutableLiveData() val searchResult: MutableLiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> = MutableLiveData() fun getSearchSuggestions(query: String, authData: AuthData) { fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { viewModelScope.launch(Dispatchers.IO) { searchSuggest.postValue(fusedAPIRepository.getSearchSuggestions(query, authData)) if (gPlayAuth.result.isSuccess()) searchSuggest.postValue(fusedAPIRepository.getSearchSuggestions(query, gPlayAuth.result.data!!)) } } fun loadData( query: String, lifecycleOwner: LifecycleOwner, authObjectList: List<AuthObject>, retryBlock: (failedObjects: List<AuthObject>) -> Boolean ) { if (query.isBlank()) return super.onLoadData(authObjectList, { successAuthList, _ -> successAuthList.find { it is AuthObject.GPlayAuth }?.run { getSearchResults(query, result.data!! as AuthData, lifecycleOwner) return@onLoadData } successAuthList.find { it is AuthObject.CleanApk }?.run { getSearchResults(query, AuthData("", ""), lifecycleOwner) return@onLoadData } }, retryBlock) } /* * 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, Loading @@ -57,6 +85,17 @@ class SearchViewModel @Inject constructor( viewModelScope.launch(Dispatchers.Main) { fusedAPIRepository.getSearchResults(query, authData).observe(lifecycleOwner) { searchResult.postValue(it) if (!it.isSuccess()) { val exception = if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) { GPlayException(it.isTimeout(), "Data load error") } else CleanApkException(it.isTimeout(), "Data load error") exceptionsList.add(exception) exceptionsLiveData.postValue(exceptionsList) } } } } Loading