Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 793bdce3 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

refactor: make change in app sources reflect on search results

parent c4f2b214
Loading
Loading
Loading
Loading
Loading
+29 −15
Original line number Diff line number Diff line
@@ -66,7 +66,7 @@ import kotlinx.coroutines.launch
import java.util.Locale
import javax.inject.Inject

@Suppress("TooManyFunctions") // TODO: Remove after refactoring is complete
@Suppress("TooManyFunctions", "MaxLineLength") // TODO: Remove after refactoring is complete
@AndroidEntryPoint
class SearchFragment :
    Fragment(R.layout.fragment_search),
@@ -98,12 +98,6 @@ class SearchFragment :
    lateinit var filterChipOpenSource: Chip
    lateinit var filterChipPWA: Chip

    /*
     * Store the string from onQueryTextSubmit() and access it from loadData()
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413
     */
    private var searchText = ""

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        _binding = FragmentSearchBinding.bind(view)
@@ -134,7 +128,7 @@ class SearchFragment :
                    if (!requireContext().isNetworkAvailable()) {
                        return
                    }
                    searchViewModel.loadMore(searchText)
                    searchViewModel.loadMore()
                }
            }
        })
@@ -145,26 +139,42 @@ class SearchFragment :
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                searchViewModel.searchUiState.collectLatest { state ->
                    when (state) {
                        is SearchResultsUiState.Initial -> {
                            searchHintLayout?.visibility = View.VISIBLE
                            noAppsFoundLayout?.visibility = View.GONE
                        }

                        is SearchResultsUiState.Loading -> {
                            noAppsFoundLayout?.visibility = View.GONE
                            searchHintLayout?.visibility = View.GONE
                            showLoadingUi()
                        }

                        is SearchResultsUiState.Success -> {
                            noAppsFoundLayout?.visibility = View.GONE
                            searchHintLayout?.visibility = View.GONE

                            listAdapter?.let { observeDownloadList(it) }
                            updateSearchResult(listAdapter, state.apps)
                            observeScrollOfSearchResult(listAdapter)
                        }

                        is SearchResultsUiState.Empty -> {
                            searchHintLayout?.visibility = View.GONE
                            noAppsFoundLayout?.visibility = View.GONE

                            stopLoadingUi()

                            noAppsFoundLayout?.visibility = View.VISIBLE

                            listAdapter?.setData(emptyList())
                        }

                        is SearchResultsUiState.Error -> {
                            stopLoadingUi()
                            searchHintLayout?.visibility = View.GONE
                            noAppsFoundLayout?.visibility = View.VISIBLE

                            stopLoadingUi()
                        }
                    }
                }
@@ -186,9 +196,9 @@ class SearchFragment :
                     * Compare lastSearch with searchText to avoid falsely updating to
                     * current query text even before submitting the new search.
                     */
                    if (lastSearch != searchText && positionStart == 0) {
                    if (lastSearch != searchViewModel.searchText && positionStart == 0) {
                        scrollToTop()
                        lastSearch = searchText
                        lastSearch = searchViewModel.searchText
                    }
                }
            }
