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.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")
|
||||||
|
@ -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"
|
||||||
|
@ -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.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
|
||||||
}
|
}
|
||||||
|
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="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>
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user