diff --git a/.gitignore b/.gitignore
index 40e7d2c03..1352b6917 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,8 +8,8 @@ captures/
*~
.weblate
*.class
-**/debug/
-**/release/
+app/debug/
+app/release/
# vscode / eclipse files
*.classpath
diff --git a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.java b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.java
new file mode 100644
index 000000000..a2d65f6f4
--- /dev/null
+++ b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsBVDLeakCanary.java
@@ -0,0 +1,20 @@
+package org.schabi.newpipe.settings;
+
+import android.content.Intent;
+
+import leakcanary.LeakCanary;
+
+/**
+ * Build variant dependent (BVD) leak canary API implementation for the debug settings fragment.
+ * This class is loaded via reflection by
+ * {@link DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI}.
+ */
+@SuppressWarnings("unused") // Class is used but loaded via reflection
+public class DebugSettingsBVDLeakCanary
+ implements DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI {
+
+ @Override
+ public Intent getNewLeakDisplayActivityIntent() {
+ return LeakCanary.INSTANCE.newLeakDisplayActivityIntent();
+ }
+}
diff --git a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsFragment.java
deleted file mode 100644
index f48be553f..000000000
--- a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsFragment.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package org.schabi.newpipe.settings;
-
-import android.os.Bundle;
-
-import androidx.preference.Preference;
-
-import org.schabi.newpipe.R;
-import org.schabi.newpipe.error.ErrorInfo;
-import org.schabi.newpipe.error.ErrorUtil;
-import org.schabi.newpipe.error.UserAction;
-import org.schabi.newpipe.util.PicassoHelper;
-
-import leakcanary.LeakCanary;
-
-public class DebugSettingsFragment extends BasePreferenceFragment {
- @Override
- public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
- addPreferencesFromResource(R.xml.debug_settings);
-
- final Preference showMemoryLeaksPreference
- = findPreference(getString(R.string.show_memory_leaks_key));
- final Preference showImageIndicatorsPreference
- = findPreference(getString(R.string.show_image_indicators_key));
- final Preference crashTheAppPreference
- = findPreference(getString(R.string.crash_the_app_key));
- final Preference showErrorSnackbarPreference
- = findPreference(getString(R.string.show_error_snackbar_key));
- final Preference createErrorNotificationPreference
- = findPreference(getString(R.string.create_error_notification_key));
-
- assert showMemoryLeaksPreference != null;
- assert showImageIndicatorsPreference != null;
- assert crashTheAppPreference != null;
- assert showErrorSnackbarPreference != null;
- assert createErrorNotificationPreference != null;
-
- showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
- startActivity(LeakCanary.INSTANCE.newLeakDisplayActivityIntent());
- return true;
- });
-
- showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
- PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
- return true;
- });
-
- crashTheAppPreference.setOnPreferenceClickListener(preference -> {
- throw new RuntimeException();
- });
-
- showErrorSnackbarPreference.setOnPreferenceClickListener(preference -> {
- ErrorUtil.showUiErrorSnackbar(DebugSettingsFragment.this,
- "Dummy", new RuntimeException("Dummy"));
- return true;
- });
-
- createErrorNotificationPreference.setOnPreferenceClickListener(preference -> {
- ErrorUtil.createNotification(requireContext(),
- new ErrorInfo(new RuntimeException("Dummy"), UserAction.UI_ERROR, "Dummy"));
- return true;
- });
- }
-}
diff --git a/app/src/main/java/org/apache/commons/text/similarity/FuzzyScore.java b/app/src/main/java/org/apache/commons/text/similarity/FuzzyScore.java
new file mode 100644
index 000000000..bbab7fd78
--- /dev/null
+++ b/app/src/main/java/org/apache/commons/text/similarity/FuzzyScore.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.text.similarity;
+
+import java.util.Locale;
+
+/**
+ * A matching algorithm that is similar to the searching algorithms implemented in editors such
+ * as Sublime Text, TextMate, Atom and others.
+ *
+ *
+ * One point is given for every matched character. Subsequent matches yield two bonus points.
+ * A higher score indicates a higher similarity.
+ *
+ *
+ *
+ * This code has been adapted from Apache Commons Lang 3.3.
+ *
+ *
+ * @since 1.0
+ *
+ * Note: This class was forked from
+ *
+ * apache/commons-text (8cfdafc) FuzzyScore.java
+ *
+ */
+public class FuzzyScore {
+
+ /**
+ * Locale used to change the case of text.
+ */
+ private final Locale locale;
+
+
+ /**
+ * This returns a {@link Locale}-specific {@link FuzzyScore}.
+ *
+ * @param locale The string matching logic is case insensitive.
+ A {@link Locale} is necessary to normalize both Strings to lower case.
+ * @throws IllegalArgumentException
+ * This is thrown if the {@link Locale} parameter is {@code null}.
+ */
+ public FuzzyScore(final Locale locale) {
+ if (locale == null) {
+ throw new IllegalArgumentException("Locale must not be null");
+ }
+ this.locale = locale;
+ }
+
+ /**
+ * Find the Fuzzy Score which indicates the similarity score between two
+ * Strings.
+ *
+ *
+ * score.fuzzyScore(null, null) = IllegalArgumentException
+ * score.fuzzyScore("not null", null) = IllegalArgumentException
+ * score.fuzzyScore(null, "not null") = IllegalArgumentException
+ * score.fuzzyScore("", "") = 0
+ * score.fuzzyScore("Workshop", "b") = 0
+ * score.fuzzyScore("Room", "o") = 1
+ * score.fuzzyScore("Workshop", "w") = 1
+ * score.fuzzyScore("Workshop", "ws") = 2
+ * score.fuzzyScore("Workshop", "wo") = 4
+ * score.fuzzyScore("Apache Software Foundation", "asf") = 3
+ *
+ *
+ * @param term a full term that should be matched against, must not be null
+ * @param query the query that will be matched against a term, must not be
+ * null
+ * @return result score
+ * @throws IllegalArgumentException if the term or query is {@code null}
+ */
+ public Integer fuzzyScore(final CharSequence term, final CharSequence query) {
+ if (term == null || query == null) {
+ throw new IllegalArgumentException("CharSequences must not be null");
+ }
+
+ // fuzzy logic is case insensitive. We normalize the Strings to lower
+ // case right from the start. Turning characters to lower case
+ // via Character.toLowerCase(char) is unfortunately insufficient
+ // as it does not accept a locale.
+ final String termLowerCase = term.toString().toLowerCase(locale);
+ final String queryLowerCase = query.toString().toLowerCase(locale);
+
+ // the resulting score
+ int score = 0;
+
+ // the position in the term which will be scanned next for potential
+ // query character matches
+ int termIndex = 0;
+
+ // index of the previously matched character in the term
+ int previousMatchingCharacterIndex = Integer.MIN_VALUE;
+
+ for (int queryIndex = 0; queryIndex < queryLowerCase.length(); queryIndex++) {
+ final char queryChar = queryLowerCase.charAt(queryIndex);
+
+ boolean termCharacterMatchFound = false;
+ for (; termIndex < termLowerCase.length()
+ && !termCharacterMatchFound; termIndex++) {
+ final char termChar = termLowerCase.charAt(termIndex);
+
+ if (queryChar == termChar) {
+ // simple character matches result in one point
+ score++;
+
+ // subsequent character matches further improve
+ // the score.
+ if (previousMatchingCharacterIndex + 1 == termIndex) {
+ score += 2;
+ }
+
+ previousMatchingCharacterIndex = termIndex;
+
+ // we can leave the nested loop. Every character in the
+ // query can match at most one character in the term.
+ termCharacterMatchFound = true;
+ }
+ }
+ }
+
+ return score;
+ }
+
+ /**
+ * Gets the locale.
+ *
+ * @return The locale
+ */
+ public Locale getLocale() {
+ return locale;
+ }
+
+}
diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
index a8fdcae26..1e5bd8799 100644
--- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
+++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
@@ -185,7 +185,11 @@ class AboutActivity : AppCompatActivity() {
SoftwareComponent(
"RxJava", "2016 - 2020", "RxJava Contributors",
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
- )
+ ),
+ SoftwareComponent(
+ "SearchPreference", "2018", "ByteHamster",
+ "https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
+ ),
)
private const val POS_ABOUT = 0
private const val POS_LICENSE = 1
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java
index 15424334d..055c27733 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java
@@ -25,7 +25,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
@@ -34,7 +33,6 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.TooltipCompat;
-import androidx.core.content.ContextCompat;
import androidx.core.text.HtmlCompat;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.ItemTouchHelper;
@@ -65,6 +63,7 @@ import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
+import org.schabi.newpipe.util.KeyboardUtil;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
@@ -670,31 +669,15 @@ public class SearchFragment extends BaseListFragment= Build.VERSION_CODES.KITKAT;
-
- private String captionSettingsKey;
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
- addPreferencesFromResource(R.xml.appearance_settings);
+ addPreferencesFromResourceRegistry();
final String themeKey = getString(R.string.theme_key);
// the key of the active theme when settings were opened (or recreated after theme change)
@@ -51,16 +46,11 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment {
} else {
removePreference(nightThemeKey);
}
-
- captionSettingsKey = getString(R.string.caption_settings_key);
- if (!CAPTIONING_SETTINGS_ACCESSIBLE) {
- removePreference(captionSettingsKey);
- }
}
@Override
public boolean onPreferenceTreeClick(final Preference preference) {
- if (preference.getKey().equals(captionSettingsKey) && CAPTIONING_SETTINGS_ACCESSIBLE) {
+ if (preference.getKey().equals(getString(R.string.caption_settings_key))) {
try {
startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS));
} catch (final ActivityNotFoundException e) {
diff --git a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java
index a745861ad..619579f3a 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java
@@ -28,6 +28,11 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
super.onCreate(savedInstanceState);
}
+ protected void addPreferencesFromResourceRegistry() {
+ addPreferencesFromResource(
+ SettingsResourceRegistry.getInstance().getPreferencesResId(this.getClass()));
+ }
+
@Override
public void onViewCreated(@NonNull final View rootView,
@Nullable final Bundle savedInstanceState) {
diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
index 1c8eb5cd2..47458ad3f 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
@@ -1,5 +1,8 @@
package org.schabi.newpipe.settings;
+import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
+import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
+
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@@ -21,7 +24,6 @@ import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorUtil;
-import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
@@ -38,9 +40,6 @@ import java.util.Date;
import java.util.Locale;
import java.util.Objects;
-import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
-
public class ContentSettingsFragment extends BasePreferenceFragment {
private static final String ZIP_MIME_TYPE = "application/zip";
@@ -70,7 +69,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
importExportDataPathKey = getString(R.string.import_export_data_path);
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
- addPreferencesFromResource(R.xml.content_settings);
+ addPreferencesFromResourceRegistry();
final Preference importDataPreference = requirePreference(R.string.import_data);
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
@@ -105,21 +104,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
.getPreferredContentCountry(requireContext());
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
- final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
- clearCookiePref.setOnPreferenceClickListener(preference -> {
- defaultPreferences.edit()
- .putString(getString(R.string.recaptcha_cookies_key), "").apply();
- DownloaderImpl.getInstance().setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, "");
- Toast.makeText(getActivity(), R.string.recaptcha_cookies_cleared,
- Toast.LENGTH_SHORT).show();
- clearCookiePref.setVisible(false);
- return true;
- });
-
- if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) {
- clearCookiePref.setVisible(false);
- }
-
findPreference(getString(R.string.download_thumbnail_key)).setOnPreferenceChangeListener(
(preference, newValue) -> {
PicassoHelper.setShouldLoadImages((Boolean) newValue);
diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java
new file mode 100644
index 000000000..395c7c0f0
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java
@@ -0,0 +1,108 @@
+package org.schabi.newpipe.settings;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.preference.Preference;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.error.ErrorInfo;
+import org.schabi.newpipe.error.ErrorUtil;
+import org.schabi.newpipe.error.UserAction;
+import org.schabi.newpipe.util.PicassoHelper;
+
+import java.util.Optional;
+
+public class DebugSettingsFragment extends BasePreferenceFragment {
+ private static final String DUMMY = "Dummy";
+
+ @Override
+ public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
+ addPreferencesFromResourceRegistry();
+
+ final Preference allowHeapDumpingPreference
+ = findPreference(getString(R.string.allow_heap_dumping_key));
+ final Preference showMemoryLeaksPreference
+ = findPreference(getString(R.string.show_memory_leaks_key));
+ final Preference showImageIndicatorsPreference
+ = findPreference(getString(R.string.show_image_indicators_key));
+ final Preference crashTheAppPreference
+ = findPreference(getString(R.string.crash_the_app_key));
+ final Preference showErrorSnackbarPreference
+ = findPreference(getString(R.string.show_error_snackbar_key));
+ final Preference createErrorNotificationPreference
+ = findPreference(getString(R.string.create_error_notification_key));
+
+ assert allowHeapDumpingPreference != null;
+ assert showMemoryLeaksPreference != null;
+ assert showImageIndicatorsPreference != null;
+ assert crashTheAppPreference != null;
+ assert showErrorSnackbarPreference != null;
+ assert createErrorNotificationPreference != null;
+
+ final Optional optBVLeakCanary = getBVDLeakCanary();
+
+ allowHeapDumpingPreference.setEnabled(optBVLeakCanary.isPresent());
+ showMemoryLeaksPreference.setEnabled(optBVLeakCanary.isPresent());
+
+ if (optBVLeakCanary.isPresent()) {
+ final DebugSettingsBVDLeakCanaryAPI pdLeakCanary = optBVLeakCanary.get();
+
+ showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
+ startActivity(pdLeakCanary.getNewLeakDisplayActivityIntent());
+ return true;
+ });
+ } else {
+ allowHeapDumpingPreference.setSummary(R.string.leak_canary_not_available);
+ showMemoryLeaksPreference.setSummary(R.string.leak_canary_not_available);
+ }
+
+ showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
+ PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
+ return true;
+ });
+
+ crashTheAppPreference.setOnPreferenceClickListener(preference -> {
+ throw new RuntimeException(DUMMY);
+ });
+
+ showErrorSnackbarPreference.setOnPreferenceClickListener(preference -> {
+ ErrorUtil.showUiErrorSnackbar(DebugSettingsFragment.this,
+ DUMMY, new RuntimeException(DUMMY));
+ return true;
+ });
+
+ createErrorNotificationPreference.setOnPreferenceClickListener(preference -> {
+ ErrorUtil.createNotification(requireContext(),
+ new ErrorInfo(new RuntimeException(DUMMY), UserAction.UI_ERROR, DUMMY));
+ return true;
+ });
+ }
+
+ /**
+ * Tries to find the {@link DebugSettingsBVDLeakCanaryAPI#IMPL_CLASS} and loads it if available.
+ * @return An {@link Optional} which is empty if the implementation class couldn't be loaded.
+ */
+ private Optional getBVDLeakCanary() {
+ try {
+ // Try to find the implementation of the LeakCanary API
+ return Optional.of((DebugSettingsBVDLeakCanaryAPI)
+ Class.forName(DebugSettingsBVDLeakCanaryAPI.IMPL_CLASS)
+ .getDeclaredConstructor()
+ .newInstance());
+ } catch (final Exception e) {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Build variant dependent (BVD) leak canary API for this fragment.
+ * Why is LeakCanary not used directly? Because it can't be assured
+ */
+ public interface DebugSettingsBVDLeakCanaryAPI {
+ String IMPL_CLASS =
+ "org.schabi.newpipe.settings.DebugSettingsBVDLeakCanary";
+
+ Intent getNewLeakDisplayActivityIntent();
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java
index 681aee409..fe327e1b5 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java
@@ -54,7 +54,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
- addPreferencesFromResource(R.xml.download_settings);
+ addPreferencesFromResourceRegistry();
downloadPathVideoPreference = getString(R.string.download_path_video_key);
downloadPathAudioPreference = getString(R.string.download_path_audio_key);
diff --git a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java
index 33e0ba16b..86e651e2b 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java
@@ -8,9 +8,11 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
+import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
+import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.InfoCache;
@@ -29,7 +31,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
- addPreferencesFromResource(R.xml.history_settings);
+ addPreferencesFromResourceRegistry();
cacheWipeKey = getString(R.string.metadata_cache_wipe_key);
viewsHistoryClearKey = getString(R.string.clear_views_history_key);
@@ -37,6 +39,21 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
searchHistoryClearKey = getString(R.string.clear_search_history_key);
recordManager = new HistoryRecordManager(getActivity());
disposables = new CompositeDisposable();
+
+ final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
+ clearCookiePref.setOnPreferenceClickListener(preference -> {
+ defaultPreferences.edit()
+ .putString(getString(R.string.recaptcha_cookies_key), "").apply();
+ DownloaderImpl.getInstance().setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, "");
+ Toast.makeText(getActivity(), R.string.recaptcha_cookies_cleared,
+ Toast.LENGTH_SHORT).show();
+ clearCookiePref.setEnabled(false);
+ return true;
+ });
+
+ if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) {
+ clearCookiePref.setEnabled(false);
+ }
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java
index 12599b828..d7fb559d6 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java
@@ -1,8 +1,11 @@
package org.schabi.newpipe.settings;
import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
-import androidx.preference.Preference;
+import androidx.annotation.NonNull;
import org.schabi.newpipe.App;
import org.schabi.newpipe.CheckForNewAppVersion;
@@ -12,16 +15,58 @@ import org.schabi.newpipe.R;
public class MainSettingsFragment extends BasePreferenceFragment {
public static final boolean DEBUG = MainActivity.DEBUG;
+ private SettingsActivity settingsActivity;
+
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
- addPreferencesFromResource(R.xml.main_settings);
+ addPreferencesFromResourceRegistry();
+ setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
+
+ // Check if the app is updatable
if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) {
- final Preference update
- = findPreference(getString(R.string.update_pref_screen_key));
- getPreferenceScreen().removePreference(update);
+ getPreferenceScreen().removePreference(
+ findPreference(getString(R.string.update_pref_screen_key)));
defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply();
}
+
+ // Hide debug preferences in RELEASE build variant
+ if (!DEBUG) {
+ getPreferenceScreen().removePreference(
+ findPreference(getString(R.string.debug_pref_screen_key)));
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(
+ @NonNull final Menu menu,
+ @NonNull final MenuInflater inflater
+ ) {
+ super.onCreateOptionsMenu(menu, inflater);
+
+ // -- Link settings activity and register menu --
+ settingsActivity = (SettingsActivity) getActivity();
+
+ inflater.inflate(R.menu.menu_settings_main_fragment, menu);
+
+ final MenuItem menuSearchItem = menu.getItem(0);
+
+ settingsActivity.setMenuSearchItem(menuSearchItem);
+
+ menuSearchItem.setOnMenuItemClickListener(ev -> {
+ settingsActivity.setSearchActive(true);
+ return true;
+ });
+ }
+
+ @Override
+ public void onDestroy() {
+ // Unlink activity so that we don't get memory problems
+ if (settingsActivity != null) {
+ settingsActivity.setMenuSearchItem(null);
+ settingsActivity = null;
+ }
+ super.onDestroy();
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.kt
index e03aa4074..6bea8b69e 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.kt
@@ -7,7 +7,7 @@ import org.schabi.newpipe.R
class NotificationSettingsFragment : BasePreferenceFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- addPreferencesFromResource(R.xml.notification_settings)
+ addPreferencesFromResourceRegistry()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
val colorizePref: Preference? = findPreference(getString(R.string.notification_colorize_key))
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java
index a766ee074..383390506 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java
@@ -1,6 +1,5 @@
package org.schabi.newpipe.settings;
-import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -51,16 +50,11 @@ public class SelectKioskFragment extends DialogFragment {
private SelectKioskAdapter selectKioskAdapter = null;
private OnSelectedListener onSelectedListener = null;
- private OnCancelListener onCancelListener = null;
public void setOnSelectedListener(final OnSelectedListener listener) {
onSelectedListener = listener;
}
- public void setOnCancelListener(final OnCancelListener listener) {
- onCancelListener = listener;
- }
-
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@@ -91,14 +85,6 @@ public class SelectKioskFragment extends DialogFragment {
// Handle actions
//////////////////////////////////////////////////////////////////////////*/
- @Override
- public void onCancel(@NonNull final DialogInterface dialogInterface) {
- super.onCancel(dialogInterface);
- if (onCancelListener != null) {
- onCancelListener.onCancel();
- }
- }
-
private void clickedItem(final SelectKioskAdapter.Entry entry) {
if (onSelectedListener != null) {
onSelectedListener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName);
@@ -114,10 +100,6 @@ public class SelectKioskFragment extends DialogFragment {
void onKioskSelected(int serviceId, String kioskId, String kioskName);
}
- public interface OnCancelListener {
- void onCancel();
- }
-
private class SelectKioskAdapter
extends RecyclerView.Adapter {
private final List kioskList = new Vector<>();
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
index 02e2538c5..3872e5172 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
@@ -1,22 +1,48 @@
package org.schabi.newpipe.settings;
+import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
+
import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
+import android.view.View;
+import android.widget.EditText;
+import androidx.annotation.IdRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
+import com.jakewharton.rxbinding4.widget.RxTextView;
+
+import org.schabi.newpipe.App;
+import org.schabi.newpipe.CheckForNewAppVersion;
+import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.SettingsLayoutBinding;
+import org.schabi.newpipe.settings.preferencesearch.PreferenceParser;
+import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchConfiguration;
+import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchFragment;
+import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchItem;
+import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchResultHighlighter;
+import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchResultListener;
+import org.schabi.newpipe.settings.preferencesearch.PreferenceSearcher;
import org.schabi.newpipe.util.DeviceUtils;
+import org.schabi.newpipe.util.KeyboardUtil;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
+import java.util.concurrent.TimeUnit;
+
+import icepick.Icepick;
+import icepick.State;
/*
* Created by Christian Schabesberger on 31.08.15.
@@ -38,21 +64,54 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
* along with NewPipe. If not, see .
*/
-public class SettingsActivity extends AppCompatActivity
- implements BasePreferenceFragment.OnPreferenceStartFragmentCallback {
+public class SettingsActivity extends AppCompatActivity implements
+ PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
+ PreferenceSearchResultListener {
+ private static final String TAG = "SettingsActivity";
+ private static final boolean DEBUG = MainActivity.DEBUG;
+
+ @IdRes
+ private static final int FRAGMENT_HOLDER_ID = R.id.settings_fragment_holder;
+
+ private PreferenceSearchFragment searchFragment;
+
+ @Nullable
+ private MenuItem menuSearchItem;
+
+ private View searchContainer;
+ private EditText searchEditText;
+
+ // State
+ @State
+ String searchText;
+ @State
+ boolean wasSearchActive;
+
@Override
protected void onCreate(final Bundle savedInstanceBundle) {
setTheme(ThemeHelper.getSettingsThemeStyle(this));
assureCorrectAppLanguage(this);
+
super.onCreate(savedInstanceBundle);
+ Icepick.restoreInstanceState(this, savedInstanceBundle);
+ final boolean restored = savedInstanceBundle != null;
final SettingsLayoutBinding settingsLayoutBinding =
SettingsLayoutBinding.inflate(getLayoutInflater());
setContentView(settingsLayoutBinding.getRoot());
+ initSearch(settingsLayoutBinding, restored);
setSupportActionBar(settingsLayoutBinding.settingsToolbarLayout.toolbar);
- if (savedInstanceBundle == null) {
+ if (restored) {
+ // Restore state
+ if (this.wasSearchActive) {
+ setSearchActive(true);
+ if (!TextUtils.isEmpty(this.searchText)) {
+ this.searchEditText.setText(this.searchText);
+ }
+ }
+ } else {
getSupportFragmentManager().beginTransaction()
.replace(R.id.settings_fragment_holder, new MainSettingsFragment())
.commit();
@@ -63,6 +122,12 @@ public class SettingsActivity extends AppCompatActivity
}
}
+ @Override
+ protected void onSaveInstanceState(@NonNull final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ Icepick.saveInstanceState(this, outState);
+ }
+
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final ActionBar actionBar = getSupportActionBar();
@@ -74,10 +139,25 @@ public class SettingsActivity extends AppCompatActivity
return super.onCreateOptionsMenu(menu);
}
+ @Override
+ public void onBackPressed() {
+ if (isSearchActive()) {
+ setSearchActive(false);
+ return;
+ }
+ super.onBackPressed();
+ }
+
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int id = item.getItemId();
if (id == android.R.id.home) {
+ // Check if the search is active and if so: Close it
+ if (isSearchActive()) {
+ setSearchActive(false);
+ return true;
+ }
+
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
finish();
} else {
@@ -91,14 +171,221 @@ public class SettingsActivity extends AppCompatActivity
@Override
public boolean onPreferenceStartFragment(final PreferenceFragmentCompat caller,
final Preference preference) {
- final Fragment fragment = Fragment
- .instantiate(this, preference.getFragment(), preference.getExtras());
+ showSettingsFragment(instantiateFragment(preference.getFragment()));
+ return true;
+ }
+
+ private Fragment instantiateFragment(@NonNull final String className) {
+ return getSupportFragmentManager()
+ .getFragmentFactory()
+ .instantiate(this.getClassLoader(), className);
+ }
+
+ private void showSettingsFragment(final Fragment fragment) {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out,
R.animator.custom_fade_in, R.animator.custom_fade_out)
- .replace(R.id.settings_fragment_holder, fragment)
+ .replace(FRAGMENT_HOLDER_ID, fragment)
.addToBackStack(null)
.commit();
- return true;
}
+
+ @Override
+ protected void onDestroy() {
+ setMenuSearchItem(null);
+ searchFragment = null;
+ super.onDestroy();
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Search
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Search
+
+ private void initSearch(
+ final SettingsLayoutBinding settingsLayoutBinding,
+ final boolean restored
+ ) {
+ searchContainer =
+ settingsLayoutBinding.settingsToolbarLayout.toolbar
+ .findViewById(R.id.toolbar_search_container);
+
+ // Configure input field for search
+ searchEditText = searchContainer.findViewById(R.id.toolbar_search_edit_text);
+ RxTextView.textChanges(searchEditText)
+ // Wait some time after the last input before actually searching
+ .debounce(200, TimeUnit.MILLISECONDS)
+ .subscribe(v -> runOnUiThread(this::onSearchChanged));
+
+ // Configure clear button
+ searchContainer.findViewById(R.id.toolbar_search_clear)
+ .setOnClickListener(ev -> resetSearchText());
+
+ ensureSearchRepresentsApplicationState();
+
+ // Build search configuration using SettingsResourceRegistry
+ final PreferenceSearchConfiguration config = new PreferenceSearchConfiguration();
+
+
+ // Build search items
+ final PreferenceParser parser = new PreferenceParser(getApplicationContext(), config);
+ final PreferenceSearcher searcher = new PreferenceSearcher(config);
+
+ // Find all searchable SettingsResourceRegistry fragments
+ SettingsResourceRegistry.getInstance().getAllEntries().stream()
+ .filter(SettingsResourceRegistry.SettingRegistryEntry::isSearchable)
+ // Get the resId
+ .map(SettingsResourceRegistry.SettingRegistryEntry::getPreferencesResId)
+ // Parse
+ .map(parser::parse)
+ // Add it to the searcher
+ .forEach(searcher::add);
+
+ if (restored) {
+ searchFragment = (PreferenceSearchFragment) getSupportFragmentManager()
+ .findFragmentByTag(PreferenceSearchFragment.NAME);
+ if (searchFragment != null) {
+ // Hide/Remove the search fragment otherwise we get an exception
+ // when adding it (because it's already present)
+ hideSearchFragment();
+ }
+ }
+ if (searchFragment == null) {
+ searchFragment = new PreferenceSearchFragment();
+ }
+ searchFragment.setSearcher(searcher);
+ }
+
+ /**
+ * Ensures that the search shows the correct/available search results.
+ *
+ * Some features are e.g. only available for debug builds, these should not
+ * be found when searching inside a release.
+ */
+ private void ensureSearchRepresentsApplicationState() {
+ // Check if the update settings are available
+ if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) {
+ SettingsResourceRegistry.getInstance()
+ .getEntryByPreferencesResId(R.xml.update_settings)
+ .setSearchable(false);
+ }
+
+ // Hide debug preferences in RELEASE build variant
+ if (DEBUG) {
+ SettingsResourceRegistry.getInstance()
+ .getEntryByPreferencesResId(R.xml.debug_settings)
+ .setSearchable(true);
+ }
+ }
+
+ public void setMenuSearchItem(final MenuItem menuSearchItem) {
+ this.menuSearchItem = menuSearchItem;
+
+ // Ensure that the item is in the correct state when adding it. This is due to
+ // Android's lifecycle (the Activity is recreated before the Fragment that registers this)
+ if (menuSearchItem != null) {
+ menuSearchItem.setVisible(!isSearchActive());
+ }
+ }
+
+ public void setSearchActive(final boolean active) {
+ if (DEBUG) {
+ Log.d(TAG, "setSearchActive called active=" + active);
+ }
+
+ // Ignore if search is already in correct state
+ if (isSearchActive() == active) {
+ return;
+ }
+
+ wasSearchActive = active;
+
+ searchContainer.setVisibility(active ? View.VISIBLE : View.GONE);
+ if (menuSearchItem != null) {
+ menuSearchItem.setVisible(!active);
+ }
+
+ if (active) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .add(FRAGMENT_HOLDER_ID, searchFragment, PreferenceSearchFragment.NAME)
+ .addToBackStack(PreferenceSearchFragment.NAME)
+ .commit();
+
+ KeyboardUtil.showKeyboard(this, searchEditText);
+ } else if (searchFragment != null) {
+ hideSearchFragment();
+ getSupportFragmentManager()
+ .popBackStack(
+ PreferenceSearchFragment.NAME,
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+
+ KeyboardUtil.hideKeyboard(this, searchEditText);
+ }
+
+ resetSearchText();
+ }
+
+ private void hideSearchFragment() {
+ getSupportFragmentManager().beginTransaction().remove(searchFragment).commit();
+ }
+
+ private void resetSearchText() {
+ searchEditText.setText("");
+ }
+
+ private boolean isSearchActive() {
+ return searchContainer.getVisibility() == View.VISIBLE;
+ }
+
+ private void onSearchChanged() {
+ if (!isSearchActive()) {
+ return;
+ }
+
+ if (searchFragment != null) {
+ searchText = this.searchEditText.getText().toString();
+ searchFragment.updateSearchResults(searchText);
+ }
+ }
+
+ @Override
+ public void onSearchResultClicked(@NonNull final PreferenceSearchItem result) {
+ if (DEBUG) {
+ Log.d(TAG, "onSearchResultClicked called result=" + result);
+ }
+
+ // Hide the search
+ setSearchActive(false);
+
+ // -- Highlight the result --
+ // Find out which fragment class we need
+ final Class extends Fragment> targetedFragmentClass =
+ SettingsResourceRegistry.getInstance()
+ .getFragmentClass(result.getSearchIndexItemResId());
+
+ if (targetedFragmentClass == null) {
+ // This should never happen
+ Log.w(TAG, "Unable to locate fragment class for resId="
+ + result.getSearchIndexItemResId());
+ return;
+ }
+
+ // Check if the currentFragment is the one which contains the result
+ Fragment currentFragment =
+ getSupportFragmentManager().findFragmentById(FRAGMENT_HOLDER_ID);
+ if (!targetedFragmentClass.equals(currentFragment.getClass())) {
+ // If it's not the correct one display the correct one
+ currentFragment = instantiateFragment(targetedFragmentClass.getName());
+ showSettingsFragment(currentFragment);
+ }
+
+ // Run the highlighting
+ if (currentFragment instanceof PreferenceFragmentCompat) {
+ PreferenceSearchResultHighlighter
+ .highlight(result, (PreferenceFragmentCompat) currentFragment);
+ }
+ }
+
+ //endregion
}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java
new file mode 100644
index 000000000..c4751abea
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java
@@ -0,0 +1,148 @@
+package org.schabi.newpipe.settings;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.XmlRes;
+import androidx.fragment.app.Fragment;
+
+import org.schabi.newpipe.R;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A registry that contains information about SettingsFragments.
+ *
+ * includes:
+ *
+ * - Class of the SettingsFragment
+ * - XML-Resource
+ * - ...
+ *
+ *
+ * E.g. used by the preference search.
+ */
+public final class SettingsResourceRegistry {
+
+ private static final SettingsResourceRegistry INSTANCE = new SettingsResourceRegistry();
+
+ private final Set registeredEntries = new HashSet<>();
+
+ private SettingsResourceRegistry() {
+ add(MainSettingsFragment.class, R.xml.main_settings).setSearchable(false);
+
+ add(AppearanceSettingsFragment.class, R.xml.appearance_settings);
+ add(ContentSettingsFragment.class, R.xml.content_settings);
+ add(DebugSettingsFragment.class, R.xml.debug_settings).setSearchable(false);
+ add(DownloadSettingsFragment.class, R.xml.download_settings);
+ add(HistorySettingsFragment.class, R.xml.history_settings);
+ add(NotificationSettingsFragment.class, R.xml.notification_settings);
+ add(UpdateSettingsFragment.class, R.xml.update_settings);
+ add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings);
+ }
+
+ private SettingRegistryEntry add(
+ @NonNull final Class extends Fragment> fragmentClass,
+ @XmlRes final int preferencesResId
+ ) {
+ final SettingRegistryEntry entry =
+ new SettingRegistryEntry(fragmentClass, preferencesResId);
+ this.registeredEntries.add(entry);
+ return entry;
+ }
+
+ public SettingRegistryEntry getEntryByFragmentClass(
+ final Class extends Fragment> fragmentClass
+ ) {
+ Objects.requireNonNull(fragmentClass);
+ return registeredEntries.stream()
+ .filter(e -> Objects.equals(e.getFragmentClass(), fragmentClass))
+ .findFirst()
+ .orElse(null);
+ }
+
+ public SettingRegistryEntry getEntryByPreferencesResId(@XmlRes final int preferencesResId) {
+ return registeredEntries.stream()
+ .filter(e -> Objects.equals(e.getPreferencesResId(), preferencesResId))
+ .findFirst()
+ .orElse(null);
+ }
+
+ public int getPreferencesResId(@NonNull final Class extends Fragment> fragmentClass) {
+ final SettingRegistryEntry entry = getEntryByFragmentClass(fragmentClass);
+ if (entry == null) {
+ return -1;
+ }
+ return entry.getPreferencesResId();
+ }
+
+ public Class extends Fragment> getFragmentClass(@XmlRes final int preferencesResId) {
+ final SettingRegistryEntry entry = getEntryByPreferencesResId(preferencesResId);
+ if (entry == null) {
+ return null;
+ }
+ return entry.getFragmentClass();
+ }
+
+ public Set getAllEntries() {
+ return new HashSet<>(registeredEntries);
+ }
+
+ public static SettingsResourceRegistry getInstance() {
+ return INSTANCE;
+ }
+
+
+ public static class SettingRegistryEntry {
+ @NonNull
+ private final Class extends Fragment> fragmentClass;
+ @XmlRes
+ private final int preferencesResId;
+
+ private boolean searchable = true;
+
+ public SettingRegistryEntry(
+ @NonNull final Class extends Fragment> fragmentClass,
+ @XmlRes final int preferencesResId
+ ) {
+ this.fragmentClass = Objects.requireNonNull(fragmentClass);
+ this.preferencesResId = preferencesResId;
+ }
+
+ @SuppressWarnings("HiddenField")
+ public SettingRegistryEntry setSearchable(final boolean searchable) {
+ this.searchable = searchable;
+ return this;
+ }
+
+ public Class extends Fragment> getFragmentClass() {
+ return fragmentClass;
+ }
+
+ public int getPreferencesResId() {
+ return preferencesResId;
+ }
+
+ public boolean isSearchable() {
+ return searchable;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final SettingRegistryEntry that = (SettingRegistryEntry) o;
+ return getPreferencesResId() == that.getPreferencesResId()
+ && getFragmentClass().equals(that.getFragmentClass());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getFragmentClass(), getPreferencesResId());
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java
index bc183d08a..04bad3815 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java
@@ -38,7 +38,7 @@ public class UpdateSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
- addPreferencesFromResource(R.xml.update_settings);
+ addPreferencesFromResourceRegistry();
findPreference(getString(R.string.update_app_key))
.setOnPreferenceChangeListener(updatePreferenceChange);
diff --git a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java
index c0d274fe0..039f00c1d 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java
@@ -23,7 +23,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
- addPreferencesFromResource(R.xml.video_audio_settings);
+ addPreferencesFromResourceRegistry();
updateSeekOptions();
diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java
new file mode 100644
index 000000000..7c231cafb
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java
@@ -0,0 +1,111 @@
+package org.schabi.newpipe.settings.preferencesearch;
+
+import android.text.TextUtils;
+
+import org.apache.commons.text.similarity.FuzzyScore;
+
+import java.util.Comparator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class PreferenceFuzzySearchFunction
+ implements PreferenceSearchConfiguration.PreferenceSearchFunction {
+
+ private static final FuzzyScore FUZZY_SCORE = new FuzzyScore(Locale.ROOT);
+
+ @Override
+ public Stream search(
+ final Stream allAvailable,
+ final String keyword
+ ) {
+ final int maxScore = (keyword.length() + 1) * 3 - 2; // First can't get +2 bonus score
+
+ return allAvailable
+ // General search
+ // Check all fields if anyone contains something that kind of matches the keyword
+ .map(item -> new FuzzySearchGeneralDTO(item, keyword))
+ .filter(dto -> dto.getScore() / maxScore >= 0.3f)
+ .map(FuzzySearchGeneralDTO::getItem)
+ // Specific search - Used for determining order of search results
+ // Calculate a score based on specific search fields
+ .map(item -> new FuzzySearchSpecificDTO(item, keyword))
+ .sorted(Comparator.comparing(FuzzySearchSpecificDTO::getScore).reversed())
+ .map(FuzzySearchSpecificDTO::getItem)
+ // Limit the amount of search results
+ .limit(20);
+ }
+
+ static class FuzzySearchGeneralDTO {
+ private final PreferenceSearchItem item;
+ private final float score;
+
+ FuzzySearchGeneralDTO(
+ final PreferenceSearchItem item,
+ final String keyword) {
+ this.item = item;
+ this.score = FUZZY_SCORE.fuzzyScore(
+ TextUtils.join(";", item.getAllRelevantSearchFields()),
+ keyword);
+ }
+
+ public PreferenceSearchItem getItem() {
+ return item;
+ }
+
+ public float getScore() {
+ return score;
+ }
+ }
+
+ static class FuzzySearchSpecificDTO {
+ private static final Map, Float> WEIGHT_MAP = Map.of(
+ // The user will most likely look for the title -> prioritize it
+ PreferenceSearchItem::getTitle, 1.5f,
+ // The summary is also important as it usually contains a larger desc
+ // Example: Searching for '4k' → 'show higher resolution' is shown
+ PreferenceSearchItem::getSummary, 1f,
+ // Entries are also important as they provide all known/possible values
+ // Example: Searching where the resolution can be changed to 720p
+ PreferenceSearchItem::getEntries, 1f
+ );
+
+ private final PreferenceSearchItem item;
+ private final float score;
+
+ FuzzySearchSpecificDTO(
+ final PreferenceSearchItem item,
+ final String keyword) {
+ this.item = item;
+
+ float attributeScoreSum = 0;
+ int countOfAttributesWithScore = 0;
+ for (final Map.Entry, Float> we
+ : WEIGHT_MAP.entrySet()) {
+ final String valueToProcess = we.getKey().apply(item);
+ if (valueToProcess.isEmpty()) {
+ continue;
+ }
+
+ attributeScoreSum +=
+ FUZZY_SCORE.fuzzyScore(valueToProcess, keyword) * we.getValue();
+ countOfAttributesWithScore++;
+ }
+
+ if (countOfAttributesWithScore != 0) {
+ this.score = attributeScoreSum / countOfAttributesWithScore;
+ } else {
+ this.score = 0;
+ }
+ }
+
+ public PreferenceSearchItem getItem() {
+ return item;
+ }
+
+ public float getScore() {
+ return score;
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java
new file mode 100644
index 000000000..1f507c7f1
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java
@@ -0,0 +1,199 @@
+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.annotation.XmlRes;
+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.stream.Collectors;
+
+/**
+ * Parses the corresponding preference-file(s).
+ */
+public class PreferenceParser {
+ private static final String TAG = "PreferenceParser";
+
+ private static final String NS_ANDROID = "http://schemas.android.com/apk/res/android";
+ private static final String NS_SEARCH = "http://schemas.android.com/apk/preferencesearch";
+
+ private final Context context;
+ private final Map allPreferences;
+ private final PreferenceSearchConfiguration searchConfiguration;
+
+ public PreferenceParser(
+ final Context context,
+ final PreferenceSearchConfiguration searchConfiguration
+ ) {
+ this.context = context;
+ this.allPreferences = PreferenceManager.getDefaultSharedPreferences(context).getAll();
+ this.searchConfiguration = searchConfiguration;
+ }
+
+ public List parse(
+ @XmlRes final int resId
+ ) {
+ final List results = new ArrayList<>();
+ final XmlPullParser xpp = context.getResources().getXml(resId);
+
+ try {
+ xpp.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ xpp.setFeature(XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES, true);
+
+ final List breadcrumbs = new ArrayList<>();
+ while (xpp.getEventType() != XmlPullParser.END_DOCUMENT) {
+ if (xpp.getEventType() == XmlPullParser.START_TAG) {
+ final PreferenceSearchItem result = parseSearchResult(
+ xpp,
+ joinBreadcrumbs(breadcrumbs),
+ resId
+ );
+
+ if (!searchConfiguration.getParserIgnoreElements().contains(xpp.getName())
+ && result.hasData()
+ && !"true".equals(getAttribute(xpp, NS_SEARCH, "ignore"))) {
+ results.add(result);
+ }
+ if (searchConfiguration.getParserContainerElements().contains(xpp.getName())) {
+ // This code adds breadcrumbs for certain containers (e.g. PreferenceScreen)
+ // Example: Video and Audio > Player
+ 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=" + resId, e);
+ }
+ return results;
+ }
+
+ private String joinBreadcrumbs(final List breadcrumbs) {
+ return breadcrumbs.stream()
+ .filter(crumb -> !TextUtils.isEmpty(crumb))
+ .collect(Collectors.joining(" > "));
+ }
+
+ 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,
+ @XmlRes 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),
+ breadcrumbs,
+ searchIndexItemResId
+ );
+ }
+
+ private String[] readStringArray(@Nullable final String s) {
+ if (s == null) {
+ return new String[0];
+ }
+ if (s.startsWith("@")) {
+ try {
+ return context.getResources().getStringArray(Integer.parseInt(s.substring(1)));
+ } catch (final Exception e) {
+ Log.w(TAG, "Unable to readStringArray from '" + s + "'", e);
+ }
+ }
+ return new String[0];
+ }
+
+ private String readString(@Nullable final String s) {
+ if (s == null) {
+ return "";
+ }
+ if (s.startsWith("@")) {
+ try {
+ return context.getString(Integer.parseInt(s.substring(1)));
+ } catch (final Exception e) {
+ Log.w(TAG, "Unable to readString from '" + s + "'", e);
+ }
+ }
+ return s;
+ }
+
+ private String tryFillInPreferenceValue(
+ @Nullable final String s,
+ @Nullable final String key,
+ final String[] entries,
+ final String[] entryValues
+ ) {
+ if (s == null) {
+ return "";
+ }
+ if (key == null) {
+ return s;
+ }
+
+ // Resolve value
+ Object prefValue = allPreferences.get(key);
+ if (prefValue == null) {
+ return s;
+ }
+
+ /*
+ * Resolve ListPreference values
+ *
+ * entryValues = Values/Keys that are saved
+ * entries = Actual human readable names
+ */
+ if (entries.length > 0 && entryValues.length == entries.length) {
+ final int entryIndex = Arrays.asList(entryValues).indexOf(prefValue);
+ if (entryIndex != -1) {
+ prefValue = entries[entryIndex];
+ }
+ }
+
+ return String.format(s, prefValue.toString());
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java
new file mode 100644
index 000000000..02fbf9577
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java
@@ -0,0 +1,87 @@
+package org.schabi.newpipe.settings.preferencesearch;
+
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.schabi.newpipe.databinding.SettingsPreferencesearchListItemResultBinding;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+class PreferenceSearchAdapter
+ extends RecyclerView.Adapter {
+ private List dataset = new ArrayList<>();
+ private Consumer onItemClickListener;
+
+ @NonNull
+ @Override
+ public PreferenceViewHolder onCreateViewHolder(
+ @NonNull final ViewGroup parent,
+ final int viewType
+ ) {
+ return new PreferenceViewHolder(
+ SettingsPreferencesearchListItemResultBinding.inflate(
+ LayoutInflater.from(parent.getContext()),
+ parent,
+ false));
+ }
+
+ @Override
+ public void onBindViewHolder(
+ @NonNull final PreferenceViewHolder holder,
+ final int position
+ ) {
+ final PreferenceSearchItem item = dataset.get(position);
+
+ holder.binding.title.setText(item.getTitle());
+
+ if (TextUtils.isEmpty(item.getSummary())) {
+ holder.binding.summary.setVisibility(View.GONE);
+ } else {
+ holder.binding.summary.setVisibility(View.VISIBLE);
+ holder.binding.summary.setText(item.getSummary());
+ }
+
+ if (TextUtils.isEmpty(item.getBreadcrumbs())) {
+ holder.binding.breadcrumbs.setVisibility(View.GONE);
+ } else {
+ holder.binding.breadcrumbs.setVisibility(View.VISIBLE);
+ holder.binding.breadcrumbs.setText(item.getBreadcrumbs());
+ }
+
+ holder.itemView.setOnClickListener(v -> {
+ if (onItemClickListener != null) {
+ onItemClickListener.accept(item);
+ }
+ });
+ }
+
+ void setContent(final List items) {
+ dataset = new ArrayList<>(items);
+ this.notifyDataSetChanged();
+ }
+
+ @Override
+ public int getItemCount() {
+ return dataset.size();
+ }
+
+ void setOnItemClickListener(final Consumer onItemClickListener) {
+ this.onItemClickListener = onItemClickListener;
+ }
+
+ static class PreferenceViewHolder extends RecyclerView.ViewHolder {
+ final SettingsPreferencesearchListItemResultBinding binding;
+
+ PreferenceViewHolder(final SettingsPreferencesearchListItemResultBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java
new file mode 100644
index 000000000..5835dcab5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java
@@ -0,0 +1,43 @@
+package org.schabi.newpipe.settings.preferencesearch;
+
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+public class PreferenceSearchConfiguration {
+ private PreferenceSearchFunction searcher = new PreferenceFuzzySearchFunction();
+
+ private final List parserIgnoreElements = Arrays.asList(
+ PreferenceCategory.class.getSimpleName());
+ private final List parserContainerElements = Arrays.asList(
+ PreferenceCategory.class.getSimpleName(),
+ PreferenceScreen.class.getSimpleName());
+
+
+ public void setSearcher(final PreferenceSearchFunction searcher) {
+ this.searcher = Objects.requireNonNull(searcher);
+ }
+
+ public PreferenceSearchFunction getSearcher() {
+ return searcher;
+ }
+
+ public List getParserIgnoreElements() {
+ return parserIgnoreElements;
+ }
+
+ public List getParserContainerElements() {
+ return parserContainerElements;
+ }
+
+ @FunctionalInterface
+ public interface PreferenceSearchFunction {
+ Stream search(
+ Stream allAvailable,
+ String keyword);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java
new file mode 100644
index 000000000..308abbc4e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java
@@ -0,0 +1,80 @@
+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 org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays the search results.
+ */
+public class PreferenceSearchFragment extends Fragment {
+ public static final String NAME = PreferenceSearchFragment.class.getSimpleName();
+
+ private PreferenceSearcher searcher;
+
+ private SettingsPreferencesearchFragmentBinding binding;
+ private PreferenceSearchAdapter adapter;
+
+ public void setSearcher(final PreferenceSearcher searcher) {
+ this.searcher = searcher;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ @NonNull final LayoutInflater inflater,
+ @Nullable final ViewGroup container,
+ @Nullable final Bundle savedInstanceState
+ ) {
+ binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false);
+
+ binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext()));
+
+ adapter = new PreferenceSearchAdapter();
+ adapter.setOnItemClickListener(this::onItemClicked);
+ binding.searchResults.setAdapter(adapter);
+
+ return binding.getRoot();
+ }
+
+ public void updateSearchResults(final String keyword) {
+ if (adapter == null || searcher == null) {
+ return;
+ }
+
+ final List results =
+ !TextUtils.isEmpty(keyword)
+ ? searcher.searchFor(keyword)
+ : new ArrayList<>();
+
+ adapter.setContent(new ArrayList<>(results));
+
+ setEmptyViewShown(results.isEmpty());
+ }
+
+ private void setEmptyViewShown(final boolean shown) {
+ binding.emptyStateView.setVisibility(shown ? View.VISIBLE : View.GONE);
+ binding.searchResults.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);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java
new file mode 100644
index 000000000..52935ef8e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java
@@ -0,0 +1,102 @@
+package org.schabi.newpipe.settings.preferencesearch;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.XmlRes;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a preference-item inside the search.
+ */
+public class PreferenceSearchItem {
+ /**
+ * Key of the setting/preference. E.g. used inside {@link android.content.SharedPreferences}.
+ */
+ @NonNull
+ private final String key;
+ /**
+ * Title of the setting, e.g. 'Default resolution' or 'Show higher resolutions'.
+ */
+ @NonNull
+ private final String title;
+ /**
+ * Summary of the setting, e.g. '480p' or 'Only some devices can play 2k/4k'.
+ */
+ @NonNull
+ private final String summary;
+ /**
+ * Possible entries of the setting, e.g. 480p,720p,...
+ */
+ @NonNull
+ private final String entries;
+ /**
+ * Breadcrumbs - a hint where the setting is located e.g. 'Video and Audio > Player'
+ */
+ @NonNull
+ private final String breadcrumbs;
+ /**
+ * The xml-resource where this item was found/built from.
+ */
+ @XmlRes
+ 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 breadcrumbs,
+ @XmlRes final int searchIndexItemResId
+ ) {
+ this.key = Objects.requireNonNull(key);
+ this.title = Objects.requireNonNull(title);
+ this.summary = Objects.requireNonNull(summary);
+ this.entries = Objects.requireNonNull(entries);
+ 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 int getSearchIndexItemResId() {
+ return searchIndexItemResId;
+ }
+
+ boolean hasData() {
+ return !key.isEmpty() && !title.isEmpty();
+ }
+
+ public List getAllRelevantSearchFields() {
+ return Arrays.asList(
+ getTitle(),
+ getSummary(),
+ getEntries(),
+ getBreadcrumbs());
+ }
+
+
+ @Override
+ public String toString() {
+ return "PreferenceItem: " + title + " " + summary + " " + key;
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java
new file mode 100644
index 000000000..418a3ea46
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java
@@ -0,0 +1,127 @@
+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.
+ *
+ * Note: This function is Thread independent (can be called from outside of the main thread).
+ *
+ * @param item The item to highlight
+ * @param prefsFragment The fragment where the items is located on
+ */
+ 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);
+ }, 200);
+ return;
+ }
+ }
+ highlightFallback(prefsFragment, prefResult);
+ }
+
+ /**
+ * Alternative highlighting (shows an → arrow in front of the setting)if ripple does not work.
+ *
+ * @param prefsFragment
+ * @param prefResult
+ */
+ private static void highlightFallback(
+ final PreferenceFragmentCompat prefsFragment,
+ final Preference prefResult
+ ) {
+ // Get primary color from text for highlight icon
+ final TypedValue typedValue = new TypedValue();
+ final Resources.Theme theme = prefsFragment.getActivity().getTheme();
+ theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true);
+ final TypedArray arr = prefsFragment.getActivity()
+ .obtainStyledAttributes(
+ typedValue.data,
+ new int[]{android.R.attr.textColorPrimary});
+ final int color = arr.getColor(0, 0xffE53935);
+ arr.recycle();
+
+ // Show highlight icon
+ final Drawable oldIcon = prefResult.getIcon();
+ final boolean oldSpaceReserved = prefResult.isIconSpaceReserved();
+ final Drawable highlightIcon =
+ AppCompatResources.getDrawable(
+ prefsFragment.requireContext(),
+ R.drawable.ic_play_arrow);
+ highlightIcon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
+ prefResult.setIcon(highlightIcon);
+
+ prefsFragment.scrollToPreference(prefResult);
+
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ prefResult.setIcon(oldIcon);
+ prefResult.setIconSpaceReserved(oldSpaceReserved);
+ }, 1000);
+ }
+
+ private static void showRippleAnimation(final RippleDrawable rippleDrawable) {
+ rippleDrawable.setState(
+ new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled});
+ new Handler(Looper.getMainLooper())
+ .postDelayed(() -> rippleDrawable.setState(new int[]{}), 1000);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java
new file mode 100644
index 000000000..1f0636454
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java
@@ -0,0 +1,7 @@
+package org.schabi.newpipe.settings.preferencesearch;
+
+import androidx.annotation.NonNull;
+
+public interface PreferenceSearchResultListener {
+ void onSearchResultClicked(@NonNull PreferenceSearchItem result);
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java
new file mode 100644
index 000000000..176dc5d14
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java
@@ -0,0 +1,35 @@
+package org.schabi.newpipe.settings.preferencesearch;
+
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class PreferenceSearcher {
+ private final List allEntries = new ArrayList<>();
+
+ private final PreferenceSearchConfiguration configuration;
+
+ public PreferenceSearcher(final PreferenceSearchConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ public void add(final List items) {
+ allEntries.addAll(items);
+ }
+
+ List searchFor(final String keyword) {
+ if (TextUtils.isEmpty(keyword)) {
+ return new ArrayList<>();
+ }
+
+ return configuration.getSearcher()
+ .search(allEntries.stream(), keyword)
+ .collect(Collectors.toList());
+ }
+
+ public void clear() {
+ allEntries.clear();
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/package-info.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/package-info.java
new file mode 100644
index 000000000..00929235e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/package-info.java
@@ -0,0 +1,10 @@
+/**
+ * Contains classes for searching inside the preferences.
+ *
+ * This code is based on
+ * ByteHamster/SearchPreference
+ * (MIT license) but was heavily modified/refactored for our use.
+ *
+ * @author litetex
+ */
+package org.schabi.newpipe.settings.preferencesearch;
diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java
index 95f7f50ba..490e299bd 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java
@@ -44,8 +44,6 @@ import java.util.List;
import static org.schabi.newpipe.settings.tabs.Tab.typeFrom;
public class ChooseTabsFragment extends Fragment {
- private static final int MENU_ITEM_RESTORE_ID = 123456;
-
private TabsManager tabsManager;
private final List tabList = new ArrayList<>();
@@ -110,21 +108,14 @@ public class ChooseTabsFragment extends Fragment {
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
- final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE,
- R.string.restore_defaults);
+ final MenuItem restoreItem = menu.add(R.string.restore_defaults);
restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(),
R.drawable.ic_settings_backup_restore));
- }
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item) {
- if (item.getItemId() == MENU_ITEM_RESTORE_ID) {
+ restoreItem.setOnMenuItemClickListener(ev -> {
restoreDefaults();
return true;
- }
-
- return super.onOptionsItemSelected(item);
+ });
}
/*//////////////////////////////////////////////////////////////////////////
diff --git a/app/src/main/java/org/schabi/newpipe/util/KeyboardUtil.java b/app/src/main/java/org/schabi/newpipe/util/KeyboardUtil.java
new file mode 100644
index 000000000..71c0d3944
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/KeyboardUtil.java
@@ -0,0 +1,43 @@
+package org.schabi.newpipe.util;
+
+import android.app.Activity;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+import androidx.core.content.ContextCompat;
+
+/**
+ * Utility class for the Android keyboard.
+ *
+ * See also https://stackoverflow.com/q/1109022
+ *
+ */
+public final class KeyboardUtil {
+ private KeyboardUtil() {
+ }
+
+ public static void showKeyboard(final Activity activity, final EditText editText) {
+ if (activity == null || editText == null) {
+ return;
+ }
+
+ if (editText.requestFocus()) {
+ final InputMethodManager imm = ContextCompat.getSystemService(activity,
+ InputMethodManager.class);
+ imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED);
+ }
+ }
+
+ public static void hideKeyboard(final Activity activity, final EditText editText) {
+ if (activity == null || editText == null) {
+ return;
+ }
+
+ final InputMethodManager imm = ContextCompat.getSystemService(activity,
+ InputMethodManager.class);
+ imm.hideSoftInputFromWindow(editText.getWindowToken(),
+ InputMethodManager.RESULT_UNCHANGED_SHOWN);
+
+ editText.clearFocus();
+ }
+}
diff --git a/app/src/main/res/layout/settings_layout.xml b/app/src/main/res/layout/settings_layout.xml
index 33237d7b0..1b7b8b5e2 100644
--- a/app/src/main/res/layout/settings_layout.xml
+++ b/app/src/main/res/layout/settings_layout.xml
@@ -6,14 +6,14 @@
android:orientation="vertical"
tools:context="org.schabi.newpipe.MainActivity">
+
+
-
-
diff --git a/app/src/main/res/layout/settings_preferencesearch_fragment.xml b/app/src/main/res/layout/settings_preferencesearch_fragment.xml
new file mode 100644
index 000000000..89a25b217
--- /dev/null
+++ b/app/src/main/res/layout/settings_preferencesearch_fragment.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/settings_preferencesearch_list_item_result.xml b/app/src/main/res/layout/settings_preferencesearch_list_item_result.xml
new file mode 100644
index 000000000..2e20f274c
--- /dev/null
+++ b/app/src/main/res/layout/settings_preferencesearch_list_item_result.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_settings_main_fragment.xml b/app/src/main/res/menu/menu_settings_main_fragment.xml
new file mode 100644
index 000000000..fbe3b4e09
--- /dev/null
+++ b/app/src/main/res/menu/menu_settings_main_fragment.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f7ad41ba8..64ed6980b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -441,6 +441,7 @@
Captions
Modify player caption text scale and background styles. Requires app restart to take effect
+ LeakCanary is not available
Memory leak monitoring may cause the app to become unresponsive when heap dumping
Show memory leaks
Report out-of-lifecycle errors
diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml
index 23b782ffd..e754b3a30 100644
--- a/app/src/main/res/xml/content_settings.xml
+++ b/app/src/main/res/xml/content_settings.xml
@@ -128,13 +128,6 @@
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
-
-
+
+
diff --git a/app/src/debug/res/xml/main_settings.xml b/app/src/main/res/xml/main_settings.xml
similarity index 100%
rename from app/src/debug/res/xml/main_settings.xml
rename to app/src/main/res/xml/main_settings.xml
diff --git a/app/src/main/res/xml/update_settings.xml b/app/src/main/res/xml/update_settings.xml
index ef121ec4e..a44555edf 100644
--- a/app/src/main/res/xml/update_settings.xml
+++ b/app/src/main/res/xml/update_settings.xml
@@ -1,7 +1,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-