1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.dialer.app.calllog;
18
19import android.database.ContentObserver;
20import android.database.Cursor;
21import android.database.DataSetObserver;
22import android.os.Handler;
23import android.support.v7.widget.RecyclerView;
24import android.util.SparseIntArray;
25
26/**
27 * Maintains a list that groups items into groups of consecutive elements which are disjoint, that
28 * is, an item can only belong to one group. This is leveraged for grouping calls in the call log
29 * received from or made to the same phone number.
30 *
31 * <p>There are two integers stored as metadata for every list item in the adapter.
32 */
33abstract class GroupingListAdapter extends RecyclerView.Adapter {
34
35  protected ContentObserver mChangeObserver =
36      new ContentObserver(new Handler()) {
37        @Override
38        public boolean deliverSelfNotifications() {
39          return true;
40        }
41
42        @Override
43        public void onChange(boolean selfChange) {
44          onContentChanged();
45        }
46      };
47  protected DataSetObserver mDataSetObserver =
48      new DataSetObserver() {
49        @Override
50        public void onChanged() {
51          notifyDataSetChanged();
52        }
53      };
54  private Cursor mCursor;
55  /**
56   * SparseIntArray, which maps the cursor position of the first element of a group to the size of
57   * the group. The index of a key in this map corresponds to the list position of that group.
58   */
59  private SparseIntArray mGroupMetadata;
60
61  private int mItemCount;
62
63  public GroupingListAdapter() {
64    reset();
65  }
66
67  /**
68   * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for each of them.
69   */
70  protected abstract void addGroups(Cursor cursor);
71
72  protected abstract void onContentChanged();
73
74  public void changeCursor(Cursor cursor) {
75    if (cursor == mCursor) {
76      return;
77    }
78
79    if (mCursor != null) {
80      mCursor.unregisterContentObserver(mChangeObserver);
81      mCursor.unregisterDataSetObserver(mDataSetObserver);
82      mCursor.close();
83    }
84
85    // Reset whenever the cursor is changed.
86    reset();
87    mCursor = cursor;
88
89    if (cursor != null) {
90      addGroups(mCursor);
91
92      // Calculate the item count by subtracting group child counts from the cursor count.
93      mItemCount = mGroupMetadata.size();
94
95      cursor.registerContentObserver(mChangeObserver);
96      cursor.registerDataSetObserver(mDataSetObserver);
97      notifyDataSetChanged();
98    }
99  }
100
101  /**
102   * Records information about grouping in the list. Should be called by the overridden {@link
103   * #addGroups} method.
104   */
105  public void addGroup(int cursorPosition, int groupSize) {
106    int lastIndex = mGroupMetadata.size() - 1;
107    if (lastIndex < 0 || cursorPosition <= mGroupMetadata.keyAt(lastIndex)) {
108      mGroupMetadata.put(cursorPosition, groupSize);
109    } else {
110      // Optimization to avoid binary search if adding groups in ascending cursor position.
111      mGroupMetadata.append(cursorPosition, groupSize);
112    }
113  }
114
115  @Override
116  public int getItemCount() {
117    return mItemCount;
118  }
119
120  /**
121   * Given the position of a list item, returns the size of the group of items corresponding to that
122   * position.
123   */
124  public int getGroupSize(int listPosition) {
125    if (listPosition < 0 || listPosition >= mGroupMetadata.size()) {
126      return 0;
127    }
128
129    return mGroupMetadata.valueAt(listPosition);
130  }
131
132  /**
133   * Given the position of a list item, returns the the first item in the group of items
134   * corresponding to that position.
135   */
136  public Object getItem(int listPosition) {
137    if (mCursor == null || listPosition < 0 || listPosition >= mGroupMetadata.size()) {
138      return null;
139    }
140
141    int cursorPosition = mGroupMetadata.keyAt(listPosition);
142    if (mCursor.moveToPosition(cursorPosition)) {
143      return mCursor;
144    } else {
145      return null;
146    }
147  }
148
149  private void reset() {
150    mItemCount = 0;
151    mGroupMetadata = new SparseIntArray();
152  }
153}
154