Home | History | Annotate | Download | only in list
      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 {
    143         public void onDataSetChangedForAnimation(long... idsInPlace);
    144         public void cacheOffsetsForDatasetChange();
    145     };
    146 
    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 
    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      */
    170     public void setInDragging(boolean inDragging) {
    171         mDelayCursorUpdates = inDragging;
    172         mInDragging = inDragging;
    173     }
    174 
    175     /** Gets whether the drag is in process. */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    346     public int getNumFrequents() {
    347         return mNumFrequents;
    348     }
    349 
    350     @Override
    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
    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
    376     public long getItemId(int position) {
    377         return getItem(position).id;
    378     }
    379 
    380     @Override
    381     public boolean hasStableIds() {
    382         return true;
    383     }
    384 
    385     @Override
    386     public boolean areAllItemsEnabled() {
    387         return true;
    388     }
    389 
    390     @Override
    391     public boolean isEnabled(int position) {
    392         return getCount() > 0;
    393     }
    394 
    395     @Override
    396     public void notifyDataSetChanged() {
    397         if (DEBUG) {
    398             Log.v(TAG, "notifyDataSetChanged");
    399         }
    400         super.notifyDataSetChanged();
    401     }
    402 
    403     @Override
    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
    428     public int getViewTypeCount() {
    429         return ViewTypes.COUNT;
    430     }
    431 
    432     @Override
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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
    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
    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
    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
    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
    690     public void onDroppedOnRemove() {
    691         if (mDraggedEntry != null) {
    692             unstarAndUnpinContact(mDraggedEntry.lookupUri);
    693             mAwaitingRemove = true;
    694         }
    695     }
    696 }
    697