Merge pull request #3546 from Stypox/search-history

Allow choosing which types of search suggestions to show
This commit is contained in:
Tobi 2021-08-24 19:27:36 +02:00 committed by GitHub
commit a6d6ed6474
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 190 additions and 105 deletions

View file

@ -57,6 +57,7 @@ import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@ -65,16 +66,19 @@ import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import icepick.State; import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
@ -143,7 +147,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Nullable private Map<Integer, String> menuItemToFilterName = null; @Nullable private Map<Integer, String> menuItemToFilterName = null;
private StreamingService service; private StreamingService service;
private Page nextPage; private Page nextPage;
private boolean isSuggestionsEnabled = true; private boolean showLocalSuggestions = true;
private boolean showRemoteSuggestions = true;
private Disposable searchDisposable; private Disposable searchDisposable;
private Disposable suggestionDisposable; private Disposable suggestionDisposable;
@ -194,26 +199,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
public void onAttach(@NonNull final Context context) { public void onAttach(@NonNull final Context context) {
super.onAttach(context); super.onAttach(context);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
suggestionListAdapter = new SuggestionListAdapter(activity); suggestionListAdapter = new SuggestionListAdapter(activity);
final SharedPreferences preferences
= PreferenceManager.getDefaultSharedPreferences(activity);
final boolean isSearchHistoryEnabled = preferences
.getBoolean(getString(R.string.enable_search_history_key), true);
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
historyRecordManager = new HistoryRecordManager(context); historyRecordManager = new HistoryRecordManager(context);
} }
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final SharedPreferences preferences
= PreferenceManager.getDefaultSharedPreferences(activity);
isSuggestionsEnabled = preferences
.getBoolean(getString(R.string.show_search_suggestions_key), true);
}
@Override @Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) { @Nullable final Bundle savedInstanceState) {
@ -554,7 +547,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onClick() called with: v = [" + v + "]"); Log.d(TAG, "onClick() called with: v = [" + v + "]");
} }
if (isSuggestionsEnabled && !isErrorPanelVisible()) { if ((showLocalSuggestions || showRemoteSuggestions) && !isErrorPanelVisible()) {
showSuggestionsPanel(); showSuggestionsPanel();
} }
if (DeviceUtils.isTv(getContext())) { if (DeviceUtils.isTv(getContext())) {
@ -567,7 +560,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "onFocusChange() called with: " Log.d(TAG, "onFocusChange() called with: "
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]"); + "v = [" + v + "], hasFocus = [" + hasFocus + "]");
} }
if (isSuggestionsEnabled && hasFocus && !isErrorPanelVisible()) { if ((showLocalSuggestions || showRemoteSuggestions)
&& hasFocus && !isErrorPanelVisible()) {
showSuggestionsPanel(); showSuggestionsPanel();
} }
}); });
@ -743,6 +737,34 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
return false; return false;
} }
private Observable<List<SuggestionItem>> getLocalSuggestionsObservable(
final String query, final int similarQueryLimit) {
return historyRecordManager
.getRelatedSearches(query, similarQueryLimit, 25)
.toObservable()
.map(searchHistoryEntries -> {
final Set<SuggestionItem> result = new HashSet<>(); // remove duplicates
for (final SearchHistoryEntry entry : searchHistoryEntries) {
result.add(new SuggestionItem(true, entry.getSearch()));
}
return new ArrayList<>(result);
});
}
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
return ExtractorHelper
.suggestionsFor(serviceId, query)
.toObservable()
.map(strings -> {
final List<SuggestionItem> result = new ArrayList<>();
for (final String entry : strings) {
result.add(new SuggestionItem(false, entry));
}
return result;
});
}
private void initSuggestionObserver() { private void initSuggestionObserver() {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "initSuggestionObserver() called"); Log.d(TAG, "initSuggestionObserver() called");
@ -753,70 +775,47 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
suggestionDisposable = suggestionPublisher suggestionDisposable = suggestionPublisher
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
.startWithItem(searchString != null .startWithItem(searchString == null ? "" : searchString)
? searchString
: "")
.filter(ss -> isSuggestionsEnabled)
.switchMap(query -> { .switchMap(query -> {
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager // Only show remote suggestions if they are enabled in settings and
.getRelatedSearches(query, 3, 25); // the query length is at least THRESHOLD_NETWORK_SUGGESTION
final Observable<List<SuggestionItem>> local = flowable.toObservable() final boolean shallShowRemoteSuggestionsNow = showRemoteSuggestions
.map(searchHistoryEntries -> { && query.length() >= THRESHOLD_NETWORK_SUGGESTION;
final List<SuggestionItem> result = new ArrayList<>();
for (final SearchHistoryEntry entry : searchHistoryEntries) {
result.add(new SuggestionItem(true, entry.getSearch()));
}
return result;
});
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) { if (showLocalSuggestions && shallShowRemoteSuggestionsNow) {
// Only pass through if the query length return Observable.zip(
// is equal or greater than THRESHOLD_NETWORK_SUGGESTION getLocalSuggestionsObservable(query, 3),
return local.materialize(); getRemoteSuggestionsObservable(query),
} (local, remote) -> {
remote.removeIf(remoteItem -> local.stream().anyMatch(
final Observable<List<SuggestionItem>> network = ExtractorHelper localItem -> localItem.equals(remoteItem)));
.suggestionsFor(serviceId, query) local.addAll(remote);
.onErrorReturn(throwable -> { return local;
if (!ExceptionUtils.isNetworkRelated(throwable)) {
showSnackBarError(new ErrorInfo(throwable,
UserAction.GET_SUGGESTIONS, searchString, serviceId));
}
return new ArrayList<>();
}) })
.materialize();
} else if (showLocalSuggestions) {
return getLocalSuggestionsObservable(query, 25)
.materialize();
} else if (shallShowRemoteSuggestionsNow) {
return getRemoteSuggestionsObservable(query)
.materialize();
} else {
return Single.fromCallable(Collections::<SuggestionItem>emptyList)
.toObservable() .toObservable()
.map(strings -> { .materialize();
final List<SuggestionItem> result = new ArrayList<>();
for (final String entry : strings) {
result.add(new SuggestionItem(false, entry));
} }
return result;
});
return Observable.zip(local, network, (localResult, networkResult) -> {
final List<SuggestionItem> result = new ArrayList<>();
if (localResult.size() > 0) {
result.addAll(localResult);
}
// Remove duplicates
networkResult.removeIf(networkItem ->
localResult.stream().anyMatch(localItem ->
localItem.query.equals(networkItem.query)));
if (networkResult.size() > 0) {
result.addAll(networkResult);
}
return result;
}).materialize();
}) })
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(listNotification -> { .subscribe(listNotification -> {
if (listNotification.isOnNext()) { if (listNotification.isOnNext()) {
if (listNotification.getValue() != null) {
handleSuggestions(listNotification.getValue()); handleSuggestions(listNotification.getValue());
} else if (listNotification.isOnError()) { }
showError(new ErrorInfo(listNotification.getError(), } else if (listNotification.isOnError()
&& listNotification.getError() != null
&& !ExceptionUtils.isInterruptedCaused(listNotification.getError())) {
showSnackBarError(new ErrorInfo(listNotification.getError(),
UserAction.GET_SUGGESTIONS, searchString, serviceId)); UserAction.GET_SUGGESTIONS, searchString, serviceId));
} }
}); });

