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