From 07fb319e886f8bee02a4d1de96ecd2185999efa0 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Dec 2021 21:35:15 +0100 Subject: [PATCH] Applied code changes for preference search framework --- .../settings/AppearanceSettingsFragment.java | 2 +- .../settings/BasePreferenceFragment.java | 5 + .../settings/ContentSettingsFragment.java | 8 +- .../settings/DownloadSettingsFragment.java | 2 +- .../settings/HistorySettingsFragment.java | 2 +- .../settings/MainSettingsFragment.java | 43 +++- .../settings/NotificationSettingsFragment.kt | 2 +- .../newpipe/settings/SettingsActivity.java | 206 +++++++++++++++++- .../settings/SettingsResourceRegistry.java | 151 +++++++++++++ .../settings/UpdateSettingsFragment.java | 2 +- .../settings/VideoAudioSettingsFragment.java | 2 +- 11 files changed, 408 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java index 4bc5a210e..e08562908 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -17,7 +17,7 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment { @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) 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..d79ea0cd5 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; @@ -38,9 +41,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 +70,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) -> { 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..868618110 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java @@ -29,7 +29,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); 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..6cd165861 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -1,7 +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.annotation.NonNull; import androidx.preference.Preference; import org.schabi.newpipe.App; @@ -12,10 +16,15 @@ 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)); @@ -24,4 +33,36 @@ public class MainSettingsFragment extends BasePreferenceFragment { defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply(); } } + + @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/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 02e2538c5..787740cc2 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,42 @@ package org.schabi.newpipe.settings; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + import android.os.Bundle; +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.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.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; /* * Created by Christian Schabesberger on 31.08.15. @@ -39,7 +59,23 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; */ public class SettingsActivity extends AppCompatActivity - implements BasePreferenceFragment.OnPreferenceStartFragmentCallback { + implements + BasePreferenceFragment.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; + @Override protected void onCreate(final Bundle savedInstanceBundle) { setTheme(ThemeHelper.getSettingsThemeStyle(this)); @@ -49,6 +85,7 @@ public class SettingsActivity extends AppCompatActivity final SettingsLayoutBinding settingsLayoutBinding = SettingsLayoutBinding.inflate(getLayoutInflater()); setContentView(settingsLayoutBinding.getRoot()); + initSearch(settingsLayoutBinding); setSupportActionBar(settingsLayoutBinding.settingsToolbarLayout.toolbar); @@ -78,6 +115,12 @@ public class SettingsActivity extends AppCompatActivity 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 +134,165 @@ 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); + super.onDestroy(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Search + //////////////////////////////////////////////////////////////////////////*/ + //region Search + + private void initSearch(final SettingsLayoutBinding settingsLayoutBinding) { + 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(() -> onSearchChanged())); + + // Configure clear button + searchContainer.findViewById(R.id.toolbar_search_clear) + .setOnClickListener(ev -> resetSearchText()); + + // Build search configuration using SettingsResourceRegistry + prepareSearchConfig(); + + final PreferenceSearchConfiguration config = new PreferenceSearchConfiguration(); + SettingsResourceRegistry.getInstance().getAllEntries().stream() + .filter(SettingsResourceRegistry.SettingRegistryEntry::isSearchable) + .map(SettingsResourceRegistry.SettingRegistryEntry::getPreferencesResId) + .forEach(config::index); + + searchFragment = new PreferenceSearchFragment(config); + } + + private void prepareSearchConfig() { + // Check if the update settings should be available + if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) { + SettingsResourceRegistry.getInstance() + .getEntryByPreferencesResId(R.xml.update_settings) + .setSearchable(false); + } + } + + public void setMenuSearchItem(final MenuItem menuSearchItem) { + this.menuSearchItem = menuSearchItem; + } + + public void setSearchActive(final boolean active) { + // Ignore if search is already in correct state + if (isSearchActive() == active) { + return; + } + + if (DEBUG) { + Log.d(TAG, "setSearchActive called active=" + active); + } + + searchContainer.setVisibility(active ? View.VISIBLE : View.GONE); + if (menuSearchItem != null) { + menuSearchItem.setVisible(!active); + } + + final FragmentManager fm = getSupportFragmentManager(); + if (active) { + fm.beginTransaction() + .add(FRAGMENT_HOLDER_ID, searchFragment, PreferenceSearchFragment.NAME) + .addToBackStack(PreferenceSearchFragment.NAME) + .commit(); + } else if (searchFragment != null) { + fm.beginTransaction().remove(searchFragment).commit(); + fm.popBackStack( + PreferenceSearchFragment.NAME, + FragmentManager.POP_BACK_STACK_INCLUSIVE); + + KeyboardUtil.hideKeyboard(this, searchEditText); + } + + resetSearchText(); + } + + private void resetSearchText() { + searchEditText.setText(""); + } + + private boolean isSearchActive() { + return searchContainer.getVisibility() == View.VISIBLE; + } + + private void onSearchChanged() { + if (!isSearchActive()) { + return; + } + + if (searchFragment != null) { + searchFragment.updateSearchResults(this.searchEditText.getText().toString()); + } + } + + @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 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..c4db9f93d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java @@ -0,0 +1,151 @@ +package org.schabi.newpipe.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +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: + * + * + * 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(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 fragmentClass, + @XmlRes final int preferencesResId + ) { + final SettingRegistryEntry entry = + new SettingRegistryEntry(fragmentClass, preferencesResId); + this.registeredEntries.add(entry); + return entry; + } + + @Nullable + public SettingRegistryEntry getEntryByFragmentClass( + final Class fragmentClass + ) { + Objects.requireNonNull(fragmentClass); + return registeredEntries.stream() + .filter(e -> Objects.equals(e.getFragmentClass(), fragmentClass)) + .findFirst() + .orElse(null); + } + + @Nullable + 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 fragmentClass) { + final SettingRegistryEntry entry = getEntryByFragmentClass(fragmentClass); + if (entry == null) { + return -1; + } + return entry.getPreferencesResId(); + } + + @Nullable + public Class 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 fragmentClass; + @XmlRes + private final int preferencesResId; + + private boolean searchable = true; + + public SettingRegistryEntry( + @NonNull final Class 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 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();