Skip to content

Commit 09e723b

Browse files
committed
Improved the UI of ApplicationsList screen & other optimisations
1 parent ea0d026 commit 09e723b

File tree

16 files changed

+404
-210
lines changed

16 files changed

+404
-210
lines changed

app/build.gradle

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
}
66

77
android {
8-
compileSdkVersion 35
8+
compileSdk 35
99

1010
namespace "tech.httptoolkit.android"
1111

@@ -29,10 +29,8 @@ android {
2929
minifyEnabled false
3030
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
3131

32-
manifestPlaceholders = [
33-
sentryEnabled: "true",
34-
sentryDsn: "https://[email protected]/1809979"
35-
]
32+
manifestPlaceholders['sentryEnabled'] = "true"
33+
manifestPlaceholders['sentryDsn'] = "https://[email protected]/1809979"
3634
}
3735
}
3836

app/src/main/java/tech/httptoolkit/android/ApplicationListActivity.kt

Lines changed: 106 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -5,82 +5,80 @@ import android.content.pm.ApplicationInfo
55
import android.content.pm.PackageInfo
66
import android.content.pm.PackageManager
77
import android.os.Bundle
8-
import android.view.MenuItem
98
import android.view.View
10-
import android.widget.PopupMenu
9+
import android.view.WindowManager
10+
import android.widget.CheckBox
11+
import android.widget.ImageView
12+
import android.widget.TextView
1113
import androidx.activity.OnBackPressedCallback
14+
import androidx.appcompat.app.AlertDialog
1215
import androidx.appcompat.app.AppCompatActivity
13-
import androidx.appcompat.view.ContextThemeWrapper
1416
import androidx.core.widget.doAfterTextChanged
1517
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
1618
import kotlinx.coroutines.*
1719
import tech.httptoolkit.android.databinding.AppsListBinding
18-
import java.util.*
19-
import kotlin.collections.ArrayList
2020

2121
// Used to both to send and return the current list of selected apps
2222
const val UNSELECTED_APPS_EXTRA = "tech.httptoolkit.android.UNSELECTED_APPS_EXTRA"
2323

2424
class ApplicationListActivity : AppCompatActivity(), SwipeRefreshLayout.OnRefreshListener,
25-
CoroutineScope by MainScope(), PopupMenu.OnMenuItemClickListener, View.OnClickListener {
25+
CoroutineScope by MainScope(), View.OnClickListener {
2626

2727
private lateinit var binding: AppsListBinding
28-
29-
private val allApps = ArrayList<PackageInfo>()
30-
private val filteredApps = ArrayList<PackageInfo>()
31-
28+
private val allApps = mutableListOf<PackageInfo>()
29+
private val filteredApps = mutableListOf<PackageInfo>()
3230
private lateinit var blockedPackages: MutableSet<String>
33-
3431
private var showSystem = false
3532
private var showEnabledOnly = false
3633
private var textFilter = ""
3734

3835
override fun onCreate(savedInstanceState: Bundle?) {
3936
super.onCreate(savedInstanceState)
4037

41-
blockedPackages = intent.getStringArrayExtra(UNSELECTED_APPS_EXTRA)!!.toHashSet()
42-
38+
blockedPackages = intent.getStringArrayExtra(UNSELECTED_APPS_EXTRA)?.toHashSet()
39+
?: mutableSetOf()
4340
binding = AppsListBinding.inflate(layoutInflater)
4441
setContentView(binding.root)
4542

46-
binding.appsListRecyclerView.adapter =
47-
ApplicationListAdapter(
43+
setupViews()
44+
setupBackNavigation()
45+
onRefresh()
46+
}
47+
48+
private fun setupViews() {
49+
binding.apply {
50+
appsListRecyclerView.adapter = ApplicationListAdapter(
4851
filteredApps,
4952
::isAppEnabled,
5053
::setAppEnabled
5154
)
52-
binding.appsListSwipeRefreshLayout.setOnRefreshListener(this)
53-
binding.appsListMoreMenu.setOnClickListener(this)
54-
55-
binding.appsListFilterEditText.doAfterTextChanged {
56-
textFilter = it.toString()
57-
applyFilters()
55+
appsListSwipeRefreshLayout.setOnRefreshListener(this@ApplicationListActivity)
56+
appsListMoreMenu.setOnClickListener(this@ApplicationListActivity)
57+
appsListFilterEditText.doAfterTextChanged {
58+
textFilter = it.toString()
59+
applyFilters()
60+
}
5861
}
62+
}
5963

64+
private fun setupBackNavigation() {
6065
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
6166
override fun handleOnBackPressed() {
62-
setResult(RESULT_OK, Intent().putExtra(
63-
UNSELECTED_APPS_EXTRA,
64-
blockedPackages.toTypedArray()
65-
))
67+
setResult(
68+
RESULT_OK,
69+
Intent().putExtra(UNSELECTED_APPS_EXTRA, blockedPackages.toTypedArray())
70+
)
6671
finish()
6772
}
6873
})
69-
70-
onRefresh()
7174
}
7275

7376
override fun onRefresh() {
74-
launch(Dispatchers.Main) {
75-
if (binding.appsListSwipeRefreshLayout.isRefreshing.not()) {
76-
binding.appsListSwipeRefreshLayout.isRefreshing = true
77-
}
78-
79-
val apps = loadAllApps()
77+
launch {
78+
binding.appsListSwipeRefreshLayout.isRefreshing = true
8079
allApps.clear()
81-
allApps.addAll(apps)
80+
allApps.addAll(loadAllApps())
8281
applyFilters()
83-
8482
binding.appsListSwipeRefreshLayout.isRefreshing = false
8583
}
8684
}
@@ -94,88 +92,95 @@ class ApplicationListActivity : AppCompatActivity(), SwipeRefreshLayout.OnRefres
9492
private fun matchesFilters(app: PackageInfo): Boolean {
9593
val appInfo = app.applicationInfo ?: return false
9694
val appLabel = AppLabelCache.getAppLabel(packageManager, appInfo)
97-
val isSystemApp = appInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
95+
val isSystemApp = (appInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0
9896

99-
return (textFilter.isEmpty() || appLabel.contains(textFilter, true)) && // Filter by name
100-
(showSystem || !isSystemApp) && // Show system apps, if that's active
101-
(!showEnabledOnly || isAppEnabled(app)) && // Only show enabled apps, if that's active
102-
app.packageName != packageName // Never show ourselves
97+
return (textFilter.isEmpty() || appLabel.contains(textFilter, true)) &&
98+
(showSystem || !isSystemApp) &&
99+
(!showEnabledOnly || isAppEnabled(app)) &&
100+
app.packageName != packageName
103101
}
104102

105-
private fun isAppEnabled(app: PackageInfo): Boolean {
106-
return !blockedPackages.contains(app.packageName)
107-
}
103+
private fun isAppEnabled(app: PackageInfo): Boolean = app.packageName !in blockedPackages
108104

109105
private fun setAppEnabled(app: PackageInfo, isEnabled: Boolean) {
110-
val wasChanged = if (!isEnabled) {
111-
blockedPackages.add(app.packageName)
112-
} else {
113-
blockedPackages.remove(app.packageName)
114-
}
106+
val modified = if (isEnabled) blockedPackages.remove(app.packageName)
107+
else blockedPackages.add(app.packageName)
115108

116-
if (wasChanged && showEnabledOnly) applyFilters()
109+
if (modified && showEnabledOnly) applyFilters()
117110
}
118111

119112
private suspend fun loadAllApps(): List<PackageInfo> =
120113
withContext(Dispatchers.IO) {
121-
return@withContext packageManager.getInstalledPackages(PackageManager.GET_META_DATA)
122-
.filter { pkg ->
123-
pkg.applicationInfo != null
124-
}.sortedBy { pkg ->
125-
AppLabelCache.getAppLabel(packageManager, pkg.applicationInfo!!).toUpperCase(
126-
Locale.getDefault()
127-
)
128-
}
114+
packageManager.getInstalledPackages(PackageManager.GET_META_DATA)
115+
.filter { it.applicationInfo != null }
116+
.sortedBy { AppLabelCache.getAppLabel(packageManager, it.applicationInfo!!).lowercase() }
129117
}
130118

131-
override fun onMenuItemClick(item: MenuItem?): Boolean {
132-
return when (item?.itemId) {
133-
R.id.action_show_system -> {
134-
showSystem = showSystem.not()
135-
applyFilters()
136-
true
137-
}
138-
R.id.action_show_enabled -> {
139-
showEnabledOnly = showEnabledOnly.not()
119+
private fun toggleAllApps() {
120+
if (blockedPackages.isEmpty()) {
121+
blockedPackages.addAll(allApps.map { it.packageName })
122+
} else {
123+
blockedPackages.clear()
124+
}
125+
binding.appsListRecyclerView.adapter?.notifyDataSetChanged()
126+
}
127+
128+
private fun showOptionsDialog() {
129+
val dialogView = layoutInflater.inflate(R.layout.dialog_options, null)
130+
val dialog = AlertDialog.Builder(this)
131+
.setView(dialogView)
132+
.create()
133+
134+
// Set rounded corners for the dialog window
135+
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
136+
137+
// Access views
138+
val toggleAll = dialogView.findViewById<TextView>(R.id.option_toggle_all)
139+
val checkboxShowEnabled = dialogView.findViewById<CheckBox>(R.id.checkbox_show_enabled)
140+
val checkboxShowSystem = dialogView.findViewById<CheckBox>(R.id.checkbox_show_system)
141+
val cancelButton = dialogView.findViewById<ImageView>(R.id.close_button)
142+
143+
checkboxShowEnabled.isChecked = showEnabledOnly
144+
checkboxShowSystem.isChecked = showSystem
145+
146+
toggleAll.text = getString(
147+
if (blockedPackages.isEmpty()) R.string.disable_all_apps else R.string.enable_all_apps
148+
)
149+
150+
toggleAll.setOnClickListener {
151+
toggleAllApps()
152+
if (showEnabledOnly) {
140153
applyFilters()
141-
true
142-
}
143-
R.id.action_toggle_all -> {
144-
if (blockedPackages.isEmpty()) {
145-
// If everything is enabled, disable everything
146-
blockedPackages.addAll(allApps.map { app -> app.packageName })
147-
} else {
148-
// Otherwise, re-enable everything
149-
blockedPackages.removeAll(allApps.map { app -> app.packageName })
150-
}
151-
152-
if (showEnabledOnly) {
153-
applyFilters()
154-
} else {
155-
binding.appsListRecyclerView.adapter?.notifyDataSetChanged()
156-
}
157-
true
154+
} else {
155+
binding.appsListRecyclerView.adapter?.notifyDataSetChanged()
158156
}
159-
else -> false
157+
dialog.dismiss()
160158
}
159+
160+
checkboxShowEnabled.setOnCheckedChangeListener { _, isChecked ->
161+
showEnabledOnly = isChecked
162+
applyFilters()
163+
dialog.dismiss()
164+
}
165+
166+
checkboxShowSystem.setOnCheckedChangeListener { _, isChecked ->
167+
showSystem = isChecked
168+
applyFilters()
169+
dialog.dismiss()
170+
}
171+
172+
cancelButton.setOnClickListener {
173+
dialog.dismiss()
174+
}
175+
176+
// Show the dialog
177+
dialog.show()
161178
}
162179

163-
override fun onClick(v: View?) {
164-
when (v?.id) {
165-
R.id.apps_list_more_menu -> {
166-
PopupMenu(ContextThemeWrapper(this, R.style.PopupMenu), binding.appsListMoreMenu).apply {
167-
this.inflate(R.menu.menu_app_list)
168-
this.menu.findItem(R.id.action_show_system).isChecked = showSystem
169-
this.menu.findItem(R.id.action_show_enabled).isChecked = showEnabledOnly
170-
this.menu.findItem(R.id.action_toggle_all).title = getString(
171-
if (blockedPackages.isEmpty())
172-
R.string.disable_all
173-
else
174-
R.string.enable_all
175-
)
176-
this.setOnMenuItemClickListener(this@ApplicationListActivity)
177-
}.show()
178-
}
180+
181+
override fun onClick(view: View?) {
182+
if (view?.id == R.id.apps_list_more_menu) {
183+
showOptionsDialog()
179184
}
180185
}
181-
}
186+
}

app/src/main/java/tech/httptoolkit/android/ApplicationListAdapter.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import androidx.recyclerview.widget.RecyclerView
77
import tech.httptoolkit.android.databinding.ItemAppRowBinding
88

99
class ApplicationListAdapter(
10-
private val data: MutableList<PackageInfo>,
10+
private val data: List<PackageInfo>,
1111
private val isAppWhitelisted: (PackageInfo) -> Boolean,
1212
private val onCheckChanged: (PackageInfo, Boolean) -> Unit
1313
) : RecyclerView.Adapter<ApplicationListAdapter.AppsViewHolder>() {
@@ -21,7 +21,7 @@ class ApplicationListAdapter(
2121
return AppsViewHolder(binding)
2222
}
2323

24-
override fun getItemCount() = data.size
24+
override fun getItemCount(): Int = data.size
2525

2626
override fun onBindViewHolder(holder: AppsViewHolder, position: Int) {
2727
holder.bind(data[position])
@@ -42,10 +42,17 @@ class ApplicationListAdapter(
4242

4343
fun bind(packageInfo: PackageInfo) {
4444
val appInfo = packageInfo.applicationInfo!!
45-
binding.rowAppIconImage.setImageDrawable(appInfo.loadIcon(packageManager))
46-
binding.rowAppName.text = AppLabelCache.getAppLabel(packageManager, appInfo)
47-
binding.rowAppPackageName.text = packageInfo.packageName
48-
binding.rowAppSwitch.isChecked = isAppWhitelisted(packageInfo)
45+
binding.apply {
46+
rowAppIconImage.setImageDrawable(appInfo.loadIcon(packageManager))
47+
rowAppName.text = AppLabelCache.getAppLabel(packageManager, appInfo)
48+
rowAppPackageName.text = packageInfo.packageName
49+
rowAppSwitch.isChecked = isAppWhitelisted(packageInfo)
50+
root.setOnClickListener {
51+
packageManager.getLaunchIntentForPackage(appInfo.packageName)?.let {
52+
itemView.context.startActivity(it)
53+
}
54+
}
55+
}
4956
}
5057
}
5158
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package tech.httptoolkit.android
2+
3+
object PrefKeys {
4+
const val VPN_START_TIME = "vpn-start-time"
5+
const val LAST_UPDATE_CHECK_TIME = "update-check-time"
6+
const val APP_CRASHED = "app-crashed"
7+
const val FIRST_RUN = "is-first-run"
8+
}

0 commit comments

Comments
 (0)