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 import com.google.common.collect.Lists; 21 22 import android.content.ContentProviderOperation; 23 import android.content.ContentUris; 24 import android.content.ContentValues; 25 import android.content.Context; 26 import android.content.OperationApplicationException; 27 import android.content.res.Resources; 28 import android.database.Cursor; 29 import android.net.Uri; 30 import android.os.RemoteException; 31 import android.provider.ContactsContract; 32 import android.provider.ContactsContract.CommonDataKinds.Phone; 33 import android.provider.ContactsContract.Contacts; 34 import android.provider.ContactsContract.PinnedPositions; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import android.util.LongSparseArray; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.BaseAdapter; 41 import android.widget.FrameLayout; 42 43 import com.android.contacts.common.ContactPhotoManager; 44 import com.android.contacts.common.ContactTileLoaderFactory; 45 import com.android.contacts.common.list.ContactEntry; 46 import com.android.contacts.common.list.ContactTileAdapter.DisplayType; 47 import com.android.contacts.common.list.ContactTileView; 48 import com.android.dialer.R; 49 50 import java.util.ArrayList; 51 import java.util.Comparator; 52 import java.util.LinkedList; 53 import java.util.List; 54 import java.util.PriorityQueue; 55 56 /** 57 * Also allows for a configurable number of columns as well as a maximum row of tiled contacts. 58 */ 59 public class PhoneFavoritesTileAdapter extends BaseAdapter implements 60 OnDragDropListener { 61 private static final String TAG = PhoneFavoritesTileAdapter.class.getSimpleName(); 62 private static final boolean DEBUG = false; 63 64 public static final int NO_ROW_LIMIT = -1; 65 66 public static final int ROW_LIMIT_DEFAULT = NO_ROW_LIMIT; 67 68 private ContactTileView.Listener mListener; 69 private OnDataSetChangedForAnimationListener mDataSetChangedListener; 70 71 private Context mContext; 72 private Resources mResources; 73 74 /** Contact data stored in cache. This is used to populate the associated view. */ 75 protected ArrayList<ContactEntry> mContactEntries = null; 76 /** Back up of the temporarily removed Contact during dragging. */ 77 private ContactEntry mDraggedEntry = null; 78 /** Position of the temporarily removed contact in the cache. */ 79 private int mDraggedEntryIndex = -1; 80 /** New position of the temporarily removed contact in the cache. */ 81 private int mDropEntryIndex = -1; 82 /** New position of the temporarily entered contact in the cache. */ 83 private int mDragEnteredEntryIndex = -1; 84 85 private boolean mAwaitingRemove = false; 86 private boolean mDelayCursorUpdates = false; 87 88 private ContactPhotoManager mPhotoManager; 89 protected int mNumFrequents; 90 protected int mNumStarred; 91 92 protected int mIdIndex; 93 protected int mLookupIndex; 94 protected int mPhotoUriIndex; 95 protected int mNameIndex; 96 protected int mPresenceIndex; 97 protected int mStatusIndex; 98 99 private int mPhoneNumberIndex; 100 private int mPhoneNumberTypeIndex; 101 private int mPhoneNumberLabelIndex; 102 private int mIsDefaultNumberIndex; 103 private int mStarredIndex; 104 protected int mPinnedIndex; 105 protected int mContactIdIndex; 106 107 /** Indicates whether a drag is in process. */ 108 private boolean mInDragging = false; 109 110 // Pinned positions start from 1, so there are a total of 20 maximum pinned contacts 111 public static final int PIN_LIMIT = 21; 112 113 /** 114 * The soft limit on how many contact tiles to show. 115 * NOTE This soft limit would not restrict the number of starred contacts to show, rather 116 * 1. If the count of starred contacts is less than this limit, show 20 tiles total. 117 * 2. If the count of starred contacts is more than or equal to this limit, 118 * show all starred tiles and no frequents. 119 */ 120 private static final int TILES_SOFT_LIMIT = 20; 121 122 final Comparator<ContactEntry> mContactEntryComparator = new Comparator<ContactEntry>() { 123 @Override 124 public int compare(ContactEntry lhs, ContactEntry rhs) { 125 return ComparisonChain.start() 126 .compare(lhs.pinned, rhs.pinned) 127 .compare(lhs.name, rhs.name) 128 .result(); 129 } 130 }; 131 132 public interface OnDataSetChangedForAnimationListener { 133 public void onDataSetChangedForAnimation(long... idsInPlace); 134 public void cacheOffsetsForDatasetChange(); 135 }; 136 137 public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, 138 OnDataSetChangedForAnimationListener dataSetChangedListener) { 139 mDataSetChangedListener = dataSetChangedListener; 140 mListener = listener; 141 mContext = context; 142 mResources = context.getResources(); 143 mNumFrequents = 0; 144 mContactEntries = new ArrayList<ContactEntry>(); 145 146 147 bindColumnIndices(); 148 } 149 150 public void setPhotoLoader(ContactPhotoManager photoLoader) { 151 mPhotoManager = photoLoader; 152 } 153 154 /** 155 * Indicates whether a drag is in process. 156 * 157 * @param inDragging Boolean variable indicating whether there is a drag in process. 158 */ 159 public void setInDragging(boolean inDragging) { 160 mDelayCursorUpdates = inDragging; 161 mInDragging = inDragging; 162 } 163 164 /** Gets whether the drag is in process. */ 165 public boolean getInDragging() { 166 return mInDragging; 167 } 168 169 /** 170 * Sets the column indices for expected {@link Cursor} 171 * based on {@link DisplayType}. 172 */ 173 protected void bindColumnIndices() { 174 mIdIndex = ContactTileLoaderFactory.CONTACT_ID; 175 mLookupIndex = ContactTileLoaderFactory.LOOKUP_KEY; 176 mPhotoUriIndex = ContactTileLoaderFactory.PHOTO_URI; 177 mNameIndex = ContactTileLoaderFactory.DISPLAY_NAME; 178 mStarredIndex = ContactTileLoaderFactory.STARRED; 179 mPresenceIndex = ContactTileLoaderFactory.CONTACT_PRESENCE; 180 mStatusIndex = ContactTileLoaderFactory.CONTACT_STATUS; 181 182 mPhoneNumberIndex = ContactTileLoaderFactory.PHONE_NUMBER; 183 mPhoneNumberTypeIndex = ContactTileLoaderFactory.PHONE_NUMBER_TYPE; 184 mPhoneNumberLabelIndex = ContactTileLoaderFactory.PHONE_NUMBER_LABEL; 185 mIsDefaultNumberIndex = ContactTileLoaderFactory.IS_DEFAULT_NUMBER; 186 mPinnedIndex = ContactTileLoaderFactory.PINNED; 187 mContactIdIndex = ContactTileLoaderFactory.CONTACT_ID_FOR_DATA; 188 } 189 190 /** 191 * Gets the number of frequents from the passed in cursor. 192 * 193 * This methods is needed so the GroupMemberTileAdapter can override this. 194 * 195 * @param cursor The cursor to get number of frequents from. 196 */ 197 protected void saveNumFrequentsFromCursor(Cursor cursor) { 198 mNumFrequents = cursor.getCount() - mNumStarred; 199 } 200 201 /** 202 * Creates {@link ContactTileView}s for each item in {@link Cursor}. 203 * 204 * Else use {@link ContactTileLoaderFactory} 205 */ 206 public void setContactCursor(Cursor cursor) { 207 if (!mDelayCursorUpdates && cursor != null && !cursor.isClosed()) { 208 mNumStarred = getNumStarredContacts(cursor); 209 if (mAwaitingRemove) { 210 mDataSetChangedListener.cacheOffsetsForDatasetChange(); 211 } 212 213 saveNumFrequentsFromCursor(cursor); 214 saveCursorToCache(cursor); 215 // cause a refresh of any views that rely on this data 216 notifyDataSetChanged(); 217 // about to start redraw 218 mDataSetChangedListener.onDataSetChangedForAnimation(); 219 } 220 } 221 222 /** 223 * Saves the cursor data to the cache, to speed up UI changes. 224 * 225 * @param cursor Returned cursor with data to populate the view. 226 */ 227 private void saveCursorToCache(Cursor cursor) { 228 mContactEntries.clear(); 229 230 cursor.moveToPosition(-1); 231 232 final LongSparseArray<Object> duplicates = new LongSparseArray<Object>(cursor.getCount()); 233 234 // Track the length of {@link #mContactEntries} and compare to {@link #TILES_SOFT_LIMIT}. 235 int counter = 0; 236 237 while (cursor.moveToNext()) { 238 239 final int starred = cursor.getInt(mStarredIndex); 240 final long id; 241 242 // We display a maximum of TILES_SOFT_LIMIT contacts, or the total number of starred 243 // whichever is greater. 244 if (starred < 1 && counter >= TILES_SOFT_LIMIT) { 245 break; 246 } else { 247 id = cursor.getLong(mContactIdIndex); 248 } 249 250 final ContactEntry existing = (ContactEntry) duplicates.get(id); 251 if (existing != null) { 252 // Check if the existing number is a default number. If not, clear the phone number 253 // and label fields so that the disambiguation dialog will show up. 254 if (!existing.isDefaultNumber) { 255 existing.phoneLabel = null; 256 existing.phoneNumber = null; 257 } 258 continue; 259 } 260 261 final String photoUri = cursor.getString(mPhotoUriIndex); 262 final String lookupKey = cursor.getString(mLookupIndex); 263 final int pinned = cursor.getInt(mPinnedIndex); 264 final String name = cursor.getString(mNameIndex); 265 final boolean isStarred = cursor.getInt(mStarredIndex) > 0; 266 final boolean isDefaultNumber = cursor.getInt(mIsDefaultNumberIndex) > 0; 267 268 final ContactEntry contact = new ContactEntry(); 269 270 contact.id = id; 271 contact.name = (!TextUtils.isEmpty(name)) ? name : 272 mResources.getString(R.string.missing_name); 273 contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null); 274 contact.lookupKey = lookupKey; 275 contact.lookupUri = ContentUris.withAppendedId( 276 Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id); 277 contact.isFavorite = isStarred; 278 contact.isDefaultNumber = isDefaultNumber; 279 280 // Set phone number and label 281 final int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex); 282 final String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex); 283 contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType, 284 phoneNumberCustomLabel); 285 contact.phoneNumber = cursor.getString(mPhoneNumberIndex); 286 287 contact.pinned = pinned; 288 mContactEntries.add(contact); 289 290 duplicates.put(id, contact); 291 292 counter++; 293 } 294 295 mAwaitingRemove = false; 296 297 arrangeContactsByPinnedPosition(mContactEntries); 298 299 notifyDataSetChanged(); 300 } 301 302 /** 303 * Iterates over the {@link Cursor} 304 * Returns position of the first NON Starred Contact 305 * Returns -1 if {@link DisplayType#STARRED_ONLY} 306 * Returns 0 if {@link DisplayType#FREQUENT_ONLY} 307 */ 308 protected int getNumStarredContacts(Cursor cursor) { 309 cursor.moveToPosition(-1); 310 while (cursor.moveToNext()) { 311 if (cursor.getInt(mStarredIndex) == 0) { 312 return cursor.getPosition(); 313 } 314 } 315 316 // There are not NON Starred contacts in cursor 317 // Set divider positon to end 318 return cursor.getCount(); 319 } 320 321 /** 322 * Returns the number of frequents that will be displayed in the list. 323 */ 324 public int getNumFrequents() { 325 return mNumFrequents; 326 } 327 328 @Override 329 public int getCount() { 330 if (mContactEntries == null) { 331 return 0; 332 } 333 334 return mContactEntries.size(); 335 } 336 337 /** 338 * Returns an ArrayList of the {@link ContactEntry}s that are to appear 339 * on the row for the given position. 340 */ 341 @Override 342 public ContactEntry getItem(int position) { 343 return mContactEntries.get(position); 344 } 345 346 /** 347 * For the top row of tiled contacts, the item id is the position of the row of 348 * contacts. 349 * For frequent contacts, the item id is the maximum number of rows of tiled contacts + 350 * the actual contact id. Since contact ids are always greater than 0, this guarantees that 351 * all items within this adapter will always have unique ids. 352 */ 353 @Override 354 public long getItemId(int position) { 355 return getItem(position).id; 356 } 357 358 @Override 359 public boolean hasStableIds() { 360 return true; 361 } 362 363 @Override 364 public boolean areAllItemsEnabled() { 365 return true; 366 } 367 368 @Override 369 public boolean isEnabled(int position) { 370 return getCount() > 0; 371 } 372 373 @Override 374 public void notifyDataSetChanged() { 375 if (DEBUG) { 376 Log.v(TAG, "notifyDataSetChanged"); 377 } 378 super.notifyDataSetChanged(); 379 } 380 381 @Override 382 public View getView(int position, View convertView, ViewGroup parent) { 383 if (DEBUG) { 384 Log.v(TAG, "get view for " + String.valueOf(position)); 385 } 386 387 int itemViewType = getItemViewType(position); 388 389 PhoneFavoriteTileView tileView = null; 390 391 if (convertView instanceof PhoneFavoriteTileView) { 392 tileView = (PhoneFavoriteTileView) convertView; 393 } 394 395 if (tileView == null) { 396 tileView = (PhoneFavoriteTileView) View.inflate(mContext, 397 R.layout.phone_favorite_tile_view, null); 398 } 399 tileView.setPhotoManager(mPhotoManager); 400 tileView.setListener(mListener); 401 tileView.loadFromContact(getItem(position)); 402 return tileView; 403 } 404 405 @Override 406 public int getViewTypeCount() { 407 return ViewTypes.COUNT; 408 } 409 410 @Override 411 public int getItemViewType(int position) { 412 return ViewTypes.TILE; 413 } 414 415 /** 416 * Temporarily removes a contact from the list for UI refresh. Stores data for this contact 417 * in the back-up variable. 418 * 419 * @param index Position of the contact to be removed. 420 */ 421 public void popContactEntry(int index) { 422 if (isIndexInBound(index)) { 423 mDraggedEntry = mContactEntries.get(index); 424 mDraggedEntryIndex = index; 425 mDragEnteredEntryIndex = index; 426 markDropArea(mDragEnteredEntryIndex); 427 } 428 } 429 430 /** 431 * @param itemIndex Position of the contact in {@link #mContactEntries}. 432 * @return True if the given index is valid for {@link #mContactEntries}. 433 */ 434 public boolean isIndexInBound(int itemIndex) { 435 return itemIndex >= 0 && itemIndex < mContactEntries.size(); 436 } 437 438 /** 439 * Mark the tile as drop area by given the item index in {@link #mContactEntries}. 440 * 441 * @param itemIndex Position of the contact in {@link #mContactEntries}. 442 */ 443 private void markDropArea(int itemIndex) { 444 if (mDraggedEntry != null && isIndexInBound(mDragEnteredEntryIndex) && 445 isIndexInBound(itemIndex)) { 446 mDataSetChangedListener.cacheOffsetsForDatasetChange(); 447 // Remove the old placeholder item and place the new placeholder item. 448 final int oldIndex = mDragEnteredEntryIndex; 449 mContactEntries.remove(mDragEnteredEntryIndex); 450 mDragEnteredEntryIndex = itemIndex; 451 mContactEntries.add(mDragEnteredEntryIndex, ContactEntry.BLANK_ENTRY); 452 ContactEntry.BLANK_ENTRY.id = mDraggedEntry.id; 453 mDataSetChangedListener.onDataSetChangedForAnimation(); 454 notifyDataSetChanged(); 455 } 456 } 457 458 /** 459 * Drops the temporarily removed contact to the desired location in the list. 460 */ 461 public void handleDrop() { 462 boolean changed = false; 463 if (mDraggedEntry != null) { 464 if (isIndexInBound(mDragEnteredEntryIndex) && 465 mDragEnteredEntryIndex != mDraggedEntryIndex) { 466 // Don't add the ContactEntry here (to prevent a double animation from occuring). 467 // When we receive a new cursor the list of contact entries will automatically be 468 // populated with the dragged ContactEntry at the correct spot. 469 mDropEntryIndex = mDragEnteredEntryIndex; 470 mContactEntries.set(mDropEntryIndex, mDraggedEntry); 471 mDataSetChangedListener.cacheOffsetsForDatasetChange(); 472 changed = true; 473 } else if (isIndexInBound(mDraggedEntryIndex)) { 474 // If {@link #mDragEnteredEntryIndex} is invalid, 475 // falls back to the original position of the contact. 476 mContactEntries.remove(mDragEnteredEntryIndex); 477 mContactEntries.add(mDraggedEntryIndex, mDraggedEntry); 478 mDropEntryIndex = mDraggedEntryIndex; 479 notifyDataSetChanged(); 480 } 481 482 if (changed && mDropEntryIndex < PIN_LIMIT) { 483 final ArrayList<ContentProviderOperation> operations = 484 getReflowedPinningOperations(mContactEntries, mDraggedEntryIndex, 485 mDropEntryIndex); 486 if (!operations.isEmpty()) { 487 // update the database here with the new pinned positions 488 try { 489 mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, 490 operations); 491 } catch (RemoteException | OperationApplicationException e) { 492 Log.e(TAG, "Exception thrown when pinning contacts", e); 493 } 494 } 495 } 496 mDraggedEntry = null; 497 } 498 } 499 500 /** 501 * Invoked when the dragged item is dropped to unsupported location. We will then move the 502 * contact back to where it was dragged from. 503 */ 504 public void dropToUnsupportedView() { 505 if (isIndexInBound(mDragEnteredEntryIndex)) { 506 mContactEntries.remove(mDragEnteredEntryIndex); 507 mContactEntries.add(mDraggedEntryIndex, mDraggedEntry); 508 notifyDataSetChanged(); 509 } 510 } 511 512 /** 513 * Clears all temporary variables at a new interaction. 514 */ 515 public void cleanTempVariables() { 516 mDraggedEntryIndex = -1; 517 mDropEntryIndex = -1; 518 mDragEnteredEntryIndex = -1; 519 mDraggedEntry = null; 520 } 521 522 /** 523 * Used when a contact is removed from speeddial. This will both unstar and set pinned position 524 * of the contact to PinnedPosition.DEMOTED so that it doesn't show up anymore in the favorites 525 * list. 526 */ 527 private void unstarAndUnpinContact(Uri contactUri) { 528 final ContentValues values = new ContentValues(2); 529 values.put(Contacts.STARRED, false); 530 values.put(Contacts.PINNED, PinnedPositions.DEMOTED); 531 mContext.getContentResolver().update(contactUri, values, null, null); 532 } 533 534 /** 535 * Given a list of contacts that each have pinned positions, rearrange the list (destructive) 536 * such that all pinned contacts are in their defined pinned positions, and unpinned contacts 537 * take the spaces between those pinned contacts. Demoted contacts should not appear in the 538 * resulting list. 539 * 540 * This method also updates the pinned positions of pinned contacts so that they are all 541 * unique positive integers within range from 0 to toArrange.size() - 1. This is because 542 * when the contact entries are read from the database, it is possible for them to have 543 * overlapping pin positions due to sync or modifications by third party apps. 544 */ 545 @VisibleForTesting 546 /* package */ void arrangeContactsByPinnedPosition(ArrayList<ContactEntry> toArrange) { 547 final PriorityQueue<ContactEntry> pinnedQueue = 548 new PriorityQueue<ContactEntry>(PIN_LIMIT, mContactEntryComparator); 549 550 final List<ContactEntry> unpinnedContacts = new LinkedList<ContactEntry>(); 551 552 final int length = toArrange.size(); 553 for (int i = 0; i < length; i++) { 554 final ContactEntry contact = toArrange.get(i); 555 // Decide whether the contact is hidden(demoted), pinned, or unpinned 556 if (contact.pinned > PIN_LIMIT || contact.pinned == PinnedPositions.UNPINNED) { 557 unpinnedContacts.add(contact); 558 } else if (contact.pinned > PinnedPositions.DEMOTED) { 559 // Demoted or contacts with negative pinned positions are ignored. 560 // Pinned contacts go into a priority queue where they are ranked by pinned 561 // position. This is required because the contacts provider does not return 562 // contacts ordered by pinned position. 563 pinnedQueue.add(contact); 564 } 565 } 566 567 final int maxToPin = Math.min(PIN_LIMIT, pinnedQueue.size() + unpinnedContacts.size()); 568 569 toArrange.clear(); 570 for (int i = 1; i < maxToPin + 1; i++) { 571 if (!pinnedQueue.isEmpty() && pinnedQueue.peek().pinned <= i) { 572 final ContactEntry toPin = pinnedQueue.poll(); 573 toPin.pinned = i; 574 toArrange.add(toPin); 575 } else if (!unpinnedContacts.isEmpty()) { 576 toArrange.add(unpinnedContacts.remove(0)); 577 } 578 } 579 580 // If there are still contacts in pinnedContacts at this point, it means that the pinned 581 // positions of these pinned contacts exceed the actual number of contacts in the list. 582 // For example, the user had 10 frequents, starred and pinned one of them at the last spot, 583 // and then cleared frequents. Contacts in this situation should become unpinned. 584 while (!pinnedQueue.isEmpty()) { 585 final ContactEntry entry = pinnedQueue.poll(); 586 entry.pinned = PinnedPositions.UNPINNED; 587 toArrange.add(entry); 588 } 589 590 // Any remaining unpinned contacts that weren't in the gaps between the pinned contacts 591 // now just get appended to the end of the list. 592 toArrange.addAll(unpinnedContacts); 593 } 594 595 /** 596 * Given an existing list of contact entries and a single entry that is to be pinned at a 597 * particular position, return a list of {@link ContentProviderOperation}s that contains new 598 * pinned positions for all contacts that are forced to be pinned at new positions, trying as 599 * much as possible to keep pinned contacts at their original location. 600 * 601 * At this point in time the pinned position of each contact in the list has already been 602 * updated by {@link #arrangeContactsByPinnedPosition}, so we can assume that all pinned 603 * positions(within {@link #PIN_LIMIT} are unique positive integers. 604 */ 605 @VisibleForTesting 606 /* package */ ArrayList<ContentProviderOperation> getReflowedPinningOperations( 607 ArrayList<ContactEntry> list, int oldPos, int newPinPos) { 608 final ArrayList<ContentProviderOperation> positions = Lists.newArrayList(); 609 final int lowerBound = Math.min(oldPos, newPinPos); 610 final int upperBound = Math.max(oldPos, newPinPos); 611 for (int i = lowerBound; i <= upperBound; i++) { 612 final ContactEntry entry = list.get(i); 613 614 // Pinned positions in the database start from 1 instead of being zero-indexed like 615 // arrays, so offset by 1. 616 final int databasePinnedPosition = i + 1; 617 if (entry.pinned == databasePinnedPosition) continue; 618 619 final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_URI, String.valueOf(entry.id)); 620 final ContentValues values = new ContentValues(); 621 values.put(Contacts.PINNED, databasePinnedPosition); 622 positions.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); 623 } 624 return positions; 625 } 626 627 protected static class ViewTypes { 628 public static final int TILE = 0; 629 public static final int COUNT = 1; 630 } 631 632 @Override 633 public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) { 634 setInDragging(true); 635 final int itemIndex = mContactEntries.indexOf(view.getContactEntry()); 636 popContactEntry(itemIndex); 637 } 638 639 @Override 640 public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) { 641 if (view == null) { 642 // The user is hovering over a view that is not a contact tile, no need to do 643 // anything here. 644 return; 645 } 646 final int itemIndex = mContactEntries.indexOf(view.getContactEntry()); 647 if (mInDragging && 648 mDragEnteredEntryIndex != itemIndex && 649 isIndexInBound(itemIndex) && 650 itemIndex < PIN_LIMIT && 651 itemIndex >= 0) { 652 markDropArea(itemIndex); 653 } 654 } 655 656 @Override 657 public void onDragFinished(int x, int y) { 658 setInDragging(false); 659 // A contact has been dragged to the RemoveView in order to be unstarred, so simply wait 660 // for the new contact cursor which will cause the UI to be refreshed without the unstarred 661 // contact. 662 if (!mAwaitingRemove) { 663 handleDrop(); 664 } 665 } 666 667 @Override 668 public void onDroppedOnRemove() { 669 if (mDraggedEntry != null) { 670 unstarAndUnpinContact(mDraggedEntry.lookupUri); 671 mAwaitingRemove = true; 672 } 673 } 674 } 675