1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.components.browser_ui.widget;
6 
7 import android.util.Pair;
8 import android.view.LayoutInflater;
9 import android.view.View;
10 import android.view.ViewGroup;
11 import android.widget.TextView;
12 
13 import androidx.annotation.IntDef;
14 import androidx.annotation.Nullable;
15 import androidx.recyclerview.widget.RecyclerView;
16 import androidx.recyclerview.widget.RecyclerView.Adapter;
17 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
18 
19 import org.chromium.base.Log;
20 import org.chromium.base.task.AsyncTask;
21 import org.chromium.base.task.BackgroundOnlyAsyncTask;
22 import org.chromium.components.browser_ui.util.date.StringUtils;
23 
24 import java.lang.annotation.Retention;
25 import java.lang.annotation.RetentionPolicy;
26 import java.util.ArrayList;
27 import java.util.Calendar;
28 import java.util.Collections;
29 import java.util.Comparator;
30 import java.util.Date;
31 import java.util.List;
32 import java.util.SortedSet;
33 import java.util.TreeSet;
34 import java.util.concurrent.ExecutionException;
35 
36 /**
37  * An {@link Adapter} that works with a {@link RecyclerView}. It sorts the given {@link List} of
38  * {@link TimedItem}s according to their date, and divides them into sub lists and displays them in
39  * different sections.
40  * <p>
41  * Subclasses should not care about the how date headers are placed in the list. Instead, they
42  * should call {@link #loadItems(List)} with a list of {@link TimedItem}, and this adapter will
43  * insert the headers automatically.
44  */
45 public abstract class DateDividedAdapter extends Adapter<RecyclerView.ViewHolder> {
46     /**
47      * Interface that the {@link Adapter} uses to interact with the items it manages.
48      */
49     public abstract static class TimedItem {
50         /** Value indicating that a TimedItem is not currently being displayed. */
51         public static final int INVALID_POSITION = -1;
52 
53         /** Position of the TimedItem in the list, or {@link #INVALID_POSITION} if not shown. */
54         private int mPosition = INVALID_POSITION;
55 
56         private boolean mIsFirstInGroup;
57         private boolean mIsLastInGroup;
58         private boolean mIsDateHeader;
59 
60         /** See {@link #mPosition}. */
setPosition(int position)61         private final void setPosition(int position) {
62             mPosition = position;
63         }
64 
65         /** See {@link #mPosition}. */
getPosition()66         public final int getPosition() {
67             return mPosition;
68         }
69 
70         /**
71          * @param isFirst Whether this item is the first in its group.
72          */
setIsFirstInGroup(boolean isFirst)73         public final void setIsFirstInGroup(boolean isFirst) {
74             mIsFirstInGroup = isFirst;
75         }
76 
77         /**
78          * @param isLast Whether this item is the last in its group.
79          */
setIsLastInGroup(boolean isLast)80         public final void setIsLastInGroup(boolean isLast) {
81             mIsLastInGroup = isLast;
82         }
83 
84         /**
85          * @return Whether this item is the first in its group.
86          */
isFirstInGroup()87         public boolean isFirstInGroup() {
88             return mIsFirstInGroup;
89         }
90 
91         /**
92          * @return Whether this item is the last in its group.
93          */
isLastInGroup()94         public boolean isLastInGroup() {
95             return mIsLastInGroup;
96         }
97 
98         /** @return The timestamp for this item. */
getTimestamp()99         public abstract long getTimestamp();
100 
101         /**
102          * Returns an ID that uniquely identifies this TimedItem and doesn't change.
103          * To avoid colliding with IDs generated for Date headers, at least one of the upper 32
104          * bits of the long should be set.
105          * @return ID that can uniquely identify the TimedItem.
106          */
getStableId()107         public abstract long getStableId();
108     }
109 
110     /**
111      * Contains information of a single header that this adapter uses to manage headers.
112      */
113     public static class HeaderItem extends TimedItem {
114         private final long mStableId;
115         private final View mView;
116 
117         /**
118          * Initialize stable id and view associated with this HeaderItem.
119          * @param position Position of this HeaderItem in the header group.
120          * @param view View associated with this HeaderItem.
121          */
HeaderItem(int position, View view)122         public HeaderItem(int position, View view) {
123             mStableId = getTimestamp() - position;
124             mView = view;
125         }
126 
127         @Override
getTimestamp()128         public long getTimestamp() {
129             return Long.MAX_VALUE;
130         }
131 
132         @Override
getStableId()133         public long getStableId() {
134             return mStableId;
135         }
136 
137         /**
138          * @return The View associated with this HeaderItem.
139          */
getView()140         public View getView() {
141             return mView;
142         }
143     }
144 
145     /**
146      * Contains information of a single footer that this adapter uses to manage footers.
147      * Share most of the same funcionality as a Header class.
148      */
149     public static class FooterItem extends HeaderItem {
FooterItem(int position, View view)150         public FooterItem(int position, View view) {
151             super(position, view);
152         }
153 
154         @Override
getTimestamp()155         public long getTimestamp() {
156             return Long.MIN_VALUE;
157         }
158     }
159 
160     /** An item representing a date header. */
161     class DateHeaderTimedItem extends TimedItem {
162         private long mTimestamp;
163 
DateHeaderTimedItem(long timestamp)164         public DateHeaderTimedItem(long timestamp) {
165             mTimestamp = getDateAtMidnight(timestamp).getTime();
166         }
167 
168         @Override
getTimestamp()169         public long getTimestamp() {
170             return mTimestamp;
171         }
172 
173         @Override
getStableId()174         public long getStableId() {
175             return getStableIdFromDate(new Date(getTimestamp()));
176         }
177     }
178 
179     /**
180      * A {@link RecyclerView.ViewHolder} that displays a date header.
181      */
182     public static class DateViewHolder extends RecyclerView.ViewHolder {
183         private TextView mTextView;
184 
DateViewHolder(View view)185         public DateViewHolder(View view) {
186             super(view);
187             if (view instanceof TextView) mTextView = (TextView) view;
188         }
189 
190         /**
191          * @param date The date that this DateViewHolder should display.
192          */
setDate(Date date)193         public void setDate(Date date) {
194             mTextView.setText(StringUtils.dateToHeaderString(date));
195         }
196     }
197 
198     protected static class BasicViewHolder extends RecyclerView.ViewHolder {
BasicViewHolder(View itemView)199         public BasicViewHolder(View itemView) {
200             super(itemView);
201         }
202     }
203 
204     protected static class SubsectionHeaderViewHolder extends RecyclerView.ViewHolder {
205         private View mView;
206 
SubsectionHeaderViewHolder(View itemView)207         public SubsectionHeaderViewHolder(View itemView) {
208             super(itemView);
209             mView = itemView;
210         }
211 
getView()212         public View getView() {
213             return mView;
214         }
215     }
216 
217     /**
218      * A bucket of items with the same date. The date header should also be an item of the group.
219      * Special groups are subclassed for list header(s) and list footers.
220      */
221     public static class ItemGroup {
222         private final Date mDate;
223         private final List<TimedItem> mItems = new ArrayList<>();
224 
225         /** Index of the header, relative to the full list.  Must be set only once.*/
226         private int mIndex;
227         private boolean mIsSorted;
228 
229         /** Constructors for groups that contain same date items. */
ItemGroup(long timestamp)230         public ItemGroup(long timestamp) {
231             mDate = new Date(timestamp);
232             mIsSorted = true;
233         }
234 
235         /**
236          * Default constructor for groups that don't contain same date items e.g. header, footer,
237          * elevated priority groups etc.
238          */
ItemGroup()239         public ItemGroup() {
240             mDate = new Date(0L);
241         }
242 
addItem(TimedItem item)243         public void addItem(TimedItem item) {
244             mItems.add(item);
245             mIsSorted = mItems.size() == 1;
246         }
247 
removeItem(TimedItem item)248         public void removeItem(TimedItem item) {
249             mItems.remove(item);
250         }
251 
removeAllItems()252         public void removeAllItems() {
253             mItems.clear();
254         }
255 
256         /** Records the position of all the TimedItems in this group, relative to the full list. */
setPosition(int index)257         public void setPosition(int index) {
258             assert mIndex == 0 || mIndex == TimedItem.INVALID_POSITION;
259             mIndex = index;
260 
261             sortIfNeeded();
262             for (int i = 0; i < mItems.size(); i++) {
263                 TimedItem item = mItems.get(i);
264                 item.setPosition(index);
265                 item.setIsFirstInGroup(i == 0);
266                 item.setIsLastInGroup(i == mItems.size() - 1);
267                 index += 1;
268             }
269         }
270 
271         /** Unsets the position of all TimedItems in this group. */
resetPosition()272         public void resetPosition() {
273             mIndex = TimedItem.INVALID_POSITION;
274             for (TimedItem item : mItems) item.setPosition(TimedItem.INVALID_POSITION);
275         }
276 
277         /**
278          * @return Whether the given date happens in the same day as the items in this group.
279          */
isSameDay(Date otherDate)280         public boolean isSameDay(Date otherDate) {
281             return compareDate(mDate, otherDate) == 0;
282         }
283 
284         /**
285          * @return The size of this group.
286          */
size()287         public int size() {
288             return mItems.size();
289         }
290 
291         /**
292          * Used for sorting list groups.
293          * @return The priority used to determine the position of this {@link ItemGroup} relative to
294          * the top of the list.
295          */
296         @GroupPriority
priority()297         public int priority() {
298             return GroupPriority.NORMAL_CONTENT;
299         }
300 
301         /**
302          * Returns the item to be displayed at the given index of this group.
303          * @param index The index of the item.
304          * @return The corresponding item.
305          */
getItemAt(int index)306         public TimedItem getItemAt(int index) {
307             assert index < size();
308             sortIfNeeded();
309             return mItems.get(index);
310         }
311 
312         /** @return The view type associated for the given index */
313         public @ItemViewType int getItemViewType(int index) {
314             return mItems.get(index).mIsDateHeader ? ItemViewType.DATE : ItemViewType.NORMAL;
315         }
316 
317         /**
318          * Rather than sorting the list each time a new item is added, the list is sorted when
319          * something requires a correct ordering of the items.
320          */
321         protected void sortIfNeeded() {
322             if (mIsSorted) return;
323             mIsSorted = true;
324 
325             Collections.sort(mItems, new Comparator<TimedItem>() {
326                 @Override
327                 public int compare(TimedItem lhs, TimedItem rhs) {
328                     return compareItem(lhs, rhs);
329                 }
330             });
331         }
332 
333         /** Sorting function that determines the ordering of the items in this group. */
334         protected int compareItem(TimedItem lhs, TimedItem rhs) {
335             if (lhs.mIsDateHeader) return -1;
336             if (rhs.mIsDateHeader) return 1;
337 
338             // More recent items are listed first.  Ideally we'd use Long.compare, but that
339             // is an API level 19 call for some inexplicable reason.
340             long timeDelta = lhs.getTimestamp() - rhs.getTimestamp();
341             if (timeDelta > 0) {
342                 return -1;
343             } else if (timeDelta == 0) {
344                 return 0;
345             } else {
346                 return 1;
347             }
348         }
349     }
350 
351     /** An item group representing the list header(s). */
352     public static class HeaderItemGroup extends ItemGroup {
353         @Override
priority()354         public @GroupPriority int priority() {
355             return GroupPriority.HEADER;
356         }
357 
358         @Override
getItemViewType(int index)359         public @ItemViewType int getItemViewType(int index) {
360             return ItemViewType.HEADER;
361         }
362     }
363 
364     /** An item group representing the list footer(s). */
365     public static class FooterItemGroup extends ItemGroup {
366         @Override
priority()367         public @GroupPriority int priority() {
368             return GroupPriority.FOOTER;
369         }
370 
371         @Override
getItemViewType(int index)372         public @ItemViewType int getItemViewType(int index) {
373             return ItemViewType.FOOTER;
374         }
375     }
376 
377     // Cached async tasks to get the two Calendar objects, which are used when comparing dates.
378     private static final AsyncTask<Calendar> sCal1 = createCalendar();
379     private static final AsyncTask<Calendar> sCal2 = createCalendar();
380 
381     /**
382      * Specifies various view types of the list items for the purpose of recycling.
383      */
384     @IntDef({ItemViewType.FOOTER, ItemViewType.HEADER, ItemViewType.DATE, ItemViewType.NORMAL,
385             ItemViewType.SUBSECTION_HEADER})
386     @Retention(RetentionPolicy.SOURCE)
387     public @interface ItemViewType {
388         int FOOTER = -2;
389         int HEADER = -1;
390         int DATE = 0;
391         int NORMAL = 1;
392         int SUBSECTION_HEADER = 2;
393     }
394 
395     /**
396      * The priorities that determine the relative position of item groups starting at the top.
397      * Default priority is GroupPriority.NORMAL_CONTENT.
398      */
399     @IntDef({GroupPriority.HEADER, GroupPriority.ELEVATED_CONTENT, GroupPriority.NORMAL_CONTENT,
400             GroupPriority.FOOTER})
401     @Retention(RetentionPolicy.SOURCE)
402     public @interface GroupPriority {
403         int HEADER = 1;
404         int ELEVATED_CONTENT = 2;
405         int NORMAL_CONTENT = 3;
406         int FOOTER = 4;
407     }
408 
409     private static final String TAG = "DateDividedAdapter";
410 
411     private int mSize;
412 
413     private SortedSet<ItemGroup> mGroups = new TreeSet<>(new Comparator<ItemGroup>() {
414         @Override
415         public int compare(ItemGroup lhs, ItemGroup rhs) {
416             if (lhs == rhs) return 0;
417 
418             if (lhs.priority() != rhs.priority()) {
419                 return lhs.priority() < rhs.priority() ? -1 : 1;
420             }
421 
422             return compareDate(lhs.mDate, rhs.mDate);
423         }
424     });
425 
426     /**
427      * Creates a {@link ViewHolder} in the given view parent.
428      * @see #onCreateViewHolder(ViewGroup, int)
429      */
createViewHolder(ViewGroup parent)430     protected abstract ViewHolder createViewHolder(ViewGroup parent);
431 
432     /**
433      * Creates a {@link BasicViewHolder} in the given view parent for the header. The default
434      * implementation will create an empty FrameLayout container as the view holder.
435      * @see #onCreateViewHolder(ViewGroup, int)
436      */
createHeader(ViewGroup parent)437     protected BasicViewHolder createHeader(ViewGroup parent) {
438         // Create an empty layout as a container for the header view.
439         View v = LayoutInflater.from(parent.getContext())
440                          .inflate(R.layout.date_divided_adapter_header_view_holder, parent, false);
441         return new BasicViewHolder(v);
442     }
443 
444     /**
445      * Creates a {@link BasicViewHolder} in the given view parent for the footer.
446      * See {@link #onCreateViewHolder(ViewGroup, int)}.
447      */
448     @Nullable
createFooter(ViewGroup parent)449     protected BasicViewHolder createFooter(ViewGroup parent) {
450         return null;
451     }
452 
453     /**
454      * Creates a {@link DateViewHolder} in the given view parent.
455      * @see #onCreateViewHolder(ViewGroup, int)
456      */
createDateViewHolder(ViewGroup parent)457     protected DateViewHolder createDateViewHolder(ViewGroup parent) {
458         return new DateViewHolder(LayoutInflater.from(parent.getContext())
459                                           .inflate(getTimedItemViewResId(), parent, false));
460     }
461 
462     /**
463      * Creates a {@link ViewHolder} for a subsection in the given view parent.
464      * @see #onCreateViewHolder(ViewGroup, int)
465      */
466     @Nullable
createSubsectionHeader(ViewGroup parent)467     protected SubsectionHeaderViewHolder createSubsectionHeader(ViewGroup parent) {
468         return null;
469     }
470 
471     /**
472      * Helper function to determine whether an item is a subsection header.
473      * @param timedItem The item.
474      * @return Whether the item is a subsection header.
475      */
isSubsectionHeader(TimedItem timedItem)476     protected boolean isSubsectionHeader(TimedItem timedItem) {
477         return false;
478     }
479 
480     /**
481      * Binds the {@link ViewHolder} with the given {@link TimedItem}.
482      * @see #onBindViewHolder(ViewHolder, int)
483      */
bindViewHolderForTimedItem(ViewHolder viewHolder, TimedItem item)484     protected abstract void bindViewHolderForTimedItem(ViewHolder viewHolder, TimedItem item);
485 
486     /**
487      * Binds the {@link SubsectionHeaderViewHolder} with the given {@link TimedItem}.
488      * @see #onBindViewHolder(ViewHolder, int)
489      */
bindViewHolderForSubsectionHeader( SubsectionHeaderViewHolder holder, TimedItem timedItem)490     protected void bindViewHolderForSubsectionHeader(
491             SubsectionHeaderViewHolder holder, TimedItem timedItem) {}
492 
493     /**
494      * Binds the {@link BasicViewHolder} with the given {@link HeaderItem}.
495      * @see #onBindViewHolder(ViewHolder, int)
496      */
bindViewHolderForHeaderItem(ViewHolder viewHolder, HeaderItem headerItem)497     protected void bindViewHolderForHeaderItem(ViewHolder viewHolder, HeaderItem headerItem) {
498         BasicViewHolder basicViewHolder = (BasicViewHolder) viewHolder;
499         View v = headerItem.getView();
500         ((ViewGroup) basicViewHolder.itemView).removeAllViews();
501         if (v.getParent() != null) ((ViewGroup) v.getParent()).removeView(v);
502         ((ViewGroup) basicViewHolder.itemView).addView(v);
503     }
504 
505     /**
506      * Binds the {@link BasicViewHolder} with the given {@link FooterItem}.
507      * @see #onBindViewHolder(ViewHolder, int)
508      */
bindViewHolderForFooterItem(ViewHolder viewHolder, FooterItem footerItem)509     protected void bindViewHolderForFooterItem(ViewHolder viewHolder, FooterItem footerItem) {
510         BasicViewHolder basicViewHolder = (BasicViewHolder) viewHolder;
511         View v = footerItem.getView();
512         ((ViewGroup) basicViewHolder.itemView).removeAllViews();
513         if (v.getParent() != null) ((ViewGroup) v.getParent()).removeView(v);
514         ((ViewGroup) basicViewHolder.itemView).addView(v);
515     }
516 
517     /**
518      * Gets the resource id of the view showing the date header.
519      * Contract for subclasses: this view should be a {@link TextView}.
520      */
getTimedItemViewResId()521     protected abstract int getTimedItemViewResId();
522 
523     /**
524      * Loads a list of {@link TimedItem}s to this adapter. Previous data will not be removed. Call
525      * {@link #clear(boolean)} to remove previous items.
526      */
loadItems(List<? extends TimedItem> timedItems)527     public void loadItems(List<? extends TimedItem> timedItems) {
528         for (TimedItem timedItem : timedItems) {
529             Date date = new Date(timedItem.getTimestamp());
530             boolean found = false;
531             for (ItemGroup group : mGroups) {
532                 if (group.isSameDay(date)) {
533                     found = true;
534                     group.addItem(timedItem);
535                     break;
536                 }
537             }
538             if (!found) {
539                 // Create a new ItemGroup with the date for the new item. Insert the date header and
540                 // the new item into the group.
541                 TimedItem dateHeader = new DateHeaderTimedItem(timedItem.getTimestamp());
542                 dateHeader.mIsDateHeader = true;
543                 ItemGroup newGroup = new ItemGroup(timedItem.getTimestamp());
544                 newGroup.addItem(dateHeader);
545                 newGroup.addItem(timedItem);
546                 mGroups.add(newGroup);
547             }
548         }
549 
550         setSizeAndGroupPositions();
551         notifyDataSetChanged();
552     }
553 
554     /**
555      * Tells each group where they start in the list. Also calculates the list size.
556      */
setSizeAndGroupPositions()557     private void setSizeAndGroupPositions() {
558         mSize = 0;
559         for (ItemGroup group : mGroups) {
560             group.resetPosition();
561             group.setPosition(mSize);
562             mSize += group.size();
563         }
564     }
565 
566     /**
567      * The utility function to add an {@link ItemGroup}.
568      * @param group The group to be added.
569      */
addGroup(ItemGroup group)570     protected void addGroup(ItemGroup group) {
571         mGroups.add(group);
572 
573         setSizeAndGroupPositions();
574         notifyDataSetChanged();
575     }
576 
577     /**
578      * Add a list of headers as the first group in this adapter. If headerItems has no items,
579      * the header group will not be created. Otherwise, header items will be added as child items
580      * to the header group. Note that any previously added header items will be removed.
581      * {@link #bindViewHolderForHeaderItem(ViewHolder, HeaderItem)} will bind the HeaderItem views
582      * to the given ViewHolder. Sub-classes may override #bindViewHolderForHeaderItem and
583      * (@link #createHeader(ViewGroup)} if custom behavior is needed.
584      *
585      * @param headerItems Zero or more header items to be add to the header item group.
586      */
setHeaders(HeaderItem... headerItems)587     public void setHeaders(HeaderItem... headerItems) {
588         if (headerItems == null || headerItems.length == 0) {
589             removeHeader();
590             return;
591         }
592 
593         if (hasListHeader()) mGroups.remove(mGroups.first());
594 
595         ItemGroup header = new HeaderItemGroup();
596         for (HeaderItem item : headerItems) {
597             header.addItem(item);
598         }
599 
600         addGroup(header);
601     }
602 
603     /**
604      * Removes the list header.
605      */
removeHeader()606     public void removeHeader() {
607         if (!hasListHeader()) return;
608         mGroups.remove(mGroups.first());
609 
610         setSizeAndGroupPositions();
611         notifyDataSetChanged();
612     }
613 
614     /**
615      * Whether the adapter has a list header.
616      */
hasListHeader()617     public boolean hasListHeader() {
618         return !mGroups.isEmpty() && mGroups.first().priority() == GroupPriority.HEADER;
619     }
620 
621     /**
622      * Whether the adapter has a list header.
623      */
hasListFooter()624     public boolean hasListFooter() {
625         return !mGroups.isEmpty() && mGroups.last().priority() == GroupPriority.FOOTER;
626     }
627 
628     /**
629      * Adds a footer as the last group in this adapter.
630      */
addFooter()631     public void addFooter() {
632         if (hasListFooter()) return;
633 
634         ItemGroup footer = new FooterItemGroup();
635         addGroup(footer);
636     }
637 
638     /**
639      * Removes the footer group if present.
640      */
removeFooter()641     public void removeFooter() {
642         if (!hasListFooter()) return;
643 
644         mGroups.remove(mGroups.last());
645         setSizeAndGroupPositions();
646         notifyDataSetChanged();
647     }
648 
649     /**
650      * Removes all items from this adapter.
651      * @param notifyDataSetChanged Whether to notify that the data set has been changed.
652      */
clear(boolean notifyDataSetChanged)653     public void clear(boolean notifyDataSetChanged) {
654         mSize = 0;
655 
656         // Unset the positions of all items in the list.
657         for (ItemGroup group : mGroups) group.resetPosition();
658         mGroups.clear();
659 
660         if (notifyDataSetChanged) notifyDataSetChanged();
661     }
662 
663     @Override
getItemId(int position)664     public long getItemId(int position) {
665         if (!hasStableIds()) return RecyclerView.NO_ID;
666 
667         Pair<Date, TimedItem> pair = getItemAt(position);
668         return pair.second == null ? getStableIdFromDate(pair.first) : pair.second.getStableId();
669     }
670 
671     /**
672      * Gets the item at the given position.
673      */
getItemAt(int position)674     public Pair<Date, TimedItem> getItemAt(int position) {
675         Pair<ItemGroup, Integer> pair = getGroupAt(position);
676         ItemGroup group = pair.first;
677         return new Pair<>(group.mDate, group.getItemAt(pair.second));
678     }
679 
680     @Override
681     @ItemViewType
getItemViewType(int position)682     public final int getItemViewType(int position) {
683         Pair<ItemGroup, Integer> pair = getGroupAt(position);
684         ItemGroup group = pair.first;
685         return group.getItemViewType(pair.second);
686     }
687 
688     @Override
onCreateViewHolder( ViewGroup parent, @ItemViewType int viewType)689     public final RecyclerView.ViewHolder onCreateViewHolder(
690             ViewGroup parent, @ItemViewType int viewType) {
691         switch (viewType) {
692             case ItemViewType.DATE:
693                 return createDateViewHolder(parent);
694             case ItemViewType.NORMAL:
695                 return createViewHolder(parent);
696             case ItemViewType.HEADER:
697                 return createHeader(parent);
698             case ItemViewType.FOOTER:
699                 return createFooter(parent);
700             case ItemViewType.SUBSECTION_HEADER:
701                 return createSubsectionHeader(parent);
702             default:
703                 assert false;
704                 return null;
705         }
706     }
707 
708     @Override
onBindViewHolder(RecyclerView.ViewHolder holder, int position)709     public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
710         Pair<ItemGroup, Integer> groupAndPosition = getGroupAt(position);
711         ItemGroup group = groupAndPosition.first;
712         @ItemViewType
713         int viewType = group.getItemViewType(groupAndPosition.second);
714 
715         Pair<Date, TimedItem> pair = getItemAt(position);
716         switch (viewType) {
717             case ItemViewType.DATE:
718                 ((DateViewHolder) holder).setDate(pair.first);
719                 break;
720             case ItemViewType.NORMAL:
721                 bindViewHolderForTimedItem(holder, pair.second);
722                 break;
723             case ItemViewType.HEADER:
724                 bindViewHolderForHeaderItem(holder, (HeaderItem) pair.second);
725                 break;
726             case ItemViewType.FOOTER:
727                 bindViewHolderForFooterItem(holder, (FooterItem) pair.second);
728                 break;
729             case ItemViewType.SUBSECTION_HEADER:
730                 bindViewHolderForSubsectionHeader((SubsectionHeaderViewHolder) holder, pair.second);
731                 break;
732         }
733     }
734 
735     @Override
getItemCount()736     public final int getItemCount() {
737         return mSize;
738     }
739 
740     /**
741      * Utility method to traverse all groups and find the {@link ItemGroup} for the given position.
742      */
getGroupAt(int position)743     protected Pair<ItemGroup, Integer> getGroupAt(int position) {
744         // TODO(ianwen): Optimize the performance if the number of groups becomes too large.
745         int i = position;
746         for (ItemGroup group : mGroups) {
747             if (i >= group.size()) {
748                 i -= group.size();
749             } else {
750                 return new Pair<>(group, i);
751             }
752         }
753         assert false;
754         return null;
755     }
756 
757     /**
758      * @param item The item to remove from the adapter.
759      */
760     // #getGroupAt() asserts false before returning null, causing findbugs to complain about
761     // a redundant nullcheck even though getGroupAt can return null.
removeItem(TimedItem item)762     protected void removeItem(TimedItem item) {
763         Pair<ItemGroup, Integer> groupPair = getGroupAt(item.getPosition());
764         if (groupPair == null) {
765             Log.e(TAG,
766                     "Failed to find group for item during remove. Item position: "
767                             + item.getPosition() + ", total size: " + mSize);
768             return;
769         }
770 
771         ItemGroup group = groupPair.first;
772         group.removeItem(item);
773 
774         // Remove the group if only the date header is left.
775         if (group.size() == 1) mGroups.remove(group);
776 
777         // Remove header if only the header is left.
778         if (hasListHeader() && mGroups.size() == 1) removeHeader();
779 
780         setSizeAndGroupPositions();
781         notifyDataSetChanged();
782     }
783 
784     /**
785      * Creates a long ID that identifies a particular day in history.
786      * @param date Date to process.
787      * @return Long that has the day of the year (1-365) in the lowest 16 bits and the year in the
788      *         next 16 bits over.
789      */
getStableIdFromDate(Date date)790     private static long getStableIdFromDate(Date date) {
791         Pair<Calendar, Calendar> pair = getCachedCalendars();
792         Calendar calendar = pair.first;
793         calendar.setTime(date);
794         long dayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
795         long year = calendar.get(Calendar.YEAR);
796         return (year << 16) + dayOfYear;
797     }
798 
799     /**
800      * Compares two {@link Date}s. Note if you already have two {@link Calendar} objects, use
801      * {@link #compareCalendar(Calendar, Calendar)} instead.
802      * @return 0 if date1 and date2 are in the same day; 1 if date1 is before date2; -1 otherwise.
803      */
compareDate(Date date1, Date date2)804     protected static int compareDate(Date date1, Date date2) {
805         Pair<Calendar, Calendar> pair = getCachedCalendars();
806         Calendar cal1 = pair.first;
807         Calendar cal2 = pair.second;
808         cal1.setTime(date1);
809         cal2.setTime(date2);
810         return compareCalendar(cal1, cal2);
811     }
812 
813     /**
814      * @return 0 if cal1 and cal2 are in the same day; 1 if cal1 happens before cal2; -1 otherwise.
815      */
compareCalendar(Calendar cal1, Calendar cal2)816     private static int compareCalendar(Calendar cal1, Calendar cal2) {
817         boolean sameDay = cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
818                 && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
819         if (sameDay) {
820             return 0;
821         } else if (cal1.before(cal2)) {
822             return 1;
823         } else {
824             return -1;
825         }
826     }
827 
828     /**
829      * Convenient getter for {@link #sCal1} and {@link #sCal2}.
830      */
getCachedCalendars()831     private static Pair<Calendar, Calendar> getCachedCalendars() {
832         Calendar cal1;
833         Calendar cal2;
834         try {
835             cal1 = sCal1.get();
836             cal2 = sCal2.get();
837         } catch (InterruptedException | ExecutionException e) {
838             // We've tried our best. If AsyncTask really does not work, we give up. :(
839             cal1 = Calendar.getInstance();
840             cal2 = Calendar.getInstance();
841         }
842         return new Pair<>(cal1, cal2);
843     }
844 
845     /**
846      * Wraps {@link Calendar#getInstance()} in an {@link AsyncTask} to avoid Strict mode violation.
847      */
createCalendar()848     private static AsyncTask<Calendar> createCalendar() {
849         return new BackgroundOnlyAsyncTask<Calendar>() {
850             @Override
851             protected Calendar doInBackground() {
852                 return Calendar.getInstance();
853             }
854         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
855     }
856 
857     /**
858      * Calculates the {@link Date} for midnight of the date represented by the |timestamp|.
859      */
860     public static Date getDateAtMidnight(long timestamp) {
861         Calendar cal = Calendar.getInstance();
862         cal.setTimeInMillis(timestamp);
863         cal.set(Calendar.HOUR_OF_DAY, 0);
864         cal.set(Calendar.MINUTE, 0);
865         cal.set(Calendar.SECOND, 0);
866         cal.set(Calendar.MILLISECOND, 0);
867         return cal.getTime();
868     }
869 }
870