| // Copyright 2016 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.widget; |
| |
| import android.support.v7.widget.RecyclerView; |
| import android.support.v7.widget.RecyclerView.Adapter; |
| import android.support.v7.widget.RecyclerView.ViewHolder; |
| import android.util.Pair; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.TextView; |
| |
| import androidx.annotation.IntDef; |
| import androidx.annotation.Nullable; |
| |
| import org.chromium.base.Log; |
| import org.chromium.base.task.AsyncTask; |
| import org.chromium.base.task.BackgroundOnlyAsyncTask; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.download.DownloadUtils; |
| import org.chromium.chrome.browser.download.home.list.UiUtils; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.SortedSet; |
| import java.util.TreeSet; |
| import java.util.concurrent.ExecutionException; |
| |
| /** |
| * An {@link Adapter} that works with a {@link RecyclerView}. It sorts the given {@link List} of |
| * {@link TimedItem}s according to their date, and divides them into sub lists and displays them in |
| * different sections. |
| * <p> |
| * Subclasses should not care about the how date headers are placed in the list. Instead, they |
| * should call {@link #loadItems(List)} with a list of {@link TimedItem}, and this adapter will |
| * insert the headers automatically. |
| */ |
| public abstract class DateDividedAdapter extends Adapter<RecyclerView.ViewHolder> { |
| /** |
| * Interface that the {@link Adapter} uses to interact with the items it manages. |
| */ |
| public abstract static class TimedItem { |
| /** Value indicating that a TimedItem is not currently being displayed. */ |
| public static final int INVALID_POSITION = -1; |
| |
| /** Position of the TimedItem in the list, or {@link #INVALID_POSITION} if not shown. */ |
| private int mPosition = INVALID_POSITION; |
| |
| private boolean mIsFirstInGroup; |
| private boolean mIsLastInGroup; |
| private boolean mIsDateHeader; |
| |
| /** See {@link #mPosition}. */ |
| private final void setPosition(int position) { |
| mPosition = position; |
| } |
| |
| /** See {@link #mPosition}. */ |
| public final int getPosition() { |
| return mPosition; |
| } |
| |
| /** |
| * @param isFirst Whether this item is the first in its group. |
| */ |
| public final void setIsFirstInGroup(boolean isFirst) { |
| mIsFirstInGroup = isFirst; |
| } |
| |
| /** |
| * @param isLast Whether this item is the last in its group. |
| */ |
| public final void setIsLastInGroup(boolean isLast) { |
| mIsLastInGroup = isLast; |
| } |
| |
| /** |
| * @return Whether this item is the first in its group. |
| */ |
| public boolean isFirstInGroup() { |
| return mIsFirstInGroup; |
| } |
| |
| /** |
| * @return Whether this item is the last in its group. |
| */ |
| public boolean isLastInGroup() { |
| return mIsLastInGroup; |
| } |
| |
| /** @return The timestamp for this item. */ |
| public abstract long getTimestamp(); |
| |
| /** |
| * Returns an ID that uniquely identifies this TimedItem and doesn't change. |
| * To avoid colliding with IDs generated for Date headers, at least one of the upper 32 |
| * bits of the long should be set. |
| * @return ID that can uniquely identify the TimedItem. |
| */ |
| public abstract long getStableId(); |
| } |
| |
| /** |
| * Contains information of a single header that this adapter uses to manage headers. |
| */ |
| public static class HeaderItem extends TimedItem { |
| private final long mStableId; |
| private final View mView; |
| |
| /** |
| * Initialize stable id and view associated with this HeaderItem. |
| * @param position Position of this HeaderItem in the header group. |
| * @param view View associated with this HeaderItem. |
| */ |
| public HeaderItem(int position, View view) { |
| mStableId = getTimestamp() - position; |
| mView = view; |
| } |
| |
| @Override |
| public long getTimestamp() { |
| return Long.MAX_VALUE; |
| } |
| |
| @Override |
| public long getStableId() { |
| return mStableId; |
| } |
| |
| /** |
| * @return The View associated with this HeaderItem. |
| */ |
| public View getView() { |
| return mView; |
| } |
| } |
| |
| /** |
| * Contains information of a single footer that this adapter uses to manage footers. |
| * Share most of the same funcionality as a Header class. |
| */ |
| public static class FooterItem extends HeaderItem { |
| public FooterItem(int position, View view) { |
| super(position, view); |
| } |
| |
| @Override |
| public long getTimestamp() { |
| return Long.MIN_VALUE; |
| } |
| } |
| |
| /** An item representing a date header. */ |
| class DateHeaderTimedItem extends TimedItem { |
| private long mTimestamp; |
| |
| public DateHeaderTimedItem(long timestamp) { |
| mTimestamp = DownloadUtils.getDateAtMidnight(timestamp).getTime(); |
| } |
| |
| @Override |
| public long getTimestamp() { |
| return mTimestamp; |
| } |
| |
| @Override |
| public long getStableId() { |
| return getStableIdFromDate(new Date(getTimestamp())); |
| } |
| } |
| |
| /** |
| * A {@link RecyclerView.ViewHolder} that displays a date header. |
| */ |
| public static class DateViewHolder extends RecyclerView.ViewHolder { |
| private TextView mTextView; |
| |
| public DateViewHolder(View view) { |
| super(view); |
| if (view instanceof TextView) mTextView = (TextView) view; |
| } |
| |
| /** |
| * @param date The date that this DateViewHolder should display. |
| */ |
| public void setDate(Date date) { |
| mTextView.setText(UiUtils.dateToHeaderString(date)); |
| } |
| } |
| |
| protected static class BasicViewHolder extends RecyclerView.ViewHolder { |
| public BasicViewHolder(View itemView) { |
| super(itemView); |
| } |
| } |
| |
| protected static class SubsectionHeaderViewHolder extends RecyclerView.ViewHolder { |
| private View mView; |
| |
| public SubsectionHeaderViewHolder(View itemView) { |
| super(itemView); |
| mView = itemView; |
| } |
| |
| public View getView() { |
| return mView; |
| } |
| } |
| |
| /** |
| * A bucket of items with the same date. The date header should also be an item of the group. |
| * Special groups are subclassed for list header(s) and list footers. |
| */ |
| public static class ItemGroup { |
| private final Date mDate; |
| private final List<TimedItem> mItems = new ArrayList<>(); |
| |
| /** Index of the header, relative to the full list. Must be set only once.*/ |
| private int mIndex; |
| private boolean mIsSorted; |
| |
| /** Constructors for groups that contain same date items. */ |
| public ItemGroup(long timestamp) { |
| mDate = new Date(timestamp); |
| mIsSorted = true; |
| } |
| |
| /** |
| * Default constructor for groups that don't contain same date items e.g. header, footer, |
| * elevated priority groups etc. |
| */ |
| public ItemGroup() { |
| mDate = new Date(0L); |
| } |
| |
| public void addItem(TimedItem item) { |
| mItems.add(item); |
| mIsSorted = mItems.size() == 1; |
| } |
| |
| public void removeItem(TimedItem item) { |
| mItems.remove(item); |
| } |
| |
| public void removeAllItems() { |
| mItems.clear(); |
| } |
| |
| /** Records the position of all the TimedItems in this group, relative to the full list. */ |
| public void setPosition(int index) { |
| assert mIndex == 0 || mIndex == TimedItem.INVALID_POSITION; |
| mIndex = index; |
| |
| sortIfNeeded(); |
| for (int i = 0; i < mItems.size(); i++) { |
| TimedItem item = mItems.get(i); |
| item.setPosition(index); |
| item.setIsFirstInGroup(i == 0); |
| item.setIsLastInGroup(i == mItems.size() - 1); |
| index += 1; |
| } |
| } |
| |
| /** Unsets the position of all TimedItems in this group. */ |
| public void resetPosition() { |
| mIndex = TimedItem.INVALID_POSITION; |
| for (TimedItem item : mItems) item.setPosition(TimedItem.INVALID_POSITION); |
| } |
| |
| /** |
| * @return Whether the given date happens in the same day as the items in this group. |
| */ |
| public boolean isSameDay(Date otherDate) { |
| return compareDate(mDate, otherDate) == 0; |
| } |
| |
| /** |
| * @return The size of this group. |
| */ |
| public int size() { |
| return mItems.size(); |
| } |
| |
| /** |
| * Used for sorting list groups. |
| * @return The priority used to determine the position of this {@link ItemGroup} relative to |
| * the top of the list. |
| */ |
| @GroupPriority |
| public int priority() { |
| return GroupPriority.NORMAL_CONTENT; |
| } |
| |
| /** |
| * Returns the item to be displayed at the given index of this group. |
| * @param index The index of the item. |
| * @return The corresponding item. |
| */ |
| public TimedItem getItemAt(int index) { |
| assert index < size(); |
| sortIfNeeded(); |
| return mItems.get(index); |
| } |
| |
| /** @return The view type associated for the given index */ |
| public @ItemViewType int getItemViewType(int index) { |
| return mItems.get(index).mIsDateHeader ? ItemViewType.DATE : ItemViewType.NORMAL; |
| } |
| |
| /** |
| * Rather than sorting the list each time a new item is added, the list is sorted when |
| * something requires a correct ordering of the items. |
| */ |
| protected void sortIfNeeded() { |
| if (mIsSorted) return; |
| mIsSorted = true; |
| |
| Collections.sort(mItems, new Comparator<TimedItem>() { |
| @Override |
| public int compare(TimedItem lhs, TimedItem rhs) { |
| return compareItem(lhs, rhs); |
| } |
| }); |
| } |
| |
| /** Sorting function that determines the ordering of the items in this group. */ |
| protected int compareItem(TimedItem lhs, TimedItem rhs) { |
| if (lhs.mIsDateHeader) return -1; |
| if (rhs.mIsDateHeader) return 1; |
| |
| // More recent items are listed first. Ideally we'd use Long.compare, but that |
| // is an API level 19 call for some inexplicable reason. |
| long timeDelta = lhs.getTimestamp() - rhs.getTimestamp(); |
| if (timeDelta > 0) { |
| return -1; |
| } else if (timeDelta == 0) { |
| return 0; |
| } else { |
| return 1; |
| } |
| } |
| } |
| |
| /** An item group representing the list header(s). */ |
| public static class HeaderItemGroup extends ItemGroup { |
| @Override |
| public @GroupPriority int priority() { |
| return GroupPriority.HEADER; |
| } |
| |
| @Override |
| public @ItemViewType int getItemViewType(int index) { |
| return ItemViewType.HEADER; |
| } |
| } |
| |
| /** An item group representing the list footer(s). */ |
| public static class FooterItemGroup extends ItemGroup { |
| @Override |
| public @GroupPriority int priority() { |
| return GroupPriority.FOOTER; |
| } |
| |
| @Override |
| public @ItemViewType int getItemViewType(int index) { |
| return ItemViewType.FOOTER; |
| } |
| } |
| |
| // Cached async tasks to get the two Calendar objects, which are used when comparing dates. |
| private static final AsyncTask<Calendar> sCal1 = createCalendar(); |
| private static final AsyncTask<Calendar> sCal2 = createCalendar(); |
| |
| /** |
| * Specifies various view types of the list items for the purpose of recycling. |
| */ |
| @IntDef({ItemViewType.FOOTER, ItemViewType.HEADER, ItemViewType.DATE, ItemViewType.NORMAL, |
| ItemViewType.SUBSECTION_HEADER}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface ItemViewType { |
| int FOOTER = -2; |
| int HEADER = -1; |
| int DATE = 0; |
| int NORMAL = 1; |
| int SUBSECTION_HEADER = 2; |
| } |
| |
| /** |
| * The priorities that determine the relative position of item groups starting at the top. |
| * Default priority is GroupPriority.NORMAL_CONTENT. |
| */ |
| @IntDef({GroupPriority.HEADER, GroupPriority.ELEVATED_CONTENT, GroupPriority.NORMAL_CONTENT, |
| GroupPriority.FOOTER}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface GroupPriority { |
| int HEADER = 1; |
| int ELEVATED_CONTENT = 2; |
| int NORMAL_CONTENT = 3; |
| int FOOTER = 4; |
| } |
| |
| private static final String TAG = "DateDividedAdapter"; |
| |
| private int mSize; |
| |
| private SortedSet<ItemGroup> mGroups = new TreeSet<>(new Comparator<ItemGroup>() { |
| @Override |
| public int compare(ItemGroup lhs, ItemGroup rhs) { |
| if (lhs == rhs) return 0; |
| |
| if (lhs.priority() != rhs.priority()) { |
| return lhs.priority() < rhs.priority() ? -1 : 1; |
| } |
| |
| return compareDate(lhs.mDate, rhs.mDate); |
| } |
| }); |
| |
| /** |
| * Creates a {@link ViewHolder} in the given view parent. |
| * @see #onCreateViewHolder(ViewGroup, int) |
| */ |
| protected abstract ViewHolder createViewHolder(ViewGroup parent); |
| |
| /** |
| * Creates a {@link BasicViewHolder} in the given view parent for the header. The default |
| * implementation will create an empty FrameLayout container as the view holder. |
| * @see #onCreateViewHolder(ViewGroup, int) |
| */ |
| protected BasicViewHolder createHeader(ViewGroup parent) { |
| // Create an empty layout as a container for the header view. |
| View v = LayoutInflater.from(parent.getContext()) |
| .inflate(R.layout.date_divided_adapter_header_view_holder, parent, false); |
| return new BasicViewHolder(v); |
| } |
| |
| /** |
| * Creates a {@link BasicViewHolder} in the given view parent for the footer. |
| * See {@link #onCreateViewHolder(ViewGroup, int)}. |
| */ |
| @Nullable |
| protected BasicViewHolder createFooter(ViewGroup parent) { |
| return null; |
| } |
| |
| /** |
| * Creates a {@link DateViewHolder} in the given view parent. |
| * @see #onCreateViewHolder(ViewGroup, int) |
| */ |
| protected DateViewHolder createDateViewHolder(ViewGroup parent) { |
| return new DateViewHolder(LayoutInflater.from(parent.getContext()).inflate( |
| getTimedItemViewResId(), parent, false)); |
| } |
| |
| /** |
| * Creates a {@link ViewHolder} for a subsection in the given view parent. |
| * @see #onCreateViewHolder(ViewGroup, int) |
| */ |
| @Nullable |
| protected SubsectionHeaderViewHolder createSubsectionHeader(ViewGroup parent) { |
| return null; |
| } |
| |
| /** |
| * Helper function to determine whether an item is a subsection header. |
| * @param timedItem The item. |
| * @return Whether the item is a subsection header. |
| */ |
| protected boolean isSubsectionHeader(TimedItem timedItem) { |
| return false; |
| } |
| |
| /** |
| * Binds the {@link ViewHolder} with the given {@link TimedItem}. |
| * @see #onBindViewHolder(ViewHolder, int) |
| */ |
| protected abstract void bindViewHolderForTimedItem(ViewHolder viewHolder, TimedItem item); |
| |
| /** |
| * Binds the {@link SubsectionHeaderViewHolder} with the given {@link TimedItem}. |
| * @see #onBindViewHolder(ViewHolder, int) |
| */ |
| protected void bindViewHolderForSubsectionHeader( |
| SubsectionHeaderViewHolder holder, TimedItem timedItem) {} |
| |
| /** |
| * Binds the {@link BasicViewHolder} with the given {@link HeaderItem}. |
| * @see #onBindViewHolder(ViewHolder, int) |
| */ |
| protected void bindViewHolderForHeaderItem(ViewHolder viewHolder, HeaderItem headerItem) { |
| BasicViewHolder basicViewHolder = (BasicViewHolder) viewHolder; |
| View v = headerItem.getView(); |
| ((ViewGroup) basicViewHolder.itemView).removeAllViews(); |
| if (v.getParent() != null) ((ViewGroup) v.getParent()).removeView(v); |
| ((ViewGroup) basicViewHolder.itemView).addView(v); |
| } |
| |
| /** |
| * Binds the {@link BasicViewHolder} with the given {@link FooterItem}. |
| * @see #onBindViewHolder(ViewHolder, int) |
| */ |
| protected void bindViewHolderForFooterItem(ViewHolder viewHolder, FooterItem footerItem) { |
| BasicViewHolder basicViewHolder = (BasicViewHolder) viewHolder; |
| View v = footerItem.getView(); |
| ((ViewGroup) basicViewHolder.itemView).removeAllViews(); |
| if (v.getParent() != null) ((ViewGroup) v.getParent()).removeView(v); |
| ((ViewGroup) basicViewHolder.itemView).addView(v); |
| } |
| |
| /** |
| * Gets the resource id of the view showing the date header. |
| * Contract for subclasses: this view should be a {@link TextView}. |
| */ |
| protected abstract int getTimedItemViewResId(); |
| |
| /** |
| * Loads a list of {@link TimedItem}s to this adapter. Previous data will not be removed. Call |
| * {@link #clear(boolean)} to remove previous items. |
| */ |
| public void loadItems(List<? extends TimedItem> timedItems) { |
| for (TimedItem timedItem : timedItems) { |
| Date date = new Date(timedItem.getTimestamp()); |
| boolean found = false; |
| for (ItemGroup group : mGroups) { |
| if (group.isSameDay(date)) { |
| found = true; |
| group.addItem(timedItem); |
| break; |
| } |
| } |
| if (!found) { |
| // Create a new ItemGroup with the date for the new item. Insert the date header and |
| // the new item into the group. |
| TimedItem dateHeader = new DateHeaderTimedItem(timedItem.getTimestamp()); |
| dateHeader.mIsDateHeader = true; |
| ItemGroup newGroup = new ItemGroup(timedItem.getTimestamp()); |
| newGroup.addItem(dateHeader); |
| newGroup.addItem(timedItem); |
| mGroups.add(newGroup); |
| } |
| } |
| |
| setSizeAndGroupPositions(); |
| notifyDataSetChanged(); |
| } |
| |
| /** |
| * Tells each group where they start in the list. Also calculates the list size. |
| */ |
| private void setSizeAndGroupPositions() { |
| mSize = 0; |
| for (ItemGroup group : mGroups) { |
| group.resetPosition(); |
| group.setPosition(mSize); |
| mSize += group.size(); |
| } |
| } |
| |
| /** |
| * The utility function to add an {@link ItemGroup}. |
| * @param group The group to be added. |
| */ |
| protected void addGroup(ItemGroup group) { |
| mGroups.add(group); |
| |
| setSizeAndGroupPositions(); |
| notifyDataSetChanged(); |
| } |
| |
| /** |
| * Add a list of headers as the first group in this adapter. If headerItems has no items, |
| * the header group will not be created. Otherwise, header items will be added as child items |
| * to the header group. Note that any previously added header items will be removed. |
| * {@link #bindViewHolderForHeaderItem(ViewHolder, HeaderItem)} will bind the HeaderItem views |
| * to the given ViewHolder. Sub-classes may override #bindViewHolderForHeaderItem and |
| * (@link #createHeader(ViewGroup)} if custom behavior is needed. |
| * |
| * @param headerItems Zero or more header items to be add to the header item group. |
| */ |
| public void setHeaders(HeaderItem... headerItems) { |
| if (headerItems == null || headerItems.length == 0) { |
| removeHeader(); |
| return; |
| } |
| |
| if (hasListHeader()) mGroups.remove(mGroups.first()); |
| |
| ItemGroup header = new HeaderItemGroup(); |
| for (HeaderItem item : headerItems) { |
| header.addItem(item); |
| } |
| |
| addGroup(header); |
| } |
| |
| /** |
| * Removes the list header. |
| */ |
| public void removeHeader() { |
| if (!hasListHeader()) return; |
| mGroups.remove(mGroups.first()); |
| |
| setSizeAndGroupPositions(); |
| notifyDataSetChanged(); |
| } |
| |
| /** |
| * Whether the adapter has a list header. |
| */ |
| public boolean hasListHeader() { |
| return !mGroups.isEmpty() && mGroups.first().priority() == GroupPriority.HEADER; |
| } |
| |
| /** |
| * Whether the adapter has a list header. |
| */ |
| public boolean hasListFooter() { |
| return !mGroups.isEmpty() && mGroups.last().priority() == GroupPriority.FOOTER; |
| } |
| |
| /** |
| * Adds a footer as the last group in this adapter. |
| */ |
| public void addFooter() { |
| if (hasListFooter()) return; |
| |
| ItemGroup footer = new FooterItemGroup(); |
| addGroup(footer); |
| } |
| |
| /** |
| * Removes the footer group if present. |
| */ |
| public void removeFooter() { |
| if (!hasListFooter()) return; |
| |
| mGroups.remove(mGroups.last()); |
| setSizeAndGroupPositions(); |
| notifyDataSetChanged(); |
| } |
| |
| /** |
| * Removes all items from this adapter. |
| * @param notifyDataSetChanged Whether to notify that the data set has been changed. |
| */ |
| public void clear(boolean notifyDataSetChanged) { |
| mSize = 0; |
| |
| // Unset the positions of all items in the list. |
| for (ItemGroup group : mGroups) group.resetPosition(); |
| mGroups.clear(); |
| |
| if (notifyDataSetChanged) notifyDataSetChanged(); |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| if (!hasStableIds()) return RecyclerView.NO_ID; |
| |
| Pair<Date, TimedItem> pair = getItemAt(position); |
| return pair.second == null ? getStableIdFromDate(pair.first) : pair.second.getStableId(); |
| } |
| |
| /** |
| * Gets the item at the given position. |
| */ |
| public Pair<Date, TimedItem> getItemAt(int position) { |
| Pair<ItemGroup, Integer> pair = getGroupAt(position); |
| ItemGroup group = pair.first; |
| return new Pair<>(group.mDate, group.getItemAt(pair.second)); |
| } |
| |
| @Override |
| @ItemViewType |
| public final int getItemViewType(int position) { |
| Pair<ItemGroup, Integer> pair = getGroupAt(position); |
| ItemGroup group = pair.first; |
| return group.getItemViewType(pair.second); |
| } |
| |
| @Override |
| public final RecyclerView.ViewHolder onCreateViewHolder( |
| ViewGroup parent, @ItemViewType int viewType) { |
| switch (viewType) { |
| case ItemViewType.DATE: |
| return createDateViewHolder(parent); |
| case ItemViewType.NORMAL: |
| return createViewHolder(parent); |
| case ItemViewType.HEADER: |
| return createHeader(parent); |
| case ItemViewType.FOOTER: |
| return createFooter(parent); |
| case ItemViewType.SUBSECTION_HEADER: |
| return createSubsectionHeader(parent); |
| default: |
| assert false; |
| return null; |
| } |
| } |
| |
| @Override |
| public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { |
| Pair<ItemGroup, Integer> groupAndPosition = getGroupAt(position); |
| ItemGroup group = groupAndPosition.first; |
| @ItemViewType |
| int viewType = group.getItemViewType(groupAndPosition.second); |
| |
| Pair<Date, TimedItem> pair = getItemAt(position); |
| switch (viewType) { |
| case ItemViewType.DATE: |
| ((DateViewHolder) holder).setDate(pair.first); |
| break; |
| case ItemViewType.NORMAL: |
| bindViewHolderForTimedItem(holder, pair.second); |
| break; |
| case ItemViewType.HEADER: |
| bindViewHolderForHeaderItem(holder, (HeaderItem) pair.second); |
| break; |
| case ItemViewType.FOOTER: |
| bindViewHolderForFooterItem(holder, (FooterItem) pair.second); |
| break; |
| case ItemViewType.SUBSECTION_HEADER: |
| bindViewHolderForSubsectionHeader((SubsectionHeaderViewHolder) holder, pair.second); |
| break; |
| } |
| } |
| |
| @Override |
| public final int getItemCount() { |
| return mSize; |
| } |
| |
| /** |
| * Utility method to traverse all groups and find the {@link ItemGroup} for the given position. |
| */ |
| protected Pair<ItemGroup, Integer> getGroupAt(int position) { |
| // TODO(ianwen): Optimize the performance if the number of groups becomes too large. |
| int i = position; |
| for (ItemGroup group : mGroups) { |
| if (i >= group.size()) { |
| i -= group.size(); |
| } else { |
| return new Pair<>(group, i); |
| } |
| } |
| assert false; |
| return null; |
| } |
| |
| /** |
| * @param item The item to remove from the adapter. |
| */ |
| // #getGroupAt() asserts false before returning null, causing findbugs to complain about |
| // a redundant nullcheck even though getGroupAt can return null. |
| protected void removeItem(TimedItem item) { |
| Pair<ItemGroup, Integer> groupPair = getGroupAt(item.getPosition()); |
| if (groupPair == null) { |
| Log.e(TAG, |
| "Failed to find group for item during remove. Item position: " |
| + item.getPosition() + ", total size: " + mSize); |
| return; |
| } |
| |
| ItemGroup group = groupPair.first; |
| group.removeItem(item); |
| |
| // Remove the group if only the date header is left. |
| if (group.size() == 1) mGroups.remove(group); |
| |
| // Remove header if only the header is left. |
| if (hasListHeader() && mGroups.size() == 1) removeHeader(); |
| |
| setSizeAndGroupPositions(); |
| notifyDataSetChanged(); |
| } |
| |
| /** |
| * Creates a long ID that identifies a particular day in history. |
| * @param date Date to process. |
| * @return Long that has the day of the year (1-365) in the lowest 16 bits and the year in the |
| * next 16 bits over. |
| */ |
| private static long getStableIdFromDate(Date date) { |
| Pair<Calendar, Calendar> pair = getCachedCalendars(); |
| Calendar calendar = pair.first; |
| calendar.setTime(date); |
| long dayOfYear = calendar.get(Calendar.DAY_OF_YEAR); |
| long year = calendar.get(Calendar.YEAR); |
| return (year << 16) + dayOfYear; |
| } |
| |
| /** |
| * Compares two {@link Date}s. Note if you already have two {@link Calendar} objects, use |
| * {@link #compareCalendar(Calendar, Calendar)} instead. |
| * @return 0 if date1 and date2 are in the same day; 1 if date1 is before date2; -1 otherwise. |
| */ |
| protected static int compareDate(Date date1, Date date2) { |
| Pair<Calendar, Calendar> pair = getCachedCalendars(); |
| Calendar cal1 = pair.first; |
| Calendar cal2 = pair.second; |
| cal1.setTime(date1); |
| cal2.setTime(date2); |
| return compareCalendar(cal1, cal2); |
| } |
| |
| /** |
| * @return 0 if cal1 and cal2 are in the same day; 1 if cal1 happens before cal2; -1 otherwise. |
| */ |
| private static int compareCalendar(Calendar cal1, Calendar cal2) { |
| boolean sameDay = cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) |
| && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR); |
| if (sameDay) { |
| return 0; |
| } else if (cal1.before(cal2)) { |
| return 1; |
| } else { |
| return -1; |
| } |
| } |
| |
| /** |
| * Convenient getter for {@link #sCal1} and {@link #sCal2}. |
| */ |
| private static Pair<Calendar, Calendar> getCachedCalendars() { |
| Calendar cal1; |
| Calendar cal2; |
| try { |
| cal1 = sCal1.get(); |
| cal2 = sCal2.get(); |
| } catch (InterruptedException | ExecutionException e) { |
| // We've tried our best. If AsyncTask really does not work, we give up. :( |
| cal1 = Calendar.getInstance(); |
| cal2 = Calendar.getInstance(); |
| } |
| return new Pair<>(cal1, cal2); |
| } |
| |
| /** |
| * Wraps {@link Calendar#getInstance()} in an {@link AsyncTask} to avoid Strict mode violation. |
| */ |
| private static AsyncTask<Calendar> createCalendar() { |
| return new BackgroundOnlyAsyncTask<Calendar>() { |
| @Override |
| protected Calendar doInBackground() { |
| return Calendar.getInstance(); |
| } |
| }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| } |
| } |