Commit e3088be5 authored by Shakti Sahu's avatar Shakti Sahu Committed by Commit Bot

Content indexing: Added mutator changes (With Divider)

This CL adds mutator changes for adding the content indexed items.
The view holders will be added in a later CL.
1 - Comparator interface was changed to Sorter
2 - Prefetch tab will show a date wise sorted list which will have
    standalone prefetch cards and group cards for content indexing.
3 - Added CardPaginator to remember the pagination for each card
4 - Added GroupCardLabelAdder to add card header and footer
5-  Introduced dividers as separate view holders to handle top,
    middle, and bottom dividers in a group card.

Bug: 1030924
Change-Id: I033ce36344d3b7095a6b9a5e6d47e9b273121954
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1941041
Commit-Queue: Shakti Sahu <shaktisahu@chromium.org>
Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Cr-Commit-Position: refs/heads/master@{#721835}
parent 23dfab8c
......@@ -578,11 +578,14 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/download/home/list/holder/PrefetchViewHolder.java",
"java/src/org/chromium/chrome/browser/download/home/list/holder/SectionTitleViewHolder.java",
"java/src/org/chromium/chrome/browser/download/home/list/holder/VideoViewHolder.java",
"java/src/org/chromium/chrome/browser/download/home/list/mutator/DateComparator.java",
"java/src/org/chromium/chrome/browser/download/home/list/mutator/CardPaginator.java",
"java/src/org/chromium/chrome/browser/download/home/list/mutator/DateSorter.java",
"java/src/org/chromium/chrome/browser/download/home/list/mutator/DateSorterForCards.java",
"java/src/org/chromium/chrome/browser/download/home/list/mutator/DateLabelAdder.java",
"java/src/org/chromium/chrome/browser/download/home/list/mutator/DateOrderedListMutator.java",
"java/src/org/chromium/chrome/browser/download/home/list/mutator/NoopLabelAdder.java",
"java/src/org/chromium/chrome/browser/download/home/list/mutator/GroupCardLabelAdder.java",
"java/src/org/chromium/chrome/browser/download/home/list/mutator/Paginator.java",
"java/src/org/chromium/chrome/browser/download/home/list/mutator/ListItemPropertySetter.java",
"java/src/org/chromium/chrome/browser/download/home/list/mutator/ScoreComparator.java",
"java/src/org/chromium/chrome/browser/download/home/list/view/AspectRatioFrameLayout.java",
"java/src/org/chromium/chrome/browser/download/home/list/view/AsyncImageView.java",
......
......@@ -12,7 +12,6 @@ import androidx.annotation.Nullable;
import org.chromium.base.Callback;
import org.chromium.base.CollectionUtil;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.GlobalDiscardableReferencePool;
import org.chromium.chrome.browser.download.home.DownloadManagerUiConfig;
import org.chromium.chrome.browser.download.home.JustNowProvider;
......@@ -30,13 +29,16 @@ import org.chromium.chrome.browser.download.home.glue.OfflineContentProviderGlue
import org.chromium.chrome.browser.download.home.glue.ThumbnailRequestGlue;
import org.chromium.chrome.browser.download.home.list.DateOrderedListCoordinator.DateOrderedListObserver;
import org.chromium.chrome.browser.download.home.list.DateOrderedListCoordinator.DeleteController;
import org.chromium.chrome.browser.download.home.list.mutator.DateComparator;
import org.chromium.chrome.browser.download.home.list.mutator.CardPaginator;
import org.chromium.chrome.browser.download.home.list.mutator.DateLabelAdder;
import org.chromium.chrome.browser.download.home.list.mutator.DateOrderedListMutator;
import org.chromium.chrome.browser.download.home.list.mutator.DateOrderedListMutator.LabelAdder;
import org.chromium.chrome.browser.download.home.list.mutator.NoopLabelAdder;
import org.chromium.chrome.browser.download.home.list.mutator.DateOrderedListMutator.Sorter;
import org.chromium.chrome.browser.download.home.list.mutator.DateSorter;
import org.chromium.chrome.browser.download.home.list.mutator.DateSorterForCards;
import org.chromium.chrome.browser.download.home.list.mutator.GroupCardLabelAdder;
import org.chromium.chrome.browser.download.home.list.mutator.ListItemPropertySetter;
import org.chromium.chrome.browser.download.home.list.mutator.Paginator;
import org.chromium.chrome.browser.download.home.list.mutator.ScoreComparator;
import org.chromium.chrome.browser.download.home.metrics.OfflineItemStartupLogger;
import org.chromium.chrome.browser.download.home.metrics.UmaUtils;
import org.chromium.chrome.browser.download.home.metrics.UmaUtils.ViewAction;
......@@ -53,7 +55,7 @@ import org.chromium.components.offline_items_collection.VisualsCallback;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
/**
......@@ -117,11 +119,14 @@ class DateOrderedListMediator {
private final TypeOfflineItemFilter mTypeFilter;
private final SearchOfflineItemFilter mSearchFilter;
private final Comparator<OfflineItem> mDateComparator;
private final Comparator<OfflineItem> mScoreComparator;
private final LabelAdder mDateLabelAdder;
private final LabelAdder mNoopLabelAdder;
private final Paginator mPaginator;
private final CardPaginator mCardPaginator;
private final Sorter mDefaultDateSorter;
private final LabelAdder mDefaultDateLabelAdder;
private final Sorter mPrefetchSorter;
private final LabelAdder mPrefetchLabelAdder;
/**
* A selection observer that correctly updates the selection state for each item in the list.
......@@ -195,13 +200,16 @@ class DateOrderedListMediator {
mTypeFilter = new TypeOfflineItemFilter(mSearchFilter);
JustNowProvider justNowProvider = new JustNowProvider(config);
mDateComparator = new DateComparator(justNowProvider);
mScoreComparator = new ScoreComparator();
mDateLabelAdder = new DateLabelAdder(config, justNowProvider);
mNoopLabelAdder = new NoopLabelAdder();
mPaginator = new Paginator();
mListMutator = new DateOrderedListMutator(
mTypeFilter, mModel, justNowProvider, mDateComparator, mDateLabelAdder, mPaginator);
mCardPaginator = new CardPaginator();
mDefaultDateSorter = new DateSorter(justNowProvider);
mDefaultDateLabelAdder = new DateLabelAdder(config, justNowProvider);
mPrefetchSorter = new DateSorterForCards();
mPrefetchLabelAdder = new GroupCardLabelAdder(mCardPaginator);
mListMutator =
new DateOrderedListMutator(mTypeFilter, mModel, justNowProvider, mDefaultDateSorter,
mDefaultDateLabelAdder, new ListItemPropertySetter(mUiConfig), mPaginator);
new OfflineItemStartupLogger(config, mInvalidStateFilter);
......@@ -240,15 +248,16 @@ class DateOrderedListMediator {
*/
public void onFilterTypeSelected(@FilterType int filter) {
mPaginator.reset();
Comparator<OfflineItem> comparator = mDateComparator;
LabelAdder labelAdder = mDateLabelAdder;
mCardPaginator.reset();
Sorter sorter = mDefaultDateSorter;
LabelAdder labelAdder = mDefaultDateLabelAdder;
if (filter == FilterType.PREFETCHED) {
if (ChromeFeatureList.isEnabled(ChromeFeatureList.OFFLINE_HOME)) {
comparator = mScoreComparator;
}
labelAdder = mNoopLabelAdder;
sorter = mPrefetchSorter;
labelAdder = mPrefetchLabelAdder;
}
mListMutator.setMutators(comparator, labelAdder);
mListMutator.setMutators(sorter, labelAdder);
try (AnimationDisableClosable closeable = new AnimationDisableClosable()) {
mTypeFilter.onFilterSelected(filter);
}
......@@ -311,6 +320,11 @@ class DateOrderedListMediator {
return mTypeFilter;
}
private void loadMoreItemsOnCard(android.util.Pair<Date, String> dateAndDomain) {
mCardPaginator.loadMore(dateAndDomain);
mListMutator.reload();
}
private void onSelection(@Nullable ListItem item) {
mSelectionDelegate.toggleSelectionForItem(item);
}
......
......@@ -4,6 +4,7 @@
package org.chromium.chrome.browser.download.home.list;
import android.util.Pair;
import android.view.View;
import androidx.annotation.VisibleForTesting;
......@@ -63,19 +64,77 @@ public abstract class ListItem {
}
}
/** A {@link ListItem} representing group card decoration such as header or footer. */
private static class CardDecorationListItem extends ListItem {
public final Pair<Date, String> dateAndDomain;
/** Creates a {@link CardDecorationListItem} instance. */
public CardDecorationListItem(Pair<Date, String> dateAndDomain, boolean isHeader) {
super(generateStableId(dateAndDomain, isHeader));
this.dateAndDomain = dateAndDomain;
}
@VisibleForTesting
static long generateStableId(Pair<Date, String> dateAndDomain, boolean isHeader) {
return isHeader ? dateAndDomain.hashCode() : ~dateAndDomain.hashCode();
}
}
/** A {@link ListItem} representing a card header. */
public static class CardHeaderListItem extends CardDecorationListItem {
/** Creates a {@link CardHeaderListItem} instance. */
public CardHeaderListItem(Pair<Date, String> dateAndDomain) {
super(dateAndDomain, true);
}
}
/** A {@link ListItem} representing a card footer. */
public static class CardFooterListItem extends CardDecorationListItem {
/** Creates a {@link CardFooterListItem} instance. */
public CardFooterListItem(Pair<Date, String> dateAndDomain) {
super(dateAndDomain, false);
}
}
/** A {@link ListItem} representing a divider in a group card. */
public static class CardDividerListItem extends ListItem {
/** The position of the divider in a group card. */
public enum Position {
/** Represents the curved border at the top of a group card. */
TOP,
/**
Represents the line divider between two items in a group card. It also contains
two side bars on left and right to make up for the padding between two items.
*/
MIDDLE,
/** Represents the curved border at the bottom of a group card. */
BOTTOM
}
public final Position position;
/** Creates a {@link CardDividerListItem} instance for a given position. */
public CardDividerListItem(long stableId, Position position) {
super(stableId);
this.position = position;
}
}
/** A {@link ListItem} representing a section header. */
public static class SectionHeaderListItem extends DateListItem {
public boolean isJustNow;
public boolean showDivider;
public boolean showTopDivider;
/**
* Creates a {@link SectionHeaderListItem} instance for a given {@code timestamp}.
*/
public SectionHeaderListItem(long timestamp, boolean isJustNow, boolean showDivider) {
public SectionHeaderListItem(long timestamp, boolean isJustNow, boolean showTopDivider) {
super(isJustNow ? StableIds.JUST_NOW_SECTION : generateStableId(timestamp),
new Date(timestamp));
this.isJustNow = isJustNow;
this.showDivider = showDivider;
this.showTopDivider = showTopDivider;
}
@VisibleForTesting
......@@ -89,6 +148,7 @@ public abstract class ListItem {
public static class OfflineItemListItem extends DateListItem {
public OfflineItem item;
public boolean spanFullWidth;
public boolean isGrouped;
/** Creates an {@link OfflineItemListItem} wrapping {@code item}. */
public OfflineItemListItem(OfflineItem item) {
......
......@@ -5,13 +5,12 @@
package org.chromium.chrome.browser.download.home.list;
import androidx.annotation.IntDef;
import androidx.annotation.StringRes;
import org.chromium.chrome.R;
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.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;
......@@ -116,26 +115,10 @@ public class ListUtils {
return ViewType.GENERIC;
}
/**
* @return The id of the string to be displayed as the section header for the given filter.
*/
public static @StringRes int getTextForSection(int filter) {
switch (filter) {
case OfflineItemFilter.PAGE:
return R.string.download_manager_ui_pages;
case OfflineItemFilter.IMAGE:
return R.string.download_manager_ui_images;
case OfflineItemFilter.VIDEO:
return R.string.download_manager_ui_video;
case OfflineItemFilter.AUDIO:
return R.string.download_manager_ui_audio;
case OfflineItemFilter.OTHER:
return R.string.download_manager_ui_other;
case OfflineItemFilter.DOCUMENT:
return R.string.download_manager_ui_documents;
default:
return R.string.download_manager_ui_all_downloads;
}
/** @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);
}
/**
......
......@@ -10,7 +10,6 @@ import android.text.format.DateUtils;
import android.text.format.Formatter;
import androidx.annotation.DrawableRes;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ContextUtils;
import org.chromium.chrome.R;
......@@ -31,16 +30,6 @@ import java.util.Date;
/** A set of helper utility methods for the UI. */
public final class UiUtils {
private static boolean sDisableUrlFormatting;
/**
* Disable url formatting for tests since tests might not native initialized.
*/
@VisibleForTesting
public static void setDisableUrlFormattingForTests(boolean disabled) {
sDisableUrlFormatting = disabled;
}
private UiUtils() {}
/**
......@@ -142,10 +131,7 @@ public final class UiUtils {
public static CharSequence generatePrefetchCaption(OfflineItem item) {
Context context = ContextUtils.getApplicationContext();
String displaySize = Formatter.formatFileSize(context, item.totalSizeBytes);
String displayUrl = item.pageUrl;
if (!sDisableUrlFormatting) {
displayUrl = UrlFormatter.formatUrlForSecurityDisplayOmitScheme(item.pageUrl);
}
String displayUrl = UrlFormatter.formatUrlForSecurityDisplayOmitScheme(item.pageUrl);
return context.getString(
R.string.download_manager_prefetch_caption, displayUrl, displaySize);
}
......@@ -157,10 +143,7 @@ public final class UiUtils {
*/
public static CharSequence generateGenericCaption(OfflineItem item) {
Context context = ContextUtils.getApplicationContext();
String displayUrl = item.pageUrl;
if (!sDisableUrlFormatting) {
displayUrl = UrlFormatter.formatUrlForSecurityDisplayOmitScheme(item.pageUrl);
}
String displayUrl = UrlFormatter.formatUrlForSecurityDisplayOmitScheme(item.pageUrl);
if (item.totalSizeBytes == 0) {
return context.getString(
......@@ -374,4 +357,11 @@ public final class UiUtils {
return "";
}
}
/** @return The domain associated with the given {@link OfflineItem}. */
public static String getDomainForItem(OfflineItem offlineItem) {
String formattedUrl =
UrlFormatter.formatUrlForSecurityDisplayOmitScheme(offlineItem.pageUrl);
return formattedUrl;
}
}
......@@ -45,6 +45,6 @@ public class SectionTitleViewHolder extends ListItemViewHolder {
R.string.download_manager_just_now)
: UiUtils.dateToHeaderString(sectionItem.date));
mDivider.setVisibility(sectionItem.showDivider ? ViewGroup.VISIBLE : ViewGroup.GONE);
mDivider.setVisibility(sectionItem.showTopDivider ? ViewGroup.VISIBLE : ViewGroup.GONE);
}
}
\ No newline at end of file
}
// Copyright 2019 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.mutator;
import android.util.Pair;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* Maintains the pagination info for the group cards to be shown.
*/
public class CardPaginator {
private static final int ITEM_COUNT_PER_PAGE = 3;
// Maintains the current page count for each card. The cards are keyed by date and domain.
private Map<Pair<Date, String>, Integer> mPageCountForCard = new HashMap<>();
/**
* Called to load one more page for the given card.
* @param dateAndDomain The date and domain for the items in the card.
*/
public void loadMore(Pair<Date, String> dateAndDomain) {
assert mPageCountForCard.containsKey(dateAndDomain);
int currentPages = mPageCountForCard.get(dateAndDomain);
mPageCountForCard.put(dateAndDomain, currentPages + 1);
}
/**
* Called to initialize a card entry in the map if it doesn't already exist.
* @param key The date and domain associated with the card.
*/
public void initializeEntry(Pair<Date, String> key) {
if (mPageCountForCard.containsKey(key)) return;
mPageCountForCard.put(key, 1);
}
/**
* Called to get the item count on the card.
* @param dateAndDomain The date and domain for the items in the card.
* @return The number of items being shown on the card.
*/
public int getItemCountForCard(Pair<Date, String> dateAndDomain) {
return mPageCountForCard.containsKey(dateAndDomain)
? mPageCountForCard.get(dateAndDomain) * ITEM_COUNT_PER_PAGE
: 0;
}
/**
* @return The minimum number of items to be shown on a group card. If item count is less than
* this number, group card cannot be used.
*/
public int minItemCountPerCard() {
return ITEM_COUNT_PER_PAGE;
}
/**
* Called to reset the item count on the card.
*/
public void reset() {
mPageCountForCard.clear();
}
}
......@@ -12,7 +12,6 @@ import org.chromium.chrome.browser.download.home.list.CalendarUtils;
import org.chromium.chrome.browser.download.home.list.ListItem;
import org.chromium.chrome.browser.download.home.list.ListItem.OfflineItemListItem;
import org.chromium.components.offline_items_collection.OfflineItem;
import org.chromium.components.offline_items_collection.OfflineItemFilter;
import java.util.ArrayList;
import java.util.Date;
......@@ -20,64 +19,49 @@ import java.util.List;
/**
* Implementation of {@link LabelAdder} that adds date headers for each date.
* Also adds Just Now header for recently completed items.
* Also adds Just Now header for recently completed items. Note that this class must be called on
* the list before adding any other labels such as card header/footer/pagination etc.
*/
public class DateLabelAdder implements DateOrderedListMutator.LabelAdder {
private final DownloadManagerUiConfig mConfig;
@Nullable
private final JustNowProvider mJustNowProvider;
public DateLabelAdder(DownloadManagerUiConfig config, JustNowProvider justNowProvider) {
public DateLabelAdder(
DownloadManagerUiConfig config, @Nullable JustNowProvider justNowProvider) {
mConfig = config;
mJustNowProvider = justNowProvider;
}
@Override
public List<ListItem> addLabels(List<OfflineItem> sortedList) {
public List<ListItem> addLabels(List<ListItem> sortedList) {
List<ListItem> listItems = new ArrayList<>();
OfflineItem previousItem = null;
for (int i = 0; i < sortedList.size(); i++) {
OfflineItem offlineItem = sortedList.get(i);
ListItem listItem = sortedList.get(i);
if (!(listItem instanceof OfflineItemListItem)) continue;
OfflineItem offlineItem = ((OfflineItemListItem) listItem).item;
if (startOfNewDay(offlineItem, previousItem)
|| justNowSectionsDiffer(offlineItem, previousItem)) {
addDateHeader(listItems, offlineItem, i);
}
addOfflineListItem(listItems, sortedList, i);
listItems.add(listItem);
previousItem = offlineItem;
}
return listItems;
}
private void addOfflineListItem(
List<ListItem> listItems, List<OfflineItem> sortedList, int index) {
OfflineItem currentItem = sortedList.get(index);
OfflineItemListItem offlineItemListItem = new OfflineItemListItem(currentItem);
listItems.add(offlineItemListItem);
if (mConfig.supportFullWidthImages && currentItem.filter == OfflineItemFilter.IMAGE) {
markFullWidthImageIfApplicable(offlineItemListItem, sortedList, index);
}
}
private void addDateHeader(List<ListItem> listItems, OfflineItem currentItem, int index) {
Date day = CalendarUtils.getStartOfDay(currentItem.creationTimeMs).getTime();
boolean isJustNow = mJustNowProvider != null && mJustNowProvider.isJustNowItem(currentItem);
ListItem.SectionHeaderListItem sectionHeaderItem =
new ListItem.SectionHeaderListItem(day.getTime(),
mJustNowProvider.isJustNowItem(currentItem), index != 0 /* showDivider */);
new ListItem.SectionHeaderListItem(day.getTime(), isJustNow, index != 0);
listItems.add(sectionHeaderItem);
}
private static void markFullWidthImageIfApplicable(
OfflineItemListItem offlineItemListItem, List<OfflineItem> sortedList, int index) {
OfflineItem previousItem = index == 0 ? null : sortedList.get(index - 1);
OfflineItem nextItem = index >= sortedList.size() - 1 ? null : sortedList.get(index + 1);
boolean previousItemIsImage =
previousItem != null && previousItem.filter == OfflineItemFilter.IMAGE;
boolean nextItemIsImage = nextItem != null && nextItem.filter == OfflineItemFilter.IMAGE;
if (!previousItemIsImage && !nextItemIsImage) offlineItemListItem.spanFullWidth = true;
}
private static boolean startOfNewDay(
OfflineItem currentItem, @Nullable OfflineItem previousItem) {
Date currentDay = CalendarUtils.getStartOfDay(currentItem.creationTimeMs).getTime();
......@@ -89,6 +73,7 @@ public class DateLabelAdder implements DateOrderedListMutator.LabelAdder {
private boolean justNowSectionsDiffer(
OfflineItem currentItem, @Nullable OfflineItem previousItem) {
if (mJustNowProvider == null) return false;
if (currentItem == null || previousItem == null) return true;
return mJustNowProvider.isJustNowItem(currentItem)
!= mJustNowProvider.isJustNowItem(previousItem);
......
......@@ -4,8 +4,6 @@
package org.chromium.chrome.browser.download.home.list.mutator;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.CollectionUtil;
import org.chromium.chrome.browser.download.home.JustNowProvider;
import org.chromium.chrome.browser.download.home.filter.OfflineItemFilterObserver;
......@@ -17,7 +15,6 @@ import org.chromium.components.offline_items_collection.OfflineItem;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
......@@ -30,7 +27,19 @@ import java.util.List;
*/
public class DateOrderedListMutator implements OfflineItemFilterObserver {
/**
* Given a sorted list of {@link OfflineItem}, generates a list of {@link ListItem} with
* Sorts a list of {@link ListItem}.
*/
public interface Sorter {
/**
* Sorts a given list as per display requirements.
* @param list The input list to be sorted.
* @return The sorted output list.
*/
ArrayList<ListItem> sort(ArrayList<ListItem> list);
}
/**
* Given a sorted list of offline items, generates a list of {@link ListItem} with
* appropriate labels inserted at the right positions as per the display requirements.
*/
public interface LabelAdder {
......@@ -39,49 +48,51 @@ public class DateOrderedListMutator implements OfflineItemFilterObserver {
* @param sortedList The input list to be displayed.
* @return The output list to be displayed on screen.
*/
List<ListItem> addLabels(List<OfflineItem> sortedList);
List<ListItem> addLabels(List<ListItem> sortedList);
}
private final OfflineItemFilterSource mSource;
private final JustNowProvider mJustNowProvider;
private final ListItemModel mModel;
private final Paginator mPaginator;
private final ArrayList<OfflineItem> mSortedItems = new ArrayList<>();
private ArrayList<ListItem> mSortedItems = new ArrayList<>();
private Comparator<OfflineItem> mComparator;
private Sorter mSorter;
private LabelAdder mLabelAdder;
private ListItemPropertySetter mPropertySetter;
/**
* 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 justNowProvider The provider for Just Now section.
* @param comparator The default comparator to be used for list item comparison
* @param sorter The default sorter to use for sorting the list.
* @param labelAdder The label adder used for producing the final list.
* @param paginator The paginator to handle pagination.
*/
public DateOrderedListMutator(OfflineItemFilterSource source, ListItemModel model,
JustNowProvider justNowProvider, Comparator<OfflineItem> comparator,
LabelAdder labelAdder, Paginator paginator) {
JustNowProvider justNowProvider, Sorter sorter, LabelAdder labelAdder,
ListItemPropertySetter propertySetter, Paginator paginator) {
mSource = source;
mModel = model;
mJustNowProvider = justNowProvider;
mPropertySetter = propertySetter;
mPaginator = paginator;
mSource.addObserver(this);
setMutators(comparator, labelAdder);
setMutators(sorter, labelAdder);
onItemsAdded(mSource.getItems());
}
/**
* Called when the desired sorting order has changed.
* @param comparator The comparator to use for list item comparison.
* @param sorter The sorter to use for sorting the list.
* @param labelAdder The label adder used for producing the final list.
*/
public void setMutators(Comparator<OfflineItem> comparator, LabelAdder labelAdder) {
if (mComparator == comparator && mLabelAdder == labelAdder) return;
mComparator = comparator;
public void setMutators(Sorter sorter, LabelAdder labelAdder) {
if (mSorter == sorter && mLabelAdder == labelAdder) return;
mSorter = sorter;
mLabelAdder = labelAdder;
Collections.sort(mSortedItems, mComparator);
mSortedItems = mSorter.sort(mSortedItems);
}
/**
......@@ -92,12 +103,19 @@ public class DateOrderedListMutator implements OfflineItemFilterObserver {
pushItemsToModel();
}
/** Called to reload the list and display. */
public void reload() {
pushItemsToModel();
}
// OfflineItemFilterObserver implementation.
@Override
public void onItemsAdded(Collection<OfflineItem> items) {
ArrayList<OfflineItem> itemsToAdd = new ArrayList<>(items);
Collections.sort(itemsToAdd, mComparator);
mergeList(mSortedItems, itemsToAdd, mComparator);
for (OfflineItem offlineItem : items) {
OfflineItemListItem listItem = new OfflineItemListItem(offlineItem);
mSortedItems.add(listItem);
}
mSortedItems = mSorter.sort(mSortedItems);
pushItemsToModel();
}
......@@ -105,7 +123,8 @@ public class DateOrderedListMutator implements OfflineItemFilterObserver {
public void onItemsRemoved(Collection<OfflineItem> items) {
for (OfflineItem itemToRemove : items) {
for (int i = 0; i < mSortedItems.size(); i++) {
if (itemToRemove.id.equals(mSortedItems.get(i).id)) mSortedItems.remove(i);
OfflineItem offlineItem = ((OfflineItemListItem) mSortedItems.get(i)).item;
if (itemToRemove.id.equals(offlineItem.id)) mSortedItems.remove(i);
}
}
pushItemsToModel();
......@@ -125,7 +144,9 @@ public class DateOrderedListMutator implements OfflineItemFilterObserver {
onItemsAdded(CollectionUtil.newArrayList(item));
} else {
for (int i = 0; i < mSortedItems.size(); i++) {
if (item.id.equals(mSortedItems.get(i).id)) mSortedItems.set(i, item);
if (item.id.equals(((OfflineItemListItem) mSortedItems.get(i)).item.id)) {
mSortedItems.set(i, new OfflineItemListItem(item));
}
}
updateModelListItem(item);
}
......@@ -149,31 +170,10 @@ public class DateOrderedListMutator implements OfflineItemFilterObserver {
private void pushItemsToModel() {
// TODO(shaktisahu): Add paginated list after finalizing UX.
mModel.set(mLabelAdder.addLabels(mSortedItems));
mModel.dispatchLastEvent();
}
/**
* Merges two sorted lists list1 and list2 and places the result in list1.
* @param list1 The first list, which is also the output.
* @param list2 The second list.
* @param comparator The comparison function to use.
*/
@VisibleForTesting
void mergeList(
List<OfflineItem> list1, List<OfflineItem> list2, Comparator<OfflineItem> comparator) {
int index1 = 0;
int index2 = 0;
while (index2 < list2.size()) {
OfflineItem itemToAdd = list2.get(index2);
boolean foundInsertionPoint =
index1 == list1.size() || comparator.compare(itemToAdd, list1.get(index1)) < 0;
if (foundInsertionPoint) {
list1.add(index1, itemToAdd);
index2++;
}
index1++;
}
List<ListItem> listItems = mLabelAdder.addLabels(mSortedItems);
mPropertySetter.setProperties(listItems);
mModel.set(listItems);
mModel.dispatchLastEvent();
}
}
......@@ -4,26 +4,39 @@
package org.chromium.chrome.browser.download.home.list.mutator;
import android.support.annotation.Nullable;
import org.chromium.chrome.browser.download.home.JustNowProvider;
import org.chromium.chrome.browser.download.home.filter.Filters;
import org.chromium.chrome.browser.download.home.list.ListItem;
import org.chromium.chrome.browser.download.home.list.ListUtils;
import org.chromium.components.offline_items_collection.OfflineItem;
import java.util.Comparator;
import java.util.ArrayList;
import java.util.Collections;
/**
* Comparator based on download date. Items having same date (day of download creation), will be
* Sorter based on download date. Items having same date (day of download creation), will be
* compared based on mime type. For further tie-breakers, timestamp and ID are used.
* Note, the input list must contain only offline items.
*/
public class DateComparator implements Comparator<OfflineItem> {
public class DateSorter implements DateOrderedListMutator.Sorter {
private final JustNowProvider mJustNowProvider;
public DateComparator(JustNowProvider justNowProvider) {
public DateSorter(@Nullable JustNowProvider justNowProvider) {
mJustNowProvider = justNowProvider;
}
@Override
public int compare(OfflineItem lhs, OfflineItem rhs) {
public ArrayList<ListItem> sort(ArrayList<ListItem> list) {
Collections.sort(list, this::compare);
return list;
}
public int compare(ListItem listItem1, ListItem listItem2) {
OfflineItem lhs = ((ListItem.OfflineItemListItem) listItem1).item;
OfflineItem rhs = ((ListItem.OfflineItemListItem) listItem2).item;
int comparison = compareItemByJustNowProvider(lhs, rhs);
if (comparison != 0) return comparison;
......@@ -41,8 +54,8 @@ public class DateComparator implements Comparator<OfflineItem> {
}
private int compareItemByJustNowProvider(OfflineItem lhs, OfflineItem rhs) {
boolean lhsIsJustNowItem = mJustNowProvider.isJustNowItem(lhs);
boolean rhsIsJustNowItem = mJustNowProvider.isJustNowItem(rhs);
boolean lhsIsJustNowItem = mJustNowProvider != null && mJustNowProvider.isJustNowItem(lhs);
boolean rhsIsJustNowItem = mJustNowProvider != null && mJustNowProvider.isJustNowItem(rhs);
if (lhsIsJustNowItem == rhsIsJustNowItem) return 0;
return lhsIsJustNowItem && !rhsIsJustNowItem ? -1 : 1;
}
......
// Copyright 2019 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.mutator;
import org.chromium.chrome.browser.download.home.list.ListItem;
import org.chromium.chrome.browser.download.home.list.ListUtils;
import org.chromium.chrome.browser.download.home.list.UiUtils;
import org.chromium.components.offline_items_collection.OfflineItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Sorter based on download date that also takes into account the items grouped into a card. For
* items grouped in a card, the timestamp of the most recent item will be used for comparison
* purposes. Note, the input list must contain only offline items.
*/
public class DateSorterForCards implements DateOrderedListMutator.Sorter {
private Map<String, Long> mTimestampForCard = new HashMap<>();
@Override
public ArrayList<ListItem> sort(ArrayList<ListItem> inputList) {
setTimestampForCards(inputList);
Collections.sort(inputList, this::compare);
return inputList;
}
public int compare(ListItem listItem1, ListItem listItem2) {
OfflineItem lhs = ((ListItem.OfflineItemListItem) listItem1).item;
OfflineItem rhs = ((ListItem.OfflineItemListItem) listItem2).item;
// Compare items by timestamp. For group items, use most recent timestamp.
int comparison =
Long.compare(getTimestampForItem(listItem2), getTimestampForItem(listItem1));
if (comparison != 0) return comparison;
// We are probably comparing two items of the same card. Show the most recent one first.
comparison = ListUtils.compareItemByTimestamp(lhs, rhs);
if (comparison != 0) return comparison;
return ListUtils.compareItemByID(lhs, rhs);
}
private void setTimestampForCards(ArrayList<ListItem> inputList) {
mTimestampForCard.clear();
// For items having same domain, use the timestamp of the most recent item for comparison.
for (ListItem listItem : inputList) {
if (!ListUtils.canGroup(listItem)) continue;
OfflineItem offlineItem = ((ListItem.OfflineItemListItem) listItem).item;
String domain = UiUtils.getDomainForItem(offlineItem);
long timestampForCard =
mTimestampForCard.containsKey(domain) ? mTimestampForCard.get(domain) : 0;
mTimestampForCard.put(domain, Math.max(timestampForCard, offlineItem.creationTimeMs));
}
}
private long getTimestampForItem(ListItem listItem) {
OfflineItem offlineItem = ((ListItem.OfflineItemListItem) listItem).item;
if (ListUtils.canGroup(listItem)) {
String domain = UiUtils.getDomainForItem(offlineItem);
return mTimestampForCard.get(domain);
}
return offlineItem.creationTimeMs;
}
}
// Copyright 2019 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.mutator;
import android.util.Pair;
import org.chromium.chrome.browser.download.home.list.CalendarUtils;
import org.chromium.chrome.browser.download.home.list.ListItem;
import org.chromium.chrome.browser.download.home.list.ListItem.CardDividerListItem;
import org.chromium.chrome.browser.download.home.list.ListUtils;
import org.chromium.chrome.browser.download.home.list.UiUtils;
import org.chromium.components.offline_items_collection.OfflineItem;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Implementation of {@link LabelAdder} that adds card header and footer items for the group cards.
*/
public class GroupCardLabelAdder implements DateOrderedListMutator.LabelAdder {
private static final long CARD_DIVIDER_MIDDLE_HASH_CODE_OFFSET = 200000;
private CardPaginator mCardPaginator;
private long mDividerIndexId;
/** Constructor. */
public GroupCardLabelAdder(CardPaginator paginator) {
mCardPaginator = paginator;
}
@Override
public List<ListItem> addLabels(List<ListItem> sortedList) {
mDividerIndexId = CARD_DIVIDER_MIDDLE_HASH_CODE_OFFSET;
List<ListItem> outList = new ArrayList<>();
List<ListItem> candidateCardItems = new ArrayList<>();
ListItem previousItem = null;
for (ListItem listItem : sortedList) {
if (listItem instanceof ListItem.OfflineItemListItem) {
((ListItem.OfflineItemListItem) listItem).isGrouped = false;
}
boolean addToExistingGroup = ListUtils.canGroup(listItem)
&& ListUtils.canGroup(previousItem)
&& getDateAndDomainForItem(listItem).equals(
getDateAndDomainForItem(previousItem));
if (addToExistingGroup) {
candidateCardItems.add(listItem);
} else {
// Add the grouped items that we have seen so far but didn't add into a card.
flushCandidateCardItemsToList(candidateCardItems, outList);
candidateCardItems.clear();
if (ListUtils.canGroup(listItem)) {
// The item is content indexed with a different domain. Start a new group.
candidateCardItems.add(listItem);
} else {
// The item is not content indexed. Just add it to the list.
outList.add(listItem);
}
}
previousItem = listItem;
}
flushCandidateCardItemsToList(candidateCardItems, outList);
return outList;
}
/**
* Flushes the candidate card items into the list. If the item count is greater than minimum
* threshold, creates a group card out of the items, otherwise adds them to the flat list. Adds
* the header and footer cards for the group card.
* @param candidateCardItems The items to be considered for creating a group card. They must
* have same domain.
* @param outList The output list that would be shown in the UI.
*/
private void flushCandidateCardItemsToList(
List<ListItem> candidateCardItems, List<ListItem> outList) {
if (candidateCardItems.isEmpty()) return;
if (candidateCardItems.size() < mCardPaginator.minItemCountPerCard()) {
// We don't have enough items to build a card. Just add them to the flat list.
outList.addAll(candidateCardItems);
return;
}
Pair<Date, String> dateAndDomain = getDateAndDomainForItem(candidateCardItems.get(0));
mCardPaginator.initializeEntry(dateAndDomain);
// Add the card header, and the divider above it.
outList.add(createDivider(CardDividerListItem.Position.TOP));
outList.add(new ListItem.CardHeaderListItem(dateAndDomain));
int itemsBeforePagination = mCardPaginator.getItemCountForCard(dateAndDomain);
int numItemsToShow = Math.min(itemsBeforePagination, candidateCardItems.size());
// Add the list items and the associated dividers.
for (int i = 0; i < numItemsToShow; i++) {
ListItem.OfflineItemListItem listItem =
(ListItem.OfflineItemListItem) candidateCardItems.get(i);
listItem.isGrouped = true;
outList.add(listItem);
if (i < numItemsToShow - 1) {
outList.add(createDivider(CardDividerListItem.Position.MIDDLE));
}
}
if (candidateCardItems.size() > itemsBeforePagination) {
// Add the card footer and the divider below.
outList.add(createDivider(CardDividerListItem.Position.MIDDLE));
outList.add(new ListItem.CardFooterListItem(dateAndDomain));
}
outList.add(createDivider(CardDividerListItem.Position.BOTTOM));
}
private ListItem createDivider(CardDividerListItem.Position position) {
return new CardDividerListItem(mDividerIndexId++, position);
}
private static Pair<Date, String> getDateAndDomainForItem(ListItem listItem) {
assert ListUtils.canGroup(listItem);
OfflineItem offlineItem = ((ListItem.OfflineItemListItem) listItem).item;
Date date = CalendarUtils.getStartOfDay(offlineItem.creationTimeMs).getTime();
String domain = UiUtils.getDomainForItem(offlineItem);
return Pair.create(date, domain);
}
}
// Copyright 2019 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.mutator;
import org.chromium.chrome.browser.download.home.DownloadManagerUiConfig;
import org.chromium.chrome.browser.download.home.list.ListItem;
import org.chromium.chrome.browser.download.home.list.ListItem.OfflineItemListItem;
import org.chromium.components.offline_items_collection.OfflineItemFilter;
import java.util.List;
/**
* Post processes the items in the list and sets properties for UI as appropriate. The properties
* being set are:
* - Image item span width.
*/
public class ListItemPropertySetter {
private final DownloadManagerUiConfig mConfig;
/** Constructor. */
public ListItemPropertySetter(DownloadManagerUiConfig config) {
mConfig = config;
}
/** Sets properties for items in the given list. */
public void setProperties(List<ListItem> sortedList) {
setWidthForImageItems(sortedList);
}
private void setWidthForImageItems(List<ListItem> listItems) {
if (!mConfig.supportFullWidthImages) return;
for (int i = 0; i < listItems.size(); i++) {
ListItem currentItem = listItems.get(i);
boolean currentItemIsImage = currentItem instanceof OfflineItemListItem
&& ((OfflineItemListItem) currentItem).item.filter == OfflineItemFilter.IMAGE;
if (!currentItemIsImage) continue;
ListItem previousItem = i == 0 ? null : listItems.get(i - 1);
ListItem nextItem = i >= listItems.size() - 1 ? null : listItems.get(i + 1);
boolean previousItemIsImage = previousItem instanceof OfflineItemListItem
&& ((OfflineItemListItem) previousItem).item.filter == OfflineItemFilter.IMAGE;
boolean nextItemIsImage = nextItem instanceof OfflineItemListItem
&& ((OfflineItemListItem) nextItem).item.filter == OfflineItemFilter.IMAGE;
if (!previousItemIsImage && !nextItemIsImage) {
((OfflineItemListItem) currentItem).spanFullWidth = true;
}
}
}
}
// Copyright 2019 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.mutator;
import org.chromium.chrome.browser.download.home.list.ListItem;
import org.chromium.components.offline_items_collection.OfflineItem;
import java.util.ArrayList;
import java.util.List;
/**
* Implementation of {@link LabelAdder} that doesn't insert any extra labels.
*/
public class NoopLabelAdder implements DateOrderedListMutator.LabelAdder {
@Override
public List<ListItem> addLabels(List<OfflineItem> sortedList) {
List<ListItem> listItems = new ArrayList<>();
for (OfflineItem offlineItem : sortedList) {
listItems.add(new ListItem.OfflineItemListItem(offlineItem));
}
return listItems;
}
}
......@@ -21,6 +21,8 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.core.AllOf.allOf;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import android.os.Handler;
import android.os.Looper;
......@@ -28,7 +30,7 @@ import android.support.test.espresso.action.ViewActions;
import android.support.test.filters.MediumTest;
import org.hamcrest.Matcher;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
......@@ -36,10 +38,10 @@ import org.mockito.MockitoAnnotations;
import org.chromium.base.Callback;
import org.chromium.base.task.PostTask;
import org.chromium.base.test.util.JniMocker;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.download.home.filter.FilterCoordinator;
import org.chromium.chrome.browser.download.home.list.UiUtils;
import org.chromium.chrome.browser.download.home.rename.RenameUtils;
import org.chromium.chrome.browser.download.home.toolbar.DownloadHomeToolbar;
import org.chromium.chrome.browser.download.items.OfflineContentAggregatorFactory;
......@@ -56,6 +58,8 @@ 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 org.chromium.components.offline_items_collection.RenameResult;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.components.url_formatter.UrlFormatterJni;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.ui.modaldialog.ModalDialogManager;
......@@ -72,6 +76,10 @@ public class DownloadActivityV2Test extends DummyUiActivityTestCase {
private Tracker mTracker;
@Mock
private SnackbarManager mSnackbarManager;
@Rule
public JniMocker mJniMocker = new JniMocker();
@Mock
private UrlFormatter.Natives mUrlFormatterJniMock;
private ModalDialogManager.Presenter mAppModalPresenter;
......@@ -81,15 +89,13 @@ public class DownloadActivityV2Test extends DummyUiActivityTestCase {
private StubbedOfflineContentProvider mStubbedOfflineContentProvider;
@BeforeClass
public static void setUpBeforeActivityLaunched() {
UiUtils.setDisableUrlFormattingForTests(true);
}
@Override
public void setUpTest() throws Exception {
super.setUpTest();
MockitoAnnotations.initMocks(this);
mJniMocker.mock(UrlFormatterJni.TEST_HOOKS, mUrlFormatterJniMock);
when(mUrlFormatterJniMock.formatUrlForSecurityDisplayOmitScheme(anyString()))
.then(inv -> inv.getArgument(0));
Map<String, Boolean> features = new HashMap<>();
features.put(ChromeFeatureList.DOWNLOADS_LOCATION_CHANGE, true);
......
......@@ -28,9 +28,10 @@ import org.chromium.chrome.browser.download.home.StableIds;
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.chrome.browser.download.home.list.mutator.DateComparator;
import org.chromium.chrome.browser.download.home.list.mutator.DateLabelAdder;
import org.chromium.chrome.browser.download.home.list.mutator.DateOrderedListMutator;
import org.chromium.chrome.browser.download.home.list.mutator.DateSorter;
import org.chromium.chrome.browser.download.home.list.mutator.ListItemPropertySetter;
import org.chromium.chrome.browser.download.home.list.mutator.Paginator;
import org.chromium.components.offline_items_collection.OfflineItem;
import org.chromium.components.offline_items_collection.OfflineItemFilter;
......@@ -889,16 +890,16 @@ public class DateOrderedListMutatorTest {
}
};
return new DateOrderedListMutator(mSource, mModel, justNowProvider,
new DateComparator(justNowProvider), new DateLabelAdder(config, justNowProvider),
new Paginator());
new DateSorter(justNowProvider), new DateLabelAdder(config, justNowProvider),
new ListItemPropertySetter(config), new Paginator());
}
private DateOrderedListMutator createMutatorWithJustNowProvider() {
DownloadManagerUiConfig config = new DownloadManagerUiConfig.Builder().build();
JustNowProvider justNowProvider = new JustNowProvider(config);
return new DateOrderedListMutator(mSource, mModel, justNowProvider,
new DateComparator(justNowProvider), new DateLabelAdder(config, justNowProvider),
new Paginator());
new DateSorter(justNowProvider), new DateLabelAdder(config, justNowProvider),
new ListItemPropertySetter(config), new Paginator());
}
private static void assertDatesAreEqual(Date date, Calendar calendar) {
......@@ -921,14 +922,14 @@ public class DateOrderedListMutatorTest {
assertDatesAreEqual(sectionHeader.date, calendar);
Assert.assertEquals(
SectionHeaderListItem.generateStableId(calendar.getTimeInMillis()), item.stableId);
Assert.assertEquals(sectionHeader.showDivider, showDivider);
Assert.assertEquals(sectionHeader.showTopDivider, showDivider);
}
private static void assertJustNowSection(ListItem item, boolean showDivider) {
Assert.assertTrue(item instanceof SectionHeaderListItem);
SectionHeaderListItem sectionHeader = (SectionHeaderListItem) item;
Assert.assertTrue(sectionHeader.isJustNow);
Assert.assertEquals(sectionHeader.showDivider, showDivider);
Assert.assertEquals(sectionHeader.showTopDivider, showDivider);
Assert.assertEquals(StableIds.JUST_NOW_SECTION, item.stableId);
}
}
......@@ -14,6 +14,7 @@ import androidx.annotation.Nullable;
public class LegacyHelpers {
// These are legacy namespaces for the purpose of ID generation that will only affect the UI.
public static final String LEGACY_OFFLINE_PAGE_NAMESPACE = "LEGACY_OFFLINE_PAGE";
public static final String LEGACY_CONTENT_INDEX_NAMESPACE = "content_index";
public static final String LEGACY_DOWNLOAD_NAMESPACE = "LEGACY_DOWNLOAD";
public static final String LEGACY_ANDROID_DOWNLOAD_NAMESPACE = "LEGACY_ANDROID_DOWNLOAD";
private static final String LEGACY_DOWNLOAD_NAMESPACE_PREFIX = "LEGACY_DOWNLOAD";
......@@ -44,6 +45,15 @@ public class LegacyHelpers {
&& id.namespace.startsWith(LEGACY_DOWNLOAD_NAMESPACE);
}
/**
* Helper to determine if a {@link ContentId} is for an content indexed item.
* @param id The {@link ContentId} to inspect.
* @return Whether or not {@code id} was built for a content indexed item.
*/
public static boolean isLegacyContentIndexedItem(@Nullable ContentId id) {
return id != null && TextUtils.equals(LEGACY_CONTENT_INDEX_NAMESPACE, id.namespace);
}
/**
* Helper to determine if a {@link ContentId} was created from
* {@link #buildLegacyContentId(boolean, String)} for an offline page ({@code true} for {@code
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment