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