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