1 /* 2 * Copyright (C) 2013 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 package com.android.dialer.list; 17 18 import com.google.common.annotations.VisibleForTesting; 19 import com.google.common.collect.ComparisonChain; 20 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.database.Cursor; 26 import android.net.Uri; 27 import android.provider.ContactsContract.CommonDataKinds.Phone; 28 import android.provider.ContactsContract.Contacts; 29 import android.provider.ContactsContract.PinnedPositions; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.util.LongSparseArray; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.ViewConfiguration; 36 import android.view.ViewGroup; 37 import android.widget.BaseAdapter; 38 import android.widget.FrameLayout; 39 40 import com.android.contacts.common.ContactPhotoManager; 41 import com.android.contacts.common.ContactTileLoaderFactory; 42 import com.android.contacts.common.R; 43 import com.android.contacts.common.list.ContactEntry; 44 import com.android.contacts.common.list.ContactTileAdapter.DisplayType; 45 import com.android.contacts.common.list.ContactTileView; 46 import com.android.dialer.list.SwipeHelper.OnItemGestureListener; 47 import com.android.dialer.list.SwipeHelper.SwipeHelperCallback; 48 49 import java.util.ArrayList; 50 import java.util.Comparator; 51 import java.util.LinkedList; 52 import java.util.List; 53 import java.util.PriorityQueue; 54 55 /** 56 * Also allows for a configurable number of columns as well as a maximum row of tiled contacts. 57 * 58 * This adapter has been rewritten to only support a maximum of one row for favorites. 59 * 60 */ 61 public class PhoneFavoritesTileAdapter extends BaseAdapter implements 62 SwipeHelper.OnItemGestureListener, OnDragDropListener { 63 private static final String TAG = PhoneFavoritesTileAdapter.class.getSimpleName(); 64 private static final boolean DEBUG = false; 65 66 public static final int NO_ROW_LIMIT = -1; 67 68 public static final int ROW_LIMIT_DEFAULT = NO_ROW_LIMIT; 69 70 private ContactTileView.Listener mListener; 71 private OnDataSetChangedForAnimationListener mDataSetChangedListener; 72 73 private Context mContext; 74 private Resources mResources; 75 76 /** Contact data stored in cache. This is used to populate the associated view. */ 77 protected ArrayList<ContactEntry> mContactEntries = null; 78 /** Back up of the temporarily removed Contact during dragging. */ 79 private ContactEntry mDraggedEntry = null; 80 /** Position of the temporarily removed contact in the cache. */ 81 private int mDraggedEntryIndex = -1; 82 /** New position of the temporarily removed contact in the cache. */ 83 private int mDropEntryIndex = -1; 84 /** New position of the temporarily entered contact in the cache. */ 85 private int mDragEnteredEntryIndex = -1; 86 /** Position of the contact pending removal. */ 87 private int mPotentialRemoveEntryIndex = -1; 88 private long mIdToKeepInPlace = -1; 89 90 private boolean mAwaitingRemove = false; 91 private boolean mDelayCursorUpdates = false; 92 93 private ContactPhotoManager mPhotoManager; 94 protected int mNumFrequents; 95 protected int mNumStarred; 96 97 protected int mColumnCount; 98 private int mMaxTiledRows = ROW_LIMIT_DEFAULT; 99 private int mStarredIndex; 100 101 protected int mIdIndex; 102 protected int mLookupIndex; 103 protected int mPhotoUriIndex; 104 protected int mNameIndex; 105 protected int mPresenceIndex; 106 protected int mStatusIndex; 107 108 private int mPhoneNumberIndex; 109 private int mPhoneNumberTypeIndex; 110 private int mPhoneNumberLabelIndex; 111 private int mIsDefaultNumberIndex; 112 protected int mPinnedIndex; 113 protected int mContactIdIndex; 114 115 private final int mPaddingInPixels; 116 117 /** Indicates whether a drag is in process. */ 118 private boolean mInDragging = false; 119 120 public static final int PIN_LIMIT = 20; 121 122 /** 123 * The soft limit on how many contact tiles to show. 124 * NOTE This soft limit would not restrict the number of starred contacts to show, rather 125 * 1. If the count of starred contacts is less than this limit, show 20 tiles total. 126 * 2. If the count of starred contacts is more than or equal to this limit, 127 * show all starred tiles and no frequents. 128 */ 129 private static final int TILES_SOFT_LIMIT = 20; 130 131 final Comparator<ContactEntry> mContactEntryComparator = new Comparator<ContactEntry>() { 132 @Override 133 public int compare(ContactEntry lhs, ContactEntry rhs) { 134 return ComparisonChain.start() 135 .compare(lhs.pinned, rhs.pinned) 136 .compare(lhs.name, rhs.name) 137 .result(); 138 } 139 }; 140 141 public interface OnDataSetChangedForAnimationListener { 142 public void onDataSetChangedForAnimation(long... idsInPlace); 143 public void cacheOffsetsForDatasetChange(); 144 }; 145 146 public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, 147 OnDataSetChangedForAnimationListener dataSetChangedListener, 148 int numCols, int maxTiledRows) { 149 mDataSetChangedListener = dataSetChangedListener; 150 mListener = listener; 151 mContext = context; 152 mResources = context.getResources(); 153 mColumnCount = numCols; 154 mNumFrequents = 0; 155 mMaxTiledRows = maxTiledRows; 156 mContactEntries = new ArrayList<ContactEntry>(); 157 // Converting padding in dips to padding in pixels 158 mPaddingInPixels = mContext.getResources() 159 .getDimensionPixelSize(R.dimen.contact_tile_divider_width); 160 161 bindColumnIndices(); 162 } 163 164 public void setPhotoLoader(ContactPhotoManager photoLoader) { 165 mPhotoManager = photoLoader; 166 } 167 168 public void setMaxRowCount(int maxRows) { 169 mMaxTiledRows = maxRows; 170 } 171 172 public void setColumnCount(int columnCount) { 173 mColumnCount = columnCount; 174 } 175 176 /** 177 * Indicates whether a drag is in process. 178 * 179 * @param inDragging Boolean variable indicating whether there is a drag in process. 180 */ 181 public void setInDragging(boolean inDragging) { 182 mDelayCursorUpdates = inDragging; 183 mInDragging = inDragging; 184 } 185 186 /** Gets whether the drag is in process. */ 187 public boolean getInDragging() { 188 return mInDragging; 189 } 190 191 /** 192 * Sets the column indices for expected {@link Cursor} 193 * based on {@link DisplayType}. 194 */ 195 protected void bindColumnIndices() { 196 mIdIndex = ContactTileLoaderFactory.CONTACT_ID; 197 mLookupIndex = ContactTileLoaderFactory.LOOKUP_KEY; 198 mPhotoUriIndex = ContactTileLoaderFactory.PHOTO_URI; 199 mNameIndex = ContactTileLoaderFactory.DISPLAY_NAME; 200 mStarredIndex = ContactTileLoaderFactory.STARRED; 201 mPresenceIndex = ContactTileLoaderFactory.CONTACT_PRESENCE; 202 mStatusIndex = ContactTileLoaderFactory.CONTACT_STATUS; 203 204 mPhoneNumberIndex = ContactTileLoaderFactory.PHONE_NUMBER; 205 mPhoneNumberTypeIndex = ContactTileLoaderFactory.PHONE_NUMBER_TYPE; 206 mPhoneNumberLabelIndex = ContactTileLoaderFactory.PHONE_NUMBER_LABEL; 207 mIsDefaultNumberIndex = ContactTileLoaderFactory.IS_DEFAULT_NUMBER; 208 mPinnedIndex = ContactTileLoaderFactory.PINNED; 209 mContactIdIndex = ContactTileLoaderFactory.CONTACT_ID_FOR_DATA; 210 } 211 212 /** 213 * Gets the number of frequents from the passed in cursor. 214 * 215 * This methods is needed so the GroupMemberTileAdapter can override this. 216 * 217 * @param cursor The cursor to get number of frequents from. 218 */ 219 protected void saveNumFrequentsFromCursor(Cursor cursor) { 220 mNumFrequents = cursor.getCount() - mNumStarred; 221 } 222 223 /** 224 * Creates {@link ContactTileView}s for each item in {@link Cursor}. 225 * 226 * Else use {@link ContactTileLoaderFactory} 227 */ 228 public void setContactCursor(Cursor cursor) { 229 if (!mDelayCursorUpdates && cursor != null && !cursor.isClosed()) { 230 mNumStarred = getNumStarredContacts(cursor); 231 if (mAwaitingRemove) { 232 mDataSetChangedListener.cacheOffsetsForDatasetChange(); 233 } 234 235 saveNumFrequentsFromCursor(cursor); 236 saveCursorToCache(cursor); 237 // cause a refresh of any views that rely on this data 238 notifyDataSetChanged(); 239 // about to start redraw 240 if (mIdToKeepInPlace != -1) { 241 mDataSetChangedListener.onDataSetChangedForAnimation(mIdToKeepInPlace); 242 } else { 243 mDataSetChangedListener.onDataSetChangedForAnimation(); 244 } 245 mIdToKeepInPlace = -1; 246 } 247 } 248 249 /** 250 * Saves the cursor data to the cache, to speed up UI changes. 251 * 252 * @param cursor Returned cursor with data to populate the view. 253 */ 254 private void saveCursorToCache(Cursor cursor) { 255 mContactEntries.clear(); 256 257 cursor.moveToPosition(-1); 258 259 final LongSparseArray<Object> duplicates = new LongSparseArray<Object>(cursor.getCount()); 260 261 // Track the length of {@link #mContactEntries} and compare to {@link #TILES_SOFT_LIMIT}. 262 int counter = 0; 263 264 while (cursor.moveToNext()) { 265 266 final int starred = cursor.getInt(mStarredIndex); 267 final long id; 268 269 // We display a maximum of TILES_SOFT_LIMIT contacts, or the total number of starred 270 // whichever is greater. 271 if (starred < 1 && counter >= TILES_SOFT_LIMIT) { 272 break; 273 } else { 274 id = cursor.getLong(mContactIdIndex); 275 } 276 277 final ContactEntry existing = (ContactEntry) duplicates.get(id); 278 if (existing != null) { 279 // Check if the existing number is a default number. If not, clear the phone number 280 // and label fields so that the disambiguation dialog will show up. 281 if (!existing.isDefaultNumber) { 282 existing.phoneLabel = null; 283 existing.phoneNumber = null; 284 } 285 continue; 286 } 287 288 final String photoUri = cursor.getString(mPhotoUriIndex); 289 final String lookupKey = cursor.getString(mLookupIndex); 290 final int pinned = cursor.getInt(mPinnedIndex); 291 final String name = cursor.getString(mNameIndex); 292 final boolean isStarred = cursor.getInt(mStarredIndex) > 0; 293 final boolean isDefaultNumber = cursor.getInt(mIsDefaultNumberIndex) > 0; 294 295 final ContactEntry contact = new ContactEntry(); 296 297 contact.id = id; 298 contact.name = (!TextUtils.isEmpty(name)) ? name : 299 mResources.getString(R.string.missing_name); 300 contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null); 301 contact.lookupKey = lookupKey; 302 contact.lookupUri = ContentUris.withAppendedId( 303 Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id); 304 contact.isFavorite = isStarred; 305 contact.isDefaultNumber = isDefaultNumber; 306 307 // Set phone number and label 308 final int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex); 309 final String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex); 310 contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType, 311 phoneNumberCustomLabel); 312 contact.phoneNumber = cursor.getString(mPhoneNumberIndex); 313 314 contact.pinned = pinned; 315 mContactEntries.add(contact); 316 317 duplicates.put(id, contact); 318 319 counter++; 320 } 321 322 mAwaitingRemove = false; 323 324 arrangeContactsByPinnedPosition(mContactEntries); 325 326 notifyDataSetChanged(); 327 } 328 329 /** 330 * Iterates over the {@link Cursor} 331 * Returns position of the first NON Starred Contact 332 * Returns -1 if {@link DisplayType#STARRED_ONLY} 333 * Returns 0 if {@link DisplayType#FREQUENT_ONLY} 334 */ 335 protected int getNumStarredContacts(Cursor cursor) { 336 cursor.moveToPosition(-1); 337 while (cursor.moveToNext()) { 338 if (cursor.getInt(mStarredIndex) == 0) { 339 return cursor.getPosition(); 340 } 341 } 342 343 // There are not NON Starred contacts in cursor 344 // Set divider positon to end 345 return cursor.getCount(); 346 } 347 348 /** 349 * Loads a contact from the cached list. 350 * 351 * @param position Position of the Contact. 352 * @return Contact at the requested position. 353 */ 354 protected ContactEntry getContactEntryFromCache(int position) { 355 if (mContactEntries.size() <= position) return null; 356 return mContactEntries.get(position); 357 } 358 359 /** 360 * Returns the number of frequents that will be displayed in the list. 361 */ 362 public int getNumFrequents() { 363 return mNumFrequents; 364 } 365 366 @Override 367 public int getCount() { 368 if (mContactEntries == null || mContactEntries.isEmpty()) { 369 return 0; 370 } 371 372 int total = mContactEntries.size(); 373 // The number of contacts that don't show up as tiles 374 final int nonTiledRows = Math.max(0, total - getMaxContactsInTiles()); 375 // The number of tiled rows 376 final int tiledRows = getRowCount(total - nonTiledRows); 377 return nonTiledRows + tiledRows; 378 } 379 380 public int getMaxTiledRows() { 381 return mMaxTiledRows; 382 } 383 384 /** 385 * Returns the number of rows required to show the provided number of entries 386 * with the current number of columns. 387 */ 388 protected int getRowCount(int entryCount) { 389 if (entryCount == 0) return 0; 390 final int nonLimitedRows = ((entryCount - 1) / mColumnCount) + 1; 391 if (mMaxTiledRows == NO_ROW_LIMIT) { 392 return nonLimitedRows; 393 } 394 return Math.min(mMaxTiledRows, nonLimitedRows); 395 } 396 397 private int getMaxContactsInTiles() { 398 if (mMaxTiledRows == NO_ROW_LIMIT) { 399 return Integer.MAX_VALUE; 400 } 401 return mColumnCount * mMaxTiledRows; 402 } 403 404 public int getRowIndex(int entryIndex) { 405 if (entryIndex < getMaxContactsInTiles()) { 406 return entryIndex / mColumnCount; 407 } else { 408 return entryIndex - mMaxTiledRows * (mColumnCount + 1); 409 } 410 } 411 412 public int getColumnCount() { 413 return mColumnCount; 414 } 415 416 /** 417 * Returns an ArrayList of the {@link ContactEntry}s that are to appear 418 * on the row for the given position. 419 */ 420 @Override 421 public ArrayList<ContactEntry> getItem(int position) { 422 ArrayList<ContactEntry> resultList = new ArrayList<ContactEntry>(mColumnCount); 423 424 final int entryIndex = getFirstContactEntryIndexForPosition(position); 425 426 final int viewType = getItemViewType(position); 427 428 final int columnCount; 429 if (viewType == ViewTypes.TOP) { 430 columnCount = mColumnCount; 431 } else { 432 columnCount = 1; 433 } 434 435 for (int i = 0; i < columnCount; i++) { 436 final ContactEntry entry = getContactEntryFromCache(entryIndex + i); 437 if (entry == null) break; // less than mColumnCount contacts 438 resultList.add(entry); 439 } 440 441 return resultList; 442 } 443 444 /* 445 * Given a position in the adapter, returns the index of the first contact entry that is to be 446 * in that row. 447 */ 448 private int getFirstContactEntryIndexForPosition(int position) { 449 final int maxContactsInTiles = getMaxContactsInTiles(); 450 if (position < getRowCount(maxContactsInTiles)) { 451 // Contacts that appear as tiles 452 return position * mColumnCount; 453 } else { 454 // Contacts that appear as rows 455 // The actual position of the contact in the cursor is simply total the number of 456 // tiled contacts + the given position 457 return maxContactsInTiles + position - mMaxTiledRows; 458 } 459 } 460 461 /** 462 * For the top row of tiled contacts, the item id is the position of the row of 463 * contacts. 464 * For frequent contacts, the item id is the maximum number of rows of tiled contacts + 465 * the actual contact id. Since contact ids are always greater than 0, this guarantees that 466 * all items within this adapter will always have unique ids. 467 */ 468 @Override 469 public long getItemId(int position) { 470 if (getItemViewType(position) == ViewTypes.FREQUENT) { 471 return getAdjustedItemId(getItem(position).get(0).id); 472 } else { 473 return position; 474 } 475 } 476 477 /** 478 * Calculates the stable itemId for a particular entry based on the entry's contact ID. This 479 * stable itemId is used for animation purposes. 480 */ 481 public long getAdjustedItemId(long id) { 482 if (mMaxTiledRows == NO_ROW_LIMIT) { 483 return id; 484 } 485 return mMaxTiledRows + id; 486 } 487 488 @Override 489 public boolean hasStableIds() { 490 return true; 491 } 492 493 @Override 494 public boolean areAllItemsEnabled() { 495 // No dividers, so all items are enabled. 496 return true; 497 } 498 499 @Override 500 public boolean isEnabled(int position) { 501 return getCount() > 0; 502 } 503 504 @Override 505 public void notifyDataSetChanged() { 506 if (DEBUG) { 507 Log.v(TAG, "notifyDataSetChanged"); 508 } 509 super.notifyDataSetChanged(); 510 } 511 512 @Override 513 public View getView(int position, View convertView, ViewGroup parent) { 514 if (DEBUG) { 515 Log.v(TAG, "get view for " + String.valueOf(position)); 516 } 517 518 int itemViewType = getItemViewType(position); 519 520 ContactTileRow contactTileRowView = null; 521 522 if (convertView instanceof ContactTileRow) { 523 contactTileRowView = (ContactTileRow) convertView; 524 } 525 526 ArrayList<ContactEntry> contactList = getItem(position); 527 528 if (contactTileRowView == null) { 529 // Creating new row if needed 530 contactTileRowView = new ContactTileRow(mContext, itemViewType, position); 531 } 532 533 contactTileRowView.configureRow(contactList, position, position == getCount() - 1); 534 535 return contactTileRowView; 536 } 537 538 private int getLayoutResourceId(int viewType) { 539 switch (viewType) { 540 case ViewTypes.FREQUENT: 541 return R.layout.phone_favorite_regular_row_view; 542 case ViewTypes.TOP: 543 return R.layout.phone_favorite_tile_view; 544 default: 545 throw new IllegalArgumentException("Unrecognized viewType " + viewType); 546 } 547 } 548 @Override 549 public int getViewTypeCount() { 550 return ViewTypes.COUNT; 551 } 552 553 @Override 554 public int getItemViewType(int position) { 555 if (position < getMaxContactsInTiles()) { 556 return ViewTypes.TOP; 557 } else { 558 return ViewTypes.FREQUENT; 559 } 560 } 561 562 /** 563 * Temporarily removes a contact from the list for UI refresh. Stores data for this contact 564 * in the back-up variable. 565 * 566 * @param index Position of the contact to be removed. 567 */ 568 public void popContactEntry(int index) { 569 if (isIndexInBound(index)) { 570 mDraggedEntry = mContactEntries.get(index); 571 mDraggedEntryIndex = index; 572 mDragEnteredEntryIndex = index; 573 markDropArea(mDragEnteredEntryIndex); 574 } 575 } 576 577 /** 578 * @param itemIndex Position of the contact in {@link #mContactEntries}. 579 * @return True if the given index is valid for {@link #mContactEntries}. 580 */ 581 private boolean isIndexInBound(int itemIndex) { 582 return itemIndex >= 0 && itemIndex < mContactEntries.size(); 583 } 584 585 /** 586 * Mark the tile as drop area by given the item index in {@link #mContactEntries}. 587 * 588 * @param itemIndex Position of the contact in {@link #mContactEntries}. 589 */ 590 private void markDropArea(int itemIndex) { 591 if (isIndexInBound(mDragEnteredEntryIndex) && isIndexInBound(itemIndex)) { 592 mDataSetChangedListener.cacheOffsetsForDatasetChange(); 593 // Remove the old placeholder item and place the new placeholder item. 594 final int oldIndex = mDragEnteredEntryIndex; 595 mContactEntries.remove(mDragEnteredEntryIndex); 596 mDragEnteredEntryIndex = itemIndex; 597 mContactEntries.add(mDragEnteredEntryIndex, ContactEntry.BLANK_ENTRY); 598 ContactEntry.BLANK_ENTRY.id = mDraggedEntry.id; 599 mDataSetChangedListener.onDataSetChangedForAnimation(); 600 notifyDataSetChanged(); 601 } 602 } 603 604 /** 605 * Drops the temporarily removed contact to the desired location in the list. 606 */ 607 public void handleDrop() { 608 boolean changed = false; 609 if (mDraggedEntry != null) { 610 if (isIndexInBound(mDragEnteredEntryIndex) && 611 mDragEnteredEntryIndex != mDraggedEntryIndex) { 612 // Don't add the ContactEntry here (to prevent a double animation from occuring). 613 // When we receive a new cursor the list of contact entries will automatically be 614 // populated with the dragged ContactEntry at the correct spot. 615 mDropEntryIndex = mDragEnteredEntryIndex; 616 mContactEntries.set(mDropEntryIndex, mDraggedEntry); 617 mIdToKeepInPlace = getAdjustedItemId(mDraggedEntry.id); 618 mDataSetChangedListener.cacheOffsetsForDatasetChange(); 619 changed = true; 620 } else if (isIndexInBound(mDraggedEntryIndex)) { 621 // If {@link #mDragEnteredEntryIndex} is invalid, 622 // falls back to the original position of the contact. 623 mContactEntries.remove(mDragEnteredEntryIndex); 624 mContactEntries.add(mDraggedEntryIndex, mDraggedEntry); 625 mDropEntryIndex = mDraggedEntryIndex; 626 notifyDataSetChanged(); 627 } 628 629 if (changed && mDropEntryIndex < PIN_LIMIT) { 630 final ContentValues cv = getReflowedPinnedPositions(mContactEntries, mDraggedEntry, 631 mDraggedEntryIndex, mDropEntryIndex); 632 final Uri pinUri = PinnedPositions.UPDATE_URI.buildUpon().build(); 633 // update the database here with the new pinned positions 634 mContext.getContentResolver().update(pinUri, cv, null, null); 635 } 636 mDraggedEntry = null; 637 } 638 } 639 640 /** 641 * Invoked when the dragged item is dropped to unsupported location. We will then move the 642 * contact back to where it was dragged from. 643 */ 644 public void dropToUnsupportedView() { 645 if (isIndexInBound(mDragEnteredEntryIndex)) { 646 mContactEntries.remove(mDragEnteredEntryIndex); 647 mContactEntries.add(mDraggedEntryIndex, mDraggedEntry); 648 notifyDataSetChanged(); 649 } 650 } 651 652 /** 653 * Sets an item to for pending removal. If the user does not click the undo button, the item 654 * will be removed at the next interaction. 655 * 656 * @param index Index of the item to be removed. 657 */ 658 public void setPotentialRemoveEntryIndex(int index) { 659 mPotentialRemoveEntryIndex = index; 660 } 661 662 /** 663 * Removes a contact entry from the list. 664 * 665 * @return True is an item is removed. False is there is no item to be removed. 666 */ 667 public boolean removePendingContactEntry() { 668 boolean removed = false; 669 if (isIndexInBound(mPotentialRemoveEntryIndex)) { 670 final ContactEntry entry = mContactEntries.get(mPotentialRemoveEntryIndex); 671 unstarAndUnpinContact(entry.lookupUri); 672 removed = true; 673 mAwaitingRemove = true; 674 } 675 cleanTempVariables(); 676 return removed; 677 } 678 679 /** 680 * Resets the item for pending removal. 681 */ 682 public void undoPotentialRemoveEntryIndex() { 683 mPotentialRemoveEntryIndex = -1; 684 } 685 686 public boolean hasPotentialRemoveEntryIndex() { 687 return isIndexInBound(mPotentialRemoveEntryIndex); 688 } 689 690 /** 691 * Clears all temporary variables at a new interaction. 692 */ 693 public void cleanTempVariables() { 694 mDraggedEntryIndex = -1; 695 mDropEntryIndex = -1; 696 mDragEnteredEntryIndex = -1; 697 mDraggedEntry = null; 698 mPotentialRemoveEntryIndex = -1; 699 } 700 701 /** 702 * Acts as a row item composed of {@link ContactTileView} 703 * 704 */ 705 public class ContactTileRow extends FrameLayout implements SwipeHelperCallback { 706 public static final int CONTACT_ENTRY_INDEX_TAG = R.id.contact_entry_index_tag; 707 708 private int mItemViewType; 709 private int mLayoutResId; 710 private final int mRowPaddingStart; 711 private final int mRowPaddingEnd; 712 private final int mRowPaddingTop; 713 private final int mRowPaddingBottom; 714 private final float mHeightToWidthRatio; 715 private int mPosition; 716 private SwipeHelper mSwipeHelper; 717 private OnItemGestureListener mOnItemSwipeListener; 718 719 public ContactTileRow(Context context, int itemViewType, int position) { 720 super(context); 721 mItemViewType = itemViewType; 722 mLayoutResId = getLayoutResourceId(mItemViewType); 723 mPosition = position; 724 725 final Resources resources = mContext.getResources(); 726 727 mHeightToWidthRatio = getResources().getFraction( 728 R.dimen.contact_tile_height_to_width_ratio, 1, 1); 729 730 if (mItemViewType == ViewTypes.TOP) { 731 // For tiled views, we still want padding to be set on the ContactTileRow. 732 // Otherwise the padding would be set around each of the tiles, which we don't want 733 mRowPaddingTop = resources.getDimensionPixelSize( 734 R.dimen.favorites_row_top_padding); 735 mRowPaddingBottom = resources.getDimensionPixelSize( 736 R.dimen.favorites_row_bottom_padding); 737 mRowPaddingStart = resources.getDimensionPixelSize( 738 R.dimen.favorites_row_start_padding); 739 mRowPaddingEnd = resources.getDimensionPixelSize( 740 R.dimen.favorites_row_end_padding); 741 } else { 742 // For row views, padding is set on the view itself. 743 mRowPaddingTop = 0; 744 mRowPaddingBottom = 0; 745 mRowPaddingStart = 0; 746 mRowPaddingEnd = 0; 747 } 748 749 setPaddingRelative(mRowPaddingStart, mRowPaddingTop, mRowPaddingEnd, 750 mRowPaddingBottom); 751 752 // Remove row (but not children) from accessibility node tree. 753 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 754 755 if (mItemViewType == ViewTypes.FREQUENT) { 756 // ListView handles swiping for this item 757 SwipeHelper.setSwipeable(this, true); 758 } else if (mItemViewType == ViewTypes.TOP) { 759 // The contact tile row has its own swipe helpers, that makes each individual 760 // tile swipeable. 761 final float densityScale = getResources().getDisplayMetrics().density; 762 final float pagingTouchSlop = ViewConfiguration.get(context) 763 .getScaledPagingTouchSlop(); 764 mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale, 765 pagingTouchSlop); 766 // Increase swipe thresholds for square tiles since they are relatively small. 767 mSwipeHelper.setChildSwipedFarEnoughFactor(0.9f); 768 mSwipeHelper.setChildSwipedFastEnoughFactor(0.1f); 769 mOnItemSwipeListener = PhoneFavoritesTileAdapter.this; 770 } 771 } 772 773 /** 774 * Configures the row to add {@link ContactEntry}s information to the views 775 */ 776 public void configureRow(ArrayList<ContactEntry> list, int position, boolean isLastRow) { 777 int columnCount = mItemViewType == ViewTypes.FREQUENT ? 1 : mColumnCount; 778 mPosition = position; 779 780 // Adding tiles to row and filling in contact information 781 for (int columnCounter = 0; columnCounter < columnCount; columnCounter++) { 782 ContactEntry entry = 783 columnCounter < list.size() ? list.get(columnCounter) : null; 784 addTileFromEntry(entry, columnCounter, isLastRow); 785 } 786 if (columnCount == 1) { 787 if (list.get(0) == ContactEntry.BLANK_ENTRY) { 788 setVisibility(View.INVISIBLE); 789 } else { 790 setVisibility(View.VISIBLE); 791 } 792 } 793 } 794 795 private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) { 796 final PhoneFavoriteTileView contactTile; 797 798 if (getChildCount() <= childIndex) { 799 800 contactTile = (PhoneFavoriteTileView) inflate(mContext, mLayoutResId, null); 801 // Note: the layoutparam set here is only actually used for FREQUENT. 802 // We override onMeasure() for STARRED and we don't care the layout param there. 803 final Resources resources = mContext.getResources(); 804 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( 805 ViewGroup.LayoutParams.WRAP_CONTENT, 806 ViewGroup.LayoutParams.WRAP_CONTENT); 807 808 params.setMargins( 809 resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 0, 810 resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 0); 811 contactTile.setLayoutParams(params); 812 contactTile.setPhotoManager(mPhotoManager); 813 contactTile.setListener(mListener); 814 addView(contactTile); 815 } else { 816 contactTile = (PhoneFavoriteTileView) getChildAt(childIndex); 817 } 818 contactTile.loadFromContact(entry); 819 820 int entryIndex = -1; 821 switch (mItemViewType) { 822 case ViewTypes.TOP: 823 // Setting divider visibilities 824 contactTile.setPaddingRelative(0, 0, 825 childIndex >= mColumnCount - 1 ? 0 : mPaddingInPixels, 0); 826 entryIndex = getFirstContactEntryIndexForPosition(mPosition) + childIndex; 827 SwipeHelper.setSwipeable(contactTile, false); 828 break; 829 case ViewTypes.FREQUENT: 830 contactTile.setHorizontalDividerVisibility( 831 isLastRow ? View.GONE : View.VISIBLE); 832 entryIndex = getFirstContactEntryIndexForPosition(mPosition); 833 SwipeHelper.setSwipeable(this, true); 834 break; 835 default: 836 break; 837 } 838 // tag the tile with the index of the contact entry it is associated with 839 if (entryIndex != -1) { 840 contactTile.setTag(CONTACT_ENTRY_INDEX_TAG, entryIndex); 841 } 842 contactTile.setupFavoriteContactCard(); 843 } 844 845 @Override 846 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 847 switch (mItemViewType) { 848 case ViewTypes.TOP: 849 onLayoutForTiles(); 850 return; 851 default: 852 super.onLayout(changed, left, top, right, bottom); 853 return; 854 } 855 } 856 857 private void onLayoutForTiles() { 858 final int count = getChildCount(); 859 860 // Just line up children horizontally. 861 int childLeft = getPaddingStart(); 862 for (int i = 0; i < count; i++) { 863 final View child = getChildAt(i); 864 865 // Note MeasuredWidth includes the padding. 866 final int childWidth = child.getMeasuredWidth(); 867 child.layout(childLeft, getPaddingTop(), childLeft + childWidth, 868 getPaddingTop() + child.getMeasuredHeight()); 869 childLeft += childWidth; 870 } 871 } 872 873 @Override 874 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 875 switch (mItemViewType) { 876 case ViewTypes.TOP: 877 onMeasureForTiles(widthMeasureSpec); 878 return; 879 default: 880 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 881 return; 882 } 883 } 884 885 private void onMeasureForTiles(int widthMeasureSpec) { 886 final int width = MeasureSpec.getSize(widthMeasureSpec); 887 888 final int childCount = getChildCount(); 889 if (childCount == 0) { 890 // Just in case... 891 setMeasuredDimension(width, 0); 892 return; 893 } 894 895 // 1. Calculate image size. 896 // = ([total width] - [total padding]) / [child count] 897 // 898 // 2. Set it to width/height of each children. 899 // If we have a remainder, some tiles will have 1 pixel larger width than its height. 900 // 901 // 3. Set the dimensions of itself. 902 // Let width = given width. 903 // Let height = image size + bottom paddding. 904 905 final int totalPaddingsInPixels = (mColumnCount - 1) * mPaddingInPixels 906 + mRowPaddingStart + mRowPaddingEnd; 907 908 // Preferred width / height for images (excluding the padding). 909 // The actual width may be 1 pixel larger than this if we have a remainder. 910 final int imageWidth = (width - totalPaddingsInPixels) / mColumnCount; 911 final int remainder = width - (imageWidth * mColumnCount) - totalPaddingsInPixels; 912 913 final int height = (int) (mHeightToWidthRatio * imageWidth); 914 915 for (int i = 0; i < childCount; i++) { 916 final View child = getChildAt(i); 917 final int childWidth = imageWidth + child.getPaddingRight() 918 // Compensate for the remainder 919 + (i < remainder ? 1 : 0); 920 child.measure( 921 MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), 922 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) 923 ); 924 } 925 setMeasuredDimension(width, height + getPaddingTop() + getPaddingBottom()); 926 } 927 928 /** 929 * Gets the index of the item at the specified coordinates. 930 * 931 * @param itemX X-coordinate of the selected item. 932 * @param itemY Y-coordinate of the selected item. 933 * @return Index of the selected item in the cached array. 934 */ 935 public int getItemIndex(float itemX, float itemY) { 936 if (mMaxTiledRows == NO_ROW_LIMIT || mPosition < mMaxTiledRows) { 937 if (DEBUG) { 938 Log.v(TAG, String.valueOf(itemX) + " " + String.valueOf(itemY)); 939 } 940 for (int i = 0; i < getChildCount(); ++i) { 941 /** If the row contains multiple tiles, checks each tile to see if the point 942 * is contained in the tile. */ 943 final View child = getChildAt(i); 944 /** The coordinates passed in are based on the ListView, 945 * translate for each child first */ 946 final int xInListView = child.getLeft() + getLeft(); 947 final int yInListView = child.getTop() + getTop(); 948 final int distanceX = (int) itemX - xInListView; 949 final int distanceY = (int) itemY - yInListView; 950 if ((distanceX > 0 && distanceX < child.getWidth()) && 951 (distanceY > 0 && distanceY < child.getHeight())) { 952 /** If the point is contained in the rectangle, computes the index of the 953 * item in the cached array. */ 954 return i + (mPosition) * mColumnCount; 955 } 956 } 957 } else { 958 /** If the selected item is one of the rows, compute the index. */ 959 return getRegularRowItemIndex(); 960 } 961 return -1; 962 } 963 964 /** 965 * Gets the index of the regular row item. 966 * 967 * @return Index of the selected item in the cached array. 968 */ 969 public int getRegularRowItemIndex() { 970 return (mPosition - mMaxTiledRows) + mColumnCount * mMaxTiledRows; 971 } 972 973 public PhoneFavoritesTileAdapter getTileAdapter() { 974 return PhoneFavoritesTileAdapter.this; 975 } 976 977 public int getPosition() { 978 return mPosition; 979 } 980 981 /** 982 * Find the view under the pointer. 983 */ 984 public View getViewAtPosition(int x, int y) { 985 // find the view under the pointer, accounting for GONE views 986 final int count = getChildCount(); 987 View view; 988 for (int childIdx = 0; childIdx < count; childIdx++) { 989 view = getChildAt(childIdx); 990 if (x >= view.getLeft() && x <= view.getRight()) { 991 return view; 992 } 993 } 994 return null; 995 } 996 997 @Override 998 public View getChildAtPosition(MotionEvent ev) { 999 final View view = getViewAtPosition((int) ev.getX(), (int) ev.getY()); 1000 if (view != null && 1001 SwipeHelper.isSwipeable(view) && 1002 view.getVisibility() != GONE) { 1003 // If this view is swipable, then return it. If not, because the removal 1004 // dialog is currently showing, then return a null view, which will simply 1005 // be ignored by the swipe helper. 1006 return view; 1007 } 1008 return null; 1009 } 1010 1011 @Override 1012 public View getChildContentView(View v) { 1013 return v.findViewById(R.id.contact_favorite_card); 1014 } 1015 1016 @Override 1017 public void onScroll() {} 1018 1019 @Override 1020 public boolean canChildBeDismissed(View v) { 1021 return true; 1022 } 1023 1024 @Override 1025 public void onBeginDrag(View v) { 1026 removePendingContactEntry(); 1027 final int index = indexOfChild(v); 1028 1029 /* 1030 if (index > 0) { 1031 detachViewFromParent(index); 1032 attachViewToParent(v, 0, v.getLayoutParams()); 1033 }*/ 1034 1035 // We do this so the underlying ScrollView knows that it won't get 1036 // the chance to intercept events anymore 1037 requestDisallowInterceptTouchEvent(true); 1038 } 1039 1040 @Override 1041 public void onChildDismissed(View v) { 1042 if (v != null) { 1043 if (mOnItemSwipeListener != null) { 1044 mOnItemSwipeListener.onSwipe(v); 1045 } 1046 } 1047 } 1048 1049 @Override 1050 public void onDragCancelled(View v) {} 1051 1052 @Override 1053 public boolean onInterceptTouchEvent(MotionEvent ev) { 1054 if (mSwipeHelper != null && isSwipeEnabled()) { 1055 return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev); 1056 } else { 1057 return super.onInterceptTouchEvent(ev); 1058 } 1059 } 1060 1061 @Override 1062 public boolean onTouchEvent(MotionEvent ev) { 1063 if (mSwipeHelper != null && isSwipeEnabled()) { 1064 return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev); 1065 } else { 1066 return super.onTouchEvent(ev); 1067 } 1068 } 1069 1070 public int getItemViewType() { 1071 return mItemViewType; 1072 } 1073 1074 public void setOnItemSwipeListener(OnItemGestureListener listener) { 1075 mOnItemSwipeListener = listener; 1076 } 1077 } 1078 1079 /** 1080 * Used when a contact is swiped away. This will both unstar and set pinned position of the 1081 * contact to PinnedPosition.DEMOTED so that it doesn't show up anymore in the favorites list. 1082 */ 1083 private void unstarAndUnpinContact(Uri contactUri) { 1084 final ContentValues values = new ContentValues(2); 1085 values.put(Contacts.STARRED, false); 1086 values.put(Contacts.PINNED, PinnedPositions.DEMOTED); 1087 mContext.getContentResolver().update(contactUri, values, null, null); 1088 } 1089 1090 /** 1091 * Given a list of contacts that each have pinned positions, rearrange the list (destructive) 1092 * such that all pinned contacts are in their defined pinned positions, and unpinned contacts 1093 * take the spaces between those pinned contacts. Demoted contacts should not appear in the 1094 * resulting list. 1095 * 1096 * This method also updates the pinned positions of pinned contacts so that they are all 1097 * unique positive integers within range from 0 to toArrange.size() - 1. This is because 1098 * when the contact entries are read from the database, it is possible for them to have 1099 * overlapping pin positions due to sync or modifications by third party apps. 1100 */ 1101 @VisibleForTesting 1102 /* package */ void arrangeContactsByPinnedPosition(ArrayList<ContactEntry> toArrange) { 1103 final PriorityQueue<ContactEntry> pinnedQueue = 1104 new PriorityQueue<ContactEntry>(PIN_LIMIT, mContactEntryComparator); 1105 1106 final List<ContactEntry> unpinnedContacts = new LinkedList<ContactEntry>(); 1107 1108 final int length = toArrange.size(); 1109 for (int i = 0; i < length; i++) { 1110 final ContactEntry contact = toArrange.get(i); 1111 // Decide whether the contact is hidden(demoted), pinned, or unpinned 1112 if (contact.pinned > PIN_LIMIT) { 1113 unpinnedContacts.add(contact); 1114 } else if (contact.pinned > PinnedPositions.DEMOTED) { 1115 // Demoted or contacts with negative pinned positions are ignored. 1116 // Pinned contacts go into a priority queue where they are ranked by pinned 1117 // position. This is required because the contacts provider does not return 1118 // contacts ordered by pinned position. 1119 pinnedQueue.add(contact); 1120 } 1121 } 1122 1123 final int maxToPin = Math.min(PIN_LIMIT, pinnedQueue.size() + unpinnedContacts.size()); 1124 1125 toArrange.clear(); 1126 for (int i = 0; i < maxToPin; i++) { 1127 if (!pinnedQueue.isEmpty() && pinnedQueue.peek().pinned <= i) { 1128 final ContactEntry toPin = pinnedQueue.poll(); 1129 toPin.pinned = i; 1130 toArrange.add(toPin); 1131 } else if (!unpinnedContacts.isEmpty()) { 1132 toArrange.add(unpinnedContacts.remove(0)); 1133 } 1134 } 1135 1136 // If there are still contacts in pinnedContacts at this point, it means that the pinned 1137 // positions of these pinned contacts exceed the actual number of contacts in the list. 1138 // For example, the user had 10 frequents, starred and pinned one of them at the last spot, 1139 // and then cleared frequents. Contacts in this situation should become unpinned. 1140 while (!pinnedQueue.isEmpty()) { 1141 final ContactEntry entry = pinnedQueue.poll(); 1142 entry.pinned = PinnedPositions.UNPINNED; 1143 toArrange.add(entry); 1144 } 1145 1146 // Any remaining unpinned contacts that weren't in the gaps between the pinned contacts 1147 // now just get appended to the end of the list. 1148 toArrange.addAll(unpinnedContacts); 1149 } 1150 1151 /** 1152 * Given an existing list of contact entries and a single entry that is to be pinned at a 1153 * particular position, return a ContentValues object that contains new pinned positions for 1154 * all contacts that are forced to be pinned at new positions, trying as much as possible to 1155 * keep pinned contacts at their original location. 1156 * 1157 * At this point in time the pinned position of each contact in the list has already been 1158 * updated by {@link #arrangeContactsByPinnedPosition}, so we can assume that all pinned 1159 * positions(within {@link #PIN_LIMIT} are unique positive integers. 1160 */ 1161 @VisibleForTesting 1162 /* package */ ContentValues getReflowedPinnedPositions(ArrayList<ContactEntry> list, 1163 ContactEntry entryToPin, int oldPos, int newPinPos) { 1164 1165 final ContentValues cv = new ContentValues(); 1166 final int lowerBound = Math.min(oldPos, newPinPos); 1167 final int upperBound = Math.max(oldPos, newPinPos); 1168 for (int i = lowerBound; i <= upperBound; i++) { 1169 final ContactEntry entry = list.get(i); 1170 if (entry.pinned == i) continue; 1171 cv.put(String.valueOf(entry.id), i); 1172 } 1173 return cv; 1174 } 1175 1176 protected static class ViewTypes { 1177 public static final int FREQUENT = 0; 1178 public static final int TOP = 1; 1179 public static final int COUNT = 2; 1180 } 1181 1182 @Override 1183 public void onSwipe(View view) { 1184 final PhoneFavoriteTileView tileView = (PhoneFavoriteTileView) view.findViewById( 1185 R.id.contact_tile); 1186 // When the view is in the removal dialog, it should no longer be swipeable 1187 SwipeHelper.setSwipeable(view, false); 1188 tileView.displayRemovalDialog(); 1189 1190 final Integer entryIndex = (Integer) tileView.getTag( 1191 ContactTileRow.CONTACT_ENTRY_INDEX_TAG); 1192 1193 setPotentialRemoveEntryIndex(entryIndex); 1194 } 1195 1196 @Override 1197 public void onTouch() { 1198 removePendingContactEntry(); 1199 return; 1200 } 1201 1202 @Override 1203 public boolean isSwipeEnabled() { 1204 return !mAwaitingRemove; 1205 } 1206 1207 @Override 1208 public void onDragStarted(int itemIndex, int x, int y, PhoneFavoriteTileView view) { 1209 setInDragging(true); 1210 popContactEntry(itemIndex); 1211 } 1212 1213 @Override 1214 public void onDragHovered(int itemIndex, int x, int y) { 1215 if (mInDragging && 1216 mDragEnteredEntryIndex != itemIndex && 1217 isIndexInBound(itemIndex) && 1218 itemIndex < PIN_LIMIT && 1219 itemIndex >= 0) { 1220 markDropArea(itemIndex); 1221 } 1222 } 1223 1224 @Override 1225 public void onDragFinished(int x, int y) { 1226 setInDragging(false); 1227 // A contact has been dragged to the RemoveView in order to be unstarred, so simply wait 1228 // for the new contact cursor which will cause the UI to be refreshed without the unstarred 1229 // contact. 1230 if (!mAwaitingRemove) { 1231 handleDrop(); 1232 } 1233 } 1234 1235 @Override 1236 public void onDroppedOnRemove() { 1237 if (mDraggedEntry != null) { 1238 unstarAndUnpinContact(mDraggedEntry.lookupUri); 1239 mAwaitingRemove = true; 1240 } 1241 } 1242 } 1243