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