@@ -5,9 +5,12 @@ import android.content.Context
55import android.content.Intent
66import android.content.IntentFilter
77import android.os.Bundle
8+ import android.text.Editable
9+ import android.text.TextWatcher
810import android.view.LayoutInflater
911import android.view.View
1012import android.view.ViewGroup
13+ import android.view.inputmethod.EditorInfo
1114import android.widget.Toast
1215import androidx.fragment.app.Fragment
1316import androidx.lifecycle.lifecycleScope
@@ -45,6 +48,7 @@ class AppListFragment : Fragment() {
4548
4649 private var showSystemApps = false
4750 private var executionMode: ExecutionMode = ExecutionMode .None
51+ private var currentSearchQuery: String = " "
4852
4953 // App cache - persists until package change detected
5054 private var cachedUserApps: List <AppInfo >? = null
@@ -83,13 +87,51 @@ class AppListFragment : Fragment() {
8387 setupHeader()
8488 setupRecyclerView()
8589 setupSwipeRefresh()
90+ setupSearch()
8691 setupChips()
8792 setupSelectionBar()
8893 setupSelectAll()
8994 registerPackageReceiver()
9095 loadApps()
9196 }
9297
98+ private fun setupSearch () {
99+ val b = binding ? : return
100+
101+ b.etSearch.addTextChangedListener(object : TextWatcher {
102+ override fun beforeTextChanged (s : CharSequence? , start : Int , count : Int , after : Int ) {}
103+ override fun onTextChanged (s : CharSequence? , start : Int , before : Int , count : Int ) {}
104+ override fun afterTextChanged (s : Editable ? ) {
105+ currentSearchQuery = s?.toString()?.trim() ? : " "
106+ filterApps()
107+ }
108+ })
109+
110+ b.etSearch.setOnEditorActionListener { _, actionId, _ ->
111+ if (actionId == EditorInfo .IME_ACTION_SEARCH ) {
112+ filterApps()
113+ true
114+ } else false
115+ }
116+ }
117+
118+ private fun filterApps () {
119+ val cachedApps = if (showSystemApps) cachedSystemApps else cachedUserApps
120+ if (cachedApps == null ) return
121+
122+ val filtered = if (currentSearchQuery.isBlank()) {
123+ cachedApps
124+ } else {
125+ val query = currentSearchQuery.lowercase()
126+ cachedApps.filter { app ->
127+ app.appName.lowercase().contains(query) ||
128+ app.packageName.lowercase().contains(query)
129+ }
130+ }
131+
132+ displayApps(filtered)
133+ }
134+
93135 private fun setupExecutionMode () {
94136 executionMode = PermissionBridge (requireContext()).detectMode()
95137
@@ -412,7 +454,7 @@ class AppListFragment : Fragment() {
412454 // Check cache first - only refresh on package change or manual refresh
413455 val cachedApps = if (showSystemApps) cachedSystemApps else cachedUserApps
414456 if (! forceRefresh && cachedApps != null ) {
415- displayApps(cachedApps)
457+ filterApps() // Apply current search filter
416458 b.swipeRefresh.isRefreshing = false
417459 return
418460 }
@@ -437,7 +479,7 @@ class AppListFragment : Fragment() {
437479 cachedUserApps = apps
438480 }
439481
440- displayApps(apps)
482+ filterApps() // Apply current search filter
441483
442484 } catch (e: TimeoutCancellationException ) {
443485 showEmptyState(
@@ -462,11 +504,21 @@ class AppListFragment : Fragment() {
462504 val b = binding ? : return
463505
464506 if (apps.isEmpty()) {
465- showEmptyState(
466- getString(R .string.empty_no_apps_title),
467- getString(R .string.empty_no_apps_message)
468- )
507+ if (currentSearchQuery.isNotBlank()) {
508+ // No search results
509+ showEmptyState(
510+ getString(R .string.search_no_results),
511+ " \" $currentSearchQuery \" "
512+ )
513+ } else {
514+ showEmptyState(
515+ getString(R .string.empty_no_apps_title),
516+ getString(R .string.empty_no_apps_message)
517+ )
518+ }
469519 } else {
520+ b.emptyState.visibility = View .GONE
521+ b.recyclerView.visibility = View .VISIBLE
470522 adapter.submitList(apps)
471523 b.tvAppCount.text = getString(R .string.app_count, apps.size)
472524 }
0 commit comments