blob: 6feec72594a08e439da5c67d5a1c45022bf6c070 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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.list;
import org.chromium.base.CollectionUtil;
import org.chromium.chrome.browser.download.home.DownloadManagerUiConfig;
import org.chromium.chrome.browser.download.home.JustNowProvider;
import org.chromium.chrome.browser.download.home.filter.Filters;
import org.chromium.chrome.browser.download.home.filter.OfflineItemFilterObserver;
import org.chromium.chrome.browser.download.home.filter.OfflineItemFilterSource;
import org.chromium.chrome.browser.download.home.list.ListItem.OfflineItemListItem;
import org.chromium.chrome.browser.download.home.list.ListItem.SectionHeaderListItem;
import org.chromium.components.offline_items_collection.ContentId;
import org.chromium.components.offline_items_collection.OfflineItem;
import org.chromium.components.offline_items_collection.OfflineItemFilter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* A class responsible for turning a {@link Collection} of {@link OfflineItem}s into a list meant
* to be displayed in the download home UI. This list has the following properties:
* - Sorted.
* - Separated by date headers for each individual day.
* - Converts changes in the form of {@link Collection}s to delta changes on the list.
*
* TODO(dtrainor): This should be optimized in the near future. There are a few key things that can
* be changed:
* - Do a single iterating across each list to merge/unmerge them. This requires sorting and
* tracking the current position across both as iterating (see {@link #onItemsRemoved(Collection)}
* for an example since that is close to doing what we want - minus the contains() call).
*/
class DateOrderedListMutator implements OfflineItemFilterObserver {
private static final Date JUST_NOW_DATE = new Date(Long.MAX_VALUE);
private final DownloadManagerUiConfig mConfig;
private final JustNowProvider mJustNowProvider;
private final ListItemModel mModel;
private final Map<Date, DateGroup> mDateGroups =
new TreeMap<>((lhs, rhs) -> { return rhs.compareTo(lhs); });
// Whether we should hide the section headers and only show the ones that show a new date. Title
// and menu button are not present.
private final boolean mHideSectionHeaders;
// Whether we shouldn't have any headers. Meant to be used for prefetch tab.
private boolean mHideAllHeaders;
// Meant to be used when a non-all chip is selected. Shouldn't have titles, only have dates. Can
// have menu button.
private boolean mHideTitleFromSectionHeaders;
/**
* Creates an DateOrderedList instance that will reflect {@code source}.
* @param source The source of data for this list.
* @param model The model that will be the storage for the updated list.
* @param config The {@link DownloadManagerUiConfig}.
* @param justNowProvider The provider for Just Now section.
*/
public DateOrderedListMutator(OfflineItemFilterSource source, ListItemModel model,
DownloadManagerUiConfig config, JustNowProvider justNowProvider) {
mModel = model;
mConfig = config;
mJustNowProvider = justNowProvider;
mHideSectionHeaders = !mConfig.showSectionHeaders;
source.addObserver(this);
onItemsAdded(source.getItems());
}
/**
* Called when the selected tab or chip has changed.
* @param filter The currently selected filter type.
*/
public void onFilterTypeSelected(@Filters.FilterType int filter) {
mHideAllHeaders = filter == Filters.FilterType.PREFETCHED;
mHideTitleFromSectionHeaders = filter != Filters.FilterType.NONE;
}
// OfflineItemFilterObserver implementation.
@Override
public void onItemsAdded(Collection<OfflineItem> items) {
for (OfflineItem item : items) {
addOrUpdateItemToDateGroups(item);
}
pushItemsToModel();
}
@Override
public void onItemsRemoved(Collection<OfflineItem> items) {
for (OfflineItem item : items) {
Date date = getSectionDateFromOfflineItem(item);
DateGroup dateGroup = mDateGroups.get(date);
if (dateGroup == null) continue;
dateGroup.removeItem(item);
if (dateGroup.sections.isEmpty()) {
mDateGroups.remove(date);
}
}
pushItemsToModel();
}
@Override
public void onItemUpdated(OfflineItem oldItem, OfflineItem item) {
assert oldItem.id.equals(item.id);
// If the update changed the creation time or filter type, remove and add the element to get
// it positioned.
if (oldItem.creationTimeMs != item.creationTimeMs || oldItem.filter != item.filter
|| shouldShowInJustNowSection(oldItem) != shouldShowInJustNowSection((item))) {
// TODO(shaktisahu): Collect UMA when this happens.
onItemsRemoved(CollectionUtil.newArrayList(oldItem));
onItemsAdded(CollectionUtil.newArrayList(item));
} else {
addOrUpdateItemToDateGroups(item);
int sectionHeaderIndex = -1;
for (int i = 0; i < mModel.size(); i++) {
ListItem listItem = mModel.get(i);
if (listItem instanceof SectionHeaderListItem) sectionHeaderIndex = i;
if (!(listItem instanceof OfflineItemListItem)) continue;
OfflineItemListItem existingItem = (OfflineItemListItem) listItem;
if (item.id.equals(existingItem.item.id)) {
existingItem.item = item;
mModel.update(i, existingItem);
if (oldItem.state != item.state) {
updateSectionHeader(sectionHeaderIndex, i);
}
break;
}
}
}
mModel.dispatchLastEvent();
}
private void addOrUpdateItemToDateGroups(OfflineItem item) {
Date date = getSectionDateFromOfflineItem(item);
DateGroup dateGroup = mDateGroups.get(date);
if (dateGroup == null) {
dateGroup = new DateGroup();
mDateGroups.put(date, dateGroup);
}
dateGroup.addOrUpdateItem(item);
}
private void updateSectionHeader(int sectionHeaderIndex, int offlineItemIndex) {
if (sectionHeaderIndex < 0 || mHideSectionHeaders) return;
SectionHeaderListItem sectionHeader =
(SectionHeaderListItem) mModel.get(sectionHeaderIndex);
OfflineItem offlineItem = ((OfflineItemListItem) mModel.get(offlineItemIndex)).item;
sectionHeader.items.set(offlineItemIndex - sectionHeaderIndex - 1, offlineItem);
mModel.update(sectionHeaderIndex, sectionHeader);
}
// Flattens out the hierarchical data and adds items to the model in the order they should be
// displayed. Date headers and section headers are added wherever necessary. The existing items
// in the model are replaced by the new set of items computed.
private void pushItemsToModel() {
List<ListItem> listItems = new ArrayList<>();
int dateIndex = 0;
for (Date date : mDateGroups.keySet()) {
DateGroup dateGroup = mDateGroups.get(date);
int sectionIndex = 0;
// For each section.
for (Integer filter : dateGroup.sections.keySet()) {
Section section = dateGroup.sections.get(filter);
// Add a section header.
if (!mHideAllHeaders && (!mHideSectionHeaders || sectionIndex == 0)) {
SectionHeaderListItem sectionHeaderItem = new SectionHeaderListItem(filter,
date.getTime(), sectionIndex == 0 /* showDate */,
date.equals(JUST_NOW_DATE) /* isJustNow */,
sectionIndex == 0 && dateIndex > 0 /* showDivider */);
if (!mHideSectionHeaders) {
sectionHeaderItem.showTitle = !mHideTitleFromSectionHeaders;
sectionHeaderItem.showMenu = filter == OfflineItemFilter.IMAGE;
sectionHeaderItem.items = new ArrayList<>(section.items);
}
listItems.add(sectionHeaderItem);
}
// Add the items in the section.
for (OfflineItem offlineItem : section.items) {
OfflineItemListItem item = new OfflineItemListItem(offlineItem);
if (mConfig.supportFullWidthImages && section.items.size() == 1
&& offlineItem.filter == OfflineItemFilter.IMAGE) {
item.spanFullWidth = true;
}
listItems.add(item);
}
sectionIndex++;
}
dateIndex++;
}
mModel.set(listItems);
mModel.dispatchLastEvent();
}
private Date getSectionDateFromOfflineItem(OfflineItem offlineItem) {
return shouldShowInJustNowSection(offlineItem)
? JUST_NOW_DATE
: CalendarUtils.getStartOfDay(offlineItem.creationTimeMs).getTime();
}
private boolean isShowingInJustNowSection(OfflineItem item) {
DateGroup justNowGroup = mDateGroups.get(JUST_NOW_DATE);
return justNowGroup != null && justNowGroup.contains(item.id);
}
private boolean shouldShowInJustNowSection(OfflineItem item) {
return mJustNowProvider.isJustNowItem(item) || isShowingInJustNowSection(item);
}
/** Represents a group of items which were downloaded on the same day. */
private static class DateGroup {
/**
* The list of sections for the day. The ordering is done in the same order as {@code
* Filters.FilterType}.
*/
public Map<Integer, Section> sections = new TreeMap<>((lhs, rhs) -> {
return ListUtils.compareFilterTypesTo(
Filters.fromOfflineItem(lhs), Filters.fromOfflineItem(rhs));
});
public void addOrUpdateItem(OfflineItem item) {
Section section = sections.get(item.filter);
if (section == null) {
section = new Section();
sections.put(item.filter, section);
}
section.addOrUpdateItem(item);
}
public void removeItem(OfflineItem item) {
Section section = sections.get(item.filter);
if (section == null) return;
section.removeItem(item);
if (section.items.isEmpty()) {
sections.remove(item.filter);
}
}
public boolean contains(ContentId id) {
for (Section section : sections.values()) {
for (OfflineItem item : section.items) {
if (item.id.equals(id)) return true;
}
}
return false;
}
}
/** Represents a group of items having the same filter type. */
private static class Section {
/**
* The list of items in a section for a day. Ordered by descending creation time then
* namespace and finally by id.
*/
public Set<OfflineItem> items = new TreeSet<>((lhs, rhs) -> {
int comparison = Long.compare(rhs.creationTimeMs, lhs.creationTimeMs);
if (comparison != 0) return comparison;
comparison = lhs.id.namespace.compareTo(rhs.id.namespace);
if (comparison != 0) return comparison;
return lhs.id.id.compareTo(rhs.id.id);
});
public void addOrUpdateItem(OfflineItem item) {
items.remove(item);
items.add(item);
}
public void removeItem(OfflineItem item) {
items.remove(item);
}
}
}