diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/BaseSearchFilterUiGenerator.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/BaseSearchFilterUiGenerator.java new file mode 100644 index 000000000..a2aed2f57 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/BaseSearchFilterUiGenerator.java @@ -0,0 +1,98 @@ +// Created by evermind-zz 2022, licensed GNU GPL version 3 or later + +package org.schabi.newpipe.fragments.list.search.filter; + + +import android.content.Context; +import android.util.DisplayMetrics; +import android.util.TypedValue; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory; + +import static android.util.TypedValue.COMPLEX_UNIT_DIP; + +/** + * Base for any search filter UI. + *

+ * It extends SearchFilterLogic and is used as a base class to implement the UI interface + * for content and sort filter dialogs eg. {@link SearchFilterDialogGenerator} + * or {@link SearchFilterOptionMenuAlikeDialogGenerator}. + */ +public abstract class BaseSearchFilterUiGenerator extends SearchFilterLogic { + protected final ICreateUiForFiltersWorker contentFilterWorker; + protected final ICreateUiForFiltersWorker sortFilterWorker; + protected final Context context; + + protected BaseSearchFilterUiGenerator(final SearchQueryHandlerFactory linkHandlerFactory, + final Callback callback, + final Context context) { + super(linkHandlerFactory, callback); + this.context = context; + this.contentFilterWorker = createContentFilterWorker(); + this.sortFilterWorker = createSortFilterWorker(); + } + + // common helpers + public static float dipToPixels(final Context ctx, final float dipValue) { + final DisplayMetrics metrics = ctx.getResources().getDisplayMetrics(); + return TypedValue.applyDimension(COMPLEX_UNIT_DIP, dipValue, metrics); + } + + /** + * {@link ICreateUiForFiltersWorker}. + * + * @return the class that implements the UI for the content filters. + */ + protected abstract ICreateUiForFiltersWorker createContentFilterWorker(); + + /** + * {@link ICreateUiForFiltersWorker}. + * + * @return the class that implements the UI for the sort filters. + */ + protected abstract ICreateUiForFiltersWorker createSortFilterWorker(); + + protected int getSeparatorLineColorFromTheme() { + final TypedValue value = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorAccent, value, true); + return value.data; + } + + /** + * Create the complete UI for the search filter dialog and make sure the initial + * visibility of the UI elements is done. + */ + public void createSearchUI() { + initContentFiltersUi(contentFilterWorker); + initSortFiltersUi(sortFilterWorker); + // make sure that only sort filters relevant to the selected content filter are shown + showSortFilterContainerUI(); + } + + /** + * If UI is implemented within an fragment/activity this method has to be called from + * its corresponding lifecyle method manually. + */ + public abstract void onResume(); + + /** + * If UI is implemented within an fragment/activity this method has to be called from + * its corresponding lifecyle method manually. + */ + public abstract void onPause(); + + /** + * Helper interface used as 'function pointer'. + */ + protected interface UiWrapperMapDelegate { + void put(int identifier, IUiItemWrapper menuItemUiWrapper); + } + + /** + * Helper interface used as 'function pointer'. + */ + protected interface UiSelectorDelegate { + void selectFilter(int identifier); + } +} 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 new file mode 100644 index 000000000..e0a476601 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterLogic.java @@ -0,0 +1,752 @@ +// Created by evermind-zz 2022, licensed GNU GPL version 3 or later + +package org.schabi.newpipe.fragments.list.search.filter; + +import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory; +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.fragments.list.search.SearchFragment; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import androidx.annotation.Nullable; + +import static org.schabi.newpipe.extractor.search.filter.FilterContainer.ITEM_IDENTIFIER_UNKNOWN; + +/** + * This class handles all the user interaction with the content and sort filters + * of NewPipeExtractor. + *

+ * The class works standalone to just get the default selected filters eg. during init phase + * eg. in {@link SearchFragment#initializeFilterData()} + */ +public class SearchFilterLogic { + + /** + * This list is used to communicate with NewPipeExtractor. + * It contains only the content filter ids that the user has selected from the UI. + */ + private final List userSelectedContentFilters = new ArrayList<>(); + /** + * This list is used to communicate with NewPipeExtractor. + * It contains only the sort filter ids that the user has selected from the UI. + */ + private final List userSelectedSortFilters = new ArrayList<>(); + private final SearchQueryHandlerFactory searchQHFactory; + private final ExclusiveGroups contentFilterExclusive = new ExclusiveGroups(); + private final ExclusiveGroups sortFilterExclusive = new ExclusiveGroups(); + private final Map contentFilterIdToUiItemMap = new HashMap<>(); + private final Map sortFilterIdToUiItemMap = new HashMap<>(); + private final Map contentFilterFidToSupersetSortFilterMap = + new HashMap<>(); + private final Callback callback; + /** + * This list is used to store via Icepick and eventual store as preset + * It contains all the content filter ids that the user has selected. It + * contains the same ids than {@link #userSelectedContentFilters} + */ + private ArrayList selectedContentFilters = new ArrayList<>(); + /** + * This list is used to store via Icepick and eventual store as preset + * It contains all the sort filter ids that the user has selected and also + * default id of none visible but selected sort filters. + * It is a superset to {@link #userSelectedContentFilters}. + */ + private ArrayList selectedSortFilters = new ArrayList<>(); + private ICreateUiForFiltersWorker uiSortFilterWorker; + + + public SearchFilterLogic(final SearchQueryHandlerFactory searchQHFactory, + final Callback callback) { + this.searchQHFactory = searchQHFactory; + this.callback = callback; + initContentFilters(); + initSortFilters(); + } + + public void reset() { + initContentFilters(); + initSortFilters(); + deselectUiItems(contentFilterIdToUiItemMap); + deselectUiItems(sortFilterIdToUiItemMap); + reselectUiItems(selectedContentFilters, contentFilterIdToUiItemMap); + reselectUiItems(selectedSortFilters, sortFilterIdToUiItemMap); + showSortFilterContainerUI(); + } + + private void reInitExclusiveFilterIds(final ArrayList selectedFilters, + final ExclusiveGroups exclusive) { + checkIfIdsAreValid(selectedFilters, exclusive); + + for (final int id : selectedFilters) { + exclusive.ifInExclusiveGroupRemovePreviouslySelectedId(id); + exclusive.addIdIfBelongsToExclusiveGroup(id); + } + } + + public void restorePreviouslySelectedFilters(final ArrayList selectedContentFilterList, + final ArrayList selectedSortFilterList) { + if (selectedContentFilterList != null && selectedSortFilterList != null + && !selectedContentFilterList.isEmpty()) { + reInitExclusiveFilterIds(selectedContentFilterList, contentFilterExclusive); + reInitExclusiveFilterIds(selectedSortFilterList, sortFilterExclusive); + + this.selectedContentFilters = selectedContentFilterList; + this.selectedSortFilters = selectedSortFilterList; + } + + createContentFilterItemListFromIdentifierList(); + createSortFilterItemListFromIdentifiersList(); + } + + private void reselectUiItems(final List selectedFilters, + final Map filterIdToUiItemMap) { + for (final int id : selectedFilters) { + final IUiItemWrapper iUiItemWrapper = filterIdToUiItemMap.get(id); + if (iUiItemWrapper != null) { + iUiItemWrapper.setChecked(true); + } + } + } + + private void deselectUiItems(final Map filterIdToUiItemMap) { + for (final Map.Entry entry : filterIdToUiItemMap.entrySet()) { + if (entry != null && entry.getValue() != null) { + final IUiItemWrapper iUiItemWrapper = entry.getValue(); + iUiItemWrapper.setChecked(false); + } + } + } + + // get copy of internal list + public ArrayList getSelectedContentFilters() { + return new ArrayList<>(this.selectedContentFilters); + } + + // get copy of internal list + public ArrayList getSelectedSortFilters() { + return new ArrayList<>(this.selectedSortFilters); + } + + // get copy of internal list, elements are not copied + public List getSelectedContentFilterItems() { + return new ArrayList<>(this.userSelectedContentFilters); + } + + // get copy of internal list, elements are not copied + public List getSelectedSortFiltersItems() { + return new ArrayList<>(this.userSelectedSortFilters); + } + + public void initContentFiltersUi(final ICreateUiForFiltersWorker createUiForFiltersWorker) { + final FilterContainer filters = searchQHFactory.getAvailableContentFilter(); + + if (filters != null && filters.getFilterGroups() != null) { + initFiltersUi(filters.getFilterGroups(), + contentFilterIdToUiItemMap, + createUiForFiltersWorker); + } + + reselectUiItems(selectedContentFilters, contentFilterIdToUiItemMap); + } + + public void initSortFiltersUi(final ICreateUiForFiltersWorker createUiForFiltersWorker) { + final FilterContainer filters = searchQHFactory.getAvailableContentFilter(); + final List sortGroups = getAllSortFilterGroups(filters); + uiSortFilterWorker = createUiForFiltersWorker; + + initFiltersUi(sortGroups.toArray(new FilterGroup[0]), + sortFilterIdToUiItemMap, + createUiForFiltersWorker); + + reselectUiItems(selectedSortFilters, sortFilterIdToUiItemMap); + } + + /** + * Create Ui elements. + * + * @param filterGroups the filter groups that whom a UI should be created + * @param filterIdToUiItemMap points to a {@link FilterItem} or {@link FilterGroup} + * corresponding actual UI element(s). This map will be first + * called clear() on here. + * @param createUiForFiltersWorker the implementation how to create the UI. + */ + private void initFiltersUi(final FilterGroup[] filterGroups, + final Map filterIdToUiItemMap, + final ICreateUiForFiltersWorker createUiForFiltersWorker) { + + filterIdToUiItemMap.clear(); + Objects.requireNonNull(createUiForFiltersWorker); + createUiForFiltersWorker.prepare(); + for (final FilterGroup filterGroup : filterGroups) { + createUiForFiltersWorker.createFilterGroupBeforeItems(filterGroup); + for (final FilterItem filterItem : filterGroup.getFilterItems()) { + createUiForFiltersWorker.createFilterItem(filterItem, filterGroup); + } + createUiForFiltersWorker.createFilterGroupAfterItems(filterGroup); + } + createUiForFiltersWorker.finish(); + } + + /** + * Init the content filter logical states. + *

+ * - create list with default id that will be preselected + * - create exclusivity lists for exclusive groups + * {@link ExclusiveGroups#filterIdToGroupIdMap} and + * {@link ExclusiveGroups#exclusiveGroupsIdSet} + * - check if {@link #selectedContentFilters} are valid ids + * + * @param filterGroups content or sort filter {@link FilterGroup} array + * @param exclusive + * @param selectedFilters + * @param fidToSupersetSortFilterMap null possible, only for content filters relevant + */ + private void initFilters( + final FilterGroup[] filterGroups, + final ExclusiveGroups exclusive, + final ArrayList selectedFilters, + @Nullable final Map fidToSupersetSortFilterMap) { + selectedFilters.clear(); + exclusive.clear(); + + for (final FilterGroup filterGroup : filterGroups) { + if (filterGroup.isOnlyOneCheckable()) { + exclusive.addGroupToExclusiveGroupsMap(filterGroup.getIdentifier()); + } + + // is the default selected filter for this group + final int defaultId = filterGroup.getDefaultSelectedFilterId(); + + for (final FilterItem item : filterGroup.getFilterItems()) { + if (fidToSupersetSortFilterMap != null) { + fidToSupersetSortFilterMap.put(item.getIdentifier(), + filterGroup.getAllSortFilters()); + } + exclusive.putFilterIdToItsGroupId(item.getIdentifier(), + filterGroup.getIdentifier()); + } + + if (defaultId != ITEM_IDENTIFIER_UNKNOWN) { + exclusive.handleIdInExclusiveGroup(defaultId, selectedFilters); + } + } + + checkIfIdsAreValid(selectedFilters, exclusive); + } + + private void checkIfIdsAreValid(final ArrayList selectedFilters, + final ExclusiveGroups exclusive) { + for (final int id : selectedFilters) { + if (!exclusive.filterIdToGroupIdMapContainsId(id)) { + throw new RuntimeException("The id " + id + " is invalid"); + } + } + } + + private void initContentFilters() { + final FilterContainer filters = searchQHFactory.getAvailableContentFilter(); + contentFilterFidToSupersetSortFilterMap.clear(); + + if (filters != null && filters.getFilterGroups() != null) { + initFilters(filters.getFilterGroups(), + contentFilterExclusive, selectedContentFilters, + contentFilterFidToSupersetSortFilterMap); + } + } + + private void initSortFilters() { + final FilterContainer filters = searchQHFactory.getAvailableContentFilter(); + final List sortGroups = getAllSortFilterGroups(filters); + + initFilters(sortGroups.toArray(new FilterGroup[0]), sortFilterExclusive, + selectedSortFilters, null); + } + + /** + * Prepare content filter list with the actual {@link FilterItem}s to send to the library. + *

+ * The list is created through the {@link #userSelectedContentFilters} identifiers list. + * This identifiers refer to {@link FilterItem}s. + *

+ * {@link #userSelectedContentFilters} will be cleared first! + */ + private void createContentFilterItemListFromIdentifierList() { + userSelectedContentFilters.clear(); + final FilterContainer filterContainer = searchQHFactory.getAvailableContentFilter(); + + for (final int contentFilterId : selectedContentFilters) { + final FilterItem contentFilterItem = filterContainer.getFilterItem(contentFilterId); + if (contentFilterItem != null) { + userSelectedContentFilters.add(contentFilterItem); + } + } + } + + /** + * Prepare sort filter list with the actual {@link FilterItem}s to send to the library. + *

+ * The list is created through the {@link #userSelectedSortFilters} identifiers list. + * This identifiers refer to {@link FilterItem}s. + *

+ * {@link #userSelectedSortFilters} will be cleared first! + */ + private void createSortFilterItemListFromIdentifiersList() { + userSelectedSortFilters.clear(); + for (final int sortFilterId : selectedSortFilters) { + for (final int contentFilterId : selectedContentFilters) { + final FilterContainer filterContainer = + searchQHFactory.getContentFilterSortFilterVariant(contentFilterId); + if (filterContainer != null) { + final FilterItem sortFilterItem = filterContainer.getFilterItem(sortFilterId); + if (sortFilterItem != null) { + userSelectedSortFilters.add(sortFilterItem); + } + } + } + } + } + + public void showSortFilterContainerUI() { + showSortFilterIdsContainerUI(selectedContentFilters); + } + + /** + * Show only that sort filter UIs that are available for selected content ids. + * + * @param contentFilterIds + */ + private void showSortFilterIdsContainerUI(final List contentFilterIds) { + for (final int contentFilterId : contentFilterIds) { + showSortFilterIdContainerUI(contentFilterId); + } + } + + private void notifySortFiltersVisibility() { + boolean sortFilterVisible = false; + if (uiSortFilterWorker != null) { + for (final int contentFilterId : selectedContentFilters) { + sortFilterVisible = searchQHFactory + .getContentFilterSortFilterVariant(contentFilterId) != null; + if (sortFilterVisible) { + break; + } + } + uiSortFilterWorker.filtersVisible(sortFilterVisible); + } + } + + /** + * Show only the sort filters that are available for a given content filter id. + * + * @param contentFilterId a content filter id and not a sort filter id. + */ + private void showSortFilterIdContainerUI(final int contentFilterId) { + final FilterContainer subsetFilterContainer = + searchQHFactory.getContentFilterSortFilterVariant(contentFilterId); + + final FilterContainer supersetFilterContainer = + contentFilterFidToSupersetSortFilterMap.get(contentFilterId); + if (subsetFilterContainer != null) { + if (supersetFilterContainer == null) { + throw new RuntimeException( + "supersetFilterContainer should never be null here"); + } + + setUiItemsVisibility(supersetFilterContainer, false, sortFilterIdToUiItemMap); + setUiItemsVisibility(subsetFilterContainer, true, sortFilterIdToUiItemMap); + } else { + if (supersetFilterContainer != null) { + setUiItemsVisibility(supersetFilterContainer, false, + sortFilterIdToUiItemMap); + } + } + notifySortFiltersVisibility(); + } + + /** + * This method is only used to show the all sort filters for measurement of the width. + *

+ * See {@link SearchFilterOptionMenuAlikeDialogGenerator} + */ + protected void showAllAvailableSortFilters() { + contentFilterFidToSupersetSortFilterMap.values().stream() + .filter(Objects::nonNull) + .distinct() + .forEach(container -> + setUiItemsVisibility(container, true, sortFilterIdToUiItemMap)); + } + + private void setUiItemsVisibility(final FilterContainer filters, + final boolean isVisible, + final Map filterIdToUiItemMap) { + if (filters != null && filters.getFilterGroups() != null) { + for (final FilterGroup filterGroup : filters.getFilterGroups()) { + setUiItemVisible(isVisible, filterIdToUiItemMap, filterGroup.getIdentifier()); + for (final FilterItem item : filterGroup.getFilterItems()) { + setUiItemVisible(isVisible, filterIdToUiItemMap, item.getIdentifier()); + } + } + } + } + + private void setUiItemVisible(final boolean isVisible, + final Map filterIdToUiItemMap, + final int id) { + final IUiItemWrapper uiWrapper = filterIdToUiItemMap.get(id); + if (uiWrapper != null) { + uiWrapper.setVisible(isVisible); + } + } + + /** + * Get all sort filter groups for the content filters. + * It has to have all content filter groups that are available for a service. + * + * @param filters the content filters + * @return the sort filter groups. Empty list if either param filters or no + * filter groups available + */ + private List getAllSortFilterGroups(final FilterContainer filters) { + if (filters != null && filters.getFilterGroups() != null) { + final List sortGroups = new ArrayList<>(); + for (final FilterGroup filterGroup : filters.getFilterGroups()) { + final FilterContainer sf = filterGroup.getAllSortFilters(); + if (sf != null && sf.getFilterGroups() != null) { + sortGroups.addAll(Arrays.asList(sf.getFilterGroups())); + } + } + return sortGroups; + } + return Collections.emptyList(); + } + + protected void handleIdInNonExclusiveGroup(final int filterId, + final IUiItemWrapper uiItemWrapper, + final List selectedFilter) { + if (uiItemWrapper != null) { + if (uiItemWrapper.isChecked()) { + if (!selectedFilter.contains(filterId)) { + selectedFilter.add(filterId); + } + } else { // remove from list + if (selectedFilter.contains(filterId)) { + selectedFilter.remove((Integer) filterId); + } + } + } else { // we have no UI + if (!selectedFilter.contains(filterId)) { + selectedFilter.add(filterId); + } else { + selectedFilter.remove((Integer) filterId); + } + } + } + + public synchronized void selectContentFilter(final int filterId) { + selectFilter(filterId, contentFilterIdToUiItemMap, selectedContentFilters, + contentFilterExclusive); + showSortFilterIdContainerUI(filterId); + } + + public synchronized void selectSortFilter(final int filterId) { + selectFilter(filterId, sortFilterIdToUiItemMap, selectedSortFilters, sortFilterExclusive); + } + + private void selectFilter(final int id, + final Map filterIdToUiItemMap, + final ArrayList selectedFilter, + final ExclusiveGroups exclusive) { + final IUiItemWrapper uiItemWrapper = + filterIdToUiItemMap.get(id); + + // here we remove/add the by the UI (de)selected id. + if (exclusive.handleIdInExclusiveGroup(id, selectedFilter)) { + if (uiItemWrapper != null && !uiItemWrapper.isChecked()) { + uiItemWrapper.setChecked(true); + } + } else { + handleIdInNonExclusiveGroup(id, uiItemWrapper, selectedFilter); + } + } + + /** + * Prepare the content and sort filters {@link FilterItem}'s lists for a now filtered + * search. + *

+ * If a callback is registered it wil be called with copy's of the local sort and + * content lists. To avoid concurrently modification of the lists. As they are progressed + * through async javarx calls. Note: The members aka {@link FilterItem}'s are not copied. + */ + public void prepareForSearch() { + createContentFilterItemListFromIdentifierList(); + createSortFilterItemListFromIdentifiersList(); + + if (callback != null) { + callback.selectedFilters(new ArrayList<>(userSelectedContentFilters), + new ArrayList<>(userSelectedSortFilters)); + } + } + + /** + * This method is meant to be called to add {@link android.view.View}s that represents + * a content filter. + *

+ * It has to be called within a subclass of {@link SearchFilterLogic} which implements + * {@link ICreateUiForFiltersWorker} itself or as an any inner class. + * + * @param id the id of a content filter + * @param uiItemWrapper the wrapped UI {@link android.view.View} for that content filter + */ + protected void addContentFilterUiWrapperToItemMap(final int id, + final IUiItemWrapper uiItemWrapper) { + contentFilterIdToUiItemMap.put(id, uiItemWrapper); + } + + /** + * This method is meant to be called to add {@link android.view.View}s that represents + * a sort filter. + *

+ * It has to be called within a subclass of {@link SearchFilterLogic} which implements + * {@link ICreateUiForFiltersWorker} itself or as an any inner class. + * + * @param id the id of a sort filter + * @param uiItemWrapper the wrapped UI {@link android.view.View} for that sort filter + */ + protected void addSortFilterUiWrapperToItemMap(final int id, + final IUiItemWrapper uiItemWrapper) { + sortFilterIdToUiItemMap.put(id, uiItemWrapper); + } + + /** + * Wrap a {@link FilterItem} or {@link FilterGroup} to their + * actual UI element(s) ({@link android.view.View}). + */ + public interface IUiItemWrapper { + /** + * set a view element visible. + * + * @param visible true if visible, false if not visible + */ + void setVisible(boolean visible); + + /** + * @return get the id of the corresponding {@link FilterItem} + */ + int getItemId(); + + /** + * @return get the id of the corresponding {@link FilterGroup} + */ + int getGroupId(); + + /** + * Is the UI element selected. + * + * @return true if selected + */ + boolean isChecked(); + + /** + * select the UI element. + * + * @param checked select UI element + */ + void setChecked(boolean checked); + } + + /** + * Creating user elements for all filters inside a {@link FilterContainer}. + * + * Note: use {@link #addContentFilterUiWrapperToItemMap(int, IUiItemWrapper)} and + * {@link #addSortFilterUiWrapperToItemMap(int, IUiItemWrapper)} to actually make + * {@link SearchFilterLogic} aware of them. + */ + public interface ICreateUiForFiltersWorker { + /** + * Will be called before any {@link FilterContainer} looping. + */ + void prepare(); + + /** + * Create Ui elements specifically related to the {@link FilterGroup} itself. + * But it could also be used for creating items. + *

+ * -> This method is called *before* the {@link #createFilterItem(FilterItem, FilterGroup)} + * + * @param filterGroup one group each time from {@link FilterContainer#getFilterGroups()} + */ + void createFilterGroupBeforeItems(FilterGroup filterGroup); + + /** + * Create Ui elements specifically related to a {@link FilterItem} itself. + * + * @param filterItem the actual item you should create a UI element here + * @param filterGroup (optional) one group each time from + * {@link FilterContainer#getFilterGroups()} + */ + void createFilterItem(FilterItem filterItem, FilterGroup filterGroup); + + /** + * Create Ui elements specifically related to the {@link FilterGroup} itself. + * But it could also be used for creating items. + *

+ * -> This method is called *after* the {@link #createFilterItem(FilterItem, FilterGroup)} + * + * @param filterGroup one group each time from {@link FilterContainer#getFilterGroups()} + */ + void createFilterGroupAfterItems(FilterGroup filterGroup); + + /** + * do anything you might want to clean up or whatever. + */ + void finish(); + + /** + * Notify if filters are visible. Eg to show or hide 'sort filter' section title + * + * @param areFiltersVisible true if filter visible + */ + void filtersVisible(boolean areFiltersVisible); + } + + /** + * This callback will be called if a search with additional filters should occur. + */ + public interface Callback { + void selectedFilters(List userSelectedContentFilter, + List userSelectedSortFilter); + } + + /** + * Track and handle filters of groups in which only one {@link FilterItem} can be selected. + *

+ * We need to track this ourselves as we otherwise rely on androids functionality or lack of + * tracking the before selected item that now is unselected. + */ + private static class ExclusiveGroups { + + final Map actualSelectedFilterIdInExclusiveGroupMap = new HashMap<>(); + /** + * To quickly determine if a content filter group supports + * only one item selected (exclusiveness), we need a set that resembles that. + */ + private final Set exclusiveGroupsIdSet = new HashSet<>(); + /** + * To quickly determine if a content filter id belongs to an exclusive group. + * This maps works in conjunction with {@link #exclusiveGroupsIdSet} + */ + private final Map filterIdToGroupIdMap = new HashMap<>(); + + /** + * Clear {@link #exclusiveGroupsIdSet} and {@link #filterIdToGroupIdMap}. + */ + public void clear() { + exclusiveGroupsIdSet.clear(); + filterIdToGroupIdMap.clear(); + actualSelectedFilterIdInExclusiveGroupMap.clear(); + } + + /** + * Check if filter id is valid. + * + * @param filterId the filter id to check + * @return true if valid + */ + public boolean filterIdToGroupIdMapContainsId(final int filterId) { + return filterIdToGroupIdMap.containsKey(filterId); + } + + public boolean isFilterIdPartOfAnExclusiveGroup(final int filterId) { + if (filterIdToGroupIdMapContainsId(filterId)) { + final int filterGroupId = filterIdToGroupIdMap.get(filterId); + return exclusiveGroupsIdSet.contains(filterGroupId); + } + return false; + } + + /** + * @param filterId + * @param selectedFilter + * @return true if exclusive group + */ + private boolean handleIdInExclusiveGroup(final int filterId, + final List selectedFilter) { + // case exclusive group selection + if (isFilterIdPartOfAnExclusiveGroup(filterId)) { + final int previousSelectedId = + ifInExclusiveGroupRemovePreviouslySelectedId(filterId); + if (selectedFilter.contains(previousSelectedId)) { + selectedFilter.remove((Integer) previousSelectedId); + selectedFilter.add(filterId); + } else if (previousSelectedId == ITEM_IDENTIFIER_UNKNOWN) { + selectedFilter.add(filterId); + } + addIdIfBelongsToExclusiveGroup(filterId); + return true; + } + return false; + } + + /** + * Insert filter ids with corresponding group ids. + *

+ * We need to know which filter belongs to which group, that we can + * determine if a selected {@link FilterItem} is part of an exclusive + * group or not. + * + * @param filterId filter identifier + * @param filterGroupId group identifier + */ + public void putFilterIdToItsGroupId(final int filterId, final int filterGroupId) { + filterIdToGroupIdMap.put(filterId, filterGroupId); + } + + /** + * Add exclusive groups to the map. + * + * @param groupId the id of the exclusive group + */ + public void addGroupToExclusiveGroupsMap(final int groupId) { + exclusiveGroupsIdSet.add(groupId); + } + + private void addIdIfBelongsToExclusiveGroup(final int filterId) { + final int filterGroupId = filterIdToGroupIdMap.get(filterId); + if (exclusiveGroupsIdSet.contains(filterGroupId)) { + actualSelectedFilterIdInExclusiveGroupMap.put(filterGroupId, filterId); + } + } + + /** + * check if the filter group id for a given filter id is already in a exclusive group. + *

+ * If so remove the group filter id. + * + * @param filterId the id of a filter that might belong to an exclusive filter group + * @return id of removed filter id from {@link #actualSelectedFilterIdInExclusiveGroupMap} + * otherwise {@link FilterContainer#ITEM_IDENTIFIER_UNKNOWN} + */ + + private int ifInExclusiveGroupRemovePreviouslySelectedId(final int filterId) { + int previousFilterId = ITEM_IDENTIFIER_UNKNOWN; + final int filterGroupId = filterIdToGroupIdMap.get(filterId); + + if (exclusiveGroupsIdSet.contains(filterGroupId) + && actualSelectedFilterIdInExclusiveGroupMap.containsKey(filterGroupId)) { + previousFilterId = actualSelectedFilterIdInExclusiveGroupMap.get(filterGroupId); + actualSelectedFilterIdInExclusiveGroupMap.remove(filterGroupId); + } + return previousFilterId; + } + } +}