// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.download.home.filter;
import org.chromium.base.CollectionUtil;
import org.chromium.base.ObserverList;
import org.chromium.components.offline_items_collection.OfflineItem;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
* An abstract class responsible for consuming a collection of {@link OfflineItem}s, filtering it
* based on subclass criteria, and exposing the filtered collection to observers. This class is
* meant to make it easy to chain various filter stages together while keeping each particular
* filter self-contained and testable.
*
* It is recommended that subclasses call {@link #onFilterChanged()} after initialization to
* populate this class with the correctly filtered set of objects. This constructor cannot call
* that method as {@link #isFilteredOut(OfflineItem)} might access unconstructed subclass state.
*/
public abstract class OfflineItemFilter
implements OfflineItemFilterObserver, OfflineItemFilterSource {
private final OfflineItemFilterSource mSource;
private final Set<OfflineItem> mItems = new HashSet<>();
private final ObserverList<OfflineItemFilterObserver> mObservers = new ObserverList<>();
/**
* Creates a filter wrapping a filter source {@code source}. After creation this class should
* represent the filtered state of {code source}. This class will also update based on any
* changes to {@code source} automatically. Note that this class will *not* unregister as an
* observer on {@code source}.
*/
public OfflineItemFilter(OfflineItemFilterSource source) {
mSource = source;
source.addObserver(this);
}
/**
* Will be called based on changes to the underlying {@link OfflineItemFilterSource}. This will
* determine which elements get filtered out of the exposed collection of {@link OfflineItem}s.
* @param item The {@link OfflineItem} to check.
* @return Whether or not the {@link OfflineItem} should be filtered out by this class.
*/
protected abstract boolean isFilteredOut(OfflineItem item);
/**
* Called by subclasses to notify this class that the filtering criteria have changed. When
* this happens this class will reevaluate the current filter state and send appropriate
* {@link OfflineItemFilterObserver#onItemsAdded(Collection)} and
* {@link OfflineItemFilterObserver#onItemsRemoved(Collection)} calls to observers downstream.
*/
protected void onFilterChanged() {
Set<OfflineItem> removed = new HashSet<>();
for (Iterator<OfflineItem> iter = mItems.iterator(); iter.hasNext(); ) {
OfflineItem item = iter.next();
if (isFilteredOut(item)) {
iter.remove();
removed.add(item);
}
}
if (!removed.isEmpty()) {
for (OfflineItemFilterObserver obs : mObservers) obs.onItemsRemoved(removed);
}
addItems(mSource.getItems());
}
// OfflineItemFilterSource implementation.
@Override
public Set<OfflineItem> getItems() {
return mItems;
}
@Override
public boolean areItemsAvailable() {
return mSource.areItemsAvailable();
}
@Override
public void addObserver(OfflineItemFilterObserver observer) {
mObservers.addObserver(observer);
}
@Override
public void removeObserver(OfflineItemFilterObserver observer) {
mObservers.removeObserver(observer);
}
// OfflineItemSetObserver implementation.
@Override
public void onItemsAdded(Collection<OfflineItem> items) {
addItems(items);
}
@Override
public void onItemsRemoved(Collection<OfflineItem> items) {
Set<OfflineItem> removed = new HashSet<>();
for (OfflineItem item : items) {
if (mItems.remove(item)) removed.add(item);
}
if (!removed.isEmpty()) {
for (OfflineItemFilterObserver obs : mObservers) obs.onItemsRemoved(removed);
}
}
@Override
public void onItemUpdated(OfflineItem oldItem, OfflineItem item) {
boolean oldInList = mItems.remove(oldItem);
boolean newInList = !isFilteredOut(item);
if (oldInList && newInList) {
mItems.add(item);
for (OfflineItemFilterObserver obs : mObservers) obs.onItemUpdated(oldItem, item);
} else if (!oldInList && newInList) {
mItems.add(item);
Collection<OfflineItem> newItems = CollectionUtil.newHashSet(item);
for (OfflineItemFilterObserver obs : mObservers) obs.onItemsAdded(newItems);
} else if (oldInList && !newInList) {
Collection<OfflineItem> oldItems = CollectionUtil.newHashSet(oldItem);
for (OfflineItemFilterObserver obs : mObservers) obs.onItemsRemoved(oldItems);
}
}
@Override
public void onItemsAvailable() {
for (OfflineItemFilterObserver obs : mObservers) obs.onItemsAvailable();
}
// Helper method to help incorporate a collection of items into this filtered version.
private void addItems(Collection<OfflineItem> items) {
Set<OfflineItem> added = new HashSet<>();
for (OfflineItem item : items) {
if (isFilteredOut(item)) continue;
if (mItems.add(item)) added.add(item);
}
if (!added.isEmpty()) {
for (OfflineItemFilterObserver obs : mObservers) obs.onItemsAdded(added);
}
}
}