Skip to content

Commit

Permalink
Support for VPN apps filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
krlvm committed Sep 1, 2024
1 parent 9d289f0 commit 95d23b1
Show file tree
Hide file tree
Showing 16 changed files with 385 additions and 1 deletion.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/res/drawable-anydpi-v24/ic_search.xml
Original file line number Diff line number Diff line change
@@ -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 added app/src/main/res/drawable-hdpi/ic_search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable-mdpi/ic_search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable-xhdpi/ic_search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable-xxhdpi/ic_search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions app/src/main/res/layout/fragment_filter.xml
Original file line number Diff line number Diff line change
@@ -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 changes: 56 additions & 0 deletions app/src/main/res/layout/item_app.xml
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions app/src/main/res/menu/menu_filter.xml
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit 95d23b1

Please sign in to comment.