@@ -331,7 +341,7 @@ class SearchFragment :
    }

    private fun initiateSearch() {
        searchViewModel.loadData(searchText)
        searchViewModel.loadData()
    }

    private fun showLoadingUi() {
@@ -378,10 +388,14 @@ class SearchFragment :
            }
        }

        if (searchText.isEmpty() && (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty()) {
        if (searchViewModel.searchText.isEmpty() && (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty()) {
            searchView?.requestFocus()
            showKeyboard()
        }

        if (searchViewModel.searchText.isNotBlank() && searchViewModel.haveSourcesChanged()) {
            initiateSearch()
        }
    }

    private fun addDownloadProgressObservers() {
@@ -392,7 +406,7 @@ class SearchFragment :
    }

    private fun shouldRefreshData() =
        searchText.isNotEmpty() && recyclerView?.adapter != null
        searchViewModel.searchText.isNotEmpty() && recyclerView?.adapter != null

    override fun onPause() {
        binding.shimmerLayout.stopShimmer()
@@ -411,7 +425,7 @@ class SearchFragment :
            /*
             * Set the search text and call for network result.
             */
            searchText = text
            searchViewModel.searchText = text
            initiateSearch()
        }
        return false
+1 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import foundation.e.apps.data.application.data.Application
 * - Error: failure with optional message/throwable
 */
sealed interface SearchResultsUiState {
    data object Initial : SearchResultsUiState
    data object Loading : SearchResultsUiState
    data class Success(val apps: List<Application>) : SearchResultsUiState
    data object Empty : SearchResultsUiState
+36 −15
Original line number Diff line number Diff line
@@ -23,6 +23,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.StoreRepository
import foundation.e.apps.data.Stores
import foundation.e.apps.data.application.ApplicationRepository
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.search.SearchRepository
@@ -48,7 +50,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject

@HiltViewModel
@@ -56,7 +57,8 @@ class SearchViewModel @Inject constructor(
    private val applicationRepository: ApplicationRepository,
    private val searchRepository: SearchRepository,
    private val privacyScoreRepository: PrivacyScoreRepository,
    private val appPrivacyInfoRepository: IAppPrivacyInfoRepository
    private val appPrivacyInfoRepository: IAppPrivacyInfoRepository,
    private val stores: Stores,
) : ViewModel() {

    companion object {
@@ -64,10 +66,13 @@ class SearchViewModel @Inject constructor(
    }

    private val _searchUiState: MutableStateFlow<SearchResultsUiState> =
        MutableStateFlow(SearchResultsUiState.Loading)
        MutableStateFlow(SearchResultsUiState.Initial)
    val searchUiState: StateFlow<SearchResultsUiState> = _searchUiState.asStateFlow()

    private var isLoading: Boolean = false
    private var isCleanApkDataLoading = false

    private var previousStores = mapOf<Source, StoreRepository>()

    @GuardedBy("mutex")
    private val accumulatedList = mutableListOf<Application>()
@@ -77,6 +82,8 @@ class SearchViewModel @Inject constructor(
    private var flagOpenSource: Boolean = false
    private var flagPWA: Boolean = false

    var searchText = ""

    val query = MutableStateFlow("")

    fun onQueryChanged(value: String) {
@@ -107,33 +114,47 @@ class SearchViewModel @Inject constructor(
        }
    }

    fun loadData(query: String) {
    fun loadData() {
        viewModelScope.launch {
            _searchUiState.value = SearchResultsUiState.Loading

        viewModelScope.launch {
            mutex.withLock {
                accumulatedList.clear()
            }

            fetchCleanApkData(query)
            fetchPlayStoreData(query)
            fetchCleanApkData(searchText)
            fetchPlayStoreData(searchText)
        }
    }

    fun haveSourcesChanged(): Boolean {
        val newStores = stores.getStores()
        if (newStores == previousStores) {
            return false
        }

        previousStores = newStores.toMap()
        return true
    }

    private fun fetchCleanApkData(query: String) {
        viewModelScope.launch(Dispatchers.IO) {
            isCleanApkDataLoading = true

            val searchResults = searchRepository.getOpenSourceSearchResults(query)
            isCleanApkDataLoading = false

            emitFilteredResults(searchResults)

        }
    }

    fun loadMore(query: String) {
    fun loadMore() {
        viewModelScope.launch(Dispatchers.Main) {
            if (isLoading) {
                Timber.d("Search result is loading....")
                return@launch
            }
            fetchPlayStoreData(query)
            fetchPlayStoreData(searchText)
        }
    }

@@ -235,10 +256,10 @@ class SearchViewModel @Inject constructor(

        val filteredList = getFilteredList()

        _searchUiState.value = if (filteredList.isEmpty()) {
            SearchResultsUiState.Empty
        } else {
            SearchResultsUiState.Success(filteredList.toList())
        _searchUiState.value = when {
            isCleanApkDataLoading -> SearchResultsUiState.Loading
            filteredList.isEmpty() -> SearchResultsUiState.Empty
            else -> SearchResultsUiState.Success(filteredList.toList())
        }
    }
}
+8 −6
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?><!--
  ~ Apps  Quickly and easily install Android apps onto your device!
  ~ Copyright (C) 2021  E FOUNDATION
  ~ Copyright (C) 2021-2025 e Foundation
  ~
  ~ This program is free software: you can redistribute it and/or modify
  ~ it under the terms of the GNU General Public License as published by
@@ -14,6 +13,7 @@
  ~
  ~ You should have received a copy of the GNU General Public License
  ~ along with this program.  If not, see <https://www.gnu.org/licenses/>.
  ~
  -->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
@@ -82,13 +82,14 @@

    <include
        android:id="@+id/searchHintLayout"
        tools:visibility="gone"
        android:visibility="gone"
        tools:visibility="visible"
        layout="@layout/fragment_search_hint" />

    <include
        android:id="@+id/noAppsFoundLayout"
        android:visibility="gone"
        tools:visibility="gone"
        tools:visibility="visible"
        layout="@layout/layout_no_apps_found" />

    <com.facebook.shimmer.ShimmerFrameLayout
@@ -96,7 +97,8 @@
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:visibility="gone">
        android:visibility="gone"
        tools:visibility="visible">

        <LinearLayout
            android:layout_width="match_parent"