View file

@ -1,5 +1,7 @@
package org.schabi.newpipe.fragments.list.search; package org.schabi.newpipe.fragments.list.search;
import androidx.annotation.NonNull;
public class SuggestionItem { public class SuggestionItem {
final boolean fromHistory; final boolean fromHistory;
public final String query; public final String query;
@ -9,6 +11,20 @@ public class SuggestionItem {
this.query = query; this.query = query;
} }
@Override
public boolean equals(final Object o) {
if (o instanceof SuggestionItem) {
return query.equals(((SuggestionItem) o).query);
}
return false;
}
@Override
public int hashCode() {
return query.hashCode();
}
@NonNull
@Override @Override
public String toString() { public String toString() {
return "[" + fromHistory + "" + query + "]"; return "[" + fromHistory + "" + query + "]";

View file

@ -19,7 +19,6 @@ public class SuggestionListAdapter
private final ArrayList<SuggestionItem> items = new ArrayList<>(); private final ArrayList<SuggestionItem> items = new ArrayList<>();
private final Context context; private final Context context;
private OnSuggestionItemSelected listener; private OnSuggestionItemSelected listener;
private boolean showSuggestionHistory = true;
public SuggestionListAdapter(final Context context) { public SuggestionListAdapter(final Context context) {
this.context = context; this.context = context;
@ -27,16 +26,7 @@ public class SuggestionListAdapter
public void setItems(final List<SuggestionItem> items) { public void setItems(final List<SuggestionItem> items) {
this.items.clear(); this.items.clear();
if (showSuggestionHistory) {
this.items.addAll(items); this.items.addAll(items);
} else {
// remove history items if history is disabled
for (final SuggestionItem item : items) {
if (!item.fromHistory) {
this.items.add(item);
}
}
}
notifyDataSetChanged(); notifyDataSetChanged();
} }
@ -44,10 +34,6 @@ public class SuggestionListAdapter
this.listener = listener; this.listener = listener;
} }
public void setShowSuggestionHistory(final boolean v) {
showSuggestionHistory = v;
}
@Override @Override
public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) { public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new SuggestionItemHolder(LayoutInflater.from(context) return new SuggestionItemHolder(LayoutInflater.from(context)

View file

@ -92,8 +92,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
.getPreferredLocalization(requireContext()); .getPreferredLocalization(requireContext());
initialSelectedContentCountry = org.schabi.newpipe.util.Localization initialSelectedContentCountry = org.schabi.newpipe.util.Localization
.getPreferredContentCountry(requireContext()); .getPreferredContentCountry(requireContext());
initialLanguage = PreferenceManager initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key); final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
clearCookiePref.setOnPreferenceClickListener(preference -> { clearCookiePref.setOnPreferenceClickListener(preference -> {
@ -147,8 +146,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
.getPreferredLocalization(requireContext()); .getPreferredLocalization(requireContext());
final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
.getPreferredContentCountry(requireContext()); .getPreferredContentCountry(requireContext());
final String selectedLanguage = PreferenceManager final String selectedLanguage =
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en"); defaultPreferences.getString(getString(R.string.app_language_key), "en");
if (!selectedLocalization.equals(initialSelectedLocalization) if (!selectedLocalization.equals(initialSelectedLocalization)
|| !selectedContentCountry.equals(initialSelectedContentCountry) || !selectedContentCountry.equals(initialSelectedContentCountry)

View file

@ -6,6 +6,7 @@ import android.os.Build;
import android.os.Environment; import android.os.Environment;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -59,6 +60,10 @@ public final class NewPipeSettings {
isFirstRun = true; isFirstRun = true;
} }
// first run migrations, then setDefaultValues, since the latter requires the correct types
SettingMigrations.initMigrations(context, isFirstRun);
// readAgain is true so that if new settings are added their default value is set
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true); PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true); PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true); PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
@ -71,8 +76,6 @@ public final class NewPipeSettings {
saveDefaultVideoDownloadDirectory(context); saveDefaultVideoDownloadDirectory(context);
saveDefaultAudioDownloadDirectory(context); saveDefaultAudioDownloadDirectory(context);
SettingMigrations.initMigrations(context, isFirstRun);
} }
static void saveDefaultVideoDownloadDirectory(final Context context) { static void saveDefaultVideoDownloadDirectory(final Context context) {
@ -124,4 +127,29 @@ public final class NewPipeSettings {
return prefs.getBoolean(key, true); return prefs.getBoolean(key, true);
} }
private static boolean showSearchSuggestions(final Context context,
final SharedPreferences sharedPreferences,
@StringRes final int key) {
final Set<String> enabledSearchSuggestions = sharedPreferences.getStringSet(
context.getString(R.string.show_search_suggestions_key), null);
if (enabledSearchSuggestions == null) {
return true; // defaults to true
} else {
return enabledSearchSuggestions.contains(context.getString(key));
}
}
public static boolean showLocalSearchSuggestions(final Context context,
final SharedPreferences sharedPreferences) {
return showSearchSuggestions(context, sharedPreferences,
R.string.show_local_search_suggestions_key);
}
public static boolean showRemoteSearchSuggestions(final Context context,
final SharedPreferences sharedPreferences) {
return showSearchSuggestions(context, sharedPreferences,
R.string.show_remote_search_suggestions_key);
}
} }

View file

@ -13,14 +13,22 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import static org.schabi.newpipe.MainActivity.DEBUG; import static org.schabi.newpipe.MainActivity.DEBUG;
public final class SettingMigrations { /**
private static final String TAG = SettingMigrations.class.toString(); * In order to add a migration, follow these steps, given P is the previous version:<br>
/** * - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in
* Version number for preferences. Must be incremented every time a migration is necessary. * the {@code migrate()} method the code that need to be run when migrating from P to P+1<br>
* - add {@code MIGRATION_P_P+1} at the end of {@link SettingMigrations#SETTING_MIGRATIONS}<br>
* - increment {@link SettingMigrations#VERSION}'s value by 1 (so it should become P+1)
*/ */
public static final int VERSION = 3; public final class SettingMigrations {
private static final String TAG = SettingMigrations.class.toString();
private static SharedPreferences sp; private static SharedPreferences sp;
public static final Migration MIGRATION_0_1 = new Migration(0, 1) { public static final Migration MIGRATION_0_1 = new Migration(0, 1) {
@ -72,6 +80,35 @@ public final class SettingMigrations {
} }
}; };
public static final Migration MIGRATION_3_4 = new Migration(3, 4) {
@Override
protected void migrate(final Context context) {
// Pull request #3546 added support for choosing the type of search suggestions to
// show, replacing the on-off switch used before, so migrate the previous user choice
final String showSearchSuggestionsKey =
context.getString(R.string.show_search_suggestions_key);
boolean addAllSearchSuggestionTypes;
try {
addAllSearchSuggestionTypes = sp.getBoolean(showSearchSuggestionsKey, true);
} catch (final ClassCastException e) {
// just in case it was not a boolean for some reason, let's consider it a "true"
addAllSearchSuggestionTypes = true;
}
final Set<String> showSearchSuggestionsValueList = new HashSet<>();
if (addAllSearchSuggestionTypes) {
// if the preference was true, all suggestions will be shown, otherwise none
Collections.addAll(showSearchSuggestionsValueList, context.getResources()
.getStringArray(R.array.show_search_suggestions_value_list));
}
sp.edit().putStringSet(
showSearchSuggestionsKey, showSearchSuggestionsValueList).apply();
}
};
/** /**
* List of all implemented migrations. * List of all implemented migrations.
* <p> * <p>
@ -81,9 +118,15 @@ public final class SettingMigrations {
private static final Migration[] SETTING_MIGRATIONS = { private static final Migration[] SETTING_MIGRATIONS = {
MIGRATION_0_1, MIGRATION_0_1,
MIGRATION_1_2, MIGRATION_1_2,
MIGRATION_2_3 MIGRATION_2_3,
MIGRATION_3_4,
}; };
/**
* Version number for preferences. Must be incremented every time a migration is necessary.
*/
public static final int VERSION = 4;
public static void initMigrations(final Context context, final boolean isFirstRun) { public static void initMigrations(final Context context, final boolean isFirstRun) {
// setup migrations and check if there is something to do // setup migrations and check if there is something to do

View file

@ -227,6 +227,16 @@
<!-- Content & History --> <!-- Content & History -->
<string name="show_search_suggestions_key" translatable="false">show_search_suggestions</string> <string name="show_search_suggestions_key" translatable="false">show_search_suggestions</string>
<string name="show_local_search_suggestions_key" translatable="false">show_local_search_suggestions</string>
<string name="show_remote_search_suggestions_key" translatable="false">show_remote_search_suggestions</string>
<string-array name="show_search_suggestions_value_list" translatable="false">
<item>@string/show_local_search_suggestions_key</item>
<item>@string/show_remote_search_suggestions_key</item>
</string-array>
<string-array name="show_search_suggestions_description_list" translatable="false">
<item>@string/local_search_suggestions</item>
<item>@string/remote_search_suggestions</item>
</string-array>
<string name="show_play_with_kodi_key" translatable="false">show_play_with_kodi</string> <string name="show_play_with_kodi_key" translatable="false">show_play_with_kodi</string>
<string name="show_comments_key" translatable="false">show_comments</string> <string name="show_comments_key" translatable="false">show_comments</string>
<string name="show_next_video_key" translatable="false">show_next_video</string> <string name="show_next_video_key" translatable="false">show_next_video</string>

View file

@ -116,7 +116,9 @@
<string name="player_gesture_controls_title">Player gesture controls</string> <string name="player_gesture_controls_title">Player gesture controls</string>
<string name="player_gesture_controls_summary">Use gestures to control player brightness and volume</string> <string name="player_gesture_controls_summary">Use gestures to control player brightness and volume</string>
<string name="show_search_suggestions_title">Search suggestions</string> <string name="show_search_suggestions_title">Search suggestions</string>
<string name="show_search_suggestions_summary">Show suggestions when searching</string> <string name="show_search_suggestions_summary">Choose the suggestions to show when searching</string>
<string name="local_search_suggestions">Local search suggestions</string>
<string name="remote_search_suggestions">Remote search suggestions</string>
<string name="enable_search_history_title">Search history</string> <string name="enable_search_history_title">Search history</string>
<string name="enable_search_history_summary">Store search queries locally</string> <string name="enable_search_history_summary">Store search queries locally</string>
<string name="enable_watch_history_title">Watch history</string> <string name="enable_watch_history_title">Watch history</string>

View file

@ -65,11 +65,13 @@
app:singleLineTitle="false" app:singleLineTitle="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<SwitchPreferenceCompat <MultiSelectListPreference
android:defaultValue="true"
android:key="@string/show_search_suggestions_key" android:key="@string/show_search_suggestions_key"
android:summary="@string/show_search_suggestions_summary" android:summary="@string/show_search_suggestions_summary"
android:title="@string/show_search_suggestions_title" android:title="@string/show_search_suggestions_title"
android:entries="@array/show_search_suggestions_description_list"
android:entryValues="@array/show_search_suggestions_value_list"
android:defaultValue="@array/show_search_suggestions_value_list"
app:singleLineTitle="false" app:singleLineTitle="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />