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 0000000..a2ec243 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_search.png differ 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 0000000..c650250 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_search.png differ 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 0000000..9e7ce1e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_search.png differ 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 0000000..64e56b9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_search.png differ 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 @@ + + + + + + + +