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