Support for VPN apps filtering

This commit is contained in:
krlvm 2024-09-01 18:10:50 +03:00
parent 9d289f096e
commit 95d23b19e7
No known key found for this signature in database
GPG Key ID: B8552A91FD265536
16 changed files with 385 additions and 1 deletions

View File

@ -73,6 +73,7 @@ dependencies {
implementation("com.takisoft.preferencex:preferencex:1.1.0") implementation("com.takisoft.preferencex:preferencex:1.1.0")
implementation("com.google.android.material:material:1.12.0") implementation("com.google.android.material:material:1.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4") 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-runtime-ktx:2.8.4")
implementation("androidx.lifecycle:lifecycle-service:2.8.4") implementation("androidx.lifecycle:lifecycle-service:2.8.4")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")

View File

@ -6,6 +6,9 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-feature android:name="android.software.leanback" <uses-feature android:name="android.software.leanback"
android:required="false" /> android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" <uses-feature android:name="android.hardware.touchscreen"

View File

@ -0,0 +1,94 @@
package io.github.dovecoteescapee.byedpi.adapters
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.util.Predicate
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import io.github.dovecoteescapee.byedpi.databinding.ItemAppBinding
class AppItemAdapter(
private val context: Context,
private val checker: Predicate<Model>,
private val onChange: (Model, Boolean) -> Unit,
) : RecyclerView.Adapter<AppItemAdapter.ViewHolder>() {
private val comparator = compareBy<Model?>({ !checker.test(it) }, { it?.label })
private val list: SortedList<Model> = SortedList(Model::class.java, SortedList.BatchedCallback(object :
SortedList.Callback<Model>() {
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<Model>) {
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
)
}

View File

@ -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<AppItemAdapter.Model>
private lateinit var checked: MutableSet<String>
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<AppItemAdapter.Model> {
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
}
}

View File

@ -3,6 +3,7 @@ package io.github.dovecoteescapee.byedpi.services
import android.app.Notification import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
@ -297,7 +298,27 @@ class ByeDpiVpnService : LifecycleVpnService() {
builder.setMetered(false) 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 return builder
} }

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="1.2266667"
android:scaleY="1.2266667"
android:translateX="-2.72"
android:translateY="-2.72">
<path
android:fillColor="@android:color/white"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activities.SettingsActivity">
<ProgressBar
android:id="@+id/progress_circular"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:padding="16dip"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
tools:src="@mipmap/ic_launcher_round" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="16dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/app_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textSize="16sp"
android:lines="1"
android:ellipsize="end"
tools:text="Application Label" />
<TextView
android:id="@+id/app_package"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="1"
android:ellipsize="end"
tools:text="com.example.app" />
</LinearLayout>
<CheckBox
android:id="@+id/checkbox"
android:clickable="false"
android:focusable="false"
android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:title="@string/search"
android:icon="@drawable/ic_search"
app:showAsAction="ifRoom|collapseActionView"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>

View File

@ -47,4 +47,13 @@
<item name="blacklist">blacklist</item> <item name="blacklist">blacklist</item>
<item name="whitelist">whitelist</item> <item name="whitelist">whitelist</item>
</array> </array>
<array name="vpn_filtering_modes">
<item name="blacklist">Blacklist apps</item>
<item name="whitelist">Whilelist apps</item>
</array>
<array name="vpn_filtering_modes_entries">
<item name="blacklist">blacklist</item>
<item name="whitelist">whitelist</item>
</array>
</resources> </resources>

View File

@ -72,4 +72,8 @@
<string name="desync_https_category">HTTPS</string> <string name="desync_https_category">HTTPS</string>
<string name="byedpi_protocols_category">Protocols</string> <string name="byedpi_protocols_category">Protocols</string>
<string name="byedpi_protocols_hint">Uncheck all to desync all traffic</string> <string name="byedpi_protocols_hint">Uncheck all to desync all traffic</string>
<string name="vpn_category">VPN</string>
<string name="vpn_filter_mode">Apps filtering mode</string>
<string name="vpn_filtered_apps">Filtered applications</string>
<string name="search">Search</string>
</resources> </resources>

View File

@ -58,6 +58,25 @@
</androidx.preference.PreferenceCategory> </androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory
android:title="@string/vpn_category">
<DropDownPreference
android:key="vpn_filter_mode"
android:title="@string/vpn_filter_mode"
android:entries="@array/vpn_filtering_modes"
android:entryValues="@array/vpn_filtering_modes_entries"
android:defaultValue="blacklist"
app:useSimpleSummaryProvider="true" />
<Preference
android:key="vpn_filtered_apps"
android:title="@string/vpn_filtered_apps"
app:useSimpleSummaryProvider="true"
app:fragment="io.github.dovecoteescapee.byedpi.fragments.VpnAppsFilterFragment" />
</androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory <androidx.preference.PreferenceCategory
android:title="@string/about_category"> android:title="@string/about_category">