@@ -14,28 +14,36 @@ import androidx.lifecycle.lifecycleScope
1414import androidx.recyclerview.widget.LinearLayoutManager
1515import com.appcontrolx.R
1616import com.appcontrolx.databinding.BottomSheetActivityLauncherBinding
17- import com.appcontrolx.ui.adapter.ActivityListAdapter
17+ import com.appcontrolx.ui.adapter.AppActivityAdapter
1818import com.google.android.material.bottomsheet.BottomSheetDialogFragment
1919import kotlinx.coroutines.Dispatchers
2020import kotlinx.coroutines.launch
2121import kotlinx.coroutines.withContext
22+ import timber.log.Timber
2223
2324class ActivityLauncherBottomSheet : BottomSheetDialogFragment () {
2425
2526 private var _binding : BottomSheetActivityLauncherBinding ? = null
2627 private val binding get() = _binding
2728
28- private lateinit var adapter: ActivityListAdapter
29- private var allActivities : List <ActivityItem > = emptyList()
29+ private lateinit var adapter: AppActivityAdapter
30+ private var allAppGroups : List <AppActivityGroup > = emptyList()
3031 private var showSystemApps = false
3132
3233 data class ActivityItem (
3334 val packageName : String ,
3435 val activityName : String ,
36+ val shortName : String ,
37+ val isExported : Boolean
38+ )
39+
40+ data class AppActivityGroup (
41+ val packageName : String ,
3542 val appName : String ,
3643 val appIcon : Drawable ? ,
3744 val isSystem : Boolean ,
38- val isExported : Boolean
45+ val activities : List <ActivityItem >,
46+ var isExpanded : Boolean = false
3947 )
4048
4149 companion object {
@@ -52,18 +60,34 @@ class ActivityLauncherBottomSheet : BottomSheetDialogFragment() {
5260 super .onViewCreated(view, savedInstanceState)
5361 setupRecyclerView()
5462 setupChips()
63+ setupSearch()
5564 loadActivities()
5665 }
5766
5867 private fun setupRecyclerView () {
5968 val b = binding ? : return
60- adapter = ActivityListAdapter { activity ->
61- launchActivity(activity)
62- }
69+ adapter = AppActivityAdapter (
70+ onAppClick = { group ->
71+ toggleExpand(group)
72+ },
73+ onActivityClick = { activity ->
74+ launchActivity(activity)
75+ }
76+ )
6377 b.recyclerView.layoutManager = LinearLayoutManager (context)
6478 b.recyclerView.adapter = adapter
6579 }
6680
81+ private fun toggleExpand (group : AppActivityGroup ) {
82+ val index = allAppGroups.indexOfFirst { it.packageName == group.packageName }
83+ if (index >= 0 ) {
84+ allAppGroups = allAppGroups.toMutableList().apply {
85+ this [index] = this [index].copy(isExpanded = ! this [index].isExpanded)
86+ }
87+ filterActivities()
88+ }
89+ }
90+
6791 private fun setupChips () {
6892 val b = binding ? : return
6993 b.chipUserApps.isChecked = true
@@ -83,63 +107,93 @@ class ActivityLauncherBottomSheet : BottomSheetDialogFragment() {
83107 }
84108 }
85109
110+ private fun setupSearch () {
111+ val b = binding ? : return
112+ b.searchView.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView .OnQueryTextListener {
113+ override fun onQueryTextSubmit (query : String? ) = false
114+ override fun onQueryTextChange (newText : String? ): Boolean {
115+ filterActivities(newText)
116+ return true
117+ }
118+ })
119+ }
120+
86121 private fun loadActivities () {
87122 val b = binding ? : return
88123 b.progressBar.visibility = View .VISIBLE
89124
90125 lifecycleScope.launch {
91- allActivities = withContext(Dispatchers .IO ) {
126+ allAppGroups = withContext(Dispatchers .IO ) {
92127 val pm = requireContext().packageManager
93128 val packages = pm.getInstalledPackages(PackageManager .GET_ACTIVITIES )
94129
95- packages.flatMap { pkg ->
130+ packages.mapNotNull { pkg ->
96131 val isSystem = (pkg.applicationInfo.flags and android.content.pm.ApplicationInfo .FLAG_SYSTEM ) != 0
97132 val appName = pkg.applicationInfo.loadLabel(pm).toString()
98133 val appIcon = try { pkg.applicationInfo.loadIcon(pm) } catch (e: Exception ) { null }
99134
100- pkg.activities?.mapNotNull { activity ->
101- // Only include valid launchable activities
135+ val activities = pkg.activities?.mapNotNull { activity ->
102136 if (isValidActivity(activity)) {
103137 ActivityItem (
104138 packageName = pkg.packageName,
105139 activityName = activity.name,
106- appName = appName,
107- appIcon = appIcon,
108- isSystem = isSystem,
140+ shortName = activity.name.substringAfterLast(" ." ),
109141 isExported = activity.exported
110142 )
111143 } else null
112144 } ? : emptyList()
145+
146+ if (activities.isNotEmpty()) {
147+ AppActivityGroup (
148+ packageName = pkg.packageName,
149+ appName = appName,
150+ appIcon = appIcon,
151+ isSystem = isSystem,
152+ activities = activities.sortedBy { it.shortName.lowercase() }
153+ )
154+ } else null
113155 }.sortedBy { it.appName.lowercase() }
114156 }
115157
116158 b.progressBar.visibility = View .GONE
159+ Timber .d(" Loaded ${allAppGroups.size} apps with activities" )
117160 filterActivities()
118161 }
119162 }
120163
121164 private fun isValidActivity (activity : ActivityInfo ): Boolean {
122- // Filter out invalid/internal activities
123165 val name = activity.name.lowercase()
124166
125167 // Skip internal/test activities
126168 if (name.contains(" test" ) || name.contains(" debug" ) || name.contains(" internal" )) {
127169 return false
128170 }
129171
130- // Skip activities that are clearly not meant to be launched
172+ // Skip non-activity components
131173 if (name.endsWith(" receiver" ) || name.endsWith(" service" ) || name.endsWith(" provider" )) {
132174 return false
133175 }
134176
135- // Prefer exported activities or activities with a label
136177 return activity.exported || activity.labelRes != 0
137178 }
138179
139- private fun filterActivities () {
140- val filtered = allActivities.filter { it.isSystem == showSystemApps }
180+ private fun filterActivities (searchQuery : String? = null) {
181+ var filtered = allAppGroups.filter { it.isSystem == showSystemApps }
182+
183+ // Apply search filter
184+ if (! searchQuery.isNullOrBlank()) {
185+ val query = searchQuery.lowercase()
186+ filtered = filtered.filter { group ->
187+ group.appName.lowercase().contains(query) ||
188+ group.packageName.lowercase().contains(query) ||
189+ group.activities.any { it.shortName.lowercase().contains(query) }
190+ }
191+ }
192+
141193 adapter.submitList(filtered)
142- binding?.tvCount?.text = getString(R .string.tools_activity_count, filtered.size)
194+
195+ val totalActivities = filtered.sumOf { it.activities.size }
196+ binding?.tvCount?.text = getString(R .string.activity_launcher_count, filtered.size, totalActivities)
143197 }
144198
145199 private fun launchActivity (activity : ActivityItem ) {
@@ -149,7 +203,9 @@ class ActivityLauncherBottomSheet : BottomSheetDialogFragment() {
149203 addFlags(Intent .FLAG_ACTIVITY_NEW_TASK )
150204 }
151205 startActivity(intent)
206+ Timber .d(" Launched activity: ${activity.activityName} " )
152207 } catch (e: Exception ) {
208+ Timber .e(e, " Failed to launch activity: ${activity.activityName} " )
153209 Toast .makeText(context, R .string.tools_activity_launch_failed, Toast .LENGTH_SHORT ).show()
154210 }
155211 }
0 commit comments