chromium/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/list/ListUtils.java

// 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.list;

import androidx.annotation.IntDef;

import org.chromium.chrome.browser.download.home.DownloadManagerUiConfig;
import org.chromium.chrome.browser.download.home.filter.Filters.FilterType;
import org.chromium.chrome.browser.download.home.list.ListItem.OfflineItemListItem;
import org.chromium.chrome.browser.download.home.list.ListItem.ViewListItem;
import org.chromium.components.browser_ui.util.date.CalendarUtils;
import org.chromium.components.offline_items_collection.LegacyHelpers;
import org.chromium.components.offline_items_collection.OfflineItem;
import org.chromium.components.offline_items_collection.OfflineItemFilter;
import org.chromium.components.offline_items_collection.OfflineItemState;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

/** Utility methods for representing {@link ListItem}s in a {@link RecyclerView} list. */
public class ListUtils {
    /** The potential types of list items that could be displayed. */
    @IntDef({
        ViewType.DATE,
        ViewType.IN_PROGRESS,
        ViewType.GENERIC,
        ViewType.VIDEO,
        ViewType.AUDIO,
        ViewType.IMAGE,
        ViewType.IMAGE_FULL_WIDTH,
        ViewType.CUSTOM_VIEW,
        ViewType.SECTION_HEADER,
        ViewType.IN_PROGRESS_VIDEO,
        ViewType.IN_PROGRESS_IMAGE,
        ViewType.PREFETCH_ARTICLE,
        ViewType.GROUP_CARD_ITEM,
        ViewType.GROUP_CARD_HEADER,
        ViewType.GROUP_CARD_FOOTER,
        ViewType.PAGINATION_HEADER,
        ViewType.GROUP_CARD_DIVIDER_TOP,
        ViewType.GROUP_CARD_DIVIDER_MIDDLE,
        ViewType.GROUP_CARD_DIVIDER_BOTTOM
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ViewType {
        int DATE = 0;
        int IN_PROGRESS = 1;
        int GENERIC = 2;
        int VIDEO = 3;
        int AUDIO = 4;
        int IMAGE = 5;
        int IMAGE_FULL_WIDTH = 6;
        int CUSTOM_VIEW = 7;
        int SECTION_HEADER = 8;
        int IN_PROGRESS_VIDEO = 9;
        int IN_PROGRESS_IMAGE = 10;
        int PREFETCH_ARTICLE = 11;
        int GROUP_CARD_ITEM = 12;
        int GROUP_CARD_HEADER = 13;
        int GROUP_CARD_FOOTER = 14;
        int GROUP_CARD_DIVIDER_TOP = 15;
        int GROUP_CARD_DIVIDER_MIDDLE = 16;
        int GROUP_CARD_DIVIDER_BOTTOM = 17;
        int PAGINATION_HEADER = 18;
    }

    /**
     * A visual ordering of the {@link Filters#FilterType}s to determine what order the sections
     * should appear in the UI.
     *
     * Note that this list should have an entry for each {@link Filters#FilterType} that can be
     * shown visually and asserts will fire if it does not.
     */
    private static final int[] FILTER_TYPE_ORDER_LIST =
            new int[] {
                FilterType.NONE,
                FilterType.VIDEOS,
                FilterType.MUSIC,
                FilterType.IMAGES,
                FilterType.SITES,
                FilterType.OTHER,
                FilterType.DOCUMENT,
                FilterType.PREFETCHED
            };

    /** Converts a given list of {@link ListItem}s to a list of {@link OfflineItem}s. */
    public static List<OfflineItem> toOfflineItems(Collection<ListItem> items) {
        List<OfflineItem> offlineItems = new ArrayList<>();
        for (ListItem item : items) {
            if (item instanceof ListItem.OfflineItemListItem) {
                offlineItems.add(((ListItem.OfflineItemListItem) item).item);
            }
        }
        return offlineItems;
    }

    /**
     * Analyzes a {@link ListItem} and finds the most appropriate {@link ViewType} based on the
     * current state.
     * @param item   The {@link ListItem} to determine the {@link ViewType} for.
     * @param config The {@link DownloadManagerUiConfig}.
     * @return       The type of {@link ViewType} to use for a particular {@link ListItem}.
     * @see          ViewType
     */
    public static @ViewType int getViewTypeForItem(ListItem item, DownloadManagerUiConfig config) {
        if (item instanceof ViewListItem) return ViewType.CUSTOM_VIEW;
        if (item instanceof ListItem.SectionHeaderListItem) return ViewType.SECTION_HEADER;
        if (item instanceof ListItem.PaginationListItem) return ViewType.PAGINATION_HEADER;
        if (item instanceof ListItem.CardHeaderListItem) {
            return ViewType.GROUP_CARD_HEADER;
        }
        if (item instanceof ListItem.CardFooterListItem) {
            return ViewType.GROUP_CARD_FOOTER;
        }

        if (item instanceof ListItem.CardDividerListItem) {
            switch (((ListItem.CardDividerListItem) item).position) {
                case TOP:
                    return ViewType.GROUP_CARD_DIVIDER_TOP;
                case MIDDLE:
                    return ViewType.GROUP_CARD_DIVIDER_MIDDLE;
                case BOTTOM:
                    return ViewType.GROUP_CARD_DIVIDER_BOTTOM;
            }
        }

        if (item instanceof OfflineItemListItem) {
            OfflineItemListItem offlineItem = (OfflineItemListItem) item;
            if (offlineItem.isGrouped) return ViewType.GROUP_CARD_ITEM;

            boolean inProgress =
                    offlineItem.item.state == OfflineItemState.IN_PROGRESS
                            || offlineItem.item.state == OfflineItemState.PAUSED
                            || offlineItem.item.state == OfflineItemState.INTERRUPTED
                            || offlineItem.item.state == OfflineItemState.PENDING
                            || offlineItem.item.state == OfflineItemState.FAILED;

            if (config.useGenericViewTypes) {
                return inProgress ? ViewType.IN_PROGRESS : ViewType.GENERIC;
            }

            if (offlineItem.item.isSuggested) {
                if (offlineItem.item.filter == OfflineItemFilter.PAGE) {
                    return ViewType.PREFETCH_ARTICLE;
                } else if (offlineItem.item.filter == OfflineItemFilter.AUDIO) {
                    return ViewType.AUDIO;
                }
            }

            switch (offlineItem.item.filter) {
                case OfflineItemFilter.VIDEO:
                    return inProgress ? ViewType.IN_PROGRESS_VIDEO : ViewType.VIDEO;
                case OfflineItemFilter.IMAGE:
                    return inProgress
                            ? ViewType.IN_PROGRESS_IMAGE
                            : (offlineItem.spanFullWidth
                                    ? ViewType.IMAGE_FULL_WIDTH
                                    : ViewType.IMAGE);
                    // case OfflineItemFilter.PAGE:
                    // case OfflineItemFilter.AUDIO:
                    // case OfflineItemFilter.OTHER:
                    // case OfflineItemFilter.DOCUMENT:
                default:
                    return inProgress ? ViewType.IN_PROGRESS : ViewType.GENERIC;
            }
        }

        assert false;
        return ViewType.GENERIC;
    }

    /** @return Whether the given {@link ListItem} can be grouped inside a card. */
    public static boolean canGroup(ListItem listItem) {
        if (!(listItem instanceof OfflineItemListItem)) return false;
        return LegacyHelpers.isLegacyContentIndexedItem(((OfflineItemListItem) listItem).item.id);
    }

    /**
     * Analyzes a {@link ListItem} and finds the best span size based on the current state.  Span
     * size determines how many columns this {@link ListItem}'s {@link View} will take up in the
     * overall list.
     * @param item      The {@link ListItem} to determine the span size for.
     * @param config    The {@link DownloadManagerUiConfig}.
     * @param spanCount The maximum span amount of columns {@code item} can take up.
     * @return          The number of columns {@code item} should take.
     * @see             GridLayoutManager.SpanSizeLookup
     */
    public static int getSpanSize(ListItem item, DownloadManagerUiConfig config, int spanCount) {
        switch (getViewTypeForItem(item, config)) {
            case ViewType.IMAGE: // Intentional fallthrough.
            case ViewType.IN_PROGRESS_IMAGE:
                return 1;
            default:
                return spanCount;
        }
    }

    /**
     * Helper method to determine which item type section to show first in the list.
     * @return -1 if {@code a} should be shown before {@code b}.
     *          0 if {@code a} == {@code b}.
     *          1 if {@code a} should be shown after {@code b}.
     */
    public static int compareFilterTypesTo(@FilterType int a, @FilterType int b) {
        int aPriority = getVisualPriorityForFilter(a);
        int bPriority = getVisualPriorityForFilter(b);
        return (aPriority < bPriority) ? -1 : ((aPriority == bPriority) ? 0 : 1);
    }

    /**
     * Helper method to compare list items based on date. Two items have equal if they both got
     * created on the same day.
     * @return -1 if {@code a} should be shown before {@code b}.
     *          0 if {@code a} == {@code b}.
     *          1 if {@code a} should be shown after {@code b}.
     */
    public static int compareItemByDate(OfflineItem a, OfflineItem b) {
        Date aDay = CalendarUtils.getStartOfDay(a.creationTimeMs).getTime();
        Date bDay = CalendarUtils.getStartOfDay(b.creationTimeMs).getTime();
        return bDay.compareTo(aDay);
    }

    /**
     * Helper method to compare list items based on timestamp.
     * @return -1 if {@code a} should be shown before {@code b}.
     *          0 if {@code a} == {@code b}.
     *          1 if {@code a} should be shown after {@code b}.
     */
    public static int compareItemByTimestamp(OfflineItem a, OfflineItem b) {
        return Long.compare(b.creationTimeMs, a.creationTimeMs);
    }

    /**
     * Helper method to compare list items based on ID.
     * @return -1 if {@code a} should be shown before {@code b}.
     *          0 if {@code a} == {@code b}.
     *          1 if {@code a} should be shown after {@code b}.
     */
    public static int compareItemByID(OfflineItem a, OfflineItem b) {
        int comparison = a.id.namespace.compareTo(b.id.namespace);
        if (comparison != 0) return comparison;
        return a.id.id.compareTo(b.id.id);
    }

    private static int getVisualPriorityForFilter(@FilterType int type) {
        for (int i = 0; i < FILTER_TYPE_ORDER_LIST.length; i++) {
            if (FILTER_TYPE_ORDER_LIST[i] == type) return i;
        }

        assert false
                : "Unexpected Filters.FilterType (did you forget to update FILTER_TYPE_ORDER_LIST?).";
        return 0;
    }
}