From 12a78a826d97a3f92a0ba06fd460ad2530f3d799 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Dec 2021 21:33:40 +0100 Subject: [PATCH] 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;