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.calllog;
18
19import android.content.Context;
20import android.database.ContentObserver;
21import android.database.Cursor;
22import android.database.DataSetObserver;
23import android.os.Handler;
24import android.support.v7.widget.RecyclerView;
25import android.util.Log;
26import android.util.SparseIntArray;
27import android.view.View;
28import android.view.ViewGroup;
29import android.widget.BaseAdapter;
30
31import com.android.contacts.common.testing.NeededForTesting;
32
33/**
34 * Maintains a list that groups adjacent items sharing the same value of a "group-by" field.
35 *
36 * The list has three types of elements: stand-alone, group header and group child. Groups are
37 * collapsible and collapsed by default. This is used by the call log to group related entries.
38 */
39abstract class GroupingListAdapter extends RecyclerView.Adapter {
40
41    private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16;
42    private static final int GROUP_METADATA_ARRAY_INCREMENT = 128;
43    private static final long GROUP_OFFSET_MASK    = 0x00000000FFFFFFFFL;
44    private static final long GROUP_SIZE_MASK     = 0x7FFFFFFF00000000L;
45    private static final long EXPANDED_GROUP_MASK = 0x8000000000000000L;
46
47    public static final int ITEM_TYPE_STANDALONE = 0;
48    public static final int ITEM_TYPE_GROUP_HEADER = 1;
49    public static final int ITEM_TYPE_IN_GROUP = 2;
50
51    /**
52     * Information about a specific list item: is it a group, if so is it expanded.
53     * Otherwise, is it a stand-alone item or a group member.
54     */
55    protected static class PositionMetadata {
56        int itemType;
57        boolean isExpanded;
58        int cursorPosition;
59        int childCount;
60        private int groupPosition;
61        private int listPosition = -1;
62    }
63
64    private Context mContext;
65    private Cursor mCursor;
66
67    /**
68     * Count of list items.
69     */
70    private int mCount;
71
72    private int mRowIdColumnIndex;
73
74    /**
75     * Count of groups in the list.
76     */
77    private int mGroupCount;
78
79    /**
80     * Information about where these groups are located in the list, how large they are
81     * and whether they are expanded.
82     */
83    private long[] mGroupMetadata;
84
85    private SparseIntArray mPositionCache = new SparseIntArray();
86    private int mLastCachedListPosition;
87    private int mLastCachedCursorPosition;
88    private int mLastCachedGroup;
89
90    /**
91     * A reusable temporary instance of PositionMetadata
92     */
93    private PositionMetadata mPositionMetadata = new PositionMetadata();
94
95    protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) {
96
97        @Override
98        public boolean deliverSelfNotifications() {
99            return true;
100        }
101
102        @Override
103        public void onChange(boolean selfChange) {
104            onContentChanged();
105        }
106    };
107
108    protected DataSetObserver mDataSetObserver = new DataSetObserver() {
109
110        @Override
111        public void onChanged() {
112            notifyDataSetChanged();
113        }
114    };
115
116    public GroupingListAdapter(Context context) {
117        mContext = context;
118        resetCache();
119    }
120
121    /**
122     * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for
123     * each of them.
124     */
125    protected abstract void addGroups(Cursor cursor);
126
127    protected abstract void onContentChanged();
128
129    /**
130     * Cache should be reset whenever the cursor changes or groups are expanded or collapsed.
131     */
132    private void resetCache() {
133        mCount = -1;
134        mLastCachedListPosition = -1;
135        mLastCachedCursorPosition = -1;
136        mLastCachedGroup = -1;
137        mPositionMetadata.listPosition = -1;
138        mPositionCache.clear();
139    }
140
141    public void changeCursor(Cursor cursor) {
142        if (cursor == mCursor) {
143            return;
144        }
145
146        if (mCursor != null) {
147            mCursor.unregisterContentObserver(mChangeObserver);
148            mCursor.unregisterDataSetObserver(mDataSetObserver);
149            mCursor.close();
150        }
151        mCursor = cursor;
152        resetCache();
153        findGroups();
154
155        if (cursor != null) {
156            cursor.registerContentObserver(mChangeObserver);
157            cursor.registerDataSetObserver(mDataSetObserver);
158            mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
159            notifyDataSetChanged();
160        }
161    }
162
163    @NeededForTesting
164    public Cursor getCursor() {
165        return mCursor;
166    }
167
168    /**
169     * Scans over the entire cursor looking for duplicate phone numbers that need
170     * to be collapsed.
171     */
172    private void findGroups() {
173        mGroupCount = 0;
174        mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE];
175
176        if (mCursor == null) {
177            return;
178        }
179
180        addGroups(mCursor);
181    }
182
183    /**
184     * Records information about grouping in the list.  Should be called by the overridden
185     * {@link #addGroups} method.
186     */
187    protected void addGroup(int cursorPosition, int size, boolean expanded) {
188        if (mGroupCount >= mGroupMetadata.length) {
189            int newSize = idealLongArraySize(
190                    mGroupMetadata.length + GROUP_METADATA_ARRAY_INCREMENT);
191            long[] array = new long[newSize];
192            System.arraycopy(mGroupMetadata, 0, array, 0, mGroupCount);
193            mGroupMetadata = array;
194        }
195
196        long metadata = ((long)size << 32) | cursorPosition;
197        if (expanded) {
198            metadata |= EXPANDED_GROUP_MASK;
199        }
200        mGroupMetadata[mGroupCount++] = metadata;
201    }
202
203    // Copy/paste from ArrayUtils
204    private int idealLongArraySize(int need) {
205        return idealByteArraySize(need * 8) / 8;
206    }
207
208    // Copy/paste from ArrayUtils
209    private int idealByteArraySize(int need) {
210        for (int i = 4; i < 32; i++)
211            if (need <= (1 << i) - 12)
212                return (1 << i) - 12;
213
214        return need;
215    }
216
217    @Override
218    public int getItemCount() {
219        if (mCursor == null) {
220            return 0;
221        }
222
223        if (mCount != -1) {
224            return mCount;
225        }
226
227        int cursorPosition = 0;
228        int count = 0;
229        for (int i = 0; i < mGroupCount; i++) {
230            long metadata = mGroupMetadata[i];
231            int offset = (int)(metadata & GROUP_OFFSET_MASK);
232            boolean expanded = (metadata & EXPANDED_GROUP_MASK) != 0;
233            int size = (int)((metadata & GROUP_SIZE_MASK) >> 32);
234
235            count += (offset - cursorPosition);
236
237            if (expanded) {
238                count += size + 1;
239            } else {
240                count++;
241            }
242
243            cursorPosition = offset + size;
244        }
245
246        mCount = count + mCursor.getCount() - cursorPosition;
247        return mCount;
248    }
249
250    /**
251     * Figures out whether the item at the specified position represents a
252     * stand-alone element, a group or a group child. Also computes the
253     * corresponding cursor position.
254     */
255    public void obtainPositionMetadata(PositionMetadata metadata, int position) {
256        // If the description object already contains requested information, just return
257        if (metadata.listPosition == position) {
258            return;
259        }
260
261        int listPosition = 0;
262        int cursorPosition = 0;
263        int firstGroupToCheck = 0;
264
265        // Check cache for the supplied position.  What we are looking for is
266        // the group descriptor immediately preceding the supplied position.
267        // Once we have that, we will be able to tell whether the position
268        // is the header of the group, a member of the group or a standalone item.
269        if (mLastCachedListPosition != -1) {
270            if (position <= mLastCachedListPosition) {
271
272                // Have SparceIntArray do a binary search for us.
273                int index = mPositionCache.indexOfKey(position);
274
275                // If we get back a positive number, the position corresponds to
276                // a group header.
277                if (index < 0) {
278
279                    // We had a cache miss, but we did obtain valuable information anyway.
280                    // The negative number will allow us to compute the location of
281                    // the group header immediately preceding the supplied position.
282                    index = ~index - 1;
283
284                    if (index >= mPositionCache.size()) {
285                        index--;
286                    }
287                }
288
289                // A non-negative index gives us the position of the group header
290                // corresponding or preceding the position, so we can
291                // search for the group information at the supplied position
292                // starting with the cached group we just found
293                if (index >= 0) {
294                    listPosition = mPositionCache.keyAt(index);
295                    firstGroupToCheck = mPositionCache.valueAt(index);
296                    long descriptor = mGroupMetadata[firstGroupToCheck];
297                    cursorPosition = (int)(descriptor & GROUP_OFFSET_MASK);
298                }
299            } else {
300
301                // If we haven't examined groups beyond the supplied position,
302                // we will start where we left off previously
303                firstGroupToCheck = mLastCachedGroup;
304                listPosition = mLastCachedListPosition;
305                cursorPosition = mLastCachedCursorPosition;
306            }
307        }
308
309        for (int i = firstGroupToCheck; i < mGroupCount; i++) {
310            long group = mGroupMetadata[i];
311            int offset = (int)(group & GROUP_OFFSET_MASK);
312
313            // Move pointers to the beginning of the group
314            listPosition += (offset - cursorPosition);
315            cursorPosition = offset;
316
317            if (i > mLastCachedGroup) {
318                mPositionCache.append(listPosition, i);
319                mLastCachedListPosition = listPosition;
320                mLastCachedCursorPosition = cursorPosition;
321                mLastCachedGroup = i;
322            }
323
324            // Now we have several possibilities:
325            // A) The requested position precedes the group
326            if (position < listPosition) {
327                metadata.itemType = ITEM_TYPE_STANDALONE;
328                metadata.cursorPosition = cursorPosition - (listPosition - position);
329                metadata.childCount = 1;
330                return;
331            }
332
333            boolean expanded = (group & EXPANDED_GROUP_MASK) != 0;
334            int size = (int) ((group & GROUP_SIZE_MASK) >> 32);
335
336            // B) The requested position is a group header
337            if (position == listPosition) {
338                metadata.itemType = ITEM_TYPE_GROUP_HEADER;
339                metadata.groupPosition = i;
340                metadata.isExpanded = expanded;
341                metadata.childCount = size;
342                metadata.cursorPosition = offset;
343                return;
344            }
345
346            if (expanded) {
347                // C) The requested position is an element in the expanded group
348                if (position < listPosition + size + 1) {
349                    metadata.itemType = ITEM_TYPE_IN_GROUP;
350                    metadata.cursorPosition = cursorPosition + (position - listPosition) - 1;
351                    return;
352                }
353
354                // D) The element is past the expanded group
355                listPosition += size + 1;
356            } else {
357
358                // E) The element is past the collapsed group
359                listPosition++;
360            }
361
362            // Move cursor past the group
363            cursorPosition += size;
364        }
365
366        // The required item is past the last group
367        metadata.itemType = ITEM_TYPE_STANDALONE;
368        metadata.cursorPosition = cursorPosition + (position - listPosition);
369        metadata.childCount = 1;
370    }
371
372    /**
373     * Returns true if the specified position in the list corresponds to a
374     * group header.
375     */
376    public boolean isGroupHeader(int position) {
377        obtainPositionMetadata(mPositionMetadata, position);
378        return mPositionMetadata.itemType == ITEM_TYPE_GROUP_HEADER;
379    }
380
381    /**
382     * Given a position of a groups header in the list, returns the size of
383     * the corresponding group.
384     */
385    public int getGroupSize(int position) {
386        obtainPositionMetadata(mPositionMetadata, position);
387        return mPositionMetadata.childCount;
388    }
389
390    /**
391     * Mark group as expanded if it is collapsed and vice versa.
392     */
393    @NeededForTesting
394    public void toggleGroup(int position) {
395        obtainPositionMetadata(mPositionMetadata, position);
396        if (mPositionMetadata.itemType != ITEM_TYPE_GROUP_HEADER) {
397            throw new IllegalArgumentException("Not a group at position " + position);
398        }
399
400        if (mPositionMetadata.isExpanded) {
401            mGroupMetadata[mPositionMetadata.groupPosition] &= ~EXPANDED_GROUP_MASK;
402        } else {
403            mGroupMetadata[mPositionMetadata.groupPosition] |= EXPANDED_GROUP_MASK;
404        }
405        resetCache();
406        notifyDataSetChanged();
407    }
408
409    public int getItemViewType(int position) {
410        obtainPositionMetadata(mPositionMetadata, position);
411        return mPositionMetadata.itemType;
412    }
413
414    public Object getItem(int position) {
415        if (mCursor == null) {
416            return null;
417        }
418
419        obtainPositionMetadata(mPositionMetadata, position);
420        if (mCursor.moveToPosition(mPositionMetadata.cursorPosition)) {
421            return mCursor;
422        } else {
423            return null;
424        }
425    }
426
427    public long getItemId(int position) {
428        Object item = getItem(position);
429        if (item != null) {
430            return mCursor.getLong(mRowIdColumnIndex);
431        } else {
432            return -1;
433        }
434    }
435}
436