Skip to content

Commit da36987

Browse files
committed
Major Enhancement: MVVM Architecture, Hilt DI, Firebase, ProGuard, Unit Tests, Activity Launcher redesign with expandable groups
1 parent fe76cca commit da36987

File tree

8 files changed

+416
-51
lines changed

8 files changed

+416
-51
lines changed

app/src/main/java/com/appcontrolx/service/BatteryPolicyManager.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,14 @@ class BatteryPolicyManager(private val executor: CommandExecutor) {
8787
}
8888
return executor.execute("pm uninstall -k --user 0 $packageName").map { }
8989
}
90+
91+
fun clearCache(packageName: String): Result<Unit> {
92+
validatePackageName(packageName).onFailure { return Result.failure(it) }
93+
return executor.execute("pm clear --cache-only $packageName").map { }
94+
}
95+
96+
fun clearData(packageName: String): Result<Unit> {
97+
validatePackageName(packageName).onFailure { return Result.failure(it) }
98+
return executor.execute("pm clear $packageName").map { }
99+
}
90100
}

app/src/main/java/com/appcontrolx/ui/ActivityLauncherBottomSheet.kt

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,36 @@ import androidx.lifecycle.lifecycleScope
1414
import androidx.recyclerview.widget.LinearLayoutManager
1515
import com.appcontrolx.R
1616
import com.appcontrolx.databinding.BottomSheetActivityLauncherBinding
17-
import com.appcontrolx.ui.adapter.ActivityListAdapter
17+
import com.appcontrolx.ui.adapter.AppActivityAdapter
1818
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
1919
import kotlinx.coroutines.Dispatchers
2020
import kotlinx.coroutines.launch
2121
import kotlinx.coroutines.withContext
22+
import timber.log.Timber
2223

2324
class 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
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.appcontrolx.ui.adapter
2+
3+
import android.view.LayoutInflater
4+
import android.view.View
5+
import android.view.ViewGroup
6+
import androidx.recyclerview.widget.DiffUtil
7+
import androidx.recyclerview.widget.LinearLayoutManager
8+
import androidx.recyclerview.widget.ListAdapter
9+
import androidx.recyclerview.widget.RecyclerView
10+
import com.appcontrolx.databinding.ItemAppActivityGroupBinding
11+
import com.appcontrolx.databinding.ItemActivitySimpleBinding
12+
import com.appcontrolx.ui.ActivityLauncherBottomSheet.ActivityItem
13+
import com.appcontrolx.ui.ActivityLauncherBottomSheet.AppActivityGroup
14+
15+
class AppActivityAdapter(
16+
private val onAppClick: (AppActivityGroup) -> Unit,
17+
private val onActivityClick: (ActivityItem) -> Unit
18+
) : ListAdapter<AppActivityGroup, AppActivityAdapter.AppViewHolder>(DiffCallback()) {
19+
20+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
21+
val binding = ItemAppActivityGroupBinding.inflate(
22+
LayoutInflater.from(parent.context), parent, false
23+
)
24+
return AppViewHolder(binding)
25+
}
26+
27+
override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
28+
holder.bind(getItem(position))
29+
}
30+
31+
inner class AppViewHolder(
32+
private val binding: ItemAppActivityGroupBinding
33+
) : RecyclerView.ViewHolder(binding.root) {
34+
35+
private val activityAdapter = ActivitySimpleAdapter { activity ->
36+
onActivityClick(activity)
37+
}
38+
39+
init {
40+
binding.rvActivities.layoutManager = LinearLayoutManager(binding.root.context)
41+
binding.rvActivities.adapter = activityAdapter
42+
}
43+
44+
fun bind(group: AppActivityGroup) {
45+
// App icon
46+
if (group.appIcon != null) {
47+
binding.ivAppIcon.setImageDrawable(group.appIcon)
48+
}
49+
50+
binding.tvAppName.text = group.appName
51+
binding.tvActivityCount.text = "${group.activities.size} activities"
52+
53+
// Expand/collapse icon
54+
binding.ivExpand.rotation = if (group.isExpanded) 180f else 0f
55+
56+
// Activities list visibility
57+
binding.rvActivities.visibility = if (group.isExpanded) View.VISIBLE else View.GONE
58+
59+
if (group.isExpanded) {
60+
activityAdapter.submitList(group.activities)
61+
}
62+
63+
// Click to expand/collapse
64+
binding.headerLayout.setOnClickListener {
65+
onAppClick(group)
66+
}
67+
}
68+
}
69+
70+
class DiffCallback : DiffUtil.ItemCallback<AppActivityGroup>() {
71+
override fun areItemsTheSame(oldItem: AppActivityGroup, newItem: AppActivityGroup): Boolean {
72+
return oldItem.packageName == newItem.packageName
73+
}
74+
75+
override fun areContentsTheSame(oldItem: AppActivityGroup, newItem: AppActivityGroup): Boolean {
76+
return oldItem.packageName == newItem.packageName &&
77+
oldItem.isExpanded == newItem.isExpanded
78+
}
79+
}
80+
}
81+
82+
class ActivitySimpleAdapter(
83+
private val onItemClick: (ActivityItem) -> Unit
84+
) : ListAdapter<ActivityItem, ActivitySimpleAdapter.ViewHolder>(DiffCallback()) {
85+
86+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
87+
val binding = ItemActivitySimpleBinding.inflate(
88+
LayoutInflater.from(parent.context), parent, false
89+
)
90+
return ViewHolder(binding)
91+
}
92+
93+
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
94+
holder.bind(getItem(position))
95+
}
96+
97+
inner class ViewHolder(
98+
private val binding: ItemActivitySimpleBinding
99+
) : RecyclerView.ViewHolder(binding.root) {
100+
101+
fun bind(item: ActivityItem) {
102+
binding.tvActivityName.text = item.shortName
103+
binding.tvExported.visibility = if (item.isExported) View.VISIBLE else View.GONE
104+
105+
binding.root.setOnClickListener {
106+
onItemClick(item)
107+
}
108+
}
109+
}
110+
111+
class DiffCallback : DiffUtil.ItemCallback<ActivityItem>() {
112+
override fun areItemsTheSame(oldItem: ActivityItem, newItem: ActivityItem): Boolean {
113+
return oldItem.activityName == newItem.activityName
114+
}
115+
116+
override fun areContentsTheSame(oldItem: ActivityItem, newItem: ActivityItem): Boolean {
117+
return oldItem == newItem
118+
}
119+
}
120+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:width="24dp"
4+
android:height="24dp"
5+
android:viewportWidth="24"
6+
android:viewportHeight="24">
7+
<path
8+
android:fillColor="@android:color/black"
9+
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
10+
</vector>

0 commit comments

Comments
 (0)