mirror of
https://github.com/dovecoteescapee/ByeDPIAndroid.git
synced 2025-01-03 04:50:05 +00:00
Support for VPN apps filtering
This commit is contained in:
parent
9d289f096e
commit
95d23b19e7
@ -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")
|
||||
|
@ -6,6 +6,9 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<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"
|
||||
android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen"
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
15
app/src/main/res/drawable-anydpi-v24/ic_search.xml
Normal file
15
app/src/main/res/drawable-anydpi-v24/ic_search.xml
Normal 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>
|
BIN
app/src/main/res/drawable-hdpi/ic_search.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_search.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 629 B |
BIN
app/src/main/res/drawable-mdpi/ic_search.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_search.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 391 B |
BIN
app/src/main/res/drawable-xhdpi/ic_search.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_search.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 812 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_search.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_search.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
20
app/src/main/res/layout/fragment_filter.xml
Normal file
20
app/src/main/res/layout/fragment_filter.xml
Normal 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>
|
56
app/src/main/res/layout/item_app.xml
Normal file
56
app/src/main/res/layout/item_app.xml
Normal 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>
|
11
app/src/main/res/menu/menu_filter.xml
Normal file
11
app/src/main/res/menu/menu_filter.xml
Normal 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>
|
@ -47,4 +47,13 @@
|
||||
<item name="blacklist">blacklist</item>
|
||||
<item name="whitelist">whitelist</item>
|
||||
</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>
|
||||
|
@ -72,4 +72,8 @@
|
||||
<string name="desync_https_category">HTTPS</string>
|
||||
<string name="byedpi_protocols_category">Protocols</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>
|
||||
|
@ -58,6 +58,25 @@
|
||||
|
||||
</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
|
||||
android:title="@string/about_category">
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user