Loading app/src/main/java/foundation/e/apps/data/enums/Source.kt +3 −3 Original line number Diff line number Diff line Loading @@ -18,10 +18,10 @@ package foundation.e.apps.data.enums enum class Source { PLAY_STORE, SYSTEM_APP, OPEN_SOURCE, PWA; PWA, SYSTEM_APP, PLAY_STORE; override fun toString(): String { return when (this) { Loading app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +22 −23 Original line number Diff line number Diff line Loading @@ -128,7 +128,6 @@ class SearchFragment : // Setup Search Results val listAdapter = setupSearchResult(view) preventLoadingLessResults() observeSearchResult(listAdapter) setupSearchFilters() Loading Loading @@ -178,29 +177,21 @@ class SearchFragment : lastSearch == currentQuery private fun observeSearchResult(listAdapter: ApplicationListRVAdapter?) { searchViewModel.searchResult.observe(viewLifecycleOwner) { if (it.data?.first.isNullOrEmpty() && it.data?.second == false) { searchViewModel.searchResult.observe(viewLifecycleOwner) { result -> val apps = result.data?.first if (apps.isNullOrEmpty()) { noAppsFoundLayout?.visibility = View.VISIBLE } else if (searchViewModel.shouldIgnoreResults()) { return@observe } else { listAdapter?.let { adapter -> observeDownloadList(adapter) } } updateSearchResult(listAdapter, it.data?.first ?: emptyList()) updateSearchResult(listAdapter, apps ?: emptyList()) observeScrollOfSearchResult(listAdapter) } } private fun preventLoadingLessResults() { searchViewModel.gplaySearchLoaded.observe(viewLifecycleOwner) { if (!it) return@observe searchViewModel.loadMoreDataIfNeeded(searchText) } } private fun observeScrollOfSearchResult(listAdapter: ApplicationListRVAdapter?) { listAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { Loading @@ -224,22 +215,29 @@ class SearchFragment : }) } /** * @return true if Search result is updated, otherwise false */ private fun updateSearchResult( listAdapter: ApplicationListRVAdapter?, apps: List<Application>, ): Boolean { ) { val currentApps = listAdapter?.currentList ?: listOf() if (!searchViewModel.isAnyAppUpdated(apps, currentApps)) { return false return } val filteredApps = searchViewModel.sortApps(apps) if (filteredApps.isEmpty()) { return } showData() val filteredApps = apps.filter { it.name.isNotBlank() }.distinctBy { it.package_name } listAdapter?.setData(filteredApps) return true listAdapter?.submitList(filteredApps) // Scroll to the top with some delays so that the recycler view has the time // to process the new results recyclerView?.postDelayed( { recyclerView?.scrollToPosition(0) }, SCROLL_TO_TOP_DELAY_MILLIS ) } private fun showData() { Loading Loading @@ -468,7 +466,7 @@ class SearchFragment : searchJob = lifecycleScope.launch(Dispatchers.Main.immediate) { delay(SEARCH_DEBOUNCE_DELAY_MILLIS) authObjects.value?.find { it is AuthObject.GPlayAuth }?.run { searchViewModel.getSearchSuggestions(text, this as AuthObject.GPlayAuth) searchViewModel.getSearchSuggestions(text) } } } Loading Loading @@ -545,5 +543,6 @@ class SearchFragment : companion object { private const val SEARCH_DEBOUNCE_DELAY_MILLIS = 500L private const val SCROLL_TO_TOP_DELAY_MILLIS = 100L } } app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +42 −58 Original line number Diff line number Diff line Loading @@ -34,6 +34,7 @@ import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository import foundation.e.apps.data.login.AuthObject import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay Loading @@ -57,13 +58,9 @@ class SearchViewModel @Inject constructor( MutableLiveData() val searchResult: LiveData<SearchResult> = _searchResult val gplaySearchLoaded: MutableLiveData<Boolean> = MutableLiveData(false) private var lastAuthObjects: List<AuthObject>? = null private var isLoading: Boolean = false private var hasGPlayBeenFetched = false @GuardedBy("mutex") private val accumulatedList = mutableListOf<Application>() Loading @@ -74,8 +71,6 @@ class SearchViewModel @Inject constructor( private var flagPWA: Boolean = false companion object { private const val DATA_LOAD_ERROR = "Data load error" private const val MIN_SEARCH_DISPLAY_ITEMS = 10 private const val PREVENT_HTTP_429_DELAY_IN_MS = 1000L } Loading @@ -93,7 +88,7 @@ class SearchViewModel @Inject constructor( } } fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { fun getSearchSuggestions(query: String) { viewModelScope.launch(IO) { searchSuggest.postValue( applicationRepository.getSearchSuggestions(query) Loading @@ -110,6 +105,12 @@ class SearchViewModel @Inject constructor( this.lastAuthObjects = authObjects super.onLoadData(authObjects, { successObjects, failedObjects -> viewModelScope.launch { mutex.withLock { accumulatedList.clear() } } successObjects.find { it is AuthObject.CleanApk }?.run { fetchCleanApkData(query) } Loading Loading @@ -137,7 +138,6 @@ class SearchViewModel @Inject constructor( viewModelScope.launch(IO) { val searchResultSupreme = applicationRepository.getCleanApkSearchResults(query) hasGPlayBeenFetched = false emitFilteredResults(searchResultSupreme) if (!searchResultSupreme.isSuccess()) { Loading @@ -147,12 +147,12 @@ class SearchViewModel @Inject constructor( } fun loadMore(query: String, autoTriggered: Boolean = false) { viewModelScope.launch(Main) { if (isLoading) { Timber.d("Search result is loading....") return return@launch } viewModelScope.launch(IO) { if (autoTriggered) { delay(PREVENT_HTTP_429_DELAY_IN_MS) } Loading @@ -160,9 +160,14 @@ class SearchViewModel @Inject constructor( } } fun sortApps(apps: List<Application>): List<Application> { return apps.filter { it.name.isNotBlank() }.sortedBy { it.source }.distinctBy { it.package_name } } private fun fetchGplayData(query: String) { viewModelScope.launch(IO) { isLoading = true val gplaySearchResult = applicationRepository.getGplaySearchResults(query) Loading @@ -172,23 +177,23 @@ class SearchViewModel @Inject constructor( } } val currentAppList = mutex.withLock { updateCurrentAppList(gplaySearchResult) } val currentAppList = updateCurrentAppList(gplaySearchResult) val finalResult = ResultSupreme.Success( Pair(currentAppList.toList(), false) ) hasGPlayBeenFetched = true emitFilteredResults(finalResult) isLoading = false } } private fun updateCurrentAppList(searchResult: SearchResult): List<Application> { val currentAppList = accumulatedList private suspend fun updateCurrentAppList(searchResult: SearchResult): List<Application> { val currentAppList = mutex.withLock { accumulatedList } currentAppList.removeIf { item -> item.isPlaceHolder } currentAppList.addAll(searchResult.data?.first ?: emptyList()) return currentAppList.distinctBy { it.package_name } Loading Loading @@ -228,6 +233,7 @@ class SearchViewModel @Inject constructor( private suspend fun getFilteredList(): List<Application> = withContext(IO) { if (flagNoTrackers) { mutex.withLock { val deferredCheck = accumulatedList.map { async { if (it.privacyScore == -1) { Loading @@ -238,7 +244,9 @@ class SearchViewModel @Inject constructor( } deferredCheck.awaitAll() } } mutex.withLock { accumulatedList.filter { if (!flagNoTrackers && !flagOpenSource && !flagPWA) return@filter true if (flagNoTrackers && !hasTrackers(it)) return@filter true Loading @@ -247,6 +255,7 @@ class SearchViewModel @Inject constructor( false } } } /** * Pass [result] as null to re-emit already loaded search results with new filters. Loading @@ -268,42 +277,17 @@ class SearchViewModel @Inject constructor( if (result != null) { result.data?.first?.let { mutex.withLock { accumulatedList.clear() accumulatedList.addAll(it) } } } val filteredList = mutex.withLock { getFilteredList() } val isMoreDataLoading = result?.data?.second ?: _searchResult.value?.data?.second ?: false val filteredList = getFilteredList() _searchResult.postValue( ResultSupreme.Success( Pair(filteredList.toList(), isMoreDataLoading) Pair(filteredList.toList(), false) ) ) gplaySearchLoaded.postValue(hasGPlayBeenFetched) } fun loadMoreDataIfNeeded(searchText: String) { val searchList = searchResult.value?.data?.first?.toMutableList() ?: emptyList() val canLoadMore = searchResult.value?.data?.second ?: false if (searchList.size < MIN_SEARCH_DISPLAY_ITEMS && canLoadMore) { loadMore(searchText, autoTriggered = true) } } fun shouldIgnoreResults(): Boolean { val appsList = _searchResult.value?.data?.first if (appsList.isNullOrEmpty()) return true val appPackageNames = appsList.map { it.package_name } return appPackageNames.all { it.isBlank() } } } gradle/libs.versions.toml +1 −1 File changed.Contains only whitespace changes. Show changes Loading
app/src/main/java/foundation/e/apps/data/enums/Source.kt +3 −3 Original line number Diff line number Diff line Loading @@ -18,10 +18,10 @@ package foundation.e.apps.data.enums enum class Source { PLAY_STORE, SYSTEM_APP, OPEN_SOURCE, PWA; PWA, SYSTEM_APP, PLAY_STORE; override fun toString(): String { return when (this) { Loading
app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +22 −23 Original line number Diff line number Diff line Loading @@ -128,7 +128,6 @@ class SearchFragment : // Setup Search Results val listAdapter = setupSearchResult(view) preventLoadingLessResults() observeSearchResult(listAdapter) setupSearchFilters() Loading Loading @@ -178,29 +177,21 @@ class SearchFragment : lastSearch == currentQuery private fun observeSearchResult(listAdapter: ApplicationListRVAdapter?) { searchViewModel.searchResult.observe(viewLifecycleOwner) { if (it.data?.first.isNullOrEmpty() && it.data?.second == false) { searchViewModel.searchResult.observe(viewLifecycleOwner) { result -> val apps = result.data?.first if (apps.isNullOrEmpty()) { noAppsFoundLayout?.visibility = View.VISIBLE } else if (searchViewModel.shouldIgnoreResults()) { return@observe } else { listAdapter?.let { adapter -> observeDownloadList(adapter) } } updateSearchResult(listAdapter, it.data?.first ?: emptyList()) updateSearchResult(listAdapter, apps ?: emptyList()) observeScrollOfSearchResult(listAdapter) } } private fun preventLoadingLessResults() { searchViewModel.gplaySearchLoaded.observe(viewLifecycleOwner) { if (!it) return@observe searchViewModel.loadMoreDataIfNeeded(searchText) } } private fun observeScrollOfSearchResult(listAdapter: ApplicationListRVAdapter?) { listAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { Loading @@ -224,22 +215,29 @@ class SearchFragment : }) } /** * @return true if Search result is updated, otherwise false */ private fun updateSearchResult( listAdapter: ApplicationListRVAdapter?, apps: List<Application>, ): Boolean { ) { val currentApps = listAdapter?.currentList ?: listOf() if (!searchViewModel.isAnyAppUpdated(apps, currentApps)) { return false return } val filteredApps = searchViewModel.sortApps(apps) if (filteredApps.isEmpty()) { return } showData() val filteredApps = apps.filter { it.name.isNotBlank() }.distinctBy { it.package_name } listAdapter?.setData(filteredApps) return true listAdapter?.submitList(filteredApps) // Scroll to the top with some delays so that the recycler view has the time // to process the new results recyclerView?.postDelayed( { recyclerView?.scrollToPosition(0) }, SCROLL_TO_TOP_DELAY_MILLIS ) } private fun showData() { Loading Loading @@ -468,7 +466,7 @@ class SearchFragment : searchJob = lifecycleScope.launch(Dispatchers.Main.immediate) { delay(SEARCH_DEBOUNCE_DELAY_MILLIS) authObjects.value?.find { it is AuthObject.GPlayAuth }?.run { searchViewModel.getSearchSuggestions(text, this as AuthObject.GPlayAuth) searchViewModel.getSearchSuggestions(text) } } } Loading Loading @@ -545,5 +543,6 @@ class SearchFragment : companion object { private const val SEARCH_DEBOUNCE_DELAY_MILLIS = 500L private const val SCROLL_TO_TOP_DELAY_MILLIS = 100L } }
app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +42 −58 Original line number Diff line number Diff line Loading @@ -34,6 +34,7 @@ import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository import foundation.e.apps.data.login.AuthObject import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay Loading @@ -57,13 +58,9 @@ class SearchViewModel @Inject constructor( MutableLiveData() val searchResult: LiveData<SearchResult> = _searchResult val gplaySearchLoaded: MutableLiveData<Boolean> = MutableLiveData(false) private var lastAuthObjects: List<AuthObject>? = null private var isLoading: Boolean = false private var hasGPlayBeenFetched = false @GuardedBy("mutex") private val accumulatedList = mutableListOf<Application>() Loading @@ -74,8 +71,6 @@ class SearchViewModel @Inject constructor( private var flagPWA: Boolean = false companion object { private const val DATA_LOAD_ERROR = "Data load error" private const val MIN_SEARCH_DISPLAY_ITEMS = 10 private const val PREVENT_HTTP_429_DELAY_IN_MS = 1000L } Loading @@ -93,7 +88,7 @@ class SearchViewModel @Inject constructor( } } fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { fun getSearchSuggestions(query: String) { viewModelScope.launch(IO) { searchSuggest.postValue( applicationRepository.getSearchSuggestions(query) Loading @@ -110,6 +105,12 @@ class SearchViewModel @Inject constructor( this.lastAuthObjects = authObjects super.onLoadData(authObjects, { successObjects, failedObjects -> viewModelScope.launch { mutex.withLock { accumulatedList.clear() } } successObjects.find { it is AuthObject.CleanApk }?.run { fetchCleanApkData(query) } Loading Loading @@ -137,7 +138,6 @@ class SearchViewModel @Inject constructor( viewModelScope.launch(IO) { val searchResultSupreme = applicationRepository.getCleanApkSearchResults(query) hasGPlayBeenFetched = false emitFilteredResults(searchResultSupreme) if (!searchResultSupreme.isSuccess()) { Loading @@ -147,12 +147,12 @@ class SearchViewModel @Inject constructor( } fun loadMore(query: String, autoTriggered: Boolean = false) { viewModelScope.launch(Main) { if (isLoading) { Timber.d("Search result is loading....") return return@launch } viewModelScope.launch(IO) { if (autoTriggered) { delay(PREVENT_HTTP_429_DELAY_IN_MS) } Loading @@ -160,9 +160,14 @@ class SearchViewModel @Inject constructor( } } fun sortApps(apps: List<Application>): List<Application> { return apps.filter { it.name.isNotBlank() }.sortedBy { it.source }.distinctBy { it.package_name } } private fun fetchGplayData(query: String) { viewModelScope.launch(IO) { isLoading = true val gplaySearchResult = applicationRepository.getGplaySearchResults(query) Loading @@ -172,23 +177,23 @@ class SearchViewModel @Inject constructor( } } val currentAppList = mutex.withLock { updateCurrentAppList(gplaySearchResult) } val currentAppList = updateCurrentAppList(gplaySearchResult) val finalResult = ResultSupreme.Success( Pair(currentAppList.toList(), false) ) hasGPlayBeenFetched = true emitFilteredResults(finalResult) isLoading = false } } private fun updateCurrentAppList(searchResult: SearchResult): List<Application> { val currentAppList = accumulatedList private suspend fun updateCurrentAppList(searchResult: SearchResult): List<Application> { val currentAppList = mutex.withLock { accumulatedList } currentAppList.removeIf { item -> item.isPlaceHolder } currentAppList.addAll(searchResult.data?.first ?: emptyList()) return currentAppList.distinctBy { it.package_name } Loading Loading @@ -228,6 +233,7 @@ class SearchViewModel @Inject constructor( private suspend fun getFilteredList(): List<Application> = withContext(IO) { if (flagNoTrackers) { mutex.withLock { val deferredCheck = accumulatedList.map { async { if (it.privacyScore == -1) { Loading @@ -238,7 +244,9 @@ class SearchViewModel @Inject constructor( } deferredCheck.awaitAll() } } mutex.withLock { accumulatedList.filter { if (!flagNoTrackers && !flagOpenSource && !flagPWA) return@filter true if (flagNoTrackers && !hasTrackers(it)) return@filter true Loading @@ -247,6 +255,7 @@ class SearchViewModel @Inject constructor( false } } } /** * Pass [result] as null to re-emit already loaded search results with new filters. Loading @@ -268,42 +277,17 @@ class SearchViewModel @Inject constructor( if (result != null) { result.data?.first?.let { mutex.withLock { accumulatedList.clear() accumulatedList.addAll(it) } } } val filteredList = mutex.withLock { getFilteredList() } val isMoreDataLoading = result?.data?.second ?: _searchResult.value?.data?.second ?: false val filteredList = getFilteredList() _searchResult.postValue( ResultSupreme.Success( Pair(filteredList.toList(), isMoreDataLoading) Pair(filteredList.toList(), false) ) ) gplaySearchLoaded.postValue(hasGPlayBeenFetched) } fun loadMoreDataIfNeeded(searchText: String) { val searchList = searchResult.value?.data?.first?.toMutableList() ?: emptyList() val canLoadMore = searchResult.value?.data?.second ?: false if (searchList.size < MIN_SEARCH_DISPLAY_ITEMS && canLoadMore) { loadMore(searchText, autoTriggered = true) } } fun shouldIgnoreResults(): Boolean { val appsList = _searchResult.value?.data?.first if (appsList.isNullOrEmpty()) return true val appPackageNames = appsList.map { it.package_name } return appPackageNames.all { it.isBlank() } } }