Home | History | Annotate | Download | only in contacts
      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 
    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      */
    127     protected abstract void addGroups(Cursor cursor);
    128 
    129     protected abstract View newStandAloneView(Context context, ViewGroup parent);
    130     protected abstract void bindStandAloneView(View view, Context context, Cursor cursor);
    131 
    132     protected abstract View newGroupView(Context context, ViewGroup parent);
    133     protected abstract void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
    134             boolean expanded);
    135 
    136     protected abstract View newChildView(Context context, ViewGroup parent);
    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      */
    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 
    151     protected void onContentChanged() {
    152     }
    153 
    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 
    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      */
    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      */
    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 
    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      */
    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      */
    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     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
    410     public int getViewTypeCount() {
    411         return 3;
    412     }
    413 
    414     @Override
    415     public int getItemViewType(int position) {
    416         obtainPositionMetadata(mPositionMetadata, position);
    417         return mPositionMetadata.itemType;
    418     }
    419 
    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 
    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 
    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