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