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