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 private 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 (isIndexInBound(mDragEnteredEntryIndex) && isIndexInBound(itemIndex)) { 445 mDataSetChangedListener.cacheOffsetsForDatasetChange(); 446 // Remove the old placeholder item and place the new placeholder item. 447 final int oldIndex = mDragEnteredEntryIndex; 448 mContactEntries.remove(mDragEnteredEntryIndex); 449 mDragEnteredEntryIndex = itemIndex; 450 mContactEntries.add(mDragEnteredEntryIndex, ContactEntry.BLANK_ENTRY); 451 ContactEntry.BLANK_ENTRY.id = mDraggedEntry.id; 452 mDataSetChangedListener.onDataSetChangedForAnimation(); 453 notifyDataSetChanged(); 454 } 455 } 456 457 /** 458 * Drops the temporarily removed contact to the desired location in the list. 459 */ 460 public void handleDrop() { 461 boolean changed = false; 462 if (mDraggedEntry != null) { 463 if (isIndexInBound(mDragEnteredEntryIndex) && 464 mDragEnteredEntryIndex != mDraggedEntryIndex) { 465 // Don't add the ContactEntry here (to prevent a double animation from occuring). 466 // When we receive a new cursor the list of contact entries will automatically be 467 // populated with the dragged ContactEntry at the correct spot. 468 mDropEntryIndex = mDragEnteredEntryIndex; 469 mContactEntries.set(mDropEntryIndex, mDraggedEntry); 470 mDataSetChangedListener.cacheOffsetsForDatasetChange(); 471 changed = true; 472 } else if (isIndexInBound(mDraggedEntryIndex)) { 473 // If {@link #mDragEnteredEntryIndex} is invalid, 474 // falls back to the original position of the contact. 475 mContactEntries.remove(mDragEnteredEntryIndex); 476 mContactEntries.add(mDraggedEntryIndex, mDraggedEntry); 477 mDropEntryIndex = mDraggedEntryIndex; 478 notifyDataSetChanged(); 479 } 480 481 if (changed && mDropEntryIndex < PIN_LIMIT) { 482 final ArrayList<ContentProviderOperation> operations = 483 getReflowedPinningOperations(mContactEntries, mDraggedEntryIndex, 484 mDropEntryIndex); 485 if (!operations.isEmpty()) { 486 // update the database here with the new pinned positions 487 try { 488 mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, 489 operations); 490 } catch (RemoteException | OperationApplicationException e) { 491 Log.e(TAG, "Exception thrown when pinning contacts", e); 492 } 493 } 494 } 495 mDraggedEntry = null; 496 } 497 } 498 499 /** 500 * Invoked when the dragged item is dropped to unsupported location. We will then move the 501 * contact back to where it was dragged from. 502 */ 503 public void dropToUnsupportedView() { 504 if (isIndexInBound(mDragEnteredEntryIndex)) { 505 mContactEntries.remove(mDragEnteredEntryIndex); 506 mContactEntries.add(mDraggedEntryIndex, mDraggedEntry); 507 notifyDataSetChanged(); 508 } 509 } 510 511 /** 512 * Clears all temporary variables at a new interaction. 513 */ 514 public void cleanTempVariables() { 515 mDraggedEntryIndex = -1; 516 mDropEntryIndex = -1; 517 mDragEnteredEntryIndex = -1; 518 mDraggedEntry = null; 519 } 520 521 /** 522 * Used when a contact is removed from speeddial. This will both unstar and set pinned position 523 * of the contact to PinnedPosition.DEMOTED so that it doesn't show up anymore in the favorites 524 * list. 525 */ 526 private void unstarAndUnpinContact(Uri contactUri) { 527 final ContentValues values = new ContentValues(2); 528 values.put(Contacts.STARRED, false); 529 values.put(Contacts.PINNED, PinnedPositions.DEMOTED); 530 mContext.getContentResolver().update(contactUri, values, null, null); 531 } 532 533 /** 534 * Given a list of contacts that each have pinned positions, rearrange the list (destructive) 535 * such that all pinned contacts are in their defined pinned positions, and unpinned contacts 536 * take the spaces between those pinned contacts. Demoted contacts should not appear in the 537 * resulting list. 538 * 539 * This method also updates the pinned positions of pinned contacts so that they are all 540 * unique positive integers within range from 0 to toArrange.size() - 1. This is because 541 * when the contact entries are read from the database, it is possible for them to have 542 * overlapping pin positions due to sync or modifications by third party apps. 543 */ 544 @VisibleForTesting 545 /* package */ void arrangeContactsByPinnedPosition(ArrayList<ContactEntry> toArrange) { 546 final PriorityQueue<ContactEntry> pinnedQueue = 547 new PriorityQueue<ContactEntry>(PIN_LIMIT, mContactEntryComparator); 548 549 final List<ContactEntry> unpinnedContacts = new LinkedList<ContactEntry>(); 550 551 final int length = toArrange.size(); 552 for (int i = 0; i < length; i++) { 553 final ContactEntry contact = toArrange.get(i); 554 // Decide whether the contact is hidden(demoted), pinned, or unpinned 555 if (contact.pinned > PIN_LIMIT || contact.pinned == PinnedPositions.UNPINNED) { 556 unpinnedContacts.add(contact); 557 } else if (contact.pinned > PinnedPositions.DEMOTED) { 558 // Demoted or contacts with negative pinned positions are ignored. 559 // Pinned contacts go into a priority queue where they are ranked by pinned 560 // position. This is required because the contacts provider does not return 561 // contacts ordered by pinned position. 562 pinnedQueue.add(contact); 563 } 564 } 565 566 final int maxToPin = Math.min(PIN_LIMIT, pinnedQueue.size() + unpinnedContacts.size()); 567 568 toArrange.clear(); 569 for (int i = 1; i < maxToPin + 1; i++) { 570 if (!pinnedQueue.isEmpty() && pinnedQueue.peek().pinned <= i) { 571 final ContactEntry toPin = pinnedQueue.poll(); 572 toPin.pinned = i; 573 toArrange.add(toPin); 574 } else if (!unpinnedContacts.isEmpty()) { 575 toArrange.add(unpinnedContacts.remove(0)); 576 } 577 } 578 579 // If there are still contacts in pinnedContacts at this point, it means that the pinned 580 // positions of these pinned contacts exceed the actual number of contacts in the list. 581 // For example, the user had 10 frequents, starred and pinned one of them at the last spot, 582 // and then cleared frequents. Contacts in this situation should become unpinned. 583 while (!pinnedQueue.isEmpty()) { 584 final ContactEntry entry = pinnedQueue.poll(); 585 entry.pinned = PinnedPositions.UNPINNED; 586 toArrange.add(entry); 587 } 588 589 // Any remaining unpinned contacts that weren't in the gaps between the pinned contacts 590 // now just get appended to the end of the list. 591 toArrange.addAll(unpinnedContacts); 592 } 593 594 /** 595 * Given an existing list of contact entries and a single entry that is to be pinned at a 596 * particular position, return a list of {@link ContentProviderOperation}s that contains new 597 * pinned positions for all contacts that are forced to be pinned at new positions, trying as 598 * much as possible to keep pinned contacts at their original location. 599 * 600 * At this point in time the pinned position of each contact in the list has already been 601 * updated by {@link #arrangeContactsByPinnedPosition}, so we can assume that all pinned 602 * positions(within {@link #PIN_LIMIT} are unique positive integers. 603 */ 604 @VisibleForTesting 605 /* package */ ArrayList<ContentProviderOperation> getReflowedPinningOperations( 606 ArrayList<ContactEntry> list, int oldPos, int newPinPos) { 607 final ArrayList<ContentProviderOperation> positions = Lists.newArrayList(); 608 final int lowerBound = Math.min(oldPos, newPinPos); 609 final int upperBound = Math.max(oldPos, newPinPos); 610 for (int i = lowerBound; i <= upperBound; i++) { 611 final ContactEntry entry = list.get(i); 612 613 // Pinned positions in the database start from 1 instead of being zero-indexed like 614 // arrays, so offset by 1. 615 final int databasePinnedPosition = i + 1; 616 if (entry.pinned == databasePinnedPosition) continue; 617 618 final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_URI, String.valueOf(entry.id)); 619 final ContentValues values = new ContentValues(); 620 values.put(Contacts.PINNED, databasePinnedPosition); 621 positions.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); 622 } 623 return positions; 624 } 625 626 protected static class ViewTypes { 627 public static final int TILE = 0; 628 public static final int COUNT = 1; 629 } 630 631 @Override 632 public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) { 633 setInDragging(true); 634 final int itemIndex = mContactEntries.indexOf(view.getContactEntry()); 635 popContactEntry(itemIndex); 636 } 637 638 @Override 639 public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) { 640 if (view == null) { 641 // The user is hovering over a view that is not a contact tile, no need to do 642 // anything here. 643 return; 644 } 645 final int itemIndex = mContactEntries.indexOf(view.getContactEntry()); 646 if (mInDragging && 647 mDragEnteredEntryIndex != itemIndex && 648 isIndexInBound(itemIndex) && 649 itemIndex < PIN_LIMIT && 650 itemIndex >= 0) { 651 markDropArea(itemIndex); 652 } 653 } 654 655 @Override 656 public void onDragFinished(int x, int y) { 657 setInDragging(false); 658 // A contact has been dragged to the RemoveView in order to be unstarred, so simply wait 659 // for the new contact cursor which will cause the UI to be refreshed without the unstarred 660 // contact. 661 if (!mAwaitingRemove) { 662 handleDrop(); 663 } 664 } 665 666 @Override 667 public void onDroppedOnRemove() { 668 if (mDraggedEntry != null) { 669 unstarAndUnpinContact(mDraggedEntry.lookupUri); 670 mAwaitingRemove = true; 671 } 672 } 673 } 674