From 95d23b19e7ecabb090a18f0bec2161e1a4a79914 Mon Sep 17 00:00:00 2001
From: krlvm <51774833+krlvm@users.noreply.github.com>
Date: Sun, 1 Sep 2024 18:10:50 +0300
Subject: [PATCH] Support for VPN apps filtering
---
app/build.gradle.kts | 1 +
app/src/main/AndroidManifest.xml | 3 +
.../byedpi/adapters/AppItemAdapter.kt | 94 +++++++++++++
.../byedpi/fragments/VpnAppsFilterFragment.kt | 131 ++++++++++++++++++
.../byedpi/services/ByeDpiVpnService.kt | 23 ++-
.../res/drawable-anydpi-v24/ic_search.xml | 15 ++
app/src/main/res/drawable-hdpi/ic_search.png | Bin 0 -> 629 bytes
app/src/main/res/drawable-mdpi/ic_search.png | Bin 0 -> 391 bytes
app/src/main/res/drawable-xhdpi/ic_search.png | Bin 0 -> 812 bytes
.../main/res/drawable-xxhdpi/ic_search.png | Bin 0 -> 1274 bytes
app/src/main/res/layout/fragment_filter.xml | 20 +++
app/src/main/res/layout/item_app.xml | 56 ++++++++
app/src/main/res/menu/menu_filter.xml | 11 ++
app/src/main/res/values/arrays.xml | 9 ++
app/src/main/res/values/strings.xml | 4 +
app/src/main/res/xml/main_settings.xml | 19 +++
16 files changed, 385 insertions(+), 1 deletion(-)
create mode 100644 app/src/main/java/io/github/dovecoteescapee/byedpi/adapters/AppItemAdapter.kt
create mode 100644 app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/VpnAppsFilterFragment.kt
create mode 100644 app/src/main/res/drawable-anydpi-v24/ic_search.xml
create mode 100644 app/src/main/res/drawable-hdpi/ic_search.png
create mode 100644 app/src/main/res/drawable-mdpi/ic_search.png
create mode 100644 app/src/main/res/drawable-xhdpi/ic_search.png
create mode 100644 app/src/main/res/drawable-xxhdpi/ic_search.png
create mode 100644 app/src/main/res/layout/fragment_filter.xml
create mode 100644 app/src/main/res/layout/item_app.xml
create mode 100644 app/src/main/res/menu/menu_filter.xml
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d7b4fbc..764ea2b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -73,6 +73,7 @@ dependencies {
implementation("com.takisoft.preferencex:preferencex:1.1.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+ implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
implementation("androidx.lifecycle:lifecycle-service:2.8.4")
testImplementation("junit:junit:4.13.2")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3dd4840..71e714b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,9 @@
+
+
,
+ private val onChange: (Model, Boolean) -> Unit,
+) : RecyclerView.Adapter() {
+
+ private val comparator = compareBy({ !checker.test(it) }, { it?.label })
+
+ private val list: SortedList = SortedList(Model::class.java, SortedList.BatchedCallback(object :
+ SortedList.Callback() {
+
+ override fun compare(o1: Model?, o2: Model?): Int {
+ return comparator.compare(o1, o2)
+ }
+
+ override fun onInserted(position: Int, count: Int) {
+ notifyItemRangeInserted(position, count)
+ }
+
+ override fun onRemoved(position: Int, count: Int) {
+ notifyItemRangeRemoved(position, count)
+ }
+
+ override fun onMoved(fromPosition: Int, toPosition: Int) {
+ notifyItemMoved(fromPosition, toPosition)
+ }
+
+ override fun onChanged(position: Int, count: Int) {
+ notifyItemRangeChanged(position, count)
+ }
+
+ override fun areItemsTheSame(item1: Model?, item2: Model?): Boolean =
+ item1?.packageName == item2?.packageName
+
+ override fun areContentsTheSame(oldItem: Model?, newItem: Model?): Boolean =
+ oldItem == newItem
+
+ }))
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): ViewHolder = ViewHolder(ItemAppBinding.inflate(LayoutInflater.from(context), parent, false))
+
+ override fun getItemCount(): Int {
+ return list.size()
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val model = list[position]
+
+ holder.binding.apply {
+ appLabel.text = model.label
+ appPackage.text = model.packageName
+ icon.setImageDrawable(model.icon)
+
+ checkbox.isChecked = checker.test(model)
+
+ root.setOnClickListener {
+ val checked = !checkbox.isChecked
+ checkbox.isChecked = checked
+ onChange(model, checked)
+ }
+ }
+ }
+
+ fun setList(newList: List) {
+ list.beginBatchedUpdates()
+ list.clear()
+ list.addAll(newList)
+ list.endBatchedUpdates()
+ }
+
+ class ViewHolder(
+ val binding: ItemAppBinding
+ ) : RecyclerView.ViewHolder(binding.root)
+
+ data class Model(
+ val label: String,
+ val packageName: String,
+ val icon: Drawable
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/VpnAppsFilterFragment.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/VpnAppsFilterFragment.kt
new file mode 100644
index 0000000..282c307
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/VpnAppsFilterFragment.kt
@@ -0,0 +1,131 @@
+package io.github.dovecoteescapee.byedpi.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.widget.SearchView
+import androidx.appcompat.widget.SearchView.OnQueryTextListener
+import androidx.core.content.edit
+import androidx.core.view.MenuProvider
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import io.github.dovecoteescapee.byedpi.adapters.AppItemAdapter
+import io.github.dovecoteescapee.byedpi.R
+import io.github.dovecoteescapee.byedpi.databinding.FragmentFilterBinding
+import io.github.dovecoteescapee.byedpi.utility.getPreferences
+
+class VpnAppsFilterFragment : Fragment(), MenuProvider {
+
+ private lateinit var binding: FragmentFilterBinding
+ private lateinit var adapter: AppItemAdapter
+
+ private lateinit var apps: List
+ private lateinit var checked: MutableSet
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentFilterBinding.inflate(inflater, container, false)
+
+ val activity = requireActivity()
+
+ checked = activity.getPreferences().getStringSet("vpn_filtered_apps", emptySet())!!
+ .toMutableSet()
+ adapter = AppItemAdapter(
+ activity,
+ { checked.contains(it.packageName) },
+ { app, status ->
+ checked.apply {
+ if (status) {
+ add(app.packageName)
+ } else {
+ remove(app.packageName)
+ }
+ }
+ activity.getPreferences().edit {
+ putStringSet("vpn_filtered_apps", checked.toSet())
+ }
+ })
+
+
+ binding.appList.layoutManager = LinearLayoutManager(activity)
+ binding.appList.addItemDecoration(
+ DividerItemDecoration(
+ binding.appList.context,
+ RecyclerView.VERTICAL
+ )
+ )
+
+ binding.appList.adapter = adapter
+
+ Thread {
+ apps = collectApps()
+
+ activity.runOnUiThread {
+ adapter.setList(apps)
+
+ binding.progressCircular.visibility = View.GONE
+ binding.appList.visibility = View.VISIBLE
+ }
+ }.start()
+
+ requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
+
+ return binding.root
+ }
+
+ private fun collectApps(): List {
+ val context = requireContext()
+ val packageManager = context.packageManager
+
+ return packageManager.getInstalledApplications(0)
+ .filter { it.packageName != context.packageName }
+ .map {
+ AppItemAdapter.Model(
+ label = packageManager.getApplicationLabel(it).toString(),
+ packageName = it.packageName,
+ icon = packageManager.getApplicationIcon(it),
+ )
+ }
+ }
+
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.menu_filter, menu)
+
+ val searchItem = menu.findItem(R.id.action_search)
+ (searchItem.actionView as SearchView).setOnQueryTextListener(object : OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String): Boolean {
+ val lQuery = query.lowercase()
+ adapter.setList(apps.filter { it.label.lowercase().contains(lQuery) })
+ return false
+ }
+
+ override fun onQueryTextChange(query: String?): Boolean {
+ return false
+ }
+ })
+ searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
+ override fun onMenuItemActionExpand(item: MenuItem): Boolean {
+ return true
+ }
+
+ override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
+ adapter.setList(apps)
+ return true
+ }
+ })
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ return false
+ }
+}
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiVpnService.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiVpnService.kt
index d2abdfd..bf62626 100644
--- a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiVpnService.kt
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiVpnService.kt
@@ -3,6 +3,7 @@ package io.github.dovecoteescapee.byedpi.services
import android.app.Notification
import android.app.PendingIntent
import android.content.Intent
+import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.ParcelFileDescriptor
@@ -297,7 +298,27 @@ class ByeDpiVpnService : LifecycleVpnService() {
builder.setMetered(false)
}
- builder.addDisallowedApplication(applicationContext.packageName)
+ val filter = getPreferences().getStringSet("vpn_filtered_apps", emptySet())!!
+ when (val filterMode = getPreferences().getString("vpn_filter_mode", "blacklist")) {
+ "blacklist" -> {
+ filter.forEach {
+ try {
+ builder.addDisallowedApplication(it)
+ } catch (ignore: PackageManager.NameNotFoundException) {}
+ }
+ builder.addDisallowedApplication(applicationContext.packageName)
+ }
+ "whitelist" -> {
+ filter.forEach {
+ try {
+ builder.addAllowedApplication(it)
+ } catch (ignore: PackageManager.NameNotFoundException) {}
+ }
+ }
+ else -> {
+ Log.w(TAG, "Invalid VPN filter mode: $filterMode")
+ }
+ }
return builder
}
diff --git a/app/src/main/res/drawable-anydpi-v24/ic_search.xml b/app/src/main/res/drawable-anydpi-v24/ic_search.xml
new file mode 100644
index 0000000..79129dd
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi-v24/ic_search.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable-hdpi/ic_search.png b/app/src/main/res/drawable-hdpi/ic_search.png
new file mode 100644
index 0000000000000000000000000000000000000000..a2ec2436942ade35e0b1cec0b24369c528fe6e62
GIT binary patch
literal 629
zcmV-*0*d{KP)KP^>@k>r-GIj2`mFn)Qp1!`@ikUr>UPDwMxkHXDU-L%tc}7xGrizg8iRL{rii83TMP5cM0TZN
zdSf&`EuEk)FhOrJ<~K0!neX&fA^#ET_vlGR#UBy2OHT@_J4en4JxL%WP&4aBU_Q;GRO*?{A@C}J+ovl=V3fq+GNa!~yi@YLvFVhu+^k!qtyY>~~IM_7b^rp-o?QbPVnL~45-Y$I|3YjfuXR{xD|7#s?BbO*-
zLfVUhQ`nXUt$C*oJY$bqC}_ld`NOGw9pU|F$D*f6-^ZmtW!oh)adt;2tlzxRytG<;
zw%ma=_VnL88idQYOmELn^XHsVLEd58P1JnGd}YW3dV7YNFLO2pbM*EMHDBRu3c7cr
z=c4A1I2-e)vDP1ouAt_RF`=K*J^Ffz`Hdb{Ci{)z#?WMjr2+o_pLSdTr7>DKCE3Zc
P00000NkvXXu0mjfWbY?K
literal 0
HcmV?d00001
diff --git a/app/src/main/res/drawable-mdpi/ic_search.png b/app/src/main/res/drawable-mdpi/ic_search.png
new file mode 100644
index 0000000000000000000000000000000000000000..c650250670ede5f366a58d9d750e508c49230b0f
GIT binary patch
literal 391
zcmV;20eJq2P)!uIVfFFAVo=yM42OiC0ZH|5DAJD2tM;!
zuk=0ZT>>WXq*02QKYz8hGZ_2_A~J$*q|F<21r0gBGZ;RA9@@Q~EP3`z#Fal$(E=)=
zDYPc9=8h!NKKU*C9(eaASFzuTxI6bm%RR`vG}(73V?RL517$CU&x1L80Vcd)FNT;a
zWiP-iDA|jF?DbC;oU<1L+0}Oz=trci*^7bfV>7d1FF@QE`U!hoAa?BYV8VU?LPIlf
z$G!#rA+&9>uh0=^IuUcH;F*0z+{t~pGUZGX4Af>=(N|I57G3V|+;R!!+>-}qIr*pD(;JkIX~`L9dZQ<&B?V`CqbL81B?T=x
l;Y@GLk{Nry#$HaI;sYCYckzr&8|nZ6002ovPDHLkV1lZkqdx!u
literal 0
HcmV?d00001
diff --git a/app/src/main/res/drawable-xhdpi/ic_search.png b/app/src/main/res/drawable-xhdpi/ic_search.png
new file mode 100644
index 0000000000000000000000000000000000000000..9e7ce1e2f9197e004bc32b05d9735f4f952229b1
GIT binary patch
literal 812
zcmV+{1JnG8P)NkljM3C4ih_zs_u~E>*R`AhSSm<961R?*xLakJ^Pz%x4
zB7&fu1bkq8#bmp-=LGKkGIQh1oU3yoA1t`cU3={_oHJ)0U0o}57-qIcIw^g(eBMdB
zr5@J*H#C?DX}Wow%d#LnkouYT&rtI|=}GgrN^ybNZAVSVrC%IL1!mc!ZK&BKyC>qa
z)XRQJQG@O53f@RZS+|J(yCQvTE?L5A)+Ir~>nwlS{E)V?CJ%X!GoXPN^d~{V&)F$*
zo8G^{!nQlZu*f9+l~CY*c8J`iw+O25w(ME;8SSROG#1u2`9xnClrzBK$Z2{@p`f))
zj*QS-7G+H_TrAL63I$zcvUp8j6)Y^JEUNEFmc2&z(N`5^K4uefhQ7iu(V-^=iao|Y
zVv4@PFwvnW1&ZCpKH?dDg<+!jN>2)uyMc|wG<}6(qL`;A1I`PhS-*yxnDyD=mjCMNGcnBG
z$*{1_vtBz};XBunjGU|+rsJDL`kjfW$xi92g&UtgL-{gu`Pb_^(phV<6p7Y3|RE
zm*U4av!-@fIElQOaE4e@J1pFxeeGxA%V`s9YKI27B@Q+VKR(Ku+Mz)XbL0*9GV6$I
z!cU^rOmxIa_;Sj-CVVEwSW`PR$Zc`3S@_8%`&QdFto3rbOn)u0a1xC(;lIz*S8FUR
qr^RdH694ZAEWBe}=kXO*NW^cts}uqct6E4#VV
literal 0
HcmV?d00001
diff --git a/app/src/main/res/drawable-xxhdpi/ic_search.png b/app/src/main/res/drawable-xxhdpi/ic_search.png
new file mode 100644
index 0000000000000000000000000000000000000000..64e56b903ab363ba2717089161585e7ab1ab62ec
GIT binary patch
literal 1274
zcmV|vV!@LT;P$_*+28xTGi6Gj
zKCMPXropDehF}kj|2&obIs+d58QA-<u&-bPoNvM)HkN$f`&`la7q*#mw;qUoX}ysVz@K7M`BvBx*i6_XuvM^+VAsTEuDUE@
zeT@P^hge|++hC7zIc30a0QMa0m);`0Xr<2OvKj$G*04GU+Wg<+_&Okd4(!+7V!ce`
zGO{AJH3(%E$KNX`YXcYVU_ZxZ1p3Y0$K(RJ
zKVU&c*gpy4U-FN!e@?_!+CHXpOmvXs7H2K6hy8Pc{DUlrc#UJCB6j=WV@NX-4*taic}AR3pge>
z(C`zBDsYnhqr(}Ax7j~8V#l;DcC&wUkmNVcTHq}E=R_)o0PrHmL6qkm|+aA&$v`*p-owV;qwVvC~2y*>4k&b32P5
zUgwx(AlX{ZPW;F*DM7vw7EnU;ivuS}Hbd-K)yEo+OM%$M;(1O^9N@U*AnB*9qJ)Pz
zE;h)qmy?#z%q^=_w<3MxVjjoE0%gC;2}{_=aoLdSDgS?>PW>ovD(o;PDWTDI*?&O9
zX;$T6lwC!~IbmvN+E&A-D45l>0=
z4QEr_ls2>~v2N1@dS9peF<;BmEmA8gNlzX>Byp+Gf&YaQ|Q_ygk$2^#^CcW*x7-$ylAflMnMI^M#WzJt&7fX8>Qmz
zAHN?K|G9J#?QZR=q#|PXY_GyT7u%8RgXtGvu82*|S|PQ+*)v|Q?~1&Lwxs3%aqS
z;D;_SbxfMgWlVa+R^oq9Ww>a^Iw?0|-7R#l*p78lZp11nbl}#vciXcB$c5BvN!s1K
zv*n~jtdc^C;B;0Gn3ark8~<82|tP07*qoM6N<$f=wxKvH$=8
literal 0
HcmV?d00001
diff --git a/app/src/main/res/layout/fragment_filter.xml b/app/src/main/res/layout/fragment_filter.xml
new file mode 100644
index 0000000..788271d
--- /dev/null
+++ b/app/src/main/res/layout/fragment_filter.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_app.xml b/app/src/main/res/layout/item_app.xml
new file mode 100644
index 0000000..ec1b05e
--- /dev/null
+++ b/app/src/main/res/layout/item_app.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_filter.xml b/app/src/main/res/menu/menu_filter.xml
new file mode 100644
index 0000000..e89c3fb
--- /dev/null
+++ b/app/src/main/res/menu/menu_filter.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 0b1744c..69f0bc8 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -47,4 +47,13 @@
- blacklist
- whitelist
+
+
+ - Blacklist apps
+ - Whilelist apps
+
+
+ - blacklist
+ - whitelist
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index dc26369..ad9ac8b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -72,4 +72,8 @@
HTTPS
Protocols
Uncheck all to desync all traffic
+ VPN
+ Apps filtering mode
+ Filtered applications
+ Search
diff --git a/app/src/main/res/xml/main_settings.xml b/app/src/main/res/xml/main_settings.xml
index e81fc6b..d08cd05 100644
--- a/app/src/main/res/xml/main_settings.xml
+++ b/app/src/main/res/xml/main_settings.xml
@@ -58,6 +58,25 @@
+
+
+
+
+
+
+
+