diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchViewModel.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchViewModel.kt index 9abc36374..3bb6a7d50 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.search.filter.FilterItem +import org.schabi.newpipe.fragments.list.search.filter.InjectFilterItem import org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic import org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.Factory.Variant @@ -38,11 +39,16 @@ class SearchViewModel( val doSearchLiveData: LiveData get() = doSearchMutableLiveData - val searchFilterLogic = SearchFilterLogic.Factory.create( - logicVariant, NewPipe.getService(serviceId).searchQHFactory, null - ) + var searchFilterLogic: SearchFilterLogic init { + // inject before creating SearchFilterLogic + InjectFilterItem.DividerBetweenYoutubeAndYoutubeMusic.run() + + searchFilterLogic = SearchFilterLogic.Factory.create( + logicVariant, + NewPipe.getService(serviceId).searchQHFactory, null + ) searchFilterLogic.restorePreviouslySelectedFilters( userSelectedContentFilterList, userSelectedSortFilterList diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/InjectFilterItem.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/InjectFilterItem.java new file mode 100644 index 000000000..8b4ccc54d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/InjectFilterItem.java @@ -0,0 +1,147 @@ +package org.schabi.newpipe.fragments.list.search.filter; + +import org.schabi.newpipe.App; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.search.filter.FilterContainer; +import org.schabi.newpipe.extractor.search.filter.FilterGroup; +import org.schabi.newpipe.extractor.search.filter.FilterItem; +import org.schabi.newpipe.extractor.search.filter.LibraryStringIds; +import org.schabi.newpipe.extractor.services.youtube.search.filter.YoutubeFilters; + +import java.util.List; + +import androidx.annotation.NonNull; + +/** + * Inject a {@link FilterItem} that actually should not be a real filter. + *

+ * This base class is meant to inject eg {@link DividerItem} (that inherits {@link FilterItem}) + * as Divider between {@link FilterItem}. It will be shown in the UI's. + *

+ * Of course you have to handle {@link DividerItem} or whatever in the Ui's. + * For that for example have a look at {@link SearchFilterDialogSpinnerAdapter}. + */ +public abstract class InjectFilterItem { + + protected InjectFilterItem( + @NonNull final String serviceName, + final int injectedAfterFilterWithId, + @NonNull final FilterItem toBeInjectedFilterItem) { + + prepareAndInject(serviceName, injectedAfterFilterWithId, toBeInjectedFilterItem); + } + + // Please refer a static boolean to determine if already injected + protected abstract boolean isAlreadyInjected(); + + // Please refer a static boolean to determine if already injected + protected abstract void setAsInjected(); + + private void prepareAndInject( + @NonNull final String serviceName, + final int injectedAfterFilterWithId, + @NonNull final FilterItem toBeInjectedFilterItem) { + + if (isAlreadyInjected()) { // already run + return; + } + + try { // using serviceName to test if we are trying to inject into the right service + final List groups = NewPipe.getService(serviceName) + .getSearchQHFactory().getAvailableContentFilter().getFilterGroups(); + injectFilterItemIntoGroup( + groups, + injectedAfterFilterWithId, + toBeInjectedFilterItem); + setAsInjected(); + } catch (final ExtractionException ignored) { + // no the service we want to prepareAndInject -> so ignore + } + } + + private void injectFilterItemIntoGroup( + @NonNull final List groups, + final int injectedAfterFilterWithId, + @NonNull final FilterItem toBeInjectedFilterItem) { + + int indexForFilterId = 0; + boolean isFilterItemFound = false; + FilterGroup groupWithTheSearchFilterItem = null; + + for (final FilterGroup group : groups) { + for (final FilterItem item : group.getFilterItems()) { + if (item.getIdentifier() == injectedAfterFilterWithId) { + isFilterItemFound = true; + break; + } + indexForFilterId++; + } + + if (isFilterItemFound) { + groupWithTheSearchFilterItem = group; + break; + } + } + + if (isFilterItemFound) { + // we want to insert after the FilterItem we've searched + indexForFilterId++; + groupWithTheSearchFilterItem.getFilterItems() + .add(indexForFilterId, toBeInjectedFilterItem); + } + } + + /** + * Inject DividerItem between YouTube content filters and YoutubeMusic content filters. + */ + public static class DividerBetweenYoutubeAndYoutubeMusic extends InjectFilterItem { + + private static boolean isYoutubeMusicDividerInjected = false; + + protected DividerBetweenYoutubeAndYoutubeMusic() { + super(App.getApp().getApplicationContext().getString(R.string.youtube), + YoutubeFilters.ID_CF_MAIN_PLAYLISTS, + new DividerItem(R.string.search_filters_youtube_music) + ); + } + + /** + * Have a static runner method to avoid creating unnecessary objects if already inserted. + */ + public static void run() { + if (!isYoutubeMusicDividerInjected) { + new DividerBetweenYoutubeAndYoutubeMusic(); + } + } + + @Override + protected boolean isAlreadyInjected() { + return isYoutubeMusicDividerInjected; + } + + @Override + protected void setAsInjected() { + isYoutubeMusicDividerInjected = true; + } + } + + /** + * Used to have a title divider between regular {@link FilterItem}s. + */ + public static class DividerItem extends FilterItem { + + private final int resId; + + public DividerItem(final int resId) { + // the LibraryStringIds.. is not needed at all I just need one to satisfy FilterItem. + super(FilterContainer.ITEM_IDENTIFIER_UNKNOWN, LibraryStringIds.SEARCH_FILTERS_ALL); + this.resId = resId; + } + + public int getStringResId() { + return this.resId; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogSpinnerAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogSpinnerAdapter.java index bdc26d9ed..dd18dce78 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogSpinnerAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogSpinnerAdapter.java @@ -23,6 +23,8 @@ import java.util.Objects; import androidx.annotation.NonNull; import androidx.collection.SparseArrayCompat; +import static org.schabi.newpipe.fragments.list.search.filter.InjectFilterItem.DividerItem; + public class SearchFilterDialogSpinnerAdapter extends BaseAdapter { private final Context context; @@ -48,12 +50,12 @@ public class SearchFilterDialogSpinnerAdapter extends BaseAdapter { @Override public int getCount() { - return group.getFilterItems().length; + return group.getFilterItems().size(); } @Override public Object getItem(final int position) { - return group.getFilterItems()[position]; + return group.getFilterItems().get(position); } @Override @@ -63,7 +65,7 @@ public class SearchFilterDialogSpinnerAdapter extends BaseAdapter { @Override public View getView(final int position, final View convertView, final ViewGroup parent) { - final FilterItem item = group.getFilterItems()[position]; + final FilterItem item = group.getFilterItems().get(position); final TextView view; if (convertView != null) { @@ -89,11 +91,12 @@ public class SearchFilterDialogSpinnerAdapter extends BaseAdapter { view.setVisibility(wrappedView.getVisibility()); view.setEnabled(wrappedView.isEnabled()); - if (item instanceof FilterItem.DividerItem) { + if (item instanceof DividerItem) { + final DividerItem dividerItem = (DividerItem) item; wrappedView.setEnabled(false); view.setEnabled(wrappedView.isEnabled()); final String menuDividerTitle = ">>>" - + ServiceHelper.getTranslatedFilterString(item.getNameId(), context) + "<<<"; + + context.getString(dividerItem.getStringResId()) + "<<<"; view.setText(menuDividerTitle); } } @@ -111,7 +114,7 @@ public class SearchFilterDialogSpinnerAdapter extends BaseAdapter { isInitialEnabled, spinner); - if (item instanceof FilterItem.DividerItem) { + if (item instanceof DividerItem) { wrappedView.setEnabled(false); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterLogic.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterLogic.java index 15a9d292f..ca7754e78 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterLogic.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterLogic.java @@ -8,7 +8,6 @@ import org.schabi.newpipe.extractor.search.filter.FilterGroup; import org.schabi.newpipe.extractor.search.filter.FilterItem; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -185,7 +184,7 @@ public class SearchFilterLogic { final List sortGroups = getAllSortFilterGroups(filters); uiSortFilterWorker = createUiForFiltersWorker; - initFiltersUi(sortGroups.toArray(new FilterGroup[0]), + initFiltersUi(sortGroups, sortFilterIdToUiItemMap, createUiForFiltersWorker); @@ -202,7 +201,7 @@ public class SearchFilterLogic { * @param createUiForFiltersWorker the implementation how to create the UI. */ private void initFiltersUi( - @NonNull final FilterGroup[] filterGroups, + @NonNull final List filterGroups, @NonNull final SparseArrayCompat filterIdToUiItemMap, @NonNull final ICreateUiForFiltersWorker createUiForFiltersWorker) { @@ -235,7 +234,7 @@ public class SearchFilterLogic { * @param fidToSupersetSortFilterMap null possible, only for content filters relevant */ private void initFilters( - @NonNull final FilterGroup[] filterGroups, + @NonNull final List filterGroups, @NonNull final ExclusiveGroups exclusive, @NonNull final List selectedFilters, @Nullable final SparseArrayCompat fidToSupersetSortFilterMap) { @@ -290,9 +289,7 @@ public class SearchFilterLogic { private void initSortFilters() { final FilterContainer filters = searchQHFactory.getAvailableContentFilter(); final List sortGroups = getAllSortFilterGroups(filters); - - initFilters(sortGroups.toArray(new FilterGroup[0]), sortFilterExclusive, - selectedSortFilters, null); + initFilters(sortGroups, sortFilterExclusive, selectedSortFilters, null); } /** @@ -450,7 +447,7 @@ public class SearchFilterLogic { for (final FilterGroup filterGroup : filters.getFilterGroups()) { final FilterContainer sf = filterGroup.getAllSortFilters(); if (sf != null && sf.getFilterGroups() != null) { - sortGroups.addAll(Arrays.asList(sf.getFilterGroups())); + sortGroups.addAll(sf.getFilterGroups()); } } return sortGroups; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterOptionMenuAlikeDialogGenerator.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterOptionMenuAlikeDialogGenerator.java index a54014c9f..adc98a5f3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterOptionMenuAlikeDialogGenerator.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterOptionMenuAlikeDialogGenerator.java @@ -23,6 +23,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import static android.util.TypedValue.COMPLEX_UNIT_DIP; +import static org.schabi.newpipe.fragments.list.search.filter.InjectFilterItem.DividerItem; public class SearchFilterOptionMenuAlikeDialogGenerator extends BaseSearchFilterUiDialogGenerator { private static final Integer NO_RESIZE_VIEW_TAG = 1; @@ -149,7 +150,7 @@ public class SearchFilterOptionMenuAlikeDialogGenerator extends BaseSearchFilter for (final FilterItem item : filterGroup.getFilterItems()) { final View view; - if (item instanceof FilterItem.DividerItem) { + if (item instanceof DividerItem) { view = createDividerTextView(item, getLayoutParamsViews()); } else { view = createViewItemRadio(item, getLayoutParamsViews()); @@ -175,7 +176,7 @@ public class SearchFilterOptionMenuAlikeDialogGenerator extends BaseSearchFilter @NonNull final UiSelectorDelegate selectorDelegate) { for (final FilterItem item : filterGroup.getFilterItems()) { final View view; - if (item instanceof FilterItem.DividerItem) { + if (item instanceof DividerItem) { view = createDividerTextView(item, getLayoutParamsViews()); } else { final CheckBox checkBox = createCheckBox(item, getLayoutParamsViews()); @@ -278,10 +279,11 @@ public class SearchFilterOptionMenuAlikeDialogGenerator extends BaseSearchFilter @NonNull private TextView createDividerTextView(@NonNull final FilterItem item, @NonNull final ViewGroup.LayoutParams layoutParams) { + final DividerItem dividerItem = (DividerItem) item; final TextView view = new TextView(context); view.setEnabled(true); final String menuDividerTitle = - ServiceHelper.getTranslatedFilterString(item.getNameId(), context); + context.getString(dividerItem.getStringResId()); view.setText(menuDividerTitle); view.setGravity(Gravity.TOP); view.setLayoutParams(layoutParams); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterUIOptionMenu.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterUIOptionMenu.java index 8291c70cc..605c15daf 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterUIOptionMenu.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterUIOptionMenu.java @@ -24,6 +24,7 @@ import androidx.appcompat.view.menu.MenuBuilder; import androidx.core.view.MenuCompat; import static android.content.ContentValues.TAG; +import static org.schabi.newpipe.fragments.list.search.filter.InjectFilterItem.DividerItem; import static org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.ICreateUiForFiltersWorker; import static org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.IUiItemWrapper; @@ -227,9 +228,10 @@ public class SearchFilterUIOptionMenu extends BaseSearchFilterUiGenerator { @NonNull final FilterGroup filterGroup) { final MenuItem item = createMenuItem(filterItem); - if (filterItem instanceof FilterItem.DividerItem) { + if (filterItem instanceof DividerItem) { + final DividerItem dividerItem = (DividerItem) filterItem; final String menuDividerTitle = ">>>" - + ServiceHelper.getTranslatedFilterString(filterItem.getNameId(), context) + + context.getString(dividerItem.getStringResId()) + "<<<"; item.setTitle(menuDividerTitle); item.setEnabled(false); diff --git a/app/src/test/java/org/schabi/newpipe/filter/InjectFilterItemTest.java b/app/src/test/java/org/schabi/newpipe/filter/InjectFilterItemTest.java new file mode 100644 index 000000000..ceb508efa --- /dev/null +++ b/app/src/test/java/org/schabi/newpipe/filter/InjectFilterItemTest.java @@ -0,0 +1,85 @@ +package org.schabi.newpipe.filter; + +import org.junit.Test; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.search.filter.FilterContainer; +import org.schabi.newpipe.extractor.search.filter.FilterGroup; +import org.schabi.newpipe.extractor.search.filter.FilterItem; +import org.schabi.newpipe.extractor.services.youtube.search.filter.YoutubeFilters; +import org.schabi.newpipe.fragments.list.search.filter.InjectFilterItem; + +import java.util.Collection; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import androidx.annotation.NonNull; + +import static junit.framework.TestCase.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class InjectFilterItemTest { + + static final String SERVICE_NAME = "YouTube"; + + @Test + public void injectIntoFilterGroupTest() throws ExtractionException { + final FilterContainer filterContainer = NewPipe.getService(SERVICE_NAME) + .getSearchQHFactory().getAvailableContentFilter(); + + final AtomicInteger itemCount = new AtomicInteger(); + assertFalse(getInjectedFilterItem(filterContainer, itemCount).isPresent()); + + InjectDividerTestClass.run(SERVICE_NAME); + + final int expectedInjectedItemPosition = 5; + final AtomicInteger injectedItemPosition = new AtomicInteger(); + assertTrue(getInjectedFilterItem(filterContainer, injectedItemPosition).isPresent()); + assertTrue(itemCount.get() > injectedItemPosition.get()); + assertEquals(expectedInjectedItemPosition, injectedItemPosition.get()); + } + + @NonNull + private Optional getInjectedFilterItem( + @NonNull final FilterContainer filterContainer, + @NonNull final AtomicInteger itemCount) { + + return filterContainer.getFilterGroups().stream() + .map(FilterGroup::getFilterItems) + .flatMap(Collection::stream) + .filter(item -> { + itemCount.getAndIncrement(); + return item instanceof InjectFilterItem.DividerItem; + }) + .findAny(); + } + + public static class InjectDividerTestClass extends InjectFilterItem { + + private static boolean isDividerInjected = false; + + protected InjectDividerTestClass(@NonNull final String serviceName) { + super(serviceName, + YoutubeFilters.ID_CF_MAIN_PLAYLISTS, + new DividerItem(0) + ); + } + + public static void run(final String serviceName) { + if (!isDividerInjected) { + new InjectDividerTestClass(serviceName); + } + } + + @Override + protected boolean isAlreadyInjected() { + return isDividerInjected; + } + + @Override + protected void setAsInjected() { + isDividerInjected = true; + } + } +} diff --git a/app/src/test/java/org/schabi/newpipe/filter/SearchFilterLogicAndUiGeneratorTest.java b/app/src/test/java/org/schabi/newpipe/filter/SearchFilterLogicAndUiGeneratorTest.java index 356277652..4a1bcdb10 100644 --- a/app/src/test/java/org/schabi/newpipe/filter/SearchFilterLogicAndUiGeneratorTest.java +++ b/app/src/test/java/org/schabi/newpipe/filter/SearchFilterLogicAndUiGeneratorTest.java @@ -17,7 +17,6 @@ import org.schabi.newpipe.fragments.list.search.filter.BaseSearchFilterUiGenerat import org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -347,7 +346,7 @@ public class SearchFilterLogicAndUiGeneratorTest { private void expectSortFiltersToBeVisible(final int id) { final FilterContainer sortFilterVariant = service.getSearchQHFactory() .getContentFilterSortFilterVariant(id); - assertTrue(sortFilterVariant.getFilterGroups().length > 0); + assertTrue(!sortFilterVariant.getFilterGroups().isEmpty()); for (final FilterGroup group : sortFilterVariant.getFilterGroups()) { for (final FilterItem item : group.getFilterItems()) { final int itemId = item.getIdentifier(); @@ -389,9 +388,9 @@ public class SearchFilterLogicAndUiGeneratorTest { final FilterContainer allSortFilters = service.getSearchQHFactory() .getContentFilterSortFilterVariant(PeertubeFilters.ID_CF_MAIN_ALL); // second way - final Optional allSortFilters2 = Arrays.stream(service.getSearchQHFactory() - .getAvailableContentFilter() - .getFilterGroups()) + final Optional allSortFilters2 = service.getSearchQHFactory() + .getAvailableContentFilter() + .getFilterGroups().stream() .filter(filterGroup -> (filterGroup.getIdentifier() == PeertubeFilters.ID_CF_MAIN_GRP)) .findFirst(); @@ -399,7 +398,7 @@ public class SearchFilterLogicAndUiGeneratorTest { assertNotNull(allSortFilters); assertTrue(allSortFilters2.isPresent()); assertEquals(allSortFilters, allSortFilters2.get().getAllSortFilters()); - assertTrue(allSortFilters.getFilterGroups().length > 0); + assertTrue(!allSortFilters.getFilterGroups().isEmpty()); assertNotNull(sortWorker.areAnySortFiltersVisible); assertTrue(sortWorker.areAnySortFiltersVisible.isPresent()); assertFalse(sortWorker.areAnySortFiltersVisible.get());