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 
17 package com.android.common.widget;
18 
19 import android.content.Context;
20 import android.database.ContentObserver;
21 import android.database.Cursor;
22 import android.database.DataSetObserver;
23 import android.os.Handler;
24 import android.util.SparseIntArray;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import 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  */
34 public 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 
GroupingListAdapter(Context context)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      */
addGroups(Cursor cursor)125     protected abstract void addGroups(Cursor cursor);
126 
newStandAloneView(Context context, ViewGroup parent)127     protected abstract View newStandAloneView(Context context, ViewGroup parent);
bindStandAloneView(View view, Context context, Cursor cursor)128     protected abstract void bindStandAloneView(View view, Context context, Cursor cursor);
129 
newGroupView(Context context, ViewGroup parent)130     protected abstract View newGroupView(Context context, ViewGroup parent);
bindGroupView(View view, Context context, Cursor cursor, int groupSize, boolean expanded)131     protected abstract void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
132             boolean expanded);
133 
newChildView(Context context, ViewGroup parent)134     protected abstract View newChildView(Context context, ViewGroup parent);
bindChildView(View view, Context context, Cursor cursor)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      */
resetCache()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 
onContentChanged()149     protected void onContentChanged() {
150     }
151 
changeCursor(Cursor cursor)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 
getCursor()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      */
findGroups()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      */
addGroup(int cursorPosition, int size, boolean expanded)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
idealLongArraySize(int need)218     private int idealLongArraySize(int need) {
219         return idealByteArraySize(need * 8) / 8;
220     }
221 
222     // Copy/paste from ArrayUtils
idealByteArraySize(int need)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 
getCount()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      */
obtainPositionMetadata(PositionMetadata metadata, int position)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      */
isGroupHeader(int position)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      */
getGroupSize(int position)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      */
toggleGroup(int position)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
getViewTypeCount()422     public int getViewTypeCount() {
423         return 3;
424     }
425 
426     @Override
getItemViewType(int position)427     public int getItemViewType(int position) {
428         obtainPositionMetadata(mPositionMetadata, position);
429         return mPositionMetadata.itemType;
430     }
431 
getItem(int position)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 
getItemId(int position)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 
getView(int position, View convertView, ViewGroup parent)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