From 683d9816cb21124cc1f0f04ee26130cb5c4e5a4c Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Dec 2021 21:27:45 +0100 Subject: [PATCH 01/27] Removed dead code --- .../newpipe/settings/AppearanceSettingsFragment.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java index 1e1b03b4f..4bc5a210e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -2,7 +2,6 @@ package org.schabi.newpipe.settings; import android.content.ActivityNotFoundException; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.widget.Toast; @@ -15,10 +14,6 @@ import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ThemeHelper; public class AppearanceSettingsFragment extends BasePreferenceFragment { - private static final boolean CAPTIONING_SETTINGS_ACCESSIBLE = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - - private String captionSettingsKey; @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { @@ -51,16 +46,11 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment { } else { removePreference(nightThemeKey); } - - captionSettingsKey = getString(R.string.caption_settings_key); - if (!CAPTIONING_SETTINGS_ACCESSIBLE) { - removePreference(captionSettingsKey); - } } @Override public boolean onPreferenceTreeClick(final Preference preference) { - if (preference.getKey().equals(captionSettingsKey) && CAPTIONING_SETTINGS_ACCESSIBLE) { + if (preference.getKey().equals(getString(R.string.caption_settings_key))) { try { startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS)); } catch (final ActivityNotFoundException e) { From 12acaf29dd824ee21baadf26dceadd3ce0284aa7 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Dec 2021 21:29:22 +0100 Subject: [PATCH 02/27] Added credit to the project which inspired the preference search --- app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt index a8fdcae26..1e5bd8799 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt @@ -185,7 +185,11 @@ class AboutActivity : AppCompatActivity() { SoftwareComponent( "RxJava", "2016 - 2020", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2 - ) + ), + SoftwareComponent( + "SearchPreference", "2018", "ByteHamster", + "https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT + ), ) private const val POS_ABOUT = 0 private const val POS_LICENSE = 1 From f3be89b503649067ba33638a3881c987adc3ec42 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Dec 2021 21:30:49 +0100 Subject: [PATCH 03/27] Abstracted methods for the Android keyboard --- .../fragments/list/search/SearchFragment.java | 23 ++-------- .../org/schabi/newpipe/util/KeyboardUtil.java | 43 +++++++++++++++++++ 2 files changed, 46 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/util/KeyboardUtil.java diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 15424334d..055c27733 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -25,7 +25,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; @@ -34,7 +33,6 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.TooltipCompat; -import androidx.core.content.ContextCompat; import androidx.core.text.HtmlCompat; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.ItemTouchHelper; @@ -65,6 +63,7 @@ import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.KeyboardUtil; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ServiceHelper; @@ -670,31 +669,15 @@ public class SearchFragment extends BaseListFragment + * See also https://stackoverflow.com/q/1109022 + *

+ */ +public final class KeyboardUtil { + private KeyboardUtil() { + } + + public static void showKeyboard(final Activity activity, final EditText editText) { + if (activity == null || editText == null) { + return; + } + + if (editText.requestFocus()) { + final InputMethodManager imm = ContextCompat.getSystemService(activity, + InputMethodManager.class); + imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED); + } + } + + public static void hideKeyboard(final Activity activity, final EditText editText) { + if (activity == null || editText == null) { + return; + } + + final InputMethodManager imm = ContextCompat.getSystemService(activity, + InputMethodManager.class); + imm.hideSoftInputFromWindow(editText.getWindowToken(), + InputMethodManager.RESULT_UNCHANGED_SHOWN); + + editText.clearFocus(); + } +} From 4a061f20edc95467673600ca387393f2fa90ff30 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Dec 2021 21:32:13 +0100 Subject: [PATCH 04/27] Code cleanup --- .../newpipe/settings/SelectKioskFragment.java | 18 ------------------ .../settings/tabs/ChooseTabsFragment.java | 15 +++------------ app/src/main/res/layout/settings_layout.xml | 8 ++++---- app/src/main/res/xml/update_settings.xml | 1 - 4 files changed, 7 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java index a766ee074..383390506 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.settings; -import android.content.DialogInterface; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -51,16 +50,11 @@ public class SelectKioskFragment extends DialogFragment { private SelectKioskAdapter selectKioskAdapter = null; private OnSelectedListener onSelectedListener = null; - private OnCancelListener onCancelListener = null; public void setOnSelectedListener(final OnSelectedListener listener) { onSelectedListener = listener; } - public void setOnCancelListener(final OnCancelListener listener) { - onCancelListener = listener; - } - /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ @@ -91,14 +85,6 @@ public class SelectKioskFragment extends DialogFragment { // Handle actions //////////////////////////////////////////////////////////////////////////*/ - @Override - public void onCancel(@NonNull final DialogInterface dialogInterface) { - super.onCancel(dialogInterface); - if (onCancelListener != null) { - onCancelListener.onCancel(); - } - } - private void clickedItem(final SelectKioskAdapter.Entry entry) { if (onSelectedListener != null) { onSelectedListener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName); @@ -114,10 +100,6 @@ public class SelectKioskFragment extends DialogFragment { void onKioskSelected(int serviceId, String kioskId, String kioskName); } - public interface OnCancelListener { - void onCancel(); - } - private class SelectKioskAdapter extends RecyclerView.Adapter { private final List kioskList = new Vector<>(); diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java index 95f7f50ba..490e299bd 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java @@ -44,8 +44,6 @@ import java.util.List; import static org.schabi.newpipe.settings.tabs.Tab.typeFrom; public class ChooseTabsFragment extends Fragment { - private static final int MENU_ITEM_RESTORE_ID = 123456; - private TabsManager tabsManager; private final List tabList = new ArrayList<>(); @@ -110,21 +108,14 @@ public class ChooseTabsFragment extends Fragment { @NonNull final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, - R.string.restore_defaults); + final MenuItem restoreItem = menu.add(R.string.restore_defaults); restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_settings_backup_restore)); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - if (item.getItemId() == MENU_ITEM_RESTORE_ID) { + restoreItem.setOnMenuItemClickListener(ev -> { restoreDefaults(); return true; - } - - return super.onOptionsItemSelected(item); + }); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/res/layout/settings_layout.xml b/app/src/main/res/layout/settings_layout.xml index 33237d7b0..1b7b8b5e2 100644 --- a/app/src/main/res/layout/settings_layout.xml +++ b/app/src/main/res/layout/settings_layout.xml @@ -6,14 +6,14 @@ android:orientation="vertical" tools:context="org.schabi.newpipe.MainActivity"> + + - - diff --git a/app/src/main/res/xml/update_settings.xml b/app/src/main/res/xml/update_settings.xml index ef121ec4e..a44555edf 100644 --- a/app/src/main/res/xml/update_settings.xml +++ b/app/src/main/res/xml/update_settings.xml @@ -1,7 +1,6 @@ Date: Fri, 24 Dec 2021 21:33:40 +0100 Subject: [PATCH 05/27] Added preference search "framework" --- .../preferencesearch/PreferenceParser.java | 201 ++++++++++++++++++ .../PreferenceSearchAdapter.java | 91 ++++++++ .../PreferenceSearchConfiguration.java | 163 ++++++++++++++ .../PreferenceSearchFragment.java | 116 ++++++++++ .../PreferenceSearchItem.java | 91 ++++++++ .../PreferenceSearchResultHighlighter.java | 125 +++++++++++ .../PreferenceSearchResultListener.java | 7 + .../preferencesearch/PreferenceSearcher.java | 36 ++++ .../preferencesearch/package-info.java | 10 + 9 files changed, 840 insertions(+) create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/package-info.java diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java new file mode 100644 index 000000000..1cf401892 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java @@ -0,0 +1,201 @@ +package org.schabi.newpipe.settings.preferencesearch; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; + +import org.xmlpull.v1.XmlPullParser; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Parses the corresponding preference-file(s). + */ +class PreferenceParser { + private static final String TAG = "PreferenceParser"; + + private static final String NS_ANDROID = "http://schemas.android.com/apk/res/android"; + private static final String NS_SEARCH = "http://schemas.android.com/apk/preferencesearch"; + + private final Context context; + private final Map allPreferences; + private final PreferenceSearchConfiguration searchConfiguration; + + PreferenceParser( + final Context context, + final PreferenceSearchConfiguration searchConfiguration + ) { + this.context = context; + this.allPreferences = PreferenceManager.getDefaultSharedPreferences(context).getAll(); + this.searchConfiguration = searchConfiguration; + } + + public List parse( + final PreferenceSearchConfiguration.SearchIndexItem item + ) { + Objects.requireNonNull(item, "item can't be null"); + + final List results = new ArrayList<>(); + final XmlPullParser xpp = context.getResources().getXml(item.getResId()); + + try { + xpp.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + xpp.setFeature(XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES, true); + + final List breadcrumbs = new ArrayList<>(); + if (!TextUtils.isEmpty(item.getBreadcrumb())) { + breadcrumbs.add(item.getBreadcrumb()); + } + while (xpp.getEventType() != XmlPullParser.END_DOCUMENT) { + if (xpp.getEventType() == XmlPullParser.START_TAG) { + final PreferenceSearchItem result = parseSearchResult( + xpp, + joinBreadcrumbs(breadcrumbs), + item.getResId() + ); + + if (!searchConfiguration.getParserIgnoreElements().contains(xpp.getName()) + && result.hasData() + && !"true".equals(getAttribute(xpp, NS_SEARCH, "ignore"))) { + results.add(result); + } + if (searchConfiguration.getParserContainerElements().contains(xpp.getName())) { + breadcrumbs.add(result.getTitle() == null ? "" : result.getTitle()); + } + } else if (xpp.getEventType() == XmlPullParser.END_TAG + && searchConfiguration.getParserContainerElements() + .contains(xpp.getName())) { + breadcrumbs.remove(breadcrumbs.size() - 1); + } + + xpp.next(); + } + } catch (final Exception e) { + Log.w(TAG, "Failed to parse resid=" + item.getResId(), e); + } + return results; + } + + private String joinBreadcrumbs(final List breadcrumbs) { + return breadcrumbs.stream() + .filter(crumb -> !TextUtils.isEmpty(crumb)) + .reduce("", searchConfiguration.getBreadcrumbConcat()); + } + + private String getAttribute( + final XmlPullParser xpp, + @NonNull final String attribute + ) { + final String nsSearchAttr = getAttribute(xpp, NS_SEARCH, attribute); + if (nsSearchAttr != null) { + return nsSearchAttr; + } + return getAttribute(xpp, NS_ANDROID, attribute); + } + + private String getAttribute( + final XmlPullParser xpp, + @NonNull final String namespace, + @NonNull final String attribute + ) { + return xpp.getAttributeValue(namespace, attribute); + } + + private PreferenceSearchItem parseSearchResult( + final XmlPullParser xpp, + final String breadcrumbs, + final int searchIndexItemResId + ) { + final String key = readString(getAttribute(xpp, "key")); + final String[] entries = readStringArray(getAttribute(xpp, "entries")); + final String[] entryValues = readStringArray(getAttribute(xpp, "entryValues")); + + return new PreferenceSearchItem( + key, + tryFillInPreferenceValue( + readString(getAttribute(xpp, "title")), + key, + entries, + entryValues), + tryFillInPreferenceValue( + readString(getAttribute(xpp, "summary")), + key, + entries, + entryValues), + TextUtils.join(",", entries), + readString(getAttribute(xpp, NS_SEARCH, "keywords")), + breadcrumbs, + searchIndexItemResId + ); + } + + private String[] readStringArray(@Nullable final String s) { + if (s == null) { + return new String[0]; + } + if (s.startsWith("@")) { + try { + return context.getResources().getStringArray(Integer.parseInt(s.substring(1))); + } catch (final Exception e) { + Log.w(TAG, "Unable to readStringArray from '" + s + "'", e); + } + } + return new String[0]; + } + + private String readString(@Nullable final String s) { + if (s == null) { + return ""; + } + if (s.startsWith("@")) { + try { + return context.getString(Integer.parseInt(s.substring(1))); + } catch (final Exception e) { + Log.w(TAG, "Unable to readString from '" + s + "'", e); + } + } + return s; + } + + private String tryFillInPreferenceValue( + @Nullable final String s, + @Nullable final String key, + final String[] entries, + final String[] entryValues + ) { + if (s == null) { + return ""; + } + if (key == null) { + return s; + } + + // Resolve value + Object prefValue = allPreferences.get(key); + if (prefValue == null) { + return s; + } + + /* + * Resolve ListPreference values + * + * entryValues = Values/Keys that are saved + * entries = Actual human readable names + */ + if (entries.length > 0 && entryValues.length == entries.length) { + final int entryIndex = Arrays.asList(entryValues).indexOf(prefValue); + if (entryIndex != -1) { + prefValue = entries[entryIndex]; + } + } + + return String.format(s, prefValue.toString()); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java new file mode 100644 index 000000000..527a4a595 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java @@ -0,0 +1,91 @@ +package org.schabi.newpipe.settings.preferencesearch; + +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.schabi.newpipe.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +class PreferenceSearchAdapter + extends RecyclerView.Adapter { + private List dataset = new ArrayList<>(); + private Consumer onItemClickListener; + + @NonNull + @Override + public PreferenceSearchAdapter.PreferenceViewHolder onCreateViewHolder( + @NonNull final ViewGroup parent, + final int viewType + ) { + return new PreferenceViewHolder( + LayoutInflater + .from(parent.getContext()) + .inflate(R.layout.settings_preferencesearch_list_item_result, parent, false)); + } + + @Override + public void onBindViewHolder( + @NonNull final PreferenceSearchAdapter.PreferenceViewHolder holder, + final int position + ) { + final PreferenceSearchItem item = dataset.get(position); + + holder.title.setText(item.getTitle()); + + if (TextUtils.isEmpty(item.getSummary())) { + holder.summary.setVisibility(View.GONE); + } else { + holder.summary.setVisibility(View.VISIBLE); + holder.summary.setText(item.getSummary()); + } + + if (TextUtils.isEmpty(item.getBreadcrumbs())) { + holder.breadcrumbs.setVisibility(View.GONE); + } else { + holder.breadcrumbs.setVisibility(View.VISIBLE); + holder.breadcrumbs.setText(item.getBreadcrumbs()); + } + + holder.itemView.setOnClickListener(v -> { + if (onItemClickListener != null) { + onItemClickListener.accept(item); + } + }); + } + + void setContent(final List items) { + dataset = new ArrayList<>(items); + this.notifyDataSetChanged(); + } + + @Override + public int getItemCount() { + return dataset.size(); + } + + void setOnItemClickListener(final Consumer onItemClickListener) { + this.onItemClickListener = onItemClickListener; + } + + static class PreferenceViewHolder extends RecyclerView.ViewHolder { + final TextView title; + final TextView summary; + final TextView breadcrumbs; + + PreferenceViewHolder(final View v) { + super(v); + title = v.findViewById(R.id.title); + summary = v.findViewById(R.id.summary); + breadcrumbs = v.findViewById(R.id.breadcrumbs); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java new file mode 100644 index 000000000..b4d1c8985 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java @@ -0,0 +1,163 @@ +package org.schabi.newpipe.settings.preferencesearch; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import androidx.annotation.XmlRes; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.BinaryOperator; +import java.util.stream.Stream; + +public class PreferenceSearchConfiguration { + private final ArrayList itemsToIndex = new ArrayList<>(); + + private BinaryOperator breadcrumbConcat = + (s1, s2) -> TextUtils.isEmpty(s1) ? s2 : (s1 + " > " + s2); + + private PreferenceSearchFunction searcher = + (itemStream, keyword) -> + itemStream + // Filter the items by the keyword + .filter(item -> item.getAllRelevantSearchFields().stream() + .filter(str -> !TextUtils.isEmpty(str)) + .anyMatch(str -> + str.toLowerCase().contains(keyword.toLowerCase()))) + // Limit the search results + .limit(100); + + private final List parserIgnoreElements = Arrays.asList( + PreferenceCategory.class.getSimpleName()); + private final List parserContainerElements = Arrays.asList( + PreferenceCategory.class.getSimpleName(), + PreferenceScreen.class.getSimpleName()); + + + public void setBreadcrumbConcat(final BinaryOperator breadcrumbConcat) { + this.breadcrumbConcat = Objects.requireNonNull(breadcrumbConcat); + } + + public void setSearcher(final PreferenceSearchFunction searcher) { + this.searcher = Objects.requireNonNull(searcher); + } + + /** + * Adds a new file to the index. + * + * @param resId The preference file to index + * @return SearchIndexItem + */ + public SearchIndexItem index(@XmlRes final int resId) { + final SearchIndexItem item = new SearchIndexItem(resId, this); + itemsToIndex.add(item); + return item; + } + + List getFiles() { + return itemsToIndex; + } + + public BinaryOperator getBreadcrumbConcat() { + return breadcrumbConcat; + } + + public PreferenceSearchFunction getSearchMatcher() { + return searcher; + } + + public List getParserIgnoreElements() { + return parserIgnoreElements; + } + + public List getParserContainerElements() { + return parserContainerElements; + } + + /** + * Adds a given R.xml resource to the search index. + */ + public static final class SearchIndexItem implements Parcelable { + private String breadcrumb = ""; + @XmlRes + private final int resId; + private final PreferenceSearchConfiguration searchConfiguration; + + /** + * Includes the given R.xml resource in the index. + * + * @param resId The resource to index + * @param searchConfiguration The configuration for the search + */ + private SearchIndexItem( + @XmlRes final int resId, + final PreferenceSearchConfiguration searchConfiguration + ) { + this.resId = resId; + this.searchConfiguration = searchConfiguration; + } + + /** + * Adds a breadcrumb. + * + * @param breadcrumb The breadcrumb to add + * @return For chaining + */ + @SuppressWarnings("HiddenField") + public SearchIndexItem withBreadcrumb(final String breadcrumb) { + this.breadcrumb = + searchConfiguration.getBreadcrumbConcat().apply(this.breadcrumb, breadcrumb); + return this; + } + + @XmlRes + int getResId() { + return resId; + } + + String getBreadcrumb() { + return breadcrumb; + } + + public static final Creator CREATOR = new Creator<>() { + @Override + public SearchIndexItem createFromParcel(final Parcel in) { + return new SearchIndexItem(in); + } + + @Override + public SearchIndexItem[] newArray(final int size) { + return new SearchIndexItem[size]; + } + }; + + private SearchIndexItem(final Parcel parcel) { + this.breadcrumb = parcel.readString(); + this.resId = parcel.readInt(); + this.searchConfiguration = null; + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeString(this.breadcrumb); + dest.writeInt(this.resId); + } + + @Override + public int describeContents() { + return 0; + } + } + + @FunctionalInterface + public interface PreferenceSearchFunction { + Stream search( + Stream allAvailable, + String keyword); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java new file mode 100644 index 000000000..a90d1084e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java @@ -0,0 +1,116 @@ +package org.schabi.newpipe.settings.preferencesearch; + +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.schabi.newpipe.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Displays the search results. + */ +public class PreferenceSearchFragment extends Fragment { + public static final String NAME = PreferenceSearchFragment.class.getSimpleName(); + + private final PreferenceSearchConfiguration searchConfiguration; + + private final PreferenceSearcher searcher; + private SearchViewHolder viewHolder; + private PreferenceSearchAdapter adapter; + + public PreferenceSearchFragment(final PreferenceSearchConfiguration searchConfiguration) { + this.searchConfiguration = searchConfiguration; + this.searcher = new PreferenceSearcher(searchConfiguration); + } + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final PreferenceParser parser = + new PreferenceParser( + getContext(), + searchConfiguration); + + searchConfiguration.getFiles().stream() + .map(parser::parse) + .forEach(searcher::add); + } + + @Nullable + @Override + public View onCreateView( + @NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState + ) { + final View rootView = + inflater.inflate(R.layout.settings_preferencesearch_fragment, container, false); + + viewHolder = new SearchViewHolder(rootView); + viewHolder.recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + + adapter = new PreferenceSearchAdapter(); + adapter.setOnItemClickListener(this::onItemClicked); + viewHolder.recyclerView.setAdapter(adapter); + + return rootView; + } + + public void updateSearchResults(final String keyword) { + if (adapter == null) { + return; + } + + final List results = + !TextUtils.isEmpty(keyword) + ? searcher.searchFor(keyword) + : new ArrayList<>(); + + adapter.setContent(new ArrayList<>(results)); + + setEmptyViewShown(!TextUtils.isEmpty(keyword) && results.isEmpty()); + } + + private void setEmptyViewShown(final boolean shown) { + viewHolder.emptyStateView.setVisibility(shown ? View.VISIBLE : View.GONE); + viewHolder.recyclerView.setVisibility(shown ? View.GONE : View.VISIBLE); + } + + public void onItemClicked(final PreferenceSearchItem item) { + if (!(getActivity() instanceof PreferenceSearchResultListener)) { + throw new ClassCastException( + getActivity().toString() + " must implement SearchPreferenceResultListener"); + } + + ((PreferenceSearchResultListener) getActivity()).onSearchResultClicked(item); + } + + @Override + public void onDestroy() { + searcher.close(); + super.onDestroy(); + } + + private static class SearchViewHolder { + private final RecyclerView recyclerView; + private final View emptyStateView; + + SearchViewHolder(final View root) { + recyclerView = Objects.requireNonNull(root.findViewById(R.id.list)); + emptyStateView = Objects.requireNonNull(root.findViewById(R.id.empty_state_view)); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java new file mode 100644 index 000000000..3030a78bb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java @@ -0,0 +1,91 @@ +package org.schabi.newpipe.settings.preferencesearch; + +import androidx.annotation.NonNull; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Represents a preference-item inside the search. + */ +public class PreferenceSearchItem { + @NonNull + private final String key; + @NonNull + private final String title; + @NonNull + private final String summary; + @NonNull + private final String entries; + @NonNull + private final String keywords; + @NonNull + private final String breadcrumbs; + private final int searchIndexItemResId; + + public PreferenceSearchItem( + @NonNull final String key, + @NonNull final String title, + @NonNull final String summary, + @NonNull final String entries, + @NonNull final String keywords, + @NonNull final String breadcrumbs, + final int searchIndexItemResId + ) { + this.key = Objects.requireNonNull(key); + this.title = Objects.requireNonNull(title); + this.summary = Objects.requireNonNull(summary); + this.entries = Objects.requireNonNull(entries); + this.keywords = Objects.requireNonNull(keywords); + this.breadcrumbs = Objects.requireNonNull(breadcrumbs); + this.searchIndexItemResId = searchIndexItemResId; + } + + public String getKey() { + return key; + } + + public String getTitle() { + return title; + } + + public String getSummary() { + return summary; + } + + public String getEntries() { + return entries; + } + + public String getBreadcrumbs() { + return breadcrumbs; + } + + public String getKeywords() { + return keywords; + } + + public int getSearchIndexItemResId() { + return searchIndexItemResId; + } + + boolean hasData() { + return !key.isEmpty() && !title.isEmpty(); + } + + public List getAllRelevantSearchFields() { + return Arrays.asList( + getTitle(), + getSummary(), + getEntries(), + getBreadcrumbs(), + getKeywords()); + } + + + @Override + public String toString() { + return "PreferenceItem: " + title + " " + summary + " " + key; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java new file mode 100644 index 000000000..4ddb2caa8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java @@ -0,0 +1,125 @@ +package org.schabi.newpipe.settings.preferencesearch; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.util.TypedValue; + +import androidx.appcompat.content.res.AppCompatResources; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceGroup; +import androidx.recyclerview.widget.RecyclerView; + +import org.schabi.newpipe.R; + + +public final class PreferenceSearchResultHighlighter { + private static final String TAG = "PrefSearchResHighlter"; + + private PreferenceSearchResultHighlighter() { + } + + /** + * Highlight the specified preference. + * + * @param item + * @param prefsFragment + */ + public static void highlight( + final PreferenceSearchItem item, + final PreferenceFragmentCompat prefsFragment + ) { + new Handler(Looper.getMainLooper()).post(() -> doHighlight(item, prefsFragment)); + } + + private static void doHighlight( + final PreferenceSearchItem item, + final PreferenceFragmentCompat prefsFragment + ) { + final Preference prefResult = prefsFragment.findPreference(item.getKey()); + + if (prefResult == null) { + Log.w(TAG, "Preference '" + item.getKey() + "' not found on '" + prefsFragment + "'"); + return; + } + + final RecyclerView recyclerView = prefsFragment.getListView(); + final RecyclerView.Adapter adapter = recyclerView.getAdapter(); + if (adapter instanceof PreferenceGroup.PreferencePositionCallback) { + final int position = ((PreferenceGroup.PreferencePositionCallback) adapter) + .getPreferenceAdapterPosition(prefResult); + if (position != RecyclerView.NO_POSITION) { + recyclerView.scrollToPosition(position); + recyclerView.postDelayed(() -> { + final RecyclerView.ViewHolder holder = + recyclerView.findViewHolderForAdapterPosition(position); + if (holder != null) { + final Drawable background = holder.itemView.getBackground(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + && background instanceof RippleDrawable) { + showRippleAnimation((RippleDrawable) background); + return; + } + } + highlightFallback(prefsFragment, prefResult); + }, 150); + return; + } + } + highlightFallback(prefsFragment, prefResult); + } + + /** + * Alternative highlighting (shows an → arrow in front of the setting)if ripple does not work. + * + * @param prefsFragment + * @param prefResult + */ + private static void highlightFallback( + final PreferenceFragmentCompat prefsFragment, + final Preference prefResult + ) { + // Get primary color from text for highlight icon + final TypedValue typedValue = new TypedValue(); + final Resources.Theme theme = prefsFragment.getActivity().getTheme(); + theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true); + final TypedArray arr = prefsFragment.getActivity() + .obtainStyledAttributes( + typedValue.data, + new int[]{android.R.attr.textColorPrimary}); + final int color = arr.getColor(0, 0xffE53935); + arr.recycle(); + + // Show highlight icon + final Drawable oldIcon = prefResult.getIcon(); + final boolean oldSpaceReserved = prefResult.isIconSpaceReserved(); + final Drawable highlightIcon = + AppCompatResources.getDrawable( + prefsFragment.requireContext(), + R.drawable.ic_play_arrow); + highlightIcon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); + prefResult.setIcon(highlightIcon); + + prefsFragment.scrollToPreference(prefResult); + + new Handler(Looper.getMainLooper()).postDelayed(() -> { + prefResult.setIcon(oldIcon); + prefResult.setIconSpaceReserved(oldSpaceReserved); + }, 1000); + } + + private static void showRippleAnimation(final RippleDrawable rippleDrawable) { + rippleDrawable.setState( + new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled}); + new Handler(Looper.getMainLooper()) + .postDelayed(() -> rippleDrawable.setState(new int[]{}), 1000); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java new file mode 100644 index 000000000..1f0636454 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java @@ -0,0 +1,7 @@ +package org.schabi.newpipe.settings.preferencesearch; + +import androidx.annotation.NonNull; + +public interface PreferenceSearchResultListener { + void onSearchResultClicked(@NonNull PreferenceSearchItem result); +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java new file mode 100644 index 000000000..f9427a1ca --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java @@ -0,0 +1,36 @@ +package org.schabi.newpipe.settings.preferencesearch; + +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +class PreferenceSearcher implements AutoCloseable { + private final List allEntries = new ArrayList<>(); + + private final PreferenceSearchConfiguration configuration; + + PreferenceSearcher(final PreferenceSearchConfiguration configuration) { + this.configuration = configuration; + } + + void add(final List items) { + allEntries.addAll(items); + } + + List searchFor(final String keyword) { + if (TextUtils.isEmpty(keyword)) { + return new ArrayList<>(); + } + + return configuration.getSearchMatcher() + .search(allEntries.stream(), keyword) + .collect(Collectors.toList()); + } + + @Override + public void close() { + allEntries.clear(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/package-info.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/package-info.java new file mode 100644 index 000000000..00929235e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/package-info.java @@ -0,0 +1,10 @@ +/** + * Contains classes for searching inside the preferences. + *
+ * This code is based on + * ByteHamster/SearchPreference + * (MIT license) but was heavily modified/refactored for our use. + * + * @author litetex + */ +package org.schabi.newpipe.settings.preferencesearch; From 07fb319e886f8bee02a4d1de96ecd2185999efa0 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Dec 2021 21:35:15 +0100 Subject: [PATCH 06/27] Applied code changes for preference search framework --- .../settings/AppearanceSettingsFragment.java | 2 +- .../settings/BasePreferenceFragment.java | 5 + .../settings/ContentSettingsFragment.java | 8 +- .../settings/DownloadSettingsFragment.java | 2 +- .../settings/HistorySettingsFragment.java | 2 +- .../settings/MainSettingsFragment.java | 43 +++- .../settings/NotificationSettingsFragment.kt | 2 +- .../newpipe/settings/SettingsActivity.java | 206 +++++++++++++++++- .../settings/SettingsResourceRegistry.java | 151 +++++++++++++ .../settings/UpdateSettingsFragment.java | 2 +- .../settings/VideoAudioSettingsFragment.java | 2 +- 11 files changed, 408 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java index 4bc5a210e..e08562908 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -17,7 +17,7 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResource(R.xml.appearance_settings); + addPreferencesFromResourceRegistry(); final String themeKey = getString(R.string.theme_key); // the key of the active theme when settings were opened (or recreated after theme change) diff --git a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java index a745861ad..619579f3a 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java @@ -28,6 +28,11 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { super.onCreate(savedInstanceState); } + protected void addPreferencesFromResourceRegistry() { + addPreferencesFromResource( + SettingsResourceRegistry.getInstance().getPreferencesResId(this.getClass())); + } + @Override public void onViewCreated(@NonNull final View rootView, @Nullable final Bundle savedInstanceState) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 1c8eb5cd2..d79ea0cd5 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -1,5 +1,8 @@ package org.schabi.newpipe.settings; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -38,9 +41,6 @@ import java.util.Date; import java.util.Locale; import java.util.Objects; -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - public class ContentSettingsFragment extends BasePreferenceFragment { private static final String ZIP_MIME_TYPE = "application/zip"; @@ -70,7 +70,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { importExportDataPathKey = getString(R.string.import_export_data_path); youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); - addPreferencesFromResource(R.xml.content_settings); + addPreferencesFromResourceRegistry(); final Preference importDataPreference = requirePreference(R.string.import_data); importDataPreference.setOnPreferenceClickListener((Preference p) -> { diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 681aee409..fe327e1b5 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -54,7 +54,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResource(R.xml.download_settings); + addPreferencesFromResourceRegistry(); downloadPathVideoPreference = getString(R.string.download_path_video_key); downloadPathAudioPreference = getString(R.string.download_path_audio_key); diff --git a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java index 33e0ba16b..868618110 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java @@ -29,7 +29,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResource(R.xml.history_settings); + addPreferencesFromResourceRegistry(); cacheWipeKey = getString(R.string.metadata_cache_wipe_key); viewsHistoryClearKey = getString(R.string.clear_views_history_key); diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java index 12599b828..6cd165861 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -1,7 +1,11 @@ package org.schabi.newpipe.settings; import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import androidx.annotation.NonNull; import androidx.preference.Preference; import org.schabi.newpipe.App; @@ -12,10 +16,15 @@ import org.schabi.newpipe.R; public class MainSettingsFragment extends BasePreferenceFragment { public static final boolean DEBUG = MainActivity.DEBUG; + private SettingsActivity settingsActivity; + @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResource(R.xml.main_settings); + addPreferencesFromResourceRegistry(); + setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called + + // Check if the app is updatable if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) { final Preference update = findPreference(getString(R.string.update_pref_screen_key)); @@ -24,4 +33,36 @@ public class MainSettingsFragment extends BasePreferenceFragment { defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply(); } } + + @Override + public void onCreateOptionsMenu( + @NonNull final Menu menu, + @NonNull final MenuInflater inflater + ) { + super.onCreateOptionsMenu(menu, inflater); + + // -- Link settings activity and register menu -- + settingsActivity = (SettingsActivity) getActivity(); + + inflater.inflate(R.menu.menu_settings_main_fragment, menu); + + final MenuItem menuSearchItem = menu.getItem(0); + + settingsActivity.setMenuSearchItem(menuSearchItem); + + menuSearchItem.setOnMenuItemClickListener(ev -> { + settingsActivity.setSearchActive(true); + return true; + }); + } + + @Override + public void onDestroy() { + // Unlink activity so that we don't get memory problems + if (settingsActivity != null) { + settingsActivity.setMenuSearchItem(null); + settingsActivity = null; + } + super.onDestroy(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.kt index e03aa4074..6bea8b69e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.kt @@ -7,7 +7,7 @@ import org.schabi.newpipe.R class NotificationSettingsFragment : BasePreferenceFragment() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.notification_settings) + addPreferencesFromResourceRegistry() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { val colorizePref: Preference? = findPreference(getString(R.string.notification_colorize_key)) diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 02e2538c5..787740cc2 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -1,22 +1,42 @@ package org.schabi.newpipe.settings; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + import android.os.Bundle; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; +import com.jakewharton.rxbinding4.widget.RxTextView; + +import org.schabi.newpipe.App; +import org.schabi.newpipe.CheckForNewAppVersion; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.SettingsLayoutBinding; +import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchConfiguration; +import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchFragment; +import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchItem; +import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchResultHighlighter; +import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchResultListener; import org.schabi.newpipe.util.DeviceUtils; +import org.schabi.newpipe.util.KeyboardUtil; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; +import java.util.concurrent.TimeUnit; /* * Created by Christian Schabesberger on 31.08.15. @@ -39,7 +59,23 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; */ public class SettingsActivity extends AppCompatActivity - implements BasePreferenceFragment.OnPreferenceStartFragmentCallback { + implements + BasePreferenceFragment.OnPreferenceStartFragmentCallback, + PreferenceSearchResultListener { + private static final String TAG = "SettingsActivity"; + private static final boolean DEBUG = MainActivity.DEBUG; + + @IdRes + private static final int FRAGMENT_HOLDER_ID = R.id.settings_fragment_holder; + + private PreferenceSearchFragment searchFragment; + + @Nullable + private MenuItem menuSearchItem; + + private View searchContainer; + private EditText searchEditText; + @Override protected void onCreate(final Bundle savedInstanceBundle) { setTheme(ThemeHelper.getSettingsThemeStyle(this)); @@ -49,6 +85,7 @@ public class SettingsActivity extends AppCompatActivity final SettingsLayoutBinding settingsLayoutBinding = SettingsLayoutBinding.inflate(getLayoutInflater()); setContentView(settingsLayoutBinding.getRoot()); + initSearch(settingsLayoutBinding); setSupportActionBar(settingsLayoutBinding.settingsToolbarLayout.toolbar); @@ -78,6 +115,12 @@ public class SettingsActivity extends AppCompatActivity public boolean onOptionsItemSelected(final MenuItem item) { final int id = item.getItemId(); if (id == android.R.id.home) { + // Check if the search is active and if so: Close it + if (isSearchActive()) { + setSearchActive(false); + return true; + } + if (getSupportFragmentManager().getBackStackEntryCount() == 0) { finish(); } else { @@ -91,14 +134,165 @@ public class SettingsActivity extends AppCompatActivity @Override public boolean onPreferenceStartFragment(final PreferenceFragmentCompat caller, final Preference preference) { - final Fragment fragment = Fragment - .instantiate(this, preference.getFragment(), preference.getExtras()); + showSettingsFragment(instantiateFragment(preference.getFragment())); + return true; + } + + private Fragment instantiateFragment(@NonNull final String className) { + return getSupportFragmentManager() + .getFragmentFactory() + .instantiate(this.getClassLoader(), className); + } + + private void showSettingsFragment(final Fragment fragment) { getSupportFragmentManager().beginTransaction() .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) - .replace(R.id.settings_fragment_holder, fragment) + .replace(FRAGMENT_HOLDER_ID, fragment) .addToBackStack(null) .commit(); - return true; } + + @Override + protected void onDestroy() { + setMenuSearchItem(null); + super.onDestroy(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Search + //////////////////////////////////////////////////////////////////////////*/ + //region Search + + private void initSearch(final SettingsLayoutBinding settingsLayoutBinding) { + searchContainer = + settingsLayoutBinding.settingsToolbarLayout.toolbar + .findViewById(R.id.toolbar_search_container); + + // Configure input field for search + searchEditText = searchContainer.findViewById(R.id.toolbar_search_edit_text); + RxTextView.textChanges(searchEditText) + // Wait some time after the last input before actually searching + .debounce(200, TimeUnit.MILLISECONDS) + .subscribe(v -> runOnUiThread(() -> onSearchChanged())); + + // Configure clear button + searchContainer.findViewById(R.id.toolbar_search_clear) + .setOnClickListener(ev -> resetSearchText()); + + // Build search configuration using SettingsResourceRegistry + prepareSearchConfig(); + + final PreferenceSearchConfiguration config = new PreferenceSearchConfiguration(); + SettingsResourceRegistry.getInstance().getAllEntries().stream() + .filter(SettingsResourceRegistry.SettingRegistryEntry::isSearchable) + .map(SettingsResourceRegistry.SettingRegistryEntry::getPreferencesResId) + .forEach(config::index); + + searchFragment = new PreferenceSearchFragment(config); + } + + private void prepareSearchConfig() { + // Check if the update settings should be available + if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) { + SettingsResourceRegistry.getInstance() + .getEntryByPreferencesResId(R.xml.update_settings) + .setSearchable(false); + } + } + + public void setMenuSearchItem(final MenuItem menuSearchItem) { + this.menuSearchItem = menuSearchItem; + } + + public void setSearchActive(final boolean active) { + // Ignore if search is already in correct state + if (isSearchActive() == active) { + return; + } + + if (DEBUG) { + Log.d(TAG, "setSearchActive called active=" + active); + } + + searchContainer.setVisibility(active ? View.VISIBLE : View.GONE); + if (menuSearchItem != null) { + menuSearchItem.setVisible(!active); + } + + final FragmentManager fm = getSupportFragmentManager(); + if (active) { + fm.beginTransaction() + .add(FRAGMENT_HOLDER_ID, searchFragment, PreferenceSearchFragment.NAME) + .addToBackStack(PreferenceSearchFragment.NAME) + .commit(); + } else if (searchFragment != null) { + fm.beginTransaction().remove(searchFragment).commit(); + fm.popBackStack( + PreferenceSearchFragment.NAME, + FragmentManager.POP_BACK_STACK_INCLUSIVE); + + KeyboardUtil.hideKeyboard(this, searchEditText); + } + + resetSearchText(); + } + + private void resetSearchText() { + searchEditText.setText(""); + } + + private boolean isSearchActive() { + return searchContainer.getVisibility() == View.VISIBLE; + } + + private void onSearchChanged() { + if (!isSearchActive()) { + return; + } + + if (searchFragment != null) { + searchFragment.updateSearchResults(this.searchEditText.getText().toString()); + } + } + + @Override + public void onSearchResultClicked(@NonNull final PreferenceSearchItem result) { + if (DEBUG) { + Log.d(TAG, "onSearchResultClicked called result=" + result); + } + + // Hide the search + setSearchActive(false); + + // -- Highlight the result -- + // Find out which fragment class we need + final Class targetedFragmentClass = + SettingsResourceRegistry.getInstance() + .getFragmentClass(result.getSearchIndexItemResId()); + + if (targetedFragmentClass == null) { + // This should never happen + Log.w(TAG, "Unable to locate fragment class for resId=" + + result.getSearchIndexItemResId()); + return; + } + + // Check if the currentFragment is the one which contains the result + Fragment currentFragment = + getSupportFragmentManager().findFragmentById(FRAGMENT_HOLDER_ID); + if (!targetedFragmentClass.equals(currentFragment.getClass())) { + // If it's not the correct one display the correct one + currentFragment = instantiateFragment(targetedFragmentClass.getName()); + showSettingsFragment(currentFragment); + } + + // Run the highlighting + if (currentFragment instanceof PreferenceFragmentCompat) { + PreferenceSearchResultHighlighter + .highlight(result, (PreferenceFragmentCompat) currentFragment); + } + } + + //endregion } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java new file mode 100644 index 000000000..c4db9f93d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java @@ -0,0 +1,151 @@ +package org.schabi.newpipe.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.XmlRes; +import androidx.fragment.app.Fragment; + +import org.schabi.newpipe.R; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * A registry that contains information about SettingsFragments. + *
+ * includes: + *
    + *
  • Class of the SettingsFragment
  • + *
  • XML-Resource
  • + *
  • ...
  • + *
+ * + * E.g. used by the preference search. + */ +public final class SettingsResourceRegistry { + + private static final SettingsResourceRegistry INSTANCE = new SettingsResourceRegistry(); + + private final Set registeredEntries = new HashSet<>(); + + private SettingsResourceRegistry() { + add(MainSettingsFragment.class, R.xml.main_settings).setSearchable(false); + + add(AppearanceSettingsFragment.class, R.xml.appearance_settings); + add(ContentSettingsFragment.class, R.xml.content_settings); + add(DownloadSettingsFragment.class, R.xml.download_settings); + add(HistorySettingsFragment.class, R.xml.history_settings); + add(NotificationSettingsFragment.class, R.xml.notification_settings); + add(UpdateSettingsFragment.class, R.xml.update_settings); + add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings); + } + + private SettingRegistryEntry add( + @NonNull final Class fragmentClass, + @XmlRes final int preferencesResId + ) { + final SettingRegistryEntry entry = + new SettingRegistryEntry(fragmentClass, preferencesResId); + this.registeredEntries.add(entry); + return entry; + } + + @Nullable + public SettingRegistryEntry getEntryByFragmentClass( + final Class fragmentClass + ) { + Objects.requireNonNull(fragmentClass); + return registeredEntries.stream() + .filter(e -> Objects.equals(e.getFragmentClass(), fragmentClass)) + .findFirst() + .orElse(null); + } + + @Nullable + public SettingRegistryEntry getEntryByPreferencesResId(@XmlRes final int preferencesResId) { + return registeredEntries.stream() + .filter(e -> Objects.equals(e.getPreferencesResId(), preferencesResId)) + .findFirst() + .orElse(null); + } + + public int getPreferencesResId(@NonNull final Class fragmentClass) { + final SettingRegistryEntry entry = getEntryByFragmentClass(fragmentClass); + if (entry == null) { + return -1; + } + return entry.getPreferencesResId(); + } + + @Nullable + public Class getFragmentClass(@XmlRes final int preferencesResId) { + final SettingRegistryEntry entry = getEntryByPreferencesResId(preferencesResId); + if (entry == null) { + return null; + } + return entry.getFragmentClass(); + } + + public Set getAllEntries() { + return new HashSet<>(registeredEntries); + } + + public static SettingsResourceRegistry getInstance() { + return INSTANCE; + } + + + public static class SettingRegistryEntry { + @NonNull + private final Class fragmentClass; + @XmlRes + private final int preferencesResId; + + private boolean searchable = true; + + public SettingRegistryEntry( + @NonNull final Class fragmentClass, + @XmlRes final int preferencesResId + ) { + this.fragmentClass = Objects.requireNonNull(fragmentClass); + this.preferencesResId = preferencesResId; + } + + @SuppressWarnings("HiddenField") + public SettingRegistryEntry setSearchable(final boolean searchable) { + this.searchable = searchable; + return this; + } + + public Class getFragmentClass() { + return fragmentClass; + } + + public int getPreferencesResId() { + return preferencesResId; + } + + public boolean isSearchable() { + return searchable; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final SettingRegistryEntry that = (SettingRegistryEntry) o; + return getPreferencesResId() == that.getPreferencesResId() + && getFragmentClass().equals(that.getFragmentClass()); + } + + @Override + public int hashCode() { + return Objects.hash(getFragmentClass(), getPreferencesResId()); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java index bc183d08a..04bad3815 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -38,7 +38,7 @@ public class UpdateSettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResource(R.xml.update_settings); + addPreferencesFromResourceRegistry(); findPreference(getString(R.string.update_app_key)) .setOnPreferenceChangeListener(updatePreferenceChange); diff --git a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java index c0d274fe0..039f00c1d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java @@ -23,7 +23,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResource(R.xml.video_audio_settings); + addPreferencesFromResourceRegistry(); updateSeekOptions(); From 8fc935b09db56f597b824e248f445cd7b30579d4 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Dec 2021 21:35:41 +0100 Subject: [PATCH 07/27] Added resource files Forgot to commit them before... --- .../settings_preferencesearch_fragment.xml | 49 +++++++++++++++++++ ...ings_preferencesearch_list_item_result.xml | 36 ++++++++++++++ .../res/menu/menu_settings_main_fragment.xml | 11 +++++ 3 files changed, 96 insertions(+) create mode 100644 app/src/main/res/layout/settings_preferencesearch_fragment.xml create mode 100644 app/src/main/res/layout/settings_preferencesearch_list_item_result.xml create mode 100644 app/src/main/res/menu/menu_settings_main_fragment.xml diff --git a/app/src/main/res/layout/settings_preferencesearch_fragment.xml b/app/src/main/res/layout/settings_preferencesearch_fragment.xml new file mode 100644 index 000000000..b8aaa60f6 --- /dev/null +++ b/app/src/main/res/layout/settings_preferencesearch_fragment.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/settings_preferencesearch_list_item_result.xml b/app/src/main/res/layout/settings_preferencesearch_list_item_result.xml new file mode 100644 index 000000000..2e20f274c --- /dev/null +++ b/app/src/main/res/layout/settings_preferencesearch_list_item_result.xml @@ -0,0 +1,36 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_settings_main_fragment.xml b/app/src/main/res/menu/menu_settings_main_fragment.xml new file mode 100644 index 000000000..fbe3b4e09 --- /dev/null +++ b/app/src/main/res/menu/menu_settings_main_fragment.xml @@ -0,0 +1,11 @@ + + + + + From 22db4175f37205f650df7a67f3017705404eda2b Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Dec 2021 21:37:04 +0100 Subject: [PATCH 08/27] Moved reset-reCAPTCHA-cookie to cache tab and made it read-only so that the search works as expected --- .../settings/ContentSettingsFragment.java | 16 ---------------- .../settings/HistorySettingsFragment.java | 17 +++++++++++++++++ app/src/main/res/xml/content_settings.xml | 7 ------- app/src/main/res/xml/history_settings.xml | 7 +++++++ 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index d79ea0cd5..47458ad3f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -24,7 +24,6 @@ import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.error.ErrorUtil; -import org.schabi.newpipe.error.ReCaptchaActivity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.Localization; @@ -105,21 +104,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment { .getPreferredContentCountry(requireContext()); initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en"); - final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key); - clearCookiePref.setOnPreferenceClickListener(preference -> { - defaultPreferences.edit() - .putString(getString(R.string.recaptcha_cookies_key), "").apply(); - DownloaderImpl.getInstance().setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, ""); - Toast.makeText(getActivity(), R.string.recaptcha_cookies_cleared, - Toast.LENGTH_SHORT).show(); - clearCookiePref.setVisible(false); - return true; - }); - - if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) { - clearCookiePref.setVisible(false); - } - findPreference(getString(R.string.download_thumbnail_key)).setOnPreferenceChangeListener( (preference, newValue) -> { PicassoHelper.setShouldLoadImages((Boolean) newValue); diff --git a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java index 868618110..86e651e2b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java @@ -8,9 +8,11 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; +import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.R; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; +import org.schabi.newpipe.error.ReCaptchaActivity; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.util.InfoCache; @@ -37,6 +39,21 @@ public class HistorySettingsFragment extends BasePreferenceFragment { searchHistoryClearKey = getString(R.string.clear_search_history_key); recordManager = new HistoryRecordManager(getActivity()); disposables = new CompositeDisposable(); + + final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key); + clearCookiePref.setOnPreferenceClickListener(preference -> { + defaultPreferences.edit() + .putString(getString(R.string.recaptcha_cookies_key), "").apply(); + DownloaderImpl.getInstance().setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, ""); + Toast.makeText(getActivity(), R.string.recaptcha_cookies_cleared, + Toast.LENGTH_SHORT).show(); + clearCookiePref.setEnabled(false); + return true; + }); + + if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) { + clearCookiePref.setEnabled(false); + } } @Override diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index 23b782ffd..e754b3a30 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -128,13 +128,6 @@ app:singleLineTitle="false" app:iconSpaceReserved="false" /> - - + +
From 7fc0a3841a206e0b82f887e7da82cb47eff18734 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Dec 2021 21:51:44 +0100 Subject: [PATCH 09/27] Fine tuning --- .../main/java/org/schabi/newpipe/settings/SettingsActivity.java | 2 ++ .../preferencesearch/PreferenceSearchResultHighlighter.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 787740cc2..88c8fcba5 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -226,6 +226,8 @@ public class SettingsActivity extends AppCompatActivity .add(FRAGMENT_HOLDER_ID, searchFragment, PreferenceSearchFragment.NAME) .addToBackStack(PreferenceSearchFragment.NAME) .commit(); + + KeyboardUtil.showKeyboard(this, searchEditText); } else if (searchFragment != null) { fm.beginTransaction().remove(searchFragment).commit(); fm.popBackStack( diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java index 4ddb2caa8..e48ca28ce 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java @@ -70,7 +70,7 @@ public final class PreferenceSearchResultHighlighter { } } highlightFallback(prefsFragment, prefResult); - }, 150); + }, 200); return; } } From 52542e04e82e36a4c201b29c98b6bef11e81fe2d Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 25 Dec 2021 00:06:06 +0100 Subject: [PATCH 10/27] Added fuzzy searching + Some minor code refactoring --- .../newpipe/settings/SettingsActivity.java | 9 ++ .../PreferenceFuzzySearchFunction.java | 121 ++++++++++++++ .../PreferenceSearchConfiguration.java | 11 +- .../PreferenceSearchFragment.java | 2 +- .../similarity/FuzzyScore.java | 148 ++++++++++++++++++ 5 files changed, 280 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/similarity/FuzzyScore.java diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 88c8fcba5..1f917e771 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -111,6 +111,15 @@ public class SettingsActivity extends AppCompatActivity return super.onCreateOptionsMenu(menu); } + @Override + public void onBackPressed() { + if (isSearchActive()) { + setSearchActive(false); + return; + } + super.onBackPressed(); + } + @Override public boolean onOptionsItemSelected(final MenuItem item) { final int id = item.getItemId(); diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java new file mode 100644 index 000000000..48f507a41 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java @@ -0,0 +1,121 @@ +package org.schabi.newpipe.settings.preferencesearch; + +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import org.schabi.newpipe.settings.preferencesearch.similarity.FuzzyScore; + +import java.util.Comparator; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +public class PreferenceFuzzySearchFunction + implements PreferenceSearchConfiguration.PreferenceSearchFunction { + + private static final FuzzyScore FUZZY_SCORE = new FuzzyScore(Locale.ROOT); + + @Override + public Stream search( + final Stream allAvailable, + final String keyword + ) { + final float maxScore = (keyword.length() + 1) * 3 - 2; // First can't get +2 bonus score + + return allAvailable + // General search + // Check all fields if anyone contains something that kind of matches the keyword + .map(item -> new FuzzySearchGeneralDTO(item, keyword)) + .filter(dto -> dto.getScore() / maxScore >= 0.3f) + .map(FuzzySearchGeneralDTO::getItem) + // Specific search - Used for determining order of search results + // Calculate a score based on specific search fields + .map(item -> new FuzzySearchSpecificDTO(item, keyword)) + .sorted(Comparator.comparing(FuzzySearchSpecificDTO::getScore).reversed()) + .map(FuzzySearchSpecificDTO::getItem) + // Limit the amount of search results + .limit(20); + } + + private float computeFuzzyScore( + @NonNull final PreferenceSearchItem item, + @NonNull final Function resolver, + @NonNull final String keyword + ) { + return FUZZY_SCORE.fuzzyScore(resolver.apply(item), keyword); + } + + static class FuzzySearchGeneralDTO { + private final PreferenceSearchItem item; + private final float score; + + FuzzySearchGeneralDTO( + final PreferenceSearchItem item, + final String keyword) { + this.item = item; + this.score = FUZZY_SCORE.fuzzyScore( + TextUtils.join(";", item.getAllRelevantSearchFields()), + keyword); + } + + public PreferenceSearchItem getItem() { + return item; + } + + public float getScore() { + return score; + } + } + + static class FuzzySearchSpecificDTO { + private static final Map, Float> WEIGHT_MAP = Map.of( + // The user will most likely look for the title -> prioritize it + PreferenceSearchItem::getTitle, 1.5f, + // The summary is also important as it usually contains a larger desc + // Example: Searching for '4k' → 'show higher resolution' is shown + PreferenceSearchItem::getSummary, 1f, + // Entries are also important as they provide all known/possible values + // Example: Searching where the resolution can be changed to 720p + PreferenceSearchItem::getEntries, 1f + ); + + private final PreferenceSearchItem item; + private final float score; + + FuzzySearchSpecificDTO( + final PreferenceSearchItem item, + final String keyword) { + this.item = item; + + float attributeScoreSum = 0; + int countOfAttributesWithScore = 0; + for (final Map.Entry, Float> we + : WEIGHT_MAP.entrySet()) { + final String valueToProcess = we.getKey().apply(item); + if (valueToProcess.isEmpty()) { + continue; + } + + attributeScoreSum += + FUZZY_SCORE.fuzzyScore(valueToProcess, keyword) * we.getValue(); + countOfAttributesWithScore++; + } + + if (countOfAttributesWithScore != 0) { + this.score = attributeScoreSum / countOfAttributesWithScore; + } else { + this.score = 0; + } + } + + public PreferenceSearchItem getItem() { + return item; + } + + public float getScore() { + return score; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java index b4d1c8985..37b98035a 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java @@ -21,16 +21,7 @@ public class PreferenceSearchConfiguration { private BinaryOperator breadcrumbConcat = (s1, s2) -> TextUtils.isEmpty(s1) ? s2 : (s1 + " > " + s2); - private PreferenceSearchFunction searcher = - (itemStream, keyword) -> - itemStream - // Filter the items by the keyword - .filter(item -> item.getAllRelevantSearchFields().stream() - .filter(str -> !TextUtils.isEmpty(str)) - .anyMatch(str -> - str.toLowerCase().contains(keyword.toLowerCase()))) - // Limit the search results - .limit(100); + private PreferenceSearchFunction searcher = new PreferenceFuzzySearchFunction(); private final List parserIgnoreElements = Arrays.asList( PreferenceCategory.class.getSimpleName()); diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java index a90d1084e..d9a17da16 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java @@ -81,7 +81,7 @@ public class PreferenceSearchFragment extends Fragment { adapter.setContent(new ArrayList<>(results)); - setEmptyViewShown(!TextUtils.isEmpty(keyword) && results.isEmpty()); + setEmptyViewShown(results.isEmpty()); } private void setEmptyViewShown(final boolean shown) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/similarity/FuzzyScore.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/similarity/FuzzyScore.java new file mode 100644 index 000000000..4ab6bf60a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/similarity/FuzzyScore.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.schabi.newpipe.settings.preferencesearch.similarity; + +import java.util.Locale; + +/** + * A matching algorithm that is similar to the searching algorithms implemented in editors such + * as Sublime Text, TextMate, Atom and others. + * + *

+ * One point is given for every matched character. Subsequent matches yield two bonus points. + * A higher score indicates a higher similarity. + *

+ * + *

+ * This code has been adapted from Apache Commons Lang 3.3. + *

+ * + * @since 1.0 + * + * Note: This class was forked from + * + * apache/commons-text (8cfdafc) FuzzyScore.java + * + */ +public class FuzzyScore { + + /** + * Locale used to change the case of text. + */ + private final Locale locale; + + + /** + * This returns a {@link Locale}-specific {@link FuzzyScore}. + * + * @param locale The string matching logic is case insensitive. + A {@link Locale} is necessary to normalize both Strings to lower case. + * @throws IllegalArgumentException + * This is thrown if the {@link Locale} parameter is {@code null}. + */ + public FuzzyScore(final Locale locale) { + if (locale == null) { + throw new IllegalArgumentException("Locale must not be null"); + } + this.locale = locale; + } + + /** + * Find the Fuzzy Score which indicates the similarity score between two + * Strings. + * + *
+     * score.fuzzyScore(null, null)                          = IllegalArgumentException
+     * score.fuzzyScore("not null", null)                    = IllegalArgumentException
+     * score.fuzzyScore(null, "not null")                    = IllegalArgumentException
+     * score.fuzzyScore("", "")                              = 0
+     * score.fuzzyScore("Workshop", "b")                     = 0
+     * score.fuzzyScore("Room", "o")                         = 1
+     * score.fuzzyScore("Workshop", "w")                     = 1
+     * score.fuzzyScore("Workshop", "ws")                    = 2
+     * score.fuzzyScore("Workshop", "wo")                    = 4
+     * score.fuzzyScore("Apache Software Foundation", "asf") = 3
+     * 
+ * + * @param term a full term that should be matched against, must not be null + * @param query the query that will be matched against a term, must not be + * null + * @return result score + * @throws IllegalArgumentException if the term or query is {@code null} + */ + public Integer fuzzyScore(final CharSequence term, final CharSequence query) { + if (term == null || query == null) { + throw new IllegalArgumentException("CharSequences must not be null"); + } + + // fuzzy logic is case insensitive. We normalize the Strings to lower + // case right from the start. Turning characters to lower case + // via Character.toLowerCase(char) is unfortunately insufficient + // as it does not accept a locale. + final String termLowerCase = term.toString().toLowerCase(locale); + final String queryLowerCase = query.toString().toLowerCase(locale); + + // the resulting score + int score = 0; + + // the position in the term which will be scanned next for potential + // query character matches + int termIndex = 0; + + // index of the previously matched character in the term + int previousMatchingCharacterIndex = Integer.MIN_VALUE; + + for (int queryIndex = 0; queryIndex < queryLowerCase.length(); queryIndex++) { + final char queryChar = queryLowerCase.charAt(queryIndex); + + boolean termCharacterMatchFound = false; + for (; termIndex < termLowerCase.length() + && !termCharacterMatchFound; termIndex++) { + final char termChar = termLowerCase.charAt(termIndex); + + if (queryChar == termChar) { + // simple character matches result in one point + score++; + + // subsequent character matches further improve + // the score. + if (previousMatchingCharacterIndex + 1 == termIndex) { + score += 2; + } + + previousMatchingCharacterIndex = termIndex; + + // we can leave the nested loop. Every character in the + // query can match at most one character in the term. + termCharacterMatchFound = true; + } + } + } + + return score; + } + + /** + * Gets the locale. + * + * @return The locale + */ + public Locale getLocale() { + return locale; + } + +} From 0f45c69388a2da0a465dff19813ec0e09face67f Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 26 Dec 2021 23:46:22 +0100 Subject: [PATCH 11/27] Code cleanup + indexing improvements * Removed unused method * Only index all settings once -> Saves performance * Fixed some SonarLint reported problems --- .../newpipe/settings/SettingsActivity.java | 23 +++++++++----- .../PreferenceFuzzySearchFunction.java | 12 +------- .../preferencesearch/PreferenceParser.java | 4 +-- .../PreferenceSearchConfiguration.java | 2 +- .../PreferenceSearchFragment.java | 30 ++++--------------- .../preferencesearch/PreferenceSearcher.java | 9 +++--- 6 files changed, 29 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 1f917e771..92bc21c9d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -26,11 +26,13 @@ import org.schabi.newpipe.CheckForNewAppVersion; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.SettingsLayoutBinding; +import org.schabi.newpipe.settings.preferencesearch.PreferenceParser; import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchConfiguration; import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchFragment; import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchItem; import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchResultHighlighter; import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchResultListener; +import org.schabi.newpipe.settings.preferencesearch.PreferenceSearcher; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.KeyboardUtil; import org.schabi.newpipe.util.ThemeHelper; @@ -58,9 +60,8 @@ import java.util.concurrent.TimeUnit; * along with NewPipe. If not, see . */ -public class SettingsActivity extends AppCompatActivity - implements - BasePreferenceFragment.OnPreferenceStartFragmentCallback, +public class SettingsActivity extends AppCompatActivity implements + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, PreferenceSearchResultListener { private static final String TAG = "SettingsActivity"; private static final boolean DEBUG = MainActivity.DEBUG; @@ -165,6 +166,7 @@ public class SettingsActivity extends AppCompatActivity @Override protected void onDestroy() { setMenuSearchItem(null); + searchFragment = null; super.onDestroy(); } @@ -183,26 +185,33 @@ public class SettingsActivity extends AppCompatActivity RxTextView.textChanges(searchEditText) // Wait some time after the last input before actually searching .debounce(200, TimeUnit.MILLISECONDS) - .subscribe(v -> runOnUiThread(() -> onSearchChanged())); + .subscribe(v -> runOnUiThread(this::onSearchChanged)); // Configure clear button searchContainer.findViewById(R.id.toolbar_search_clear) .setOnClickListener(ev -> resetSearchText()); - // Build search configuration using SettingsResourceRegistry prepareSearchConfig(); + // Build search configuration using SettingsResourceRegistry final PreferenceSearchConfiguration config = new PreferenceSearchConfiguration(); SettingsResourceRegistry.getInstance().getAllEntries().stream() .filter(SettingsResourceRegistry.SettingRegistryEntry::isSearchable) .map(SettingsResourceRegistry.SettingRegistryEntry::getPreferencesResId) .forEach(config::index); - searchFragment = new PreferenceSearchFragment(config); + // Build search items + final PreferenceParser parser = new PreferenceParser(getApplicationContext(), config); + final PreferenceSearcher searcher = new PreferenceSearcher(config); + config.getFiles().stream() + .map(parser::parse) + .forEach(searcher::add); + + searchFragment = new PreferenceSearchFragment(searcher); } private void prepareSearchConfig() { - // Check if the update settings should be available + // Check if the update settings are available if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) { SettingsResourceRegistry.getInstance() .getEntryByPreferencesResId(R.xml.update_settings) diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java index 48f507a41..88ef790c8 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java @@ -2,8 +2,6 @@ package org.schabi.newpipe.settings.preferencesearch; import android.text.TextUtils; -import androidx.annotation.NonNull; - import org.schabi.newpipe.settings.preferencesearch.similarity.FuzzyScore; import java.util.Comparator; @@ -22,7 +20,7 @@ public class PreferenceFuzzySearchFunction final Stream allAvailable, final String keyword ) { - final float maxScore = (keyword.length() + 1) * 3 - 2; // First can't get +2 bonus score + final int maxScore = (keyword.length() + 1) * 3 - 2; // First can't get +2 bonus score return allAvailable // General search @@ -39,14 +37,6 @@ public class PreferenceFuzzySearchFunction .limit(20); } - private float computeFuzzyScore( - @NonNull final PreferenceSearchItem item, - @NonNull final Function resolver, - @NonNull final String keyword - ) { - return FUZZY_SCORE.fuzzyScore(resolver.apply(item), keyword); - } - static class FuzzySearchGeneralDTO { private final PreferenceSearchItem item; private final float score; diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java index 1cf401892..b52daeb5b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java @@ -18,7 +18,7 @@ import java.util.Objects; /** * Parses the corresponding preference-file(s). */ -class PreferenceParser { +public class PreferenceParser { private static final String TAG = "PreferenceParser"; private static final String NS_ANDROID = "http://schemas.android.com/apk/res/android"; @@ -28,7 +28,7 @@ class PreferenceParser { private final Map allPreferences; private final PreferenceSearchConfiguration searchConfiguration; - PreferenceParser( + public PreferenceParser( final Context context, final PreferenceSearchConfiguration searchConfiguration ) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java index 37b98035a..1c9dc99aa 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java @@ -50,7 +50,7 @@ public class PreferenceSearchConfiguration { return item; } - List getFiles() { + public List getFiles() { return itemsToIndex; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java index d9a17da16..1b1d627c1 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java @@ -24,29 +24,15 @@ import java.util.Objects; public class PreferenceSearchFragment extends Fragment { public static final String NAME = PreferenceSearchFragment.class.getSimpleName(); - private final PreferenceSearchConfiguration searchConfiguration; - private final PreferenceSearcher searcher; + private SearchViewHolder viewHolder; private PreferenceSearchAdapter adapter; - public PreferenceSearchFragment(final PreferenceSearchConfiguration searchConfiguration) { - this.searchConfiguration = searchConfiguration; - this.searcher = new PreferenceSearcher(searchConfiguration); - } - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - final PreferenceParser parser = - new PreferenceParser( - getContext(), - searchConfiguration); - - searchConfiguration.getFiles().stream() - .map(parser::parse) - .forEach(searcher::add); + public PreferenceSearchFragment( + final PreferenceSearcher searcher + ) { + this.searcher = searcher; } @Nullable @@ -98,12 +84,6 @@ public class PreferenceSearchFragment extends Fragment { ((PreferenceSearchResultListener) getActivity()).onSearchResultClicked(item); } - @Override - public void onDestroy() { - searcher.close(); - super.onDestroy(); - } - private static class SearchViewHolder { private final RecyclerView recyclerView; private final View emptyStateView; diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java index f9427a1ca..87dd11693 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java @@ -6,16 +6,16 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; -class PreferenceSearcher implements AutoCloseable { +public class PreferenceSearcher { private final List allEntries = new ArrayList<>(); private final PreferenceSearchConfiguration configuration; - PreferenceSearcher(final PreferenceSearchConfiguration configuration) { + public PreferenceSearcher(final PreferenceSearchConfiguration configuration) { this.configuration = configuration; } - void add(final List items) { + public void add(final List items) { allEntries.addAll(items); } @@ -29,8 +29,7 @@ class PreferenceSearcher implements AutoCloseable { .collect(Collectors.toList()); } - @Override - public void close() { + public void clear() { allEntries.clear(); } } From d59314801c035ea1292e5c685437ff0b09982c54 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Mon, 27 Dec 2021 18:57:17 +0100 Subject: [PATCH 12/27] Code rework --- .../preferencesearch/PreferenceParser.java | 4 +-- .../PreferenceSearchItem.java | 33 ++++++++++++------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java index b52daeb5b..afd2c1b4f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java @@ -5,6 +5,7 @@ import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.XmlRes; import androidx.preference.PreferenceManager; import org.xmlpull.v1.XmlPullParser; @@ -111,7 +112,7 @@ public class PreferenceParser { private PreferenceSearchItem parseSearchResult( final XmlPullParser xpp, final String breadcrumbs, - final int searchIndexItemResId + @XmlRes final int searchIndexItemResId ) { final String key = readString(getAttribute(xpp, "key")); final String[] entries = readStringArray(getAttribute(xpp, "entries")); @@ -130,7 +131,6 @@ public class PreferenceParser { entries, entryValues), TextUtils.join(",", entries), - readString(getAttribute(xpp, NS_SEARCH, "keywords")), breadcrumbs, searchIndexItemResId ); diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java index 3030a78bb..52935ef8e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.settings.preferencesearch; import androidx.annotation.NonNull; +import androidx.annotation.XmlRes; import java.util.Arrays; import java.util.List; @@ -10,18 +11,35 @@ import java.util.Objects; * Represents a preference-item inside the search. */ public class PreferenceSearchItem { + /** + * Key of the setting/preference. E.g. used inside {@link android.content.SharedPreferences}. + */ @NonNull private final String key; + /** + * Title of the setting, e.g. 'Default resolution' or 'Show higher resolutions'. + */ @NonNull private final String title; + /** + * Summary of the setting, e.g. '480p' or 'Only some devices can play 2k/4k'. + */ @NonNull private final String summary; + /** + * Possible entries of the setting, e.g. 480p,720p,... + */ @NonNull private final String entries; - @NonNull - private final String keywords; + /** + * Breadcrumbs - a hint where the setting is located e.g. 'Video and Audio > Player' + */ @NonNull private final String breadcrumbs; + /** + * The xml-resource where this item was found/built from. + */ + @XmlRes private final int searchIndexItemResId; public PreferenceSearchItem( @@ -29,15 +47,13 @@ public class PreferenceSearchItem { @NonNull final String title, @NonNull final String summary, @NonNull final String entries, - @NonNull final String keywords, @NonNull final String breadcrumbs, - final int searchIndexItemResId + @XmlRes final int searchIndexItemResId ) { this.key = Objects.requireNonNull(key); this.title = Objects.requireNonNull(title); this.summary = Objects.requireNonNull(summary); this.entries = Objects.requireNonNull(entries); - this.keywords = Objects.requireNonNull(keywords); this.breadcrumbs = Objects.requireNonNull(breadcrumbs); this.searchIndexItemResId = searchIndexItemResId; } @@ -62,10 +78,6 @@ public class PreferenceSearchItem { return breadcrumbs; } - public String getKeywords() { - return keywords; - } - public int getSearchIndexItemResId() { return searchIndexItemResId; } @@ -79,8 +91,7 @@ public class PreferenceSearchItem { getTitle(), getSummary(), getEntries(), - getBreadcrumbs(), - getKeywords()); + getBreadcrumbs()); } From 6b23df06592ccbe52534d43448310125685c9eaa Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Wed, 29 Dec 2021 18:12:33 +0100 Subject: [PATCH 13/27] Made debug settings searchable (debug only) * Consolidated main-setttings into a single file * Debug settings are only enabled in the DEBUG build * Moved LeakCanary (debug) specific stuff into a small class that's only shipped with the debug build * Other minor fixes --- .../settings/DebugSettingsFragment.java | 50 ++++++++++++++++--- .../settings/MainSettingsFragment.java | 12 +++-- .../newpipe/settings/SettingsActivity.java | 7 +++ .../settings/SettingsResourceRegistry.java | 5 +- app/src/main/res/values/strings.xml | 1 + .../{debug => main}/res/xml/main_settings.xml | 0 app/src/release/res/xml/main_settings.xml | 49 ------------------ 7 files changed, 61 insertions(+), 63 deletions(-) rename app/src/{debug => main}/java/org/schabi/newpipe/settings/DebugSettingsFragment.java (54%) rename app/src/{debug => main}/res/xml/main_settings.xml (100%) delete mode 100644 app/src/release/res/xml/main_settings.xml diff --git a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java similarity index 54% rename from app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsFragment.java rename to app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index f48be553f..710a440e1 100644 --- a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.settings; +import android.content.Intent; import android.os.Bundle; import androidx.preference.Preference; @@ -10,13 +11,15 @@ import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.util.PicassoHelper; -import leakcanary.LeakCanary; +import java.util.Optional; public class DebugSettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResource(R.xml.debug_settings); + addPreferencesFromResourceRegistry(); + final Preference allowHeapDumpingPreference + = findPreference(getString(R.string.allow_heap_dumping_key)); final Preference showMemoryLeaksPreference = findPreference(getString(R.string.show_memory_leaks_key)); final Preference showImageIndicatorsPreference @@ -28,16 +31,29 @@ public class DebugSettingsFragment extends BasePreferenceFragment { final Preference createErrorNotificationPreference = findPreference(getString(R.string.create_error_notification_key)); + assert allowHeapDumpingPreference != null; assert showMemoryLeaksPreference != null; assert showImageIndicatorsPreference != null; assert crashTheAppPreference != null; assert showErrorSnackbarPreference != null; assert createErrorNotificationPreference != null; - showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> { - startActivity(LeakCanary.INSTANCE.newLeakDisplayActivityIntent()); - return true; - }); + final Optional optPDLeakCanary = getBVLeakCanary(); + + allowHeapDumpingPreference.setEnabled(optPDLeakCanary.isPresent()); + showMemoryLeaksPreference.setEnabled(optPDLeakCanary.isPresent()); + + if (optPDLeakCanary.isPresent()) { + final DebugSettingsBVLeakCanaryAPI pdLeakCanary = optPDLeakCanary.get(); + + showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> { + startActivity(pdLeakCanary.getNewLeakDisplayActivityIntent()); + return true; + }); + } else { + allowHeapDumpingPreference.setSummary(R.string.leak_canary_not_available); + showMemoryLeaksPreference.setSummary(R.string.leak_canary_not_available); + } showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> { PicassoHelper.setIndicatorsEnabled((Boolean) newValue); @@ -60,4 +76,26 @@ public class DebugSettingsFragment extends BasePreferenceFragment { return true; }); } + + private Optional getBVLeakCanary() { + try { + // Try to find the implementation of the LeakCanary API + return Optional.of((DebugSettingsBVLeakCanaryAPI) + Class.forName(DebugSettingsBVLeakCanaryAPI.IMPL_CLASS).newInstance()); + } catch (final ClassNotFoundException + | IllegalAccessException | java.lang.InstantiationException e) { + return Optional.empty(); + } + } + + /** + * Build variant dependent leak canary API for this fragment. + * Why is LeakCanary not used directly? Because it can't be assured + */ + public interface DebugSettingsBVLeakCanaryAPI { + String IMPL_CLASS = + "org.schabi.newpipe.settings.DebugSettingsBVLeakCanary"; + + Intent getNewLeakDisplayActivityIntent(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java index 6cd165861..d7fb559d6 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -6,7 +6,6 @@ import android.view.MenuInflater; import android.view.MenuItem; import androidx.annotation.NonNull; -import androidx.preference.Preference; import org.schabi.newpipe.App; import org.schabi.newpipe.CheckForNewAppVersion; @@ -26,12 +25,17 @@ public class MainSettingsFragment extends BasePreferenceFragment { // Check if the app is updatable if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) { - final Preference update - = findPreference(getString(R.string.update_pref_screen_key)); - getPreferenceScreen().removePreference(update); + getPreferenceScreen().removePreference( + findPreference(getString(R.string.update_pref_screen_key))); defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply(); } + + // Hide debug preferences in RELEASE build variant + if (!DEBUG) { + getPreferenceScreen().removePreference( + findPreference(getString(R.string.debug_pref_screen_key))); + } } @Override diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 92bc21c9d..25d22dc9f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -217,6 +217,13 @@ public class SettingsActivity extends AppCompatActivity implements .getEntryByPreferencesResId(R.xml.update_settings) .setSearchable(false); } + + // Hide debug preferences in RELEASE build variant + if (DEBUG) { + SettingsResourceRegistry.getInstance() + .getEntryByPreferencesResId(R.xml.debug_settings) + .setSearchable(true); + } } public void setMenuSearchItem(final MenuItem menuSearchItem) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java index c4db9f93d..c4751abea 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java @@ -1,7 +1,6 @@ package org.schabi.newpipe.settings; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.XmlRes; import androidx.fragment.app.Fragment; @@ -34,6 +33,7 @@ public final class SettingsResourceRegistry { add(AppearanceSettingsFragment.class, R.xml.appearance_settings); add(ContentSettingsFragment.class, R.xml.content_settings); + add(DebugSettingsFragment.class, R.xml.debug_settings).setSearchable(false); add(DownloadSettingsFragment.class, R.xml.download_settings); add(HistorySettingsFragment.class, R.xml.history_settings); add(NotificationSettingsFragment.class, R.xml.notification_settings); @@ -51,7 +51,6 @@ public final class SettingsResourceRegistry { return entry; } - @Nullable public SettingRegistryEntry getEntryByFragmentClass( final Class fragmentClass ) { @@ -62,7 +61,6 @@ public final class SettingsResourceRegistry { .orElse(null); } - @Nullable public SettingRegistryEntry getEntryByPreferencesResId(@XmlRes final int preferencesResId) { return registeredEntries.stream() .filter(e -> Objects.equals(e.getPreferencesResId(), preferencesResId)) @@ -78,7 +76,6 @@ public final class SettingsResourceRegistry { return entry.getPreferencesResId(); } - @Nullable public Class getFragmentClass(@XmlRes final int preferencesResId) { final SettingRegistryEntry entry = getEntryByPreferencesResId(preferencesResId); if (entry == null) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7ad41ba8..64ed6980b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -441,6 +441,7 @@ Captions Modify player caption text scale and background styles. Requires app restart to take effect + LeakCanary is not available Memory leak monitoring may cause the app to become unresponsive when heap dumping Show memory leaks Report out-of-lifecycle errors diff --git a/app/src/debug/res/xml/main_settings.xml b/app/src/main/res/xml/main_settings.xml similarity index 100% rename from app/src/debug/res/xml/main_settings.xml rename to app/src/main/res/xml/main_settings.xml diff --git a/app/src/release/res/xml/main_settings.xml b/app/src/release/res/xml/main_settings.xml deleted file mode 100644 index 1d5241102..000000000 --- a/app/src/release/res/xml/main_settings.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - From 658168eb8d97e54ac23f1dfdccb7fd8ec2786035 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Wed, 29 Dec 2021 18:29:46 +0100 Subject: [PATCH 14/27] Fixed some sonar warnings --- .../newpipe/settings/DebugSettingsFragment.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index 710a440e1..915531779 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -11,9 +11,12 @@ import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.util.PicassoHelper; +import java.lang.reflect.InvocationTargetException; import java.util.Optional; public class DebugSettingsFragment extends BasePreferenceFragment { + private static final String DUMMY = "Dummy"; + @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResourceRegistry(); @@ -61,18 +64,18 @@ public class DebugSettingsFragment extends BasePreferenceFragment { }); crashTheAppPreference.setOnPreferenceClickListener(preference -> { - throw new RuntimeException(); + throw new RuntimeException(DUMMY); }); showErrorSnackbarPreference.setOnPreferenceClickListener(preference -> { ErrorUtil.showUiErrorSnackbar(DebugSettingsFragment.this, - "Dummy", new RuntimeException("Dummy")); + DUMMY, new RuntimeException(DUMMY)); return true; }); createErrorNotificationPreference.setOnPreferenceClickListener(preference -> { ErrorUtil.createNotification(requireContext(), - new ErrorInfo(new RuntimeException("Dummy"), UserAction.UI_ERROR, "Dummy")); + new ErrorInfo(new RuntimeException(DUMMY), UserAction.UI_ERROR, DUMMY)); return true; }); } @@ -81,9 +84,10 @@ public class DebugSettingsFragment extends BasePreferenceFragment { try { // Try to find the implementation of the LeakCanary API return Optional.of((DebugSettingsBVLeakCanaryAPI) - Class.forName(DebugSettingsBVLeakCanaryAPI.IMPL_CLASS).newInstance()); - } catch (final ClassNotFoundException - | IllegalAccessException | java.lang.InstantiationException e) { + Class.forName(DebugSettingsBVLeakCanaryAPI.IMPL_CLASS) + .getDeclaredConstructor() + .newInstance()); + } catch (final Exception e) { return Optional.empty(); } } From bebd2b449cbf59bfe721a2c4b825eac9243051c0 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Wed, 29 Dec 2021 18:46:45 +0100 Subject: [PATCH 15/27] Removed unused import --- .../java/org/schabi/newpipe/settings/DebugSettingsFragment.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index 915531779..a42d47313 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -11,7 +11,6 @@ import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.util.PicassoHelper; -import java.lang.reflect.InvocationTargetException; import java.util.Optional; public class DebugSettingsFragment extends BasePreferenceFragment { From c5a06243a6ed6982bcac663f40543df820803402 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Wed, 29 Dec 2021 18:49:37 +0100 Subject: [PATCH 16/27] Fixed variable name --- .../schabi/newpipe/settings/DebugSettingsFragment.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index a42d47313..3e28a6065 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -40,13 +40,13 @@ public class DebugSettingsFragment extends BasePreferenceFragment { assert showErrorSnackbarPreference != null; assert createErrorNotificationPreference != null; - final Optional optPDLeakCanary = getBVLeakCanary(); + final Optional optBVLeakCanary = getBVLeakCanary(); - allowHeapDumpingPreference.setEnabled(optPDLeakCanary.isPresent()); - showMemoryLeaksPreference.setEnabled(optPDLeakCanary.isPresent()); + allowHeapDumpingPreference.setEnabled(optBVLeakCanary.isPresent()); + showMemoryLeaksPreference.setEnabled(optBVLeakCanary.isPresent()); - if (optPDLeakCanary.isPresent()) { - final DebugSettingsBVLeakCanaryAPI pdLeakCanary = optPDLeakCanary.get(); + if (optBVLeakCanary.isPresent()) { + final DebugSettingsBVLeakCanaryAPI pdLeakCanary = optBVLeakCanary.get(); showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> { startActivity(pdLeakCanary.getNewLeakDisplayActivityIntent()); From 8bbc3e531c591539ab89ceac42c94586c130a94a Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Wed, 29 Dec 2021 22:17:30 +0100 Subject: [PATCH 17/27] Fixed gitignore and commited missing file --- .gitignore | 4 ++-- .../settings/DebugSettingsBVLeakCanary.java | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVLeakCanary.java diff --git a/.gitignore b/.gitignore index 40e7d2c03..1352b6917 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,8 @@ captures/ *~ .weblate *.class -**/debug/ -**/release/ +app/debug/ +app/release/ # vscode / eclipse files *.classpath diff --git a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVLeakCanary.java b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVLeakCanary.java new file mode 100644 index 000000000..f8fd7f969 --- /dev/null +++ b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVLeakCanary.java @@ -0,0 +1,15 @@ +package org.schabi.newpipe.settings; + +import android.content.Intent; + +import leakcanary.LeakCanary; + +@SuppressWarnings("unused") // Class is used but loaded via reflection +public class DebugSettingsBVLeakCanary + implements DebugSettingsFragment.DebugSettingsBVLeakCanaryAPI { + + @Override + public Intent getNewLeakDisplayActivityIntent() { + return LeakCanary.INSTANCE.newLeakDisplayActivityIntent(); + } +} From ce4dd33eab241ed79233a44706fb0ce00ba24dd1 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 31 Dec 2021 14:50:27 +0100 Subject: [PATCH 18/27] Fixed problems with Android's lifecycle (restore) --- .../newpipe/settings/SettingsActivity.java | 84 +++++++++++++++---- .../PreferenceSearchFragment.java | 8 +- 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 25d22dc9f..4dfbf6c08 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.settings; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import android.os.Bundle; +import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -40,6 +41,9 @@ import org.schabi.newpipe.views.FocusOverlayView; import java.util.concurrent.TimeUnit; +import icepick.Icepick; +import icepick.State; + /* * Created by Christian Schabesberger on 31.08.15. * @@ -77,20 +81,37 @@ public class SettingsActivity extends AppCompatActivity implements private View searchContainer; private EditText searchEditText; + // State + @State + String searchText; + @State + boolean wasSearchActive; + @Override protected void onCreate(final Bundle savedInstanceBundle) { setTheme(ThemeHelper.getSettingsThemeStyle(this)); assureCorrectAppLanguage(this); + super.onCreate(savedInstanceBundle); + Icepick.restoreInstanceState(this, savedInstanceBundle); + final boolean restored = savedInstanceBundle != null; final SettingsLayoutBinding settingsLayoutBinding = SettingsLayoutBinding.inflate(getLayoutInflater()); setContentView(settingsLayoutBinding.getRoot()); - initSearch(settingsLayoutBinding); + initSearch(settingsLayoutBinding, restored); setSupportActionBar(settingsLayoutBinding.settingsToolbarLayout.toolbar); - if (savedInstanceBundle == null) { + if (restored) { + // Restore state + if (this.wasSearchActive) { + setSearchActive(true); + if (!TextUtils.isEmpty(this.searchText)) { + this.searchEditText.setText(this.searchText); + } + } + } else { getSupportFragmentManager().beginTransaction() .replace(R.id.settings_fragment_holder, new MainSettingsFragment()) .commit(); @@ -101,6 +122,12 @@ public class SettingsActivity extends AppCompatActivity implements } } + @Override + protected void onSaveInstanceState(@NonNull final Bundle outState) { + super.onSaveInstanceState(outState); + Icepick.saveInstanceState(this, outState); + } + @Override public boolean onCreateOptionsMenu(final Menu menu) { final ActionBar actionBar = getSupportActionBar(); @@ -175,7 +202,10 @@ public class SettingsActivity extends AppCompatActivity implements //////////////////////////////////////////////////////////////////////////*/ //region Search - private void initSearch(final SettingsLayoutBinding settingsLayoutBinding) { + private void initSearch( + final SettingsLayoutBinding settingsLayoutBinding, + final boolean restored + ) { searchContainer = settingsLayoutBinding.settingsToolbarLayout.toolbar .findViewById(R.id.toolbar_search_container); @@ -207,7 +237,19 @@ public class SettingsActivity extends AppCompatActivity implements .map(parser::parse) .forEach(searcher::add); - searchFragment = new PreferenceSearchFragment(searcher); + if (restored) { + searchFragment = (PreferenceSearchFragment) getSupportFragmentManager() + .findFragmentByTag(PreferenceSearchFragment.NAME); + if (searchFragment != null) { + // Hide/Remove the search fragment otherwise we get an exception + // when adding it (because it's already present) + hideSearchFragment(); + } + } + if (searchFragment == null) { + searchFragment = new PreferenceSearchFragment(); + } + searchFragment.setSearcher(searcher); } private void prepareSearchConfig() { @@ -228,36 +270,45 @@ public class SettingsActivity extends AppCompatActivity implements public void setMenuSearchItem(final MenuItem menuSearchItem) { this.menuSearchItem = menuSearchItem; + + // Ensure that the item is in the correct state when adding it. This is due to + // Android's lifecycle (the Activity is recreated before the Fragment that registers this) + if (menuSearchItem != null) { + menuSearchItem.setVisible(!isSearchActive()); + } } public void setSearchActive(final boolean active) { + if (DEBUG) { + Log.d(TAG, "setSearchActive called active=" + active); + } + // Ignore if search is already in correct state if (isSearchActive() == active) { return; } - if (DEBUG) { - Log.d(TAG, "setSearchActive called active=" + active); - } + wasSearchActive = active; searchContainer.setVisibility(active ? View.VISIBLE : View.GONE); if (menuSearchItem != null) { menuSearchItem.setVisible(!active); } - final FragmentManager fm = getSupportFragmentManager(); if (active) { - fm.beginTransaction() + getSupportFragmentManager() + .beginTransaction() .add(FRAGMENT_HOLDER_ID, searchFragment, PreferenceSearchFragment.NAME) .addToBackStack(PreferenceSearchFragment.NAME) .commit(); KeyboardUtil.showKeyboard(this, searchEditText); } else if (searchFragment != null) { - fm.beginTransaction().remove(searchFragment).commit(); - fm.popBackStack( - PreferenceSearchFragment.NAME, - FragmentManager.POP_BACK_STACK_INCLUSIVE); + hideSearchFragment(); + getSupportFragmentManager() + .popBackStack( + PreferenceSearchFragment.NAME, + FragmentManager.POP_BACK_STACK_INCLUSIVE); KeyboardUtil.hideKeyboard(this, searchEditText); } @@ -265,6 +316,10 @@ public class SettingsActivity extends AppCompatActivity implements resetSearchText(); } + private void hideSearchFragment() { + getSupportFragmentManager().beginTransaction().remove(searchFragment).commit(); + } + private void resetSearchText() { searchEditText.setText(""); } @@ -279,7 +334,8 @@ public class SettingsActivity extends AppCompatActivity implements } if (searchFragment != null) { - searchFragment.updateSearchResults(this.searchEditText.getText().toString()); + searchText = this.searchEditText.getText().toString(); + searchFragment.updateSearchResults(searchText); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java index 1b1d627c1..38c87ea76 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java @@ -24,14 +24,12 @@ import java.util.Objects; public class PreferenceSearchFragment extends Fragment { public static final String NAME = PreferenceSearchFragment.class.getSimpleName(); - private final PreferenceSearcher searcher; + private PreferenceSearcher searcher; private SearchViewHolder viewHolder; private PreferenceSearchAdapter adapter; - public PreferenceSearchFragment( - final PreferenceSearcher searcher - ) { + public void setSearcher(final PreferenceSearcher searcher) { this.searcher = searcher; } @@ -56,7 +54,7 @@ public class PreferenceSearchFragment extends Fragment { } public void updateSearchResults(final String keyword) { - if (adapter == null) { + if (adapter == null || searcher == null) { return; } From 9b2c86a37be7ce57f5a26dd222f854be0479a771 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 31 Dec 2021 16:14:01 +0100 Subject: [PATCH 19/27] Improved documentation --- .../settings/DebugSettingsBVDLeakCanary.java | 20 +++++++++++++++++++ .../settings/DebugSettingsBVLeakCanary.java | 15 -------------- .../settings/DebugSettingsFragment.java | 20 +++++++++++-------- 3 files changed, 32 insertions(+), 23 deletions(-) create mode 100644 app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.java delete mode 100644 app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVLeakCanary.java diff --git a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.java b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.java new file mode 100644 index 000000000..a2d65f6f4 --- /dev/null +++ b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.java @@ -0,0 +1,20 @@ +package org.schabi.newpipe.settings; + +import android.content.Intent; + +import leakcanary.LeakCanary; + +/** + * Build variant dependent (BVD) leak canary API implementation for the debug settings fragment. + * This class is loaded via reflection by + * {@link DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI}. + */ +@SuppressWarnings("unused") // Class is used but loaded via reflection +public class DebugSettingsBVDLeakCanary + implements DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI { + + @Override + public Intent getNewLeakDisplayActivityIntent() { + return LeakCanary.INSTANCE.newLeakDisplayActivityIntent(); + } +} diff --git a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVLeakCanary.java b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVLeakCanary.java deleted file mode 100644 index f8fd7f969..000000000 --- a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVLeakCanary.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.schabi.newpipe.settings; - -import android.content.Intent; - -import leakcanary.LeakCanary; - -@SuppressWarnings("unused") // Class is used but loaded via reflection -public class DebugSettingsBVLeakCanary - implements DebugSettingsFragment.DebugSettingsBVLeakCanaryAPI { - - @Override - public Intent getNewLeakDisplayActivityIntent() { - return LeakCanary.INSTANCE.newLeakDisplayActivityIntent(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index 3e28a6065..395c7c0f0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -40,13 +40,13 @@ public class DebugSettingsFragment extends BasePreferenceFragment { assert showErrorSnackbarPreference != null; assert createErrorNotificationPreference != null; - final Optional optBVLeakCanary = getBVLeakCanary(); + final Optional optBVLeakCanary = getBVDLeakCanary(); allowHeapDumpingPreference.setEnabled(optBVLeakCanary.isPresent()); showMemoryLeaksPreference.setEnabled(optBVLeakCanary.isPresent()); if (optBVLeakCanary.isPresent()) { - final DebugSettingsBVLeakCanaryAPI pdLeakCanary = optBVLeakCanary.get(); + final DebugSettingsBVDLeakCanaryAPI pdLeakCanary = optBVLeakCanary.get(); showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> { startActivity(pdLeakCanary.getNewLeakDisplayActivityIntent()); @@ -79,11 +79,15 @@ public class DebugSettingsFragment extends BasePreferenceFragment { }); } - private Optional getBVLeakCanary() { + /** + * Tries to find the {@link DebugSettingsBVDLeakCanaryAPI#IMPL_CLASS} and loads it if available. + * @return An {@link Optional} which is empty if the implementation class couldn't be loaded. + */ + private Optional getBVDLeakCanary() { try { // Try to find the implementation of the LeakCanary API - return Optional.of((DebugSettingsBVLeakCanaryAPI) - Class.forName(DebugSettingsBVLeakCanaryAPI.IMPL_CLASS) + return Optional.of((DebugSettingsBVDLeakCanaryAPI) + Class.forName(DebugSettingsBVDLeakCanaryAPI.IMPL_CLASS) .getDeclaredConstructor() .newInstance()); } catch (final Exception e) { @@ -92,12 +96,12 @@ public class DebugSettingsFragment extends BasePreferenceFragment { } /** - * Build variant dependent leak canary API for this fragment. + * Build variant dependent (BVD) leak canary API for this fragment. * Why is LeakCanary not used directly? Because it can't be assured */ - public interface DebugSettingsBVLeakCanaryAPI { + public interface DebugSettingsBVDLeakCanaryAPI { String IMPL_CLASS = - "org.schabi.newpipe.settings.DebugSettingsBVLeakCanary"; + "org.schabi.newpipe.settings.DebugSettingsBVDLeakCanary"; Intent getNewLeakDisplayActivityIntent(); } From 37cd71328c943e7ccc88baa8ea17ff650d3c922a Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 9 Jan 2022 14:38:36 +0100 Subject: [PATCH 20/27] Moved ``FuzzyScore`` to original Apache package --- .../commons/text}/similarity/FuzzyScore.java | 2 +- .../preferencesearch/PreferenceFuzzySearchFunction.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/src/main/java/org/{schabi/newpipe/settings/preferencesearch => apache/commons/text}/similarity/FuzzyScore.java (98%) diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/similarity/FuzzyScore.java b/app/src/main/java/org/apache/commons/text/similarity/FuzzyScore.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/settings/preferencesearch/similarity/FuzzyScore.java rename to app/src/main/java/org/apache/commons/text/similarity/FuzzyScore.java index 4ab6bf60a..bbab7fd78 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/similarity/FuzzyScore.java +++ b/app/src/main/java/org/apache/commons/text/similarity/FuzzyScore.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.schabi.newpipe.settings.preferencesearch.similarity; +package org.apache.commons.text.similarity; import java.util.Locale; diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java index 88ef790c8..7c231cafb 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java @@ -2,7 +2,7 @@ package org.schabi.newpipe.settings.preferencesearch; import android.text.TextUtils; -import org.schabi.newpipe.settings.preferencesearch.similarity.FuzzyScore; +import org.apache.commons.text.similarity.FuzzyScore; import java.util.Comparator; import java.util.Locale; From b16e972710e0bac6ce8cf7c9f56b3ef64a89ed7b Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 9 Jan 2022 14:58:10 +0100 Subject: [PATCH 21/27] Improved doc --- .../preferencesearch/PreferenceSearchResultHighlighter.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java index e48ca28ce..418a3ea46 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java @@ -29,9 +29,11 @@ public final class PreferenceSearchResultHighlighter { /** * Highlight the specified preference. + *
+ * Note: This function is Thread independent (can be called from outside of the main thread). * - * @param item - * @param prefsFragment + * @param item The item to highlight + * @param prefsFragment The fragment where the items is located on */ public static void highlight( final PreferenceSearchItem item, From e2f449f0c8782e19ae2a9cf64936931b9e03a578 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 9 Jan 2022 15:22:05 +0100 Subject: [PATCH 22/27] Code improvements * Renamed methods so that they are more understandable * Removed ``SearchIndexItem`` --- .../newpipe/settings/SettingsActivity.java | 25 +++-- .../preferencesearch/PreferenceParser.java | 13 +-- .../PreferenceSearchConfiguration.java | 99 +------------------ .../preferencesearch/PreferenceSearcher.java | 2 +- 4 files changed, 24 insertions(+), 115 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 4dfbf6c08..7d56863eb 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -171,6 +171,7 @@ public class SettingsActivity extends AppCompatActivity implements @Override public boolean onPreferenceStartFragment(final PreferenceFragmentCompat caller, final Preference preference) { + preference.getExtras() showSettingsFragment(instantiateFragment(preference.getFragment())); return true; } @@ -221,20 +222,24 @@ public class SettingsActivity extends AppCompatActivity implements searchContainer.findViewById(R.id.toolbar_search_clear) .setOnClickListener(ev -> resetSearchText()); - prepareSearchConfig(); + ensureSearchRepresentsApplicationState(); // Build search configuration using SettingsResourceRegistry final PreferenceSearchConfiguration config = new PreferenceSearchConfiguration(); - SettingsResourceRegistry.getInstance().getAllEntries().stream() - .filter(SettingsResourceRegistry.SettingRegistryEntry::isSearchable) - .map(SettingsResourceRegistry.SettingRegistryEntry::getPreferencesResId) - .forEach(config::index); + // Build search items final PreferenceParser parser = new PreferenceParser(getApplicationContext(), config); final PreferenceSearcher searcher = new PreferenceSearcher(config); - config.getFiles().stream() + + // Find all searchable SettingsResourceRegistry fragments + SettingsResourceRegistry.getInstance().getAllEntries().stream() + .filter(SettingsResourceRegistry.SettingRegistryEntry::isSearchable) + // Get the resId + .map(SettingsResourceRegistry.SettingRegistryEntry::getPreferencesResId) + // Parse .map(parser::parse) + // Add it to the searcher .forEach(searcher::add); if (restored) { @@ -252,7 +257,13 @@ public class SettingsActivity extends AppCompatActivity implements searchFragment.setSearcher(searcher); } - private void prepareSearchConfig() { + /** + * Ensures that the search shows the correct/available search results. + *
+ * Some features are e.g. only available for debug builds, these should not + * be found when searching inside a release. + */ + private void ensureSearchRepresentsApplicationState() { // Check if the update settings are available if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) { SettingsResourceRegistry.getInstance() diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java index afd2c1b4f..ccddee97b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java @@ -39,27 +39,22 @@ public class PreferenceParser { } public List parse( - final PreferenceSearchConfiguration.SearchIndexItem item + @XmlRes final int resId ) { - Objects.requireNonNull(item, "item can't be null"); - final List results = new ArrayList<>(); - final XmlPullParser xpp = context.getResources().getXml(item.getResId()); + final XmlPullParser xpp = context.getResources().getXml(resId); try { xpp.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); xpp.setFeature(XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES, true); final List breadcrumbs = new ArrayList<>(); - if (!TextUtils.isEmpty(item.getBreadcrumb())) { - breadcrumbs.add(item.getBreadcrumb()); - } while (xpp.getEventType() != XmlPullParser.END_DOCUMENT) { if (xpp.getEventType() == XmlPullParser.START_TAG) { final PreferenceSearchItem result = parseSearchResult( xpp, joinBreadcrumbs(breadcrumbs), - item.getResId() + resId ); if (!searchConfiguration.getParserIgnoreElements().contains(xpp.getName()) @@ -79,7 +74,7 @@ public class PreferenceParser { xpp.next(); } } catch (final Exception e) { - Log.w(TAG, "Failed to parse resid=" + item.getResId(), e); + Log.w(TAG, "Failed to parse resid=" + resId, e); } return results; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java index 1c9dc99aa..3d49140fb 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java @@ -1,14 +1,10 @@ package org.schabi.newpipe.settings.preferencesearch; -import android.os.Parcel; -import android.os.Parcelable; import android.text.TextUtils; -import androidx.annotation.XmlRes; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -16,8 +12,6 @@ import java.util.function.BinaryOperator; import java.util.stream.Stream; public class PreferenceSearchConfiguration { - private final ArrayList itemsToIndex = new ArrayList<>(); - private BinaryOperator breadcrumbConcat = (s1, s2) -> TextUtils.isEmpty(s1) ? s2 : (s1 + " > " + s2); @@ -38,27 +32,11 @@ public class PreferenceSearchConfiguration { this.searcher = Objects.requireNonNull(searcher); } - /** - * Adds a new file to the index. - * - * @param resId The preference file to index - * @return SearchIndexItem - */ - public SearchIndexItem index(@XmlRes final int resId) { - final SearchIndexItem item = new SearchIndexItem(resId, this); - itemsToIndex.add(item); - return item; - } - - public List getFiles() { - return itemsToIndex; - } - public BinaryOperator getBreadcrumbConcat() { return breadcrumbConcat; } - public PreferenceSearchFunction getSearchMatcher() { + public PreferenceSearchFunction getSearcher() { return searcher; } @@ -70,81 +48,6 @@ public class PreferenceSearchConfiguration { return parserContainerElements; } - /** - * Adds a given R.xml resource to the search index. - */ - public static final class SearchIndexItem implements Parcelable { - private String breadcrumb = ""; - @XmlRes - private final int resId; - private final PreferenceSearchConfiguration searchConfiguration; - - /** - * Includes the given R.xml resource in the index. - * - * @param resId The resource to index - * @param searchConfiguration The configuration for the search - */ - private SearchIndexItem( - @XmlRes final int resId, - final PreferenceSearchConfiguration searchConfiguration - ) { - this.resId = resId; - this.searchConfiguration = searchConfiguration; - } - - /** - * Adds a breadcrumb. - * - * @param breadcrumb The breadcrumb to add - * @return For chaining - */ - @SuppressWarnings("HiddenField") - public SearchIndexItem withBreadcrumb(final String breadcrumb) { - this.breadcrumb = - searchConfiguration.getBreadcrumbConcat().apply(this.breadcrumb, breadcrumb); - return this; - } - - @XmlRes - int getResId() { - return resId; - } - - String getBreadcrumb() { - return breadcrumb; - } - - public static final Creator CREATOR = new Creator<>() { - @Override - public SearchIndexItem createFromParcel(final Parcel in) { - return new SearchIndexItem(in); - } - - @Override - public SearchIndexItem[] newArray(final int size) { - return new SearchIndexItem[size]; - } - }; - - private SearchIndexItem(final Parcel parcel) { - this.breadcrumb = parcel.readString(); - this.resId = parcel.readInt(); - this.searchConfiguration = null; - } - - @Override - public void writeToParcel(final Parcel dest, final int flags) { - dest.writeString(this.breadcrumb); - dest.writeInt(this.resId); - } - - @Override - public int describeContents() { - return 0; - } - } - @FunctionalInterface public interface PreferenceSearchFunction { Stream search( diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java index 87dd11693..176dc5d14 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java @@ -24,7 +24,7 @@ public class PreferenceSearcher { return new ArrayList<>(); } - return configuration.getSearchMatcher() + return configuration.getSearcher() .search(allEntries.stream(), keyword) .collect(Collectors.toList()); } From 03bb2123f24a9ef874d508db94fcd779526c8c88 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 9 Jan 2022 16:49:16 +0100 Subject: [PATCH 23/27] Removed breadcrumbs customization --- .../org/schabi/newpipe/settings/SettingsActivity.java | 1 - .../settings/preferencesearch/PreferenceParser.java | 3 ++- .../PreferenceSearchConfiguration.java | 11 ----------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 7d56863eb..3872e5172 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -171,7 +171,6 @@ public class SettingsActivity extends AppCompatActivity implements @Override public boolean onPreferenceStartFragment(final PreferenceFragmentCompat caller, final Preference preference) { - preference.getExtras() showSettingsFragment(instantiateFragment(preference.getFragment())); return true; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java index ccddee97b..77136e08d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java @@ -15,6 +15,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; /** * Parses the corresponding preference-file(s). @@ -82,7 +83,7 @@ public class PreferenceParser { private String joinBreadcrumbs(final List breadcrumbs) { return breadcrumbs.stream() .filter(crumb -> !TextUtils.isEmpty(crumb)) - .reduce("", searchConfiguration.getBreadcrumbConcat()); + .collect(Collectors.joining(" > ")); } private String getAttribute( diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java index 3d49140fb..50371b78b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java @@ -12,9 +12,6 @@ import java.util.function.BinaryOperator; import java.util.stream.Stream; public class PreferenceSearchConfiguration { - private BinaryOperator breadcrumbConcat = - (s1, s2) -> TextUtils.isEmpty(s1) ? s2 : (s1 + " > " + s2); - private PreferenceSearchFunction searcher = new PreferenceFuzzySearchFunction(); private final List parserIgnoreElements = Arrays.asList( @@ -24,18 +21,10 @@ public class PreferenceSearchConfiguration { PreferenceScreen.class.getSimpleName()); - public void setBreadcrumbConcat(final BinaryOperator breadcrumbConcat) { - this.breadcrumbConcat = Objects.requireNonNull(breadcrumbConcat); - } - public void setSearcher(final PreferenceSearchFunction searcher) { this.searcher = Objects.requireNonNull(searcher); } - public BinaryOperator getBreadcrumbConcat() { - return breadcrumbConcat; - } - public PreferenceSearchFunction getSearcher() { return searcher; } From 7067ebdd128f525e039b3bd3942a4bdcf73c1980 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 9 Jan 2022 16:53:36 +0100 Subject: [PATCH 24/27] Fixed imports --- .../newpipe/settings/preferencesearch/PreferenceParser.java | 4 +++- .../preferencesearch/PreferenceSearchConfiguration.java | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java index 77136e08d..1f507c7f1 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.settings.preferencesearch; import android.content.Context; import android.text.TextUtils; import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.XmlRes; @@ -14,7 +15,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; /** @@ -64,6 +64,8 @@ public class PreferenceParser { results.add(result); } if (searchConfiguration.getParserContainerElements().contains(xpp.getName())) { + // This code adds breadcrumbs for certain containers (e.g. PreferenceScreen) + // Example: Video and Audio > Player breadcrumbs.add(result.getTitle() == null ? "" : result.getTitle()); } } else if (xpp.getEventType() == XmlPullParser.END_TAG diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java index 50371b78b..5835dcab5 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java @@ -1,14 +1,11 @@ package org.schabi.newpipe.settings.preferencesearch; -import android.text.TextUtils; - import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.function.BinaryOperator; import java.util.stream.Stream; public class PreferenceSearchConfiguration { From f55e8ea3aad21b2897e7bcecb84bb3d077d514b3 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 9 Jan 2022 17:40:16 +0100 Subject: [PATCH 25/27] Use ViewBinding --- .../PreferenceSearchFragment.java | 31 ++++++------------- .../settings_preferencesearch_fragment.xml | 2 +- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java index 38c87ea76..f0944876e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java @@ -10,13 +10,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding; import java.util.ArrayList; import java.util.List; -import java.util.Objects; /** * Displays the search results. @@ -26,7 +24,7 @@ public class PreferenceSearchFragment extends Fragment { private PreferenceSearcher searcher; - private SearchViewHolder viewHolder; + private SettingsPreferencesearchFragmentBinding binding; private PreferenceSearchAdapter adapter; public void setSearcher(final PreferenceSearcher searcher) { @@ -40,17 +38,16 @@ public class PreferenceSearchFragment extends Fragment { @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState ) { - final View rootView = - inflater.inflate(R.layout.settings_preferencesearch_fragment, container, false); + // SettingsPreferenceSearchFragmentBinding. + binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false); - viewHolder = new SearchViewHolder(rootView); - viewHolder.recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext())); adapter = new PreferenceSearchAdapter(); adapter.setOnItemClickListener(this::onItemClicked); - viewHolder.recyclerView.setAdapter(adapter); + binding.searchResults.setAdapter(adapter); - return rootView; + return binding.getRoot(); } public void updateSearchResults(final String keyword) { @@ -69,8 +66,8 @@ public class PreferenceSearchFragment extends Fragment { } private void setEmptyViewShown(final boolean shown) { - viewHolder.emptyStateView.setVisibility(shown ? View.VISIBLE : View.GONE); - viewHolder.recyclerView.setVisibility(shown ? View.GONE : View.VISIBLE); + binding.emptyStateView.setVisibility(shown ? View.VISIBLE : View.GONE); + binding.searchResults.setVisibility(shown ? View.GONE : View.VISIBLE); } public void onItemClicked(final PreferenceSearchItem item) { @@ -81,14 +78,4 @@ public class PreferenceSearchFragment extends Fragment { ((PreferenceSearchResultListener) getActivity()).onSearchResultClicked(item); } - - private static class SearchViewHolder { - private final RecyclerView recyclerView; - private final View emptyStateView; - - SearchViewHolder(final View root) { - recyclerView = Objects.requireNonNull(root.findViewById(R.id.list)); - emptyStateView = Objects.requireNonNull(root.findViewById(R.id.empty_state_view)); - } - } } diff --git a/app/src/main/res/layout/settings_preferencesearch_fragment.xml b/app/src/main/res/layout/settings_preferencesearch_fragment.xml index b8aaa60f6..89a25b217 100644 --- a/app/src/main/res/layout/settings_preferencesearch_fragment.xml +++ b/app/src/main/res/layout/settings_preferencesearch_fragment.xml @@ -41,7 +41,7 @@ Date: Mon, 24 Jan 2022 21:08:06 +0100 Subject: [PATCH 26/27] Use view binding inside ``PreferenceViewHolder`` --- .../PreferenceSearchAdapter.java | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java index 527a4a595..02fbf9577 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java @@ -4,12 +4,11 @@ import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.SettingsPreferencesearchListItemResultBinding; import java.util.ArrayList; import java.util.List; @@ -22,37 +21,38 @@ class PreferenceSearchAdapter @NonNull @Override - public PreferenceSearchAdapter.PreferenceViewHolder onCreateViewHolder( + public PreferenceViewHolder onCreateViewHolder( @NonNull final ViewGroup parent, final int viewType ) { return new PreferenceViewHolder( - LayoutInflater - .from(parent.getContext()) - .inflate(R.layout.settings_preferencesearch_list_item_result, parent, false)); + SettingsPreferencesearchListItemResultBinding.inflate( + LayoutInflater.from(parent.getContext()), + parent, + false)); } @Override public void onBindViewHolder( - @NonNull final PreferenceSearchAdapter.PreferenceViewHolder holder, + @NonNull final PreferenceViewHolder holder, final int position ) { final PreferenceSearchItem item = dataset.get(position); - holder.title.setText(item.getTitle()); + holder.binding.title.setText(item.getTitle()); if (TextUtils.isEmpty(item.getSummary())) { - holder.summary.setVisibility(View.GONE); + holder.binding.summary.setVisibility(View.GONE); } else { - holder.summary.setVisibility(View.VISIBLE); - holder.summary.setText(item.getSummary()); + holder.binding.summary.setVisibility(View.VISIBLE); + holder.binding.summary.setText(item.getSummary()); } if (TextUtils.isEmpty(item.getBreadcrumbs())) { - holder.breadcrumbs.setVisibility(View.GONE); + holder.binding.breadcrumbs.setVisibility(View.GONE); } else { - holder.breadcrumbs.setVisibility(View.VISIBLE); - holder.breadcrumbs.setText(item.getBreadcrumbs()); + holder.binding.breadcrumbs.setVisibility(View.VISIBLE); + holder.binding.breadcrumbs.setText(item.getBreadcrumbs()); } holder.itemView.setOnClickListener(v -> { @@ -77,15 +77,11 @@ class PreferenceSearchAdapter } static class PreferenceViewHolder extends RecyclerView.ViewHolder { - final TextView title; - final TextView summary; - final TextView breadcrumbs; + final SettingsPreferencesearchListItemResultBinding binding; - PreferenceViewHolder(final View v) { - super(v); - title = v.findViewById(R.id.title); - summary = v.findViewById(R.id.summary); - breadcrumbs = v.findViewById(R.id.breadcrumbs); + PreferenceViewHolder(final SettingsPreferencesearchListItemResultBinding binding) { + super(binding.getRoot()); + this.binding = binding; } } } From 8a069b497f88c8f9f4c236e57131da98f49cffad Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Tue, 25 Jan 2022 20:47:53 +0100 Subject: [PATCH 27/27] Code cleanup Co-authored-by: Stypox --- .../settings/preferencesearch/PreferenceSearchFragment.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java index f0944876e..308abbc4e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java @@ -38,7 +38,6 @@ public class PreferenceSearchFragment extends Fragment { @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState ) { - // SettingsPreferenceSearchFragmentBinding. binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false); binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext()));