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