From: Soren Stoutner Date: Thu, 28 May 2026 21:17:10 +0000 (-0700) Subject: Add searching of filter lists. https://redmine.stoutner.com/issues/1293 X-Git-Url: https://gitweb.stoutner.com/?a=commitdiff_plain;h=76ede55dc8530645e6c9dcfca3337cb647590783;p=PrivacyBrowserAndroid.git Add searching of filter lists. https://redmine.stoutner.com/issues/1293 --- diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/FilterListsActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/FilterListsActivity.kt index f07baf54..92ef6591 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/FilterListsActivity.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/FilterListsActivity.kt @@ -24,9 +24,12 @@ import android.database.Cursor import android.database.MatrixCursor import android.os.Bundle import android.os.Handler +import android.text.Editable +import android.text.TextWatcher import android.view.View import android.view.WindowManager import android.widget.AdapterView +import android.widget.EditText import android.widget.ListView import android.widget.Spinner import android.widget.TextView @@ -41,6 +44,7 @@ import androidx.preference.PreferenceManager import com.stoutner.privacybrowser.R import com.stoutner.privacybrowser.adapters.FilterListArrayAdapter import com.stoutner.privacybrowser.dataclasses.FilterListDataClass +import com.stoutner.privacybrowser.dataclasses.FilterListEntryDataClass import com.stoutner.privacybrowser.dialogs.ViewFilterListEntryDialog import com.stoutner.privacybrowser.helpers.easyListDataClass import com.stoutner.privacybrowser.helpers.easyPrivacyDataClass @@ -74,9 +78,12 @@ class FilterListsActivity : AppCompatActivity(), ViewFilterListEntryDialog.ViewF private lateinit var filterListListView: ListView private lateinit var filterSublistSpinner: Spinner - // Define the class variables + // Define the class variables. private var filterSublistSpinnerSelectedPosition = 0 + // Declare the class variables. + private lateinit var currentFilterListDataClassArrayAdapter: FilterListArrayAdapter + public override fun onCreate(savedInstanceState: Bundle?) { // Get a handle for the shared preferences. val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) @@ -117,11 +124,12 @@ class FilterListsActivity : AppCompatActivity(), ViewFilterListEntryDialog.ViewF appBarSpinner = findViewById(R.id.spinner) filterSublistSpinner = findViewById(R.id.filter_sublist_spinner) filterListListView = findViewById(R.id.filter_list_listview) + val searchEditText = findViewById(R.id.search_edittext) // Store the activity context. activityContext = this - // Setup a matrix cursor for the app bar spinner. + // Set up a matrix cursor for the app bar spinner. val appBarSpinnerCursor = MatrixCursor(arrayOf("_id", "Filter List")) appBarSpinnerCursor.addRow(arrayOf(ULTRAPRIVACY, getString(R.string.ultraprivacy) + " - " + ultraPrivacyDataClass.versionString)) appBarSpinnerCursor.addRow(arrayOf(ULTRALIST, getString(R.string.ultralist) + " - " + ultraListDataClass.versionString)) @@ -164,6 +172,7 @@ class FilterListsActivity : AppCompatActivity(), ViewFilterListEntryDialog.ViewF } } + // Handle taps on the filter sublist spinner dropdown. filterSublistSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { // Update the filter sublist spinner selected position. @@ -181,10 +190,14 @@ class FilterListsActivity : AppCompatActivity(), ViewFilterListEntryDialog.ViewF } // Get an adapter for the current filter list data class. - val currentFilterListDataClassArrayAdapter = FilterListArrayAdapter(activityContext, currentFilterListDataClass) + currentFilterListDataClassArrayAdapter = FilterListArrayAdapter(activityContext, currentFilterListDataClass) // Populate the list view with the current filter list data class adapter. filterListListView.adapter = currentFilterListDataClassArrayAdapter + + // Reapply the search if the search edit is not empty. + if (searchEditText.text.isNotEmpty()) + currentFilterListDataClassArrayAdapter.filter.filter(searchEditText.text) } override fun onNothingSelected(parent: AdapterView<*>?) { @@ -192,6 +205,22 @@ class FilterListsActivity : AppCompatActivity(), ViewFilterListEntryDialog.ViewF } } + // Search for the string in the list view whenever a character changes in the search edit text. + searchEditText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, start: Int, count: Int, after: Int) { + // Do nothing. + } + + override fun onTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) { + // Do nothing. + } + + override fun afterTextChanged(editable: Editable) { + // Search for the text in the list view. + currentFilterListDataClassArrayAdapter.filter.filter(editable.toString()) + } + }) + // Listen for taps on entries in the list view. filterListListView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long -> // Display the view filter list entry dialog. the list view is 0 based, so the position must be incremented by 1. @@ -258,9 +287,12 @@ class FilterListsActivity : AppCompatActivity(), ViewFilterListEntryDialog.ViewF // Determine if this is the last entry in the list. val isLastEntry = (id == filterListListView.count) + // Get the filter list entry data class. The list is 0 based, so the position is the ID - 1. + val filterListEntryDataClass = filterListListView.getItemAtPosition(id - 1) as FilterListEntryDataClass + // Create a view filter list entry dialog. val viewFilterListsEntryDialogFragment: DialogFragment = ViewFilterListEntryDialog.entry(id, isLastEntry, appBarSpinner.selectedItemPosition, - filterSublistSpinner.selectedItemPosition) + filterSublistSpinner.selectedItemPosition, filterListEntryDataClass) // Make it so. viewFilterListsEntryDialogFragment.show(supportFragmentManager, getString(R.string.filterlist_entry)) diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt index 846aef5e..04295ede 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt @@ -4385,20 +4385,6 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook } }) - // Set the `check mark` button for the find on page edit text keyboard to close the soft keyboard. - findOnPageEditText.setOnKeyListener { _: View?, keyCode: Int, keyEvent: KeyEvent -> - if ((keyEvent.action == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed. - // Hide the soft keyboard. - inputMethodManager.hideSoftInputFromWindow(currentWebView!!.windowToken, 0) - - // Consume the event. - return@setOnKeyListener true - } else { // A different key was pressed. - // Do not consume the event. - return@setOnKeyListener false - } - } - // Implement swipe to refresh. swipeRefreshLayout.setOnRefreshListener { // Reload the website. diff --git a/app/src/main/java/com/stoutner/privacybrowser/adapters/FilterListArrayAdapter.kt b/app/src/main/java/com/stoutner/privacybrowser/adapters/FilterListArrayAdapter.kt index a7244394..6d0c406f 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/adapters/FilterListArrayAdapter.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/adapters/FilterListArrayAdapter.kt @@ -24,13 +24,34 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter +import android.widget.Filter import android.widget.TextView import com.stoutner.privacybrowser.R import com.stoutner.privacybrowser.dataclasses.FilterListEntryDataClass +import java.util.Locale.getDefault + // `0` is the `textViewResourceId`, which is unused in this implementation. -class FilterListArrayAdapter(context: Context, filterListEntryDataClassList: List) : ArrayAdapter(context, 0, filterListEntryDataClassList) { +class FilterListArrayAdapter(context: Context, private val entireFilterListEntryDataClassList: List) : ArrayAdapter(context, 0, entireFilterListEntryDataClassList) { + // Define the class variables. + private var searchedFilterListEntryDataClassList = entireFilterListEntryDataClassList + + override fun getCount(): Int { + // Return the searched filter list entry data class list size. + return searchedFilterListEntryDataClassList.size + } + + override fun getItem(position: Int): FilterListEntryDataClass { + // Return the searched filter list entry data class list item. + return searchedFilterListEntryDataClassList[position] + } + + override fun getItemId(position: Int): Long { + // Return the position as a long. + return position.toLong() + } + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { // Copy the input view to a new view. var newView = convertView @@ -44,7 +65,7 @@ class FilterListArrayAdapter(context: Context, filterListEntryDataClassList: Lis val originalEntryTextView = newView!!.findViewById(R.id.original_entry_textview) // Get this filter list entry data class. - val filterListEntryDataClass = getItem(position)!! + val filterListEntryDataClass = getItem(position) // The ID is one greater than the position because it is 0 based. val id = position + 1 @@ -55,4 +76,41 @@ class FilterListArrayAdapter(context: Context, filterListEntryDataClassList: Lis // Return the modified view. return newView } + + override fun getFilter(): Filter { + // Return the custom filter. + return object : Filter() { + override fun performFiltering(charSequence: CharSequence?): FilterResults { + // Get the search string, converting to lower case. + val searchString = charSequence?.toString()?.lowercase(getDefault()) + + // Create a new filter results. + val filterResults = FilterResults() + + // Filter the list. + filterResults.values = if (searchString.isNullOrEmpty()) { // The search string is null or empty. + // Return the entire filter list entry data class list. + entireFilterListEntryDataClassList + } else { // The search string contains data. + // Filter the list by the original entry string. + entireFilterListEntryDataClassList.filter { + it.originalEntryString.lowercase(getDefault()).contains(searchString) + } + } + + // Return the filter results. + return filterResults + } + + override fun publishResults(charSequence: CharSequence?, filterResults: FilterResults) { + @Suppress("UNCHECKED_CAST") + // Store the searched data filter list entry data class list. + searchedFilterListEntryDataClassList = filterResults.values as List + + // Update the GUI. + notifyDataSetChanged() + } + + } + } } diff --git a/app/src/main/java/com/stoutner/privacybrowser/dataclasses/FilterListEntryDataClass.kt b/app/src/main/java/com/stoutner/privacybrowser/dataclasses/FilterListEntryDataClass.kt index 3af1a6f3..5e4427cb 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dataclasses/FilterListEntryDataClass.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/dataclasses/FilterListEntryDataClass.kt @@ -19,12 +19,17 @@ package com.stoutner.privacybrowser.dataclasses +import android.os.Parcelable + +import kotlinx.parcelize.Parcelize + enum class FilterOptionDisposition(val int: Int) { Null (0), Apply (1), Override (2) } +@Parcelize data class FilterListEntryDataClass ( // The strings. var originalEntryString: String = "", @@ -48,4 +53,4 @@ data class FilterListEntryDataClass ( var filterList: FilterList = FilterList.UltraPrivacy, var sublist: Sublist = Sublist.MainAllowList, var thirdParty: FilterOptionDisposition = FilterOptionDisposition.Null -) +) : Parcelable diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewFilterListEntryDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewFilterListEntryDialog.kt index 7ce0049e..304596a8 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewFilterListEntryDialog.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewFilterListEntryDialog.kt @@ -43,20 +43,15 @@ import com.stoutner.privacybrowser.activities.REGULAR_EXPRESSION_ALLOWLIST import com.stoutner.privacybrowser.activities.REGULAR_EXPRESSION_BLOCKLIST import com.stoutner.privacybrowser.activities.ULTRALIST import com.stoutner.privacybrowser.activities.ULTRAPRIVACY -import com.stoutner.privacybrowser.dataclasses.FilterListDataClass import com.stoutner.privacybrowser.dataclasses.FilterListEntryDataClass import com.stoutner.privacybrowser.dataclasses.FilterOptionDisposition -import com.stoutner.privacybrowser.helpers.easyListDataClass -import com.stoutner.privacybrowser.helpers.easyPrivacyDataClass -import com.stoutner.privacybrowser.helpers.fanboysAnnoyanceDataClass -import com.stoutner.privacybrowser.helpers.ultraListDataClass -import com.stoutner.privacybrowser.helpers.ultraPrivacyDataClass // Define the class constants. private const val ENTRY_ID = "entry_id" private const val FILTER_LIST_INT = "filter_list_int" private const val IS_LAST_ENTRY = "is_last_entry" private const val SUBLIST_INT = "sublist_int" +private const val FILTER_LIST_ENTRY_DATA_CLASS = "filter_list_entry_data_class" // Define the private variables. private var blueColor = 0 @@ -64,7 +59,7 @@ private var redColor = 0 class ViewFilterListEntryDialog : DialogFragment() { companion object { - fun entry(entryId: Int, isLastEntry: Boolean, filterListInt: Int, sublistInt: Int): ViewFilterListEntryDialog { + fun entry(entryId: Int, isLastEntry: Boolean, filterListInt: Int, sublistInt: Int, filterListEntryDataClass: FilterListEntryDataClass): ViewFilterListEntryDialog { // Create a bundle. val bundle = Bundle() @@ -73,6 +68,7 @@ class ViewFilterListEntryDialog : DialogFragment() { bundle.putBoolean(IS_LAST_ENTRY, isLastEntry) bundle.putInt(FILTER_LIST_INT, filterListInt) bundle.putInt(SUBLIST_INT, sublistInt) + bundle.putParcelable(FILTER_LIST_ENTRY_DATA_CLASS, filterListEntryDataClass) // Create a new instance of the view filter list entry dialog. val viewFilterListEntryDialog = ViewFilterListEntryDialog() @@ -111,6 +107,8 @@ class ViewFilterListEntryDialog : DialogFragment() { val isLastEntry = requireArguments().getBoolean(IS_LAST_ENTRY) val filterListInt = requireArguments().getInt(FILTER_LIST_INT) val sublistInt = requireArguments().getInt(SUBLIST_INT) + // The deprecated `getParcelable()` can be replaced once the minimum API >= 33. + @Suppress("DEPRECATION") val filterListEntryDataClass = requireArguments().getParcelable(FILTER_LIST_ENTRY_DATA_CLASS)!! // Use an alert dialog builder to create the alert dialog. val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog) @@ -176,9 +174,6 @@ class ViewFilterListEntryDialog : DialogFragment() { // Disable the next button if the last filter list entry is displayed. nextButton.isEnabled = !isLastEntry - // Get the filter list entry data class. - val filterListEntryDataClass = getFilterListEntryDataClass(filterListInt, sublistInt, entryId) - // Populate the filter list entry views. filterListTextView.text = getFilterListName(filterListInt) sublistTextView.text = getSublistName(sublistInt) @@ -259,31 +254,6 @@ class ViewFilterListEntryDialog : DialogFragment() { return stringBuilder.toString() } - private fun getFilterListEntryDataClass(filterListInt: Int, sublistInt: Int, entryId: Int) : FilterListEntryDataClass { - // Get the filter list entry according to the filter list. - return when (filterListInt) { - ULTRAPRIVACY -> getFilterListEntryDataClassFromSublist(ultraPrivacyDataClass, sublistInt, entryId) - ULTRALIST -> getFilterListEntryDataClassFromSublist(ultraListDataClass, sublistInt, entryId) - EASYPRIVACY -> getFilterListEntryDataClassFromSublist(easyPrivacyDataClass, sublistInt, entryId) - EASYLIST -> getFilterListEntryDataClassFromSublist(easyListDataClass, sublistInt, entryId) - FANBOYS_ANNOYANCE_LIST -> getFilterListEntryDataClassFromSublist(fanboysAnnoyanceDataClass!!, sublistInt, entryId) - else -> getFilterListEntryDataClassFromSublist(FilterListDataClass(), sublistInt, entryId) - } - } - - private fun getFilterListEntryDataClassFromSublist(filterListDataClass: FilterListDataClass, sublistInt: Int, entryId: Int) : FilterListEntryDataClass { - // Return the filter list entry data class. The list is 0 based, so the entry ID needs to be decremented by 1. - return when (sublistInt) { - MAIN_ALLOWLIST -> filterListDataClass.mainAllowList[entryId - 1] - INITIAL_DOMAIN_ALLOWLIST -> filterListDataClass.initialDomainAllowList[entryId - 1] - REGULAR_EXPRESSION_ALLOWLIST -> filterListDataClass.regularExpressionAllowList[entryId - 1] - MAIN_BLOCKLIST -> filterListDataClass.mainBlockList[entryId - 1] - INITIAL_DOMAIN_BLOCKLIST -> filterListDataClass.initialDomainBlockList[entryId - 1] - REGULAR_EXPRESSION_BLOCKLIST -> filterListDataClass.regularExpressionBlockList[entryId - 1] - else -> FilterListEntryDataClass() - } - } - private fun getFilterListName(filterListInt: Int) : String { // Return the filter list name. return when (filterListInt) { diff --git a/app/src/main/res/layout/filter_lists_bottom_appbar.xml b/app/src/main/res/layout/filter_lists_bottom_appbar.xml index 43cd95aa..b8c47f94 100644 --- a/app/src/main/res/layout/filter_lists_bottom_appbar.xml +++ b/app/src/main/res/layout/filter_lists_bottom_appbar.xml @@ -2,7 +2,7 @@ + + + SPDX-FileCopyrightText: 2025-2026 Soren Stoutner This file is part of Privacy Browser Android . @@ -58,6 +58,18 @@ android:layout_width="wrap_content" android:layout_marginBottom="15dp" /> + + + diff --git a/app/src/main/res/layout/logcat_bottom_appbar.xml b/app/src/main/res/layout/logcat_bottom_appbar.xml index e88f3c9f..6bd72f5a 100644 --- a/app/src/main/res/layout/logcat_bottom_appbar.xml +++ b/app/src/main/res/layout/logcat_bottom_appbar.xml @@ -2,7 +2,7 @@ + You should have received a copy of the GNU General Public License along with + this program. If not, see . --> + android:importantForAutofill="no" /> diff --git a/app/src/main/res/layout/view_headers_appbar_custom_view.xml b/app/src/main/res/layout/view_headers_appbar_custom_view.xml index 0dc83f6f..dfe80600 100644 --- a/app/src/main/res/layout/view_headers_appbar_custom_view.xml +++ b/app/src/main/res/layout/view_headers_appbar_custom_view.xml @@ -1,27 +1,27 @@ + You should have received a copy of the GNU General Public License along with + this program. If not, see . --> + android:importantForAutofill="no" /> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d8c190b5..13254331 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -480,6 +480,8 @@ Benutzer-Oberfläche + Orbot oder Tor-Services nicht installiert + Die Nutzung eines Proxys über Tor steht erst zur Verfügung, wenn die Apps "Orbot" oder "TorServices" auf Ihrem Gerät installiert sind. I2P ist nicht installiert Der I2P-Proxy kann erst nach Installation der I2P-App genutzt werden. Die benutzerdefinierte Proxy-URL ist ungültig. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 7967a23c..97d17a65 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -479,6 +479,8 @@ Interfaz + Orbot o TorServices No Instalados + El uso de Tor como proxy no funcionará a menos que tenga instalada la aplicación Orbot o TorServices. I2P No Instalado El proxy a través de I2P no funcionará a menos que la aplicación I2P esté instalada. La URL del proxy personalizado no es válida. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 09e33bcf..d489eb8c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -478,6 +478,8 @@ Interfaccia + Orbot or TorServices non installati + Il Proxy attraverso Tor non funziona se non sono installate le app Orbot o TorServices. I2P Non Installato Il Proxy con I2P non funziona se non è installata la app I2P. La URL del proxy personalizzato non è valida. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4baf0c5c..4a555754 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -475,6 +475,8 @@ Интерфейс + Orbot или TorServices не установлены + Проксирование через Tor работает только если установлено приложение Orbot или TorServices. I2P не установлен Прокси через I2P работать не будет, если приложение I2P не установлено. URL пользовательского прокси недействителен.