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