summaryrefslogtreecommitdiff
path: root/src/com/android/dialer/calllog/GroupingListAdapter.java
blob: 501e88df05f08151974d514d0acc9b04bc200930 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dialer.calllog;

import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Handler;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

import com.android.contacts.common.testing.NeededForTesting;

/**
 * Maintains a list that groups adjacent items sharing the same value of a "group-by" field.
 *
 * The list has three types of elements: stand-alone, group header and group child. Groups are
 * collapsible and collapsed by default. This is used by the call log to group related entries.
 */
abstract class GroupingListAdapter extends RecyclerView.Adapter {

    private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16;
    private static final int GROUP_METADATA_ARRAY_INCREMENT = 128;
    private static final long GROUP_OFFSET_MASK    = 0x00000000FFFFFFFFL;
    private static final long GROUP_SIZE_MASK     = 0x7FFFFFFF00000000L;
    private static final long EXPANDED_GROUP_MASK = 0x8000000000000000L;

    public static final int ITEM_TYPE_STANDALONE = 0;
    public static final int ITEM_TYPE_GROUP_HEADER = 1;
    public static final int ITEM_TYPE_IN_GROUP = 2;

    /**
     * Information about a specific list item: is it a group, if so is it expanded.
     * Otherwise, is it a stand-alone item or a group member.
     */
    protected static class PositionMetadata {
        int itemType;
        boolean isExpanded;
        int cursorPosition;
        int childCount;
        private int groupPosition;
        private int listPosition = -1;
    }

    private Context mContext;
    private Cursor mCursor;

    /**
     * Count of list items.
     */
    private int mCount;

    private int mRowIdColumnIndex;

    /**
     * Count of groups in the list.
     */
    private int mGroupCount;

    /**
     * Information about where these groups are located in the list, how large they are
     * and whether they are expanded.
     */
    private long[] mGroupMetadata;

    private SparseIntArray mPositionCache = new SparseIntArray();
    private int mLastCachedListPosition;
    private int mLastCachedCursorPosition;
    private int mLastCachedGroup;

    /**
     * A reusable temporary instance of PositionMetadata
     */
    private PositionMetadata mPositionMetadata = new PositionMetadata();

    protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) {

        @Override
        public boolean deliverSelfNotifications() {
            return true;
        }

        @Override
        public void onChange(boolean selfChange) {
            onContentChanged();
        }
    };

    protected DataSetObserver mDataSetObserver = new DataSetObserver() {

        @Override
        public void onChanged() {
            notifyDataSetChanged();
        }
    };

    public GroupingListAdapter(Context context) {
        mContext = context;
        resetCache();
    }

    /**
     * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for
     * each of them.
     */
    protected abstract void addGroups(Cursor cursor);

    protected abstract void onContentChanged();

    /**
     * Cache should be reset whenever the cursor changes or groups are expanded or collapsed.
     */
    private void resetCache() {
        mCount = -1;
        mLastCachedListPosition = -1;
        mLastCachedCursorPosition = -1;
        mLastCachedGroup = -1;
        mPositionMetadata.listPosition = -1;
        mPositionCache.clear();
    }

    public void changeCursor(Cursor cursor) {
        if (cursor == mCursor) {
            return;
        }

        if (mCursor != null) {
            mCursor.unregisterContentObserver(mChangeObserver);
            mCursor.unregisterDataSetObserver(mDataSetObserver);
            mCursor.close();
        }
        mCursor = cursor;
        resetCache();
        findGroups();

        if (cursor != null) {
            cursor.registerContentObserver(mChangeObserver);
            cursor.registerDataSetObserver(mDataSetObserver);
            mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
            notifyDataSetChanged();
        }
    }

    @NeededForTesting
    public Cursor getCursor() {
        return mCursor;
    }

    /**
     * Scans over the entire cursor looking for duplicate phone numbers that need
     * to be collapsed.
     */
    private void findGroups() {
        mGroupCount = 0;
        mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE];

        if (mCursor == null) {
            return;
        }

        addGroups(mCursor);
    }

    /**
     * Records information about grouping in the list.  Should be called by the overridden
     * {@link #addGroups} method.
     */
    protected void addGroup(int cursorPosition, int size, boolean expanded) {
        if (mGroupCount >= mGroupMetadata.length) {
            int newSize = idealLongArraySize(
                    mGroupMetadata.length + GROUP_METADATA_ARRAY_INCREMENT);
            long[] array = new long[newSize];
            System.arraycopy(mGroupMetadata, 0, array, 0, mGroupCount);
            mGroupMetadata = array;
        }

        long metadata = ((long)size << 32) | cursorPosition;
        if (expanded) {
            metadata |= EXPANDED_GROUP_MASK;
        }
        mGroupMetadata[mGroupCount++] = metadata;
    }

    // Copy/paste from ArrayUtils
    private int idealLongArraySize(int need) {
        return idealByteArraySize(need * 8) / 8;
    }

    // Copy/paste from ArrayUtils
    private int idealByteArraySize(int need) {
        for (int i = 4; i < 32; i++)
            if (need <= (1 << i) - 12)
                return (1 << i) - 12;

        return need;
    }

    @Override
    public int getItemCount() {
        if (mCursor == null) {
            return 0;
        }

        if (mCount != -1) {
            return mCount;
        }

        int cursorPosition = 0;
        int count = 0;
        for (int i = 0; i < mGroupCount; i++) {
            long metadata = mGroupMetadata[i];
            int offset = (int)(metadata & GROUP_OFFSET_MASK);
            boolean expanded = (metadata & EXPANDED_GROUP_MASK) != 0;
            int size = (int)((metadata & GROUP_SIZE_MASK) >> 32);

            count += (offset - cursorPosition);

            if (expanded) {
                count += size + 1;
            } else {
                count++;
            }

            cursorPosition = offset + size;
        }

        mCount = count + mCursor.getCount() - cursorPosition;
        return mCount;
    }

    /**
     * Figures out whether the item at the specified position represents a
     * stand-alone element, a group or a group child. Also computes the
     * corresponding cursor position.
     */
    public void obtainPositionMetadata(PositionMetadata metadata, int position) {

        // If the description object already contains requested information, just return
        if (metadata.listPosition == position) {
            return;
        }

        int listPosition = 0;
        int cursorPosition = 0;
        int firstGroupToCheck = 0;

        // Check cache for the supplied position.  What we are looking for is
        // the group descriptor immediately preceding the supplied position.
        // Once we have that, we will be able to tell whether the position
        // is the header of the group, a member of the group or a standalone item.
        if (mLastCachedListPosition != -1) {
            if (position <= mLastCachedListPosition) {

                // Have SparceIntArray do a binary search for us.
                int index = mPositionCache.indexOfKey(position);

                // If we get back a positive number, the position corresponds to
                // a group header.
                if (index < 0) {

                    // We had a cache miss, but we did obtain valuable information anyway.
                    // The negative number will allow us to compute the location of
                    // the group header immediately preceding the supplied position.
                    index = ~index - 1;

                    if (index >= mPositionCache.size()) {
                        index--;
                    }
                }

                // A non-negative index gives us the position of the group header
                // corresponding or preceding the position, so we can
                // search for the group information at the supplied position
                // starting with the cached group we just found
                if (index >= 0) {
                    listPosition = mPositionCache.keyAt(index);
                    firstGroupToCheck = mPositionCache.valueAt(index);
                    long descriptor = mGroupMetadata[firstGroupToCheck];
                    cursorPosition = (int)(descriptor & GROUP_OFFSET_MASK);
                }
            } else {

                // If we haven't examined groups beyond the supplied position,
                // we will start where we left off previously
                firstGroupToCheck = mLastCachedGroup;
                listPosition = mLastCachedListPosition;
                cursorPosition = mLastCachedCursorPosition;
            }
        }

        for (int i = firstGroupToCheck; i < mGroupCount; i++) {
            long group = mGroupMetadata[i];
            int offset = (int)(group & GROUP_OFFSET_MASK);

            // Move pointers to the beginning of the group
            listPosition += (offset - cursorPosition);
            cursorPosition = offset;

            if (i > mLastCachedGroup) {
                mPositionCache.append(listPosition, i);
                mLastCachedListPosition = listPosition;
                mLastCachedCursorPosition = cursorPosition;
                mLastCachedGroup = i;
            }

            // Now we have several possibilities:
            // A) The requested position precedes the group
            if (position < listPosition) {
                metadata.itemType = ITEM_TYPE_STANDALONE;
                metadata.cursorPosition = cursorPosition - (listPosition - position);
                metadata.childCount = 1;
                return;
            }

            boolean expanded = (group & EXPANDED_GROUP_MASK) != 0;
            int size = (int) ((group & GROUP_SIZE_MASK) >> 32);

            // B) The requested position is a group header
            if (position == listPosition) {
                metadata.itemType = ITEM_TYPE_GROUP_HEADER;
                metadata.groupPosition = i;
                metadata.isExpanded = expanded;
                metadata.childCount = size;
                metadata.cursorPosition = offset;
                return;
            }

            if (expanded) {
                // C) The requested position is an element in the expanded group
                if (position < listPosition + size + 1) {
                    metadata.itemType = ITEM_TYPE_IN_GROUP;
                    metadata.cursorPosition = cursorPosition + (position - listPosition) - 1;
                    return;
                }

                // D) The element is past the expanded group
                listPosition += size + 1;
            } else {

                // E) The element is past the collapsed group
                listPosition++;
            }

            // Move cursor past the group
            cursorPosition += size;
        }

        // The required item is past the last group
        metadata.itemType = ITEM_TYPE_STANDALONE;
        metadata.cursorPosition = cursorPosition + (position - listPosition);
        metadata.childCount = 1;
    }

    /**
     * Returns true if the specified position in the list corresponds to a
     * group header.
     */
    public boolean isGroupHeader(int position) {
        obtainPositionMetadata(mPositionMetadata, position);
        return mPositionMetadata.itemType == ITEM_TYPE_GROUP_HEADER;
    }

    /**
     * Given a position of a groups header in the list, returns the size of
     * the corresponding group.
     */
    public int getGroupSize(int position) {
        obtainPositionMetadata(mPositionMetadata, position);
        return mPositionMetadata.childCount;
    }

    /**
     * Mark group as expanded if it is collapsed and vice versa.
     */
    @NeededForTesting
    public void toggleGroup(int position) {
        obtainPositionMetadata(mPositionMetadata, position);
        if (mPositionMetadata.itemType != ITEM_TYPE_GROUP_HEADER) {
            throw new IllegalArgumentException("Not a group at position " + position);
        }

        if (mPositionMetadata.isExpanded) {
            mGroupMetadata[mPositionMetadata.groupPosition] &= ~EXPANDED_GROUP_MASK;
        } else {
            mGroupMetadata[mPositionMetadata.groupPosition] |= EXPANDED_GROUP_MASK;
        }
        resetCache();
        notifyDataSetChanged();
    }

    public int getItemViewType(int position) {
        obtainPositionMetadata(mPositionMetadata, position);
        return mPositionMetadata.itemType;
    }

    public Object getItem(int position) {
        if (mCursor == null) {
            return null;
        }

        obtainPositionMetadata(mPositionMetadata, position);
        if (mCursor.moveToPosition(mPositionMetadata.cursorPosition)) {
            return mCursor;
        } else {
            return null;
        }
    }

    public long getItemId(int position) {
        Object item = getItem(position);
        if (item != null) {
            return mCursor.getLong(mRowIdColumnIndex);
        } else {
            return -1;
        }
    }

    /**
     * Used for setting the cursor without triggering a UI thread update.
     */
    @NeededForTesting
    public void setCursorForTesting(Cursor cursor) {
        if (cursor != null) {
            mCursor = cursor;
            cursor.registerContentObserver(mChangeObserver);
            cursor.registerDataSetObserver(mDataSetObserver);
            mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
        }
    }
}