Added preference search "framework"
This commit is contained in:
parent
4a061f20ed
commit
12a78a826d
|
@ -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;
|
Loading…
Reference in New Issue