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<4#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&Lwxs3a^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 @@ + + + + + + + +