Loading app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +46 −81 Original line number Diff line number Diff line Loading @@ -21,18 +21,13 @@ package foundation.e.apps.ui.search import android.app.Activity import android.content.Context import android.database.MatrixCursor import android.os.Bundle import android.provider.BaseColumns import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.CompoundButton.OnCheckedChangeListener import android.widget.EditText import android.widget.ImageView import android.widget.LinearLayout import androidx.appcompat.widget.SearchView import androidx.cursoradapter.widget.CursorAdapter import androidx.cursoradapter.widget.SimpleCursorAdapter import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels Loading @@ -48,7 +43,6 @@ import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.application.ApplicationInstaller import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.search.SearchSuggestion import foundation.e.apps.data.enums.Status import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.databinding.FragmentSearchBinding Loading @@ -66,13 +60,9 @@ import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject @Suppress("TooManyFunctions", "MaxLineLength") // TODO: Remove after refactoring is complete @AndroidEntryPoint class SearchFragment : Fragment(R.layout.fragment_search), SearchView.OnQueryTextListener, SearchView.OnSuggestionListener, ApplicationInstaller { class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, SearchViewHandler.SearchViewListener { @Inject lateinit var pwaManager: PwaManager Loading @@ -86,10 +76,11 @@ class SearchFragment : val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() private val SUGGESTION_KEY = "suggestion" private var lastSearch = "" private var searchView: SearchView? = null private var searchViewHandler: SearchViewHandler? = null private var shimmerLayout: ShimmerFrameLayout? = null private var recyclerView: RecyclerView? = null private var searchHintLayout: LinearLayout? = null Loading @@ -111,8 +102,8 @@ class SearchFragment : filterChipOpenSource = binding.filterChipOpenSource filterChipPWA = binding.filterChipPWA setupSearchView() setupSearchViewSuggestions() setupSearchViewHandler() setHasOptionsMenu(true) // Setup Search Results val listAdapter = setupSearchResult(view) Loading Loading @@ -261,19 +252,26 @@ class SearchFragment : return listAdapter } private fun setupSearchViewSuggestions() { val from = arrayOf(SUGGESTION_KEY) val to = intArrayOf(android.R.id.text1) searchView?.suggestionsAdapter = SimpleCursorAdapter( context, R.layout.custom_simple_list_item, null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER private fun setupSearchViewHandler() { searchView?.let { searchViewHandler = SearchViewHandler( searchView = it, context = requireContext(), listener = this, ) searchViewHandler?.setup() } if (searchViewModel.searchText.isNotBlank()) { searchView?.setQuery( searchViewModel.searchText, false ) // 'false' prevents re-submitting the query } viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { searchViewModel.searchSuggestions.collectLatest { populateSuggestionsAdapter(it) searchViewModel.searchSuggestions.collectLatest { suggestions -> searchViewHandler?.populateSuggestionsAdapter(suggestions) } } } Loading @@ -297,13 +295,6 @@ class SearchFragment : filterChipPWA.setOnCheckedChangeListener(listener) } private fun setupSearchView() { setHasOptionsMenu(true) searchView?.setOnSuggestionListener(this) searchView?.setOnQueryTextListener(this) searchView?.let { configureCloseButton(it) } } private fun showPaidAppMessage(application: Application) { ApplicationDialogFragment( title = getString(R.string.dialog_title_paid_app, application.name), Loading Loading @@ -388,7 +379,9 @@ class SearchFragment : } } if (searchViewModel.searchText.isEmpty() && (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty()) { if (searchViewModel.searchText.isEmpty() && (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty() ) { searchView?.requestFocus() showKeyboard() } Loading @@ -414,41 +407,6 @@ class SearchFragment : super.onPause() } override fun onQueryTextSubmit(query: String?): Boolean { query?.let { text -> if (text.isNotEmpty()) { hideKeyboard(activity as Activity) } view?.requestFocus() searchHintLayout?.visibility = View.GONE /* * Set the search text and call for network result. */ searchViewModel.searchText = text initiateSearch() } return false } override fun onQueryTextChange(newText: String?): Boolean { searchViewModel.onQueryChanged(newText.orEmpty()) return true } override fun onSuggestionSelect(position: Int): Boolean { return true } override fun onSuggestionClick(position: Int): Boolean { searchViewModel.searchSuggestions.value.let { if (it.isNotEmpty()) { searchView?.setQuery(it[position].suggestion, true) } } return true } override fun onDestroyView() { super.onDestroyView() _binding = null Loading @@ -458,13 +416,7 @@ class SearchFragment : recyclerView = null searchHintLayout = null noAppsFoundLayout = null } private fun configureCloseButton(searchView: SearchView) { val searchClose = searchView.javaClass.getDeclaredField("mCloseButton") searchClose.isAccessible = true val closeImage = searchClose.get(searchView) as ImageView closeImage.setImageResource(R.drawable.ic_close) searchViewHandler = null } private fun hideKeyboard(activity: Activity) { Loading @@ -488,14 +440,27 @@ class SearchFragment : } } private fun populateSuggestionsAdapter(suggestions: List<SearchSuggestion>?) { val cursor = MatrixCursor(arrayOf(BaseColumns._ID, SUGGESTION_KEY)) suggestions?.let { for (i in it.indices) { cursor.addRow(arrayOf(i, it[i].suggestion)) override fun onQuerySubmitted(query: String) { query.let { text -> if (text.isNotEmpty()) { hideKeyboard(activity as Activity) } view?.requestFocus() searchHintLayout?.visibility = View.GONE searchViewModel.searchText = text initiateSearch() } } searchView?.suggestionsAdapter?.changeCursor(cursor) override fun onQueryChanged(newText: String) { searchViewModel.onSearchQueryChanged(newText) } override fun onSuggestionClicked(position: Int) { val suggestion = searchViewModel.searchSuggestions.value.getOrNull(position)?.suggestion suggestion?.let { searchView?.setQuery(it, true) } } override fun installApplication(app: Application) { Loading app/src/main/java/foundation/e/apps/ui/search/SearchViewHandler.kt 0 → 100644 +100 −0 Original line number Diff line number Diff line /* * Copyright (C) 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 * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.ui.search import android.content.Context import android.database.MatrixCursor import android.provider.BaseColumns import android.widget.ImageView import androidx.appcompat.widget.SearchView import androidx.cursoradapter.widget.CursorAdapter import androidx.cursoradapter.widget.SimpleCursorAdapter import foundation.e.apps.R import foundation.e.apps.data.application.search.SearchSuggestion class SearchViewHandler( private val searchView: SearchView, private val context: Context, private val listener: SearchViewListener, ) : SearchView.OnQueryTextListener, SearchView.OnSuggestionListener { interface SearchViewListener { fun onQuerySubmitted(query: String) fun onQueryChanged(newText: String) fun onSuggestionClicked(position: Int) } fun setup() { searchView.setOnQueryTextListener(this) searchView.setOnSuggestionListener(this) setupSuggestions() configureCloseButton() } private fun setupSuggestions() { val from = arrayOf(SUGGESTION_KEY) val to = intArrayOf(android.R.id.text1) searchView.suggestionsAdapter = SimpleCursorAdapter( context, R.layout.custom_simple_list_item, null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER ) } internal fun populateSuggestionsAdapter(suggestions: List<SearchSuggestion>?) { val cursor = MatrixCursor(arrayOf(BaseColumns._ID, SUGGESTION_KEY)) suggestions?.let { for (i in it.indices) { cursor.addRow(arrayOf(i, it[i].suggestion)) } } searchView.suggestionsAdapter?.changeCursor(cursor) } private fun configureCloseButton() { val searchClose = searchView.javaClass.getDeclaredField("mCloseButton") searchClose.isAccessible = true val closeImage = searchClose.get(searchView) as ImageView closeImage.setImageResource(R.drawable.ic_close) } override fun onQueryTextSubmit(query: String?): Boolean { query?.let { text -> listener.onQuerySubmitted(text) } return false } override fun onQueryTextChange(newText: String?): Boolean { listener.onQueryChanged(newText.orEmpty()) return true } override fun onSuggestionSelect(position: Int): Boolean { return true } override fun onSuggestionClick(position: Int): Boolean { listener.onSuggestionClicked(position) return true } companion object { private const val SUGGESTION_KEY = "suggestion" } } app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +1 −1 Original line number Diff line number Diff line Loading @@ -86,7 +86,7 @@ class SearchViewModel @Inject constructor( val query = MutableStateFlow("") fun onQueryChanged(value: String) { fun onSearchQueryChanged(value: String) { query.value = value } Loading Loading
app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +46 −81 Original line number Diff line number Diff line Loading @@ -21,18 +21,13 @@ package foundation.e.apps.ui.search import android.app.Activity import android.content.Context import android.database.MatrixCursor import android.os.Bundle import android.provider.BaseColumns import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.CompoundButton.OnCheckedChangeListener import android.widget.EditText import android.widget.ImageView import android.widget.LinearLayout import androidx.appcompat.widget.SearchView import androidx.cursoradapter.widget.CursorAdapter import androidx.cursoradapter.widget.SimpleCursorAdapter import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels Loading @@ -48,7 +43,6 @@ import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.application.ApplicationInstaller import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.search.SearchSuggestion import foundation.e.apps.data.enums.Status import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.databinding.FragmentSearchBinding Loading @@ -66,13 +60,9 @@ import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject @Suppress("TooManyFunctions", "MaxLineLength") // TODO: Remove after refactoring is complete @AndroidEntryPoint class SearchFragment : Fragment(R.layout.fragment_search), SearchView.OnQueryTextListener, SearchView.OnSuggestionListener, ApplicationInstaller { class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, SearchViewHandler.SearchViewListener { @Inject lateinit var pwaManager: PwaManager Loading @@ -86,10 +76,11 @@ class SearchFragment : val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() private val SUGGESTION_KEY = "suggestion" private var lastSearch = "" private var searchView: SearchView? = null private var searchViewHandler: SearchViewHandler? = null private var shimmerLayout: ShimmerFrameLayout? = null private var recyclerView: RecyclerView? = null private var searchHintLayout: LinearLayout? = null Loading @@ -111,8 +102,8 @@ class SearchFragment : filterChipOpenSource = binding.filterChipOpenSource filterChipPWA = binding.filterChipPWA setupSearchView() setupSearchViewSuggestions() setupSearchViewHandler() setHasOptionsMenu(true) // Setup Search Results val listAdapter = setupSearchResult(view) Loading Loading @@ -261,19 +252,26 @@ class SearchFragment : return listAdapter } private fun setupSearchViewSuggestions() { val from = arrayOf(SUGGESTION_KEY) val to = intArrayOf(android.R.id.text1) searchView?.suggestionsAdapter = SimpleCursorAdapter( context, R.layout.custom_simple_list_item, null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER private fun setupSearchViewHandler() { searchView?.let { searchViewHandler = SearchViewHandler( searchView = it, context = requireContext(), listener = this, ) searchViewHandler?.setup() } if (searchViewModel.searchText.isNotBlank()) { searchView?.setQuery( searchViewModel.searchText, false ) // 'false' prevents re-submitting the query } viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { searchViewModel.searchSuggestions.collectLatest { populateSuggestionsAdapter(it) searchViewModel.searchSuggestions.collectLatest { suggestions -> searchViewHandler?.populateSuggestionsAdapter(suggestions) } } } Loading @@ -297,13 +295,6 @@ class SearchFragment : filterChipPWA.setOnCheckedChangeListener(listener) } private fun setupSearchView() { setHasOptionsMenu(true) searchView?.setOnSuggestionListener(this) searchView?.setOnQueryTextListener(this) searchView?.let { configureCloseButton(it) } } private fun showPaidAppMessage(application: Application) { ApplicationDialogFragment( title = getString(R.string.dialog_title_paid_app, application.name), Loading Loading @@ -388,7 +379,9 @@ class SearchFragment : } } if (searchViewModel.searchText.isEmpty() && (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty()) { if (searchViewModel.searchText.isEmpty() && (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty() ) { searchView?.requestFocus() showKeyboard() } Loading @@ -414,41 +407,6 @@ class SearchFragment : super.onPause() } override fun onQueryTextSubmit(query: String?): Boolean { query?.let { text -> if (text.isNotEmpty()) { hideKeyboard(activity as Activity) } view?.requestFocus() searchHintLayout?.visibility = View.GONE /* * Set the search text and call for network result. */ searchViewModel.searchText = text initiateSearch() } return false } override fun onQueryTextChange(newText: String?): Boolean { searchViewModel.onQueryChanged(newText.orEmpty()) return true } override fun onSuggestionSelect(position: Int): Boolean { return true } override fun onSuggestionClick(position: Int): Boolean { searchViewModel.searchSuggestions.value.let { if (it.isNotEmpty()) { searchView?.setQuery(it[position].suggestion, true) } } return true } override fun onDestroyView() { super.onDestroyView() _binding = null Loading @@ -458,13 +416,7 @@ class SearchFragment : recyclerView = null searchHintLayout = null noAppsFoundLayout = null } private fun configureCloseButton(searchView: SearchView) { val searchClose = searchView.javaClass.getDeclaredField("mCloseButton") searchClose.isAccessible = true val closeImage = searchClose.get(searchView) as ImageView closeImage.setImageResource(R.drawable.ic_close) searchViewHandler = null } private fun hideKeyboard(activity: Activity) { Loading @@ -488,14 +440,27 @@ class SearchFragment : } } private fun populateSuggestionsAdapter(suggestions: List<SearchSuggestion>?) { val cursor = MatrixCursor(arrayOf(BaseColumns._ID, SUGGESTION_KEY)) suggestions?.let { for (i in it.indices) { cursor.addRow(arrayOf(i, it[i].suggestion)) override fun onQuerySubmitted(query: String) { query.let { text -> if (text.isNotEmpty()) { hideKeyboard(activity as Activity) } view?.requestFocus() searchHintLayout?.visibility = View.GONE searchViewModel.searchText = text initiateSearch() } } searchView?.suggestionsAdapter?.changeCursor(cursor) override fun onQueryChanged(newText: String) { searchViewModel.onSearchQueryChanged(newText) } override fun onSuggestionClicked(position: Int) { val suggestion = searchViewModel.searchSuggestions.value.getOrNull(position)?.suggestion suggestion?.let { searchView?.setQuery(it, true) } } override fun installApplication(app: Application) { Loading
app/src/main/java/foundation/e/apps/ui/search/SearchViewHandler.kt 0 → 100644 +100 −0 Original line number Diff line number Diff line /* * Copyright (C) 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 * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.ui.search import android.content.Context import android.database.MatrixCursor import android.provider.BaseColumns import android.widget.ImageView import androidx.appcompat.widget.SearchView import androidx.cursoradapter.widget.CursorAdapter import androidx.cursoradapter.widget.SimpleCursorAdapter import foundation.e.apps.R import foundation.e.apps.data.application.search.SearchSuggestion class SearchViewHandler( private val searchView: SearchView, private val context: Context, private val listener: SearchViewListener, ) : SearchView.OnQueryTextListener, SearchView.OnSuggestionListener { interface SearchViewListener { fun onQuerySubmitted(query: String) fun onQueryChanged(newText: String) fun onSuggestionClicked(position: Int) } fun setup() { searchView.setOnQueryTextListener(this) searchView.setOnSuggestionListener(this) setupSuggestions() configureCloseButton() } private fun setupSuggestions() { val from = arrayOf(SUGGESTION_KEY) val to = intArrayOf(android.R.id.text1) searchView.suggestionsAdapter = SimpleCursorAdapter( context, R.layout.custom_simple_list_item, null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER ) } internal fun populateSuggestionsAdapter(suggestions: List<SearchSuggestion>?) { val cursor = MatrixCursor(arrayOf(BaseColumns._ID, SUGGESTION_KEY)) suggestions?.let { for (i in it.indices) { cursor.addRow(arrayOf(i, it[i].suggestion)) } } searchView.suggestionsAdapter?.changeCursor(cursor) } private fun configureCloseButton() { val searchClose = searchView.javaClass.getDeclaredField("mCloseButton") searchClose.isAccessible = true val closeImage = searchClose.get(searchView) as ImageView closeImage.setImageResource(R.drawable.ic_close) } override fun onQueryTextSubmit(query: String?): Boolean { query?.let { text -> listener.onQuerySubmitted(text) } return false } override fun onQueryTextChange(newText: String?): Boolean { listener.onQueryChanged(newText.orEmpty()) return true } override fun onSuggestionSelect(position: Int): Boolean { return true } override fun onSuggestionClick(position: Int): Boolean { listener.onSuggestionClicked(position) return true } companion object { private const val SUGGESTION_KEY = "suggestion" } }
app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +1 −1 Original line number Diff line number Diff line Loading @@ -86,7 +86,7 @@ class SearchViewModel @Inject constructor( val query = MutableStateFlow("") fun onQueryChanged(value: String) { fun onSearchQueryChanged(value: String) { query.value = value } Loading