mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-30 14:52:59 +00:00 
			
		
		
		
	Added preference search "framework"
This commit is contained in:
		| @@ -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<String, ?> 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<PreferenceSearchItem> parse( | ||||
|             final PreferenceSearchConfiguration.SearchIndexItem item | ||||
|     ) { | ||||
|         Objects.requireNonNull(item, "item can't be null"); | ||||
|  | ||||
|         final List<PreferenceSearchItem> 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<String> 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<String> 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()); | ||||
|     } | ||||
| } | ||||
| @@ -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<PreferenceSearchAdapter.PreferenceViewHolder> { | ||||
|     private List<PreferenceSearchItem> dataset = new ArrayList<>(); | ||||
|     private Consumer<PreferenceSearchItem> 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<PreferenceSearchItem> items) { | ||||
|         dataset = new ArrayList<>(items); | ||||
|         this.notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int getItemCount() { | ||||
|         return dataset.size(); | ||||
|     } | ||||
|  | ||||
|     void setOnItemClickListener(final Consumer<PreferenceSearchItem> 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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<SearchIndexItem> itemsToIndex = new ArrayList<>(); | ||||
|  | ||||
|     private BinaryOperator<String> 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<String> parserIgnoreElements = Arrays.asList( | ||||
|             PreferenceCategory.class.getSimpleName()); | ||||
|     private final List<String> parserContainerElements = Arrays.asList( | ||||
|             PreferenceCategory.class.getSimpleName(), | ||||
|             PreferenceScreen.class.getSimpleName()); | ||||
|  | ||||
|  | ||||
|     public void setBreadcrumbConcat(final BinaryOperator<String> 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<SearchIndexItem> getFiles() { | ||||
|         return itemsToIndex; | ||||
|     } | ||||
|  | ||||
|     public BinaryOperator<String> getBreadcrumbConcat() { | ||||
|         return breadcrumbConcat; | ||||
|     } | ||||
|  | ||||
|     public PreferenceSearchFunction getSearchMatcher() { | ||||
|         return searcher; | ||||
|     } | ||||
|  | ||||
|     public List<String> getParserIgnoreElements() { | ||||
|         return parserIgnoreElements; | ||||
|     } | ||||
|  | ||||
|     public List<String> 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<SearchIndexItem> 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<PreferenceSearchItem> search( | ||||
|                 Stream<PreferenceSearchItem> allAvailable, | ||||
|                 String keyword); | ||||
|     } | ||||
| } | ||||
| @@ -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<PreferenceSearchItem> 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)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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<String> getAllRelevantSearchFields() { | ||||
|         return Arrays.asList( | ||||
|             getTitle(), | ||||
|             getSummary(), | ||||
|             getEntries(), | ||||
|             getBreadcrumbs(), | ||||
|             getKeywords()); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     @Override | ||||
|     public String toString() { | ||||
|         return "PreferenceItem: " + title + " " + summary + " " + key; | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| package org.schabi.newpipe.settings.preferencesearch; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
|  | ||||
| public interface PreferenceSearchResultListener { | ||||
|     void onSearchResultClicked(@NonNull PreferenceSearchItem result); | ||||
| } | ||||
| @@ -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<PreferenceSearchItem> allEntries = new ArrayList<>(); | ||||
|  | ||||
|     private final PreferenceSearchConfiguration configuration; | ||||
|  | ||||
|     PreferenceSearcher(final PreferenceSearchConfiguration configuration) { | ||||
|         this.configuration = configuration; | ||||
|     } | ||||
|  | ||||
|     void add(final List<PreferenceSearchItem> items) { | ||||
|         allEntries.addAll(items); | ||||
|     } | ||||
|  | ||||
|     List<PreferenceSearchItem> 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(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,10 @@ | ||||
| /** | ||||
|  * Contains classes for searching inside the preferences. | ||||
|  * <br/> | ||||
|  * This code is based on | ||||
|  * <a href="https://github.com/ByteHamster/SearchPreference">ByteHamster/SearchPreference</a> | ||||
|  * (MIT license) but was heavily modified/refactored for our use. | ||||
|  * | ||||
|  * @author litetex | ||||
|  */ | ||||
| package org.schabi.newpipe.settings.preferencesearch; | ||||
		Reference in New Issue
	
	Block a user
	 litetex
					litetex