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