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 
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.res.Resources;
     25 import android.database.Cursor;
     26 import android.net.Uri;
     27 import android.provider.ContactsContract.CommonDataKinds.Phone;
     28 import android.provider.ContactsContract.Contacts;
     29 import android.provider.ContactsContract.PinnedPositions;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 import android.util.LongSparseArray;
     33 import android.view.MotionEvent;
     34 import android.view.View;
     35 import android.view.ViewConfiguration;
     36 import android.view.ViewGroup;
     37 import android.widget.BaseAdapter;
     38 import android.widget.FrameLayout;
     39 
     40 import com.android.contacts.common.ContactPhotoManager;
     41 import com.android.contacts.common.ContactTileLoaderFactory;
     42 import com.android.contacts.common.R;
     43 import com.android.contacts.common.list.ContactEntry;
     44 import com.android.contacts.common.list.ContactTileAdapter.DisplayType;
     45 import com.android.contacts.common.list.ContactTileView;
     46 import com.android.dialer.list.SwipeHelper.OnItemGestureListener;
     47 import com.android.dialer.list.SwipeHelper.SwipeHelperCallback;
     48 
     49 import java.util.ArrayList;
     50 import java.util.Comparator;
     51 import java.util.LinkedList;
     52 import java.util.List;
     53 import java.util.PriorityQueue;
     54 
     55 /**
     56  * Also allows for a configurable number of columns as well as a maximum row of tiled contacts.
     57  *
     58  * This adapter has been rewritten to only support a maximum of one row for favorites.
     59  *
     60  */
     61 public class PhoneFavoritesTileAdapter extends BaseAdapter implements
     62         SwipeHelper.OnItemGestureListener, OnDragDropListener {
     63     private static final String TAG = PhoneFavoritesTileAdapter.class.getSimpleName();
     64     private static final boolean DEBUG = false;
     65 
     66     public static final int NO_ROW_LIMIT = -1;
     67 
     68     public static final int ROW_LIMIT_DEFAULT = NO_ROW_LIMIT;
     69 
     70     private ContactTileView.Listener mListener;
     71     private OnDataSetChangedForAnimationListener mDataSetChangedListener;
     72 
     73     private Context mContext;
     74     private Resources mResources;
     75 
     76     /** Contact data stored in cache. This is used to populate the associated view. */
     77     protected ArrayList<ContactEntry> mContactEntries = null;
     78     /** Back up of the temporarily removed Contact during dragging. */
     79     private ContactEntry mDraggedEntry = null;
     80     /** Position of the temporarily removed contact in the cache. */
     81     private int mDraggedEntryIndex = -1;
     82     /** New position of the temporarily removed contact in the cache. */
     83     private int mDropEntryIndex = -1;
     84     /** New position of the temporarily entered contact in the cache. */
     85     private int mDragEnteredEntryIndex = -1;
     86     /** Position of the contact pending removal. */
     87     private int mPotentialRemoveEntryIndex = -1;
     88     private long mIdToKeepInPlace = -1;
     89 
     90     private boolean mAwaitingRemove = false;
     91     private boolean mDelayCursorUpdates = false;
     92 
     93     private ContactPhotoManager mPhotoManager;
     94     protected int mNumFrequents;
     95     protected int mNumStarred;
     96 
     97     protected int mColumnCount;
     98     private int mMaxTiledRows = ROW_LIMIT_DEFAULT;
     99     private int mStarredIndex;
    100 
    101     protected int mIdIndex;
    102     protected int mLookupIndex;
    103     protected int mPhotoUriIndex;
    104     protected int mNameIndex;
    105     protected int mPresenceIndex;
    106     protected int mStatusIndex;
    107 
    108     private int mPhoneNumberIndex;
    109     private int mPhoneNumberTypeIndex;
    110     private int mPhoneNumberLabelIndex;
    111     private int mIsDefaultNumberIndex;
    112     protected int mPinnedIndex;
    113     protected int mContactIdIndex;
    114 
    115     private final int mPaddingInPixels;
    116 
    117     /** Indicates whether a drag is in process. */
    118     private boolean mInDragging = false;
    119 
    120     public static final int PIN_LIMIT = 20;
    121 
    122     /**
    123      * The soft limit on how many contact tiles to show.
    124      * NOTE This soft limit would not restrict the number of starred contacts to show, rather
    125      * 1. If the count of starred contacts is less than this limit, show 20 tiles total.
    126      * 2. If the count of starred contacts is more than or equal to this limit,
    127      * show all starred tiles and no frequents.
    128      */
    129     private static final int TILES_SOFT_LIMIT = 20;
    130 
    131     final Comparator<ContactEntry> mContactEntryComparator = new Comparator<ContactEntry>() {
    132         @Override
    133         public int compare(ContactEntry lhs, ContactEntry rhs) {
    134             return ComparisonChain.start()
    135                     .compare(lhs.pinned, rhs.pinned)
    136                     .compare(lhs.name, rhs.name)
    137                     .result();
    138         }
    139     };
    140 
    141     public interface OnDataSetChangedForAnimationListener {
    142         public void onDataSetChangedForAnimation(long... idsInPlace);
    143         public void cacheOffsetsForDatasetChange();
    144     };
    145 
    146     public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener,
    147             OnDataSetChangedForAnimationListener dataSetChangedListener,
    148             int numCols, int maxTiledRows) {
    149         mDataSetChangedListener = dataSetChangedListener;
    150         mListener = listener;
    151         mContext = context;
    152         mResources = context.getResources();
    153         mColumnCount = numCols;
    154         mNumFrequents = 0;
    155         mMaxTiledRows = maxTiledRows;
    156         mContactEntries = new ArrayList<ContactEntry>();
    157         // Converting padding in dips to padding in pixels
    158         mPaddingInPixels = mContext.getResources()
    159                 .getDimensionPixelSize(R.dimen.contact_tile_divider_width);
    160 
    161         bindColumnIndices();
    162     }
    163 
    164     public void setPhotoLoader(ContactPhotoManager photoLoader) {
    165         mPhotoManager = photoLoader;
    166     }
    167 
    168     public void setMaxRowCount(int maxRows) {
    169         mMaxTiledRows = maxRows;
    170     }
    171 
    172     public void setColumnCount(int columnCount) {
    173         mColumnCount = columnCount;
    174     }
    175 
    176     /**
    177      * Indicates whether a drag is in process.
    178      *
    179      * @param inDragging Boolean variable indicating whether there is a drag in process.
    180      */
    181     public void setInDragging(boolean inDragging) {
    182         mDelayCursorUpdates = inDragging;
    183         mInDragging = inDragging;
    184     }
    185 
    186     /** Gets whether the drag is in process. */
    187     public boolean getInDragging() {
    188         return mInDragging;
    189     }
    190 
    191     /**
    192      * Sets the column indices for expected {@link Cursor}
    193      * based on {@link DisplayType}.
    194      */
    195     protected void bindColumnIndices() {
    196         mIdIndex = ContactTileLoaderFactory.CONTACT_ID;
    197         mLookupIndex = ContactTileLoaderFactory.LOOKUP_KEY;
    198         mPhotoUriIndex = ContactTileLoaderFactory.PHOTO_URI;
    199         mNameIndex = ContactTileLoaderFactory.DISPLAY_NAME;
    200         mStarredIndex = ContactTileLoaderFactory.STARRED;
    201         mPresenceIndex = ContactTileLoaderFactory.CONTACT_PRESENCE;
    202         mStatusIndex = ContactTileLoaderFactory.CONTACT_STATUS;
    203 
    204         mPhoneNumberIndex = ContactTileLoaderFactory.PHONE_NUMBER;
    205         mPhoneNumberTypeIndex = ContactTileLoaderFactory.PHONE_NUMBER_TYPE;
    206         mPhoneNumberLabelIndex = ContactTileLoaderFactory.PHONE_NUMBER_LABEL;
    207         mIsDefaultNumberIndex = ContactTileLoaderFactory.IS_DEFAULT_NUMBER;
    208         mPinnedIndex = ContactTileLoaderFactory.PINNED;
    209         mContactIdIndex = ContactTileLoaderFactory.CONTACT_ID_FOR_DATA;
    210     }
    211 
    212     /**
    213      * Gets the number of frequents from the passed in cursor.
    214      *
    215      * This methods is needed so the GroupMemberTileAdapter can override this.
    216      *
    217      * @param cursor The cursor to get number of frequents from.
    218      */
    219     protected void saveNumFrequentsFromCursor(Cursor cursor) {
    220         mNumFrequents = cursor.getCount() - mNumStarred;
    221     }
    222 
    223     /**
    224      * Creates {@link ContactTileView}s for each item in {@link Cursor}.
    225      *
    226      * Else use {@link ContactTileLoaderFactory}
    227      */
    228     public void setContactCursor(Cursor cursor) {
    229         if (!mDelayCursorUpdates && cursor != null && !cursor.isClosed()) {
    230             mNumStarred = getNumStarredContacts(cursor);
    231             if (mAwaitingRemove) {
    232                 mDataSetChangedListener.cacheOffsetsForDatasetChange();
    233             }
    234 
    235             saveNumFrequentsFromCursor(cursor);
    236             saveCursorToCache(cursor);
    237             // cause a refresh of any views that rely on this data
    238             notifyDataSetChanged();
    239             // about to start redraw
    240             if (mIdToKeepInPlace != -1) {
    241                 mDataSetChangedListener.onDataSetChangedForAnimation(mIdToKeepInPlace);
    242             } else {
    243                 mDataSetChangedListener.onDataSetChangedForAnimation();
    244             }
    245             mIdToKeepInPlace = -1;
    246         }
    247     }
    248 
    249     /**
    250      * Saves the cursor data to the cache, to speed up UI changes.
    251      *
    252      * @param cursor Returned cursor with data to populate the view.
    253      */
    254     private void saveCursorToCache(Cursor cursor) {
    255         mContactEntries.clear();
    256 
    257         cursor.moveToPosition(-1);
    258 
    259         final LongSparseArray<Object> duplicates = new LongSparseArray<Object>(cursor.getCount());
    260 
    261         // Track the length of {@link #mContactEntries} and compare to {@link #TILES_SOFT_LIMIT}.
    262         int counter = 0;
    263 
    264         while (cursor.moveToNext()) {
    265 
    266             final int starred = cursor.getInt(mStarredIndex);
    267             final long id;
    268 
    269             // We display a maximum of TILES_SOFT_LIMIT contacts, or the total number of starred
    270             // whichever is greater.
    271             if (starred < 1 && counter >= TILES_SOFT_LIMIT) {
    272                 break;
    273             } else {
    274                 id = cursor.getLong(mContactIdIndex);
    275             }
    276 
    277             final ContactEntry existing = (ContactEntry) duplicates.get(id);
    278             if (existing != null) {
    279                 // Check if the existing number is a default number. If not, clear the phone number
    280                 // and label fields so that the disambiguation dialog will show up.
    281                 if (!existing.isDefaultNumber) {
    282                     existing.phoneLabel = null;
    283                     existing.phoneNumber = null;
    284                 }
    285                 continue;
    286             }
    287 
    288             final String photoUri = cursor.getString(mPhotoUriIndex);
    289             final String lookupKey = cursor.getString(mLookupIndex);
    290             final int pinned = cursor.getInt(mPinnedIndex);
    291             final String name = cursor.getString(mNameIndex);
    292             final boolean isStarred = cursor.getInt(mStarredIndex) > 0;
    293             final boolean isDefaultNumber = cursor.getInt(mIsDefaultNumberIndex) > 0;
    294 
    295             final ContactEntry contact = new ContactEntry();
    296 
    297             contact.id = id;
    298             contact.name = (!TextUtils.isEmpty(name)) ? name :
    299                     mResources.getString(R.string.missing_name);
    300             contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null);
    301             contact.lookupKey = lookupKey;
    302             contact.lookupUri = ContentUris.withAppendedId(
    303                     Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id);
    304             contact.isFavorite = isStarred;
    305             contact.isDefaultNumber = isDefaultNumber;
    306 
    307             // Set phone number and label
    308             final int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex);
    309             final String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex);
    310             contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType,
    311                     phoneNumberCustomLabel);
    312             contact.phoneNumber = cursor.getString(mPhoneNumberIndex);
    313 
    314             contact.pinned = pinned;
    315             mContactEntries.add(contact);
    316 
    317             duplicates.put(id, contact);
    318 
    319             counter++;
    320         }
    321 
    322         mAwaitingRemove = false;
    323 
    324         arrangeContactsByPinnedPosition(mContactEntries);
    325 
    326         notifyDataSetChanged();
    327     }
    328 
    329     /**
    330      * Iterates over the {@link Cursor}
    331      * Returns position of the first NON Starred Contact
    332      * Returns -1 if {@link DisplayType#STARRED_ONLY}
    333      * Returns 0 if {@link DisplayType#FREQUENT_ONLY}
    334      */
    335     protected int getNumStarredContacts(Cursor cursor) {
    336         cursor.moveToPosition(-1);
    337         while (cursor.moveToNext()) {
    338             if (cursor.getInt(mStarredIndex) == 0) {
    339                 return cursor.getPosition();
    340             }
    341         }
    342 
    343         // There are not NON Starred contacts in cursor
    344         // Set divider positon to end
    345         return cursor.getCount();
    346     }
    347 
    348     /**
    349      * Loads a contact from the cached list.
    350      *
    351      * @param position Position of the Contact.
    352      * @return Contact at the requested position.
    353      */
    354     protected ContactEntry getContactEntryFromCache(int position) {
    355         if (mContactEntries.size() <= position) return null;
    356         return mContactEntries.get(position);
    357     }
    358 
    359     /**
    360      * Returns the number of frequents that will be displayed in the list.
    361      */
    362     public int getNumFrequents() {
    363         return mNumFrequents;
    364     }
    365 
    366     @Override
    367     public int getCount() {
    368         if (mContactEntries == null || mContactEntries.isEmpty()) {
    369             return 0;
    370         }
    371 
    372         int total = mContactEntries.size();
    373         // The number of contacts that don't show up as tiles
    374         final int nonTiledRows = Math.max(0, total - getMaxContactsInTiles());
    375         // The number of tiled rows
    376         final int tiledRows = getRowCount(total - nonTiledRows);
    377         return nonTiledRows + tiledRows;
    378     }
    379 
    380     public int getMaxTiledRows() {
    381         return mMaxTiledRows;
    382     }
    383 
    384     /**
    385      * Returns the number of rows required to show the provided number of entries
    386      * with the current number of columns.
    387      */
    388     protected int getRowCount(int entryCount) {
    389         if (entryCount == 0) return 0;
    390         final int nonLimitedRows = ((entryCount - 1) / mColumnCount) + 1;
    391         if (mMaxTiledRows == NO_ROW_LIMIT) {
    392             return nonLimitedRows;
    393         }
    394         return Math.min(mMaxTiledRows, nonLimitedRows);
    395     }
    396 
    397     private int getMaxContactsInTiles() {
    398         if (mMaxTiledRows == NO_ROW_LIMIT) {
    399             return Integer.MAX_VALUE;
    400         }
    401         return mColumnCount * mMaxTiledRows;
    402     }
    403 
    404     public int getRowIndex(int entryIndex) {
    405         if (entryIndex < getMaxContactsInTiles()) {
    406             return entryIndex / mColumnCount;
    407         } else {
    408             return entryIndex - mMaxTiledRows * (mColumnCount + 1);
    409         }
    410     }
    411 
    412     public int getColumnCount() {
    413         return mColumnCount;
    414     }
    415 
    416     /**
    417      * Returns an ArrayList of the {@link ContactEntry}s that are to appear
    418      * on the row for the given position.
    419      */
    420     @Override
    421     public ArrayList<ContactEntry> getItem(int position) {
    422         ArrayList<ContactEntry> resultList = new ArrayList<ContactEntry>(mColumnCount);
    423 
    424         final int entryIndex = getFirstContactEntryIndexForPosition(position);
    425 
    426         final int viewType = getItemViewType(position);
    427 
    428         final int columnCount;
    429         if (viewType == ViewTypes.TOP) {
    430             columnCount = mColumnCount;
    431         } else {
    432             columnCount = 1;
    433         }
    434 
    435         for (int i = 0; i < columnCount; i++) {
    436             final ContactEntry entry = getContactEntryFromCache(entryIndex + i);
    437             if (entry == null) break; // less than mColumnCount contacts
    438             resultList.add(entry);
    439         }
    440 
    441         return resultList;
    442     }
    443 
    444     /*
    445      * Given a position in the adapter, returns the index of the first contact entry that is to be
    446      * in that row.
    447      */
    448     private int getFirstContactEntryIndexForPosition(int position) {
    449         final int maxContactsInTiles = getMaxContactsInTiles();
    450         if (position < getRowCount(maxContactsInTiles)) {
    451             // Contacts that appear as tiles
    452             return position * mColumnCount;
    453         } else {
    454             // Contacts that appear as rows
    455             // The actual position of the contact in the cursor is simply total the number of
    456             // tiled contacts + the given position
    457             return maxContactsInTiles + position - mMaxTiledRows;
    458         }
    459     }
    460 
    461     /**
    462      * For the top row of tiled contacts, the item id is the position of the row of
    463      * contacts.
    464      * For frequent contacts, the item id is the maximum number of rows of tiled contacts +
    465      * the actual contact id. Since contact ids are always greater than 0, this guarantees that
    466      * all items within this adapter will always have unique ids.
    467      */
    468     @Override
    469     public long getItemId(int position) {
    470         if (getItemViewType(position) == ViewTypes.FREQUENT) {
    471             return getAdjustedItemId(getItem(position).get(0).id);
    472         } else {
    473             return position;
    474         }
    475     }
    476 
    477     /**
    478      * Calculates the stable itemId for a particular entry based on the entry's contact ID. This
    479      * stable itemId is used for animation purposes.
    480      */
    481     public long getAdjustedItemId(long id) {
    482         if (mMaxTiledRows == NO_ROW_LIMIT) {
    483             return id;
    484         }
    485         return mMaxTiledRows + id;
    486     }
    487 
    488     @Override
    489     public boolean hasStableIds() {
    490         return true;
    491     }
    492 
    493     @Override
    494     public boolean areAllItemsEnabled() {
    495         // No dividers, so all items are enabled.
    496         return true;
    497     }
    498 
    499     @Override
    500     public boolean isEnabled(int position) {
    501         return getCount() > 0;
    502     }
    503 
    504     @Override
    505     public void notifyDataSetChanged() {
    506         if (DEBUG) {
    507             Log.v(TAG, "notifyDataSetChanged");
    508         }
    509         super.notifyDataSetChanged();
    510     }
    511 
    512     @Override
    513     public View getView(int position, View convertView, ViewGroup parent) {
    514         if (DEBUG) {
    515             Log.v(TAG, "get view for " + String.valueOf(position));
    516         }
    517 
    518         int itemViewType = getItemViewType(position);
    519 
    520         ContactTileRow contactTileRowView = null;
    521 
    522         if (convertView instanceof  ContactTileRow) {
    523             contactTileRowView  = (ContactTileRow) convertView;
    524         }
    525 
    526         ArrayList<ContactEntry> contactList = getItem(position);
    527 
    528         if (contactTileRowView == null) {
    529             // Creating new row if needed
    530             contactTileRowView = new ContactTileRow(mContext, itemViewType, position);
    531         }
    532 
    533         contactTileRowView.configureRow(contactList, position, position == getCount() - 1);
    534 
    535         return contactTileRowView;
    536     }
    537 
    538     private int getLayoutResourceId(int viewType) {
    539         switch (viewType) {
    540             case ViewTypes.FREQUENT:
    541                 return R.layout.phone_favorite_regular_row_view;
    542             case ViewTypes.TOP:
    543                 return R.layout.phone_favorite_tile_view;
    544             default:
    545                 throw new IllegalArgumentException("Unrecognized viewType " + viewType);
    546         }
    547     }
    548     @Override
    549     public int getViewTypeCount() {
    550         return ViewTypes.COUNT;
    551     }
    552 
    553     @Override
    554     public int getItemViewType(int position) {
    555         if (position < getMaxContactsInTiles()) {
    556             return ViewTypes.TOP;
    557         } else {
    558             return ViewTypes.FREQUENT;
    559         }
    560     }
    561 
    562     /**
    563      * Temporarily removes a contact from the list for UI refresh. Stores data for this contact
    564      * in the back-up variable.
    565      *
    566      * @param index Position of the contact to be removed.
    567      */
    568     public void popContactEntry(int index) {
    569         if (isIndexInBound(index)) {
    570             mDraggedEntry = mContactEntries.get(index);
    571             mDraggedEntryIndex = index;
    572             mDragEnteredEntryIndex = index;
    573             markDropArea(mDragEnteredEntryIndex);
    574         }
    575     }
    576 
    577     /**
    578      * @param itemIndex Position of the contact in {@link #mContactEntries}.
    579      * @return True if the given index is valid for {@link #mContactEntries}.
    580      */
    581     private boolean isIndexInBound(int itemIndex) {
    582         return itemIndex >= 0 && itemIndex < mContactEntries.size();
    583     }
    584 
    585     /**
    586      * Mark the tile as drop area by given the item index in {@link #mContactEntries}.
    587      *
    588      * @param itemIndex Position of the contact in {@link #mContactEntries}.
    589      */
    590     private void markDropArea(int itemIndex) {
    591         if (isIndexInBound(mDragEnteredEntryIndex) && isIndexInBound(itemIndex)) {
    592             mDataSetChangedListener.cacheOffsetsForDatasetChange();
    593             // Remove the old placeholder item and place the new placeholder item.
    594             final int oldIndex = mDragEnteredEntryIndex;
    595             mContactEntries.remove(mDragEnteredEntryIndex);
    596             mDragEnteredEntryIndex = itemIndex;
    597             mContactEntries.add(mDragEnteredEntryIndex, ContactEntry.BLANK_ENTRY);
    598             ContactEntry.BLANK_ENTRY.id = mDraggedEntry.id;
    599             mDataSetChangedListener.onDataSetChangedForAnimation();
    600             notifyDataSetChanged();
    601         }
    602     }
    603 
    604     /**
    605      * Drops the temporarily removed contact to the desired location in the list.
    606      */
    607     public void handleDrop() {
    608         boolean changed = false;
    609         if (mDraggedEntry != null) {
    610             if (isIndexInBound(mDragEnteredEntryIndex) &&
    611                     mDragEnteredEntryIndex != mDraggedEntryIndex) {
    612                 // Don't add the ContactEntry here (to prevent a double animation from occuring).
    613                 // When we receive a new cursor the list of contact entries will automatically be
    614                 // populated with the dragged ContactEntry at the correct spot.
    615                 mDropEntryIndex = mDragEnteredEntryIndex;
    616                 mContactEntries.set(mDropEntryIndex, mDraggedEntry);
    617                 mIdToKeepInPlace = getAdjustedItemId(mDraggedEntry.id);
    618                 mDataSetChangedListener.cacheOffsetsForDatasetChange();
    619                 changed = true;
    620             } else if (isIndexInBound(mDraggedEntryIndex)) {
    621                 // If {@link #mDragEnteredEntryIndex} is invalid,
    622                 // falls back to the original position of the contact.
    623                 mContactEntries.remove(mDragEnteredEntryIndex);
    624                 mContactEntries.add(mDraggedEntryIndex, mDraggedEntry);
    625                 mDropEntryIndex = mDraggedEntryIndex;
    626                 notifyDataSetChanged();
    627             }
    628 
    629             if (changed && mDropEntryIndex < PIN_LIMIT) {
    630                 final ContentValues cv = getReflowedPinnedPositions(mContactEntries, mDraggedEntry,
    631                         mDraggedEntryIndex, mDropEntryIndex);
    632                 final Uri pinUri = PinnedPositions.UPDATE_URI.buildUpon().build();
    633                 // update the database here with the new pinned positions
    634                 mContext.getContentResolver().update(pinUri, cv, null, null);
    635             }
    636             mDraggedEntry = null;
    637         }
    638     }
    639 
    640     /**
    641      * Invoked when the dragged item is dropped to unsupported location. We will then move the
    642      * contact back to where it was dragged from.
    643      */
    644     public void dropToUnsupportedView() {
    645         if (isIndexInBound(mDragEnteredEntryIndex)) {
    646             mContactEntries.remove(mDragEnteredEntryIndex);
    647             mContactEntries.add(mDraggedEntryIndex, mDraggedEntry);
    648             notifyDataSetChanged();
    649         }
    650     }
    651 
    652     /**
    653      * Sets an item to for pending removal. If the user does not click the undo button, the item
    654      * will be removed at the next interaction.
    655      *
    656      * @param index Index of the item to be removed.
    657      */
    658     public void setPotentialRemoveEntryIndex(int index) {
    659         mPotentialRemoveEntryIndex = index;
    660     }
    661 
    662     /**
    663      * Removes a contact entry from the list.
    664      *
    665      * @return True is an item is removed. False is there is no item to be removed.
    666      */
    667     public boolean removePendingContactEntry() {
    668         boolean removed = false;
    669         if (isIndexInBound(mPotentialRemoveEntryIndex)) {
    670             final ContactEntry entry = mContactEntries.get(mPotentialRemoveEntryIndex);
    671             unstarAndUnpinContact(entry.lookupUri);
    672             removed = true;
    673             mAwaitingRemove = true;
    674         }
    675         cleanTempVariables();
    676         return removed;
    677     }
    678 
    679     /**
    680      * Resets the item for pending removal.
    681      */
    682     public void undoPotentialRemoveEntryIndex() {
    683         mPotentialRemoveEntryIndex = -1;
    684     }
    685 
    686     public boolean hasPotentialRemoveEntryIndex() {
    687         return isIndexInBound(mPotentialRemoveEntryIndex);
    688     }
    689 
    690     /**
    691      * Clears all temporary variables at a new interaction.
    692      */
    693     public void cleanTempVariables() {
    694         mDraggedEntryIndex = -1;
    695         mDropEntryIndex = -1;
    696         mDragEnteredEntryIndex = -1;
    697         mDraggedEntry = null;
    698         mPotentialRemoveEntryIndex = -1;
    699     }
    700 
    701     /**
    702      * Acts as a row item composed of {@link ContactTileView}
    703      *
    704      */
    705     public class ContactTileRow extends FrameLayout implements SwipeHelperCallback {
    706         public static final int CONTACT_ENTRY_INDEX_TAG = R.id.contact_entry_index_tag;
    707 
    708         private int mItemViewType;
    709         private int mLayoutResId;
    710         private final int mRowPaddingStart;
    711         private final int mRowPaddingEnd;
    712         private final int mRowPaddingTop;
    713         private final int mRowPaddingBottom;
    714         private final float mHeightToWidthRatio;
    715         private int mPosition;
    716         private SwipeHelper mSwipeHelper;
    717         private OnItemGestureListener mOnItemSwipeListener;
    718 
    719         public ContactTileRow(Context context, int itemViewType, int position) {
    720             super(context);
    721             mItemViewType = itemViewType;
    722             mLayoutResId = getLayoutResourceId(mItemViewType);
    723             mPosition = position;
    724 
    725             final Resources resources = mContext.getResources();
    726 
    727             mHeightToWidthRatio = getResources().getFraction(
    728                     R.dimen.contact_tile_height_to_width_ratio, 1, 1);
    729 
    730             if (mItemViewType == ViewTypes.TOP) {
    731                 // For tiled views, we still want padding to be set on the ContactTileRow.
    732                 // Otherwise the padding would be set around each of the tiles, which we don't want
    733                 mRowPaddingTop = resources.getDimensionPixelSize(
    734                         R.dimen.favorites_row_top_padding);
    735                 mRowPaddingBottom = resources.getDimensionPixelSize(
    736                         R.dimen.favorites_row_bottom_padding);
    737                 mRowPaddingStart = resources.getDimensionPixelSize(
    738                         R.dimen.favorites_row_start_padding);
    739                 mRowPaddingEnd = resources.getDimensionPixelSize(
    740                         R.dimen.favorites_row_end_padding);
    741             } else {
    742                 // For row views, padding is set on the view itself.
    743                 mRowPaddingTop = 0;
    744                 mRowPaddingBottom = 0;
    745                 mRowPaddingStart = 0;
    746                 mRowPaddingEnd = 0;
    747             }
    748 
    749             setPaddingRelative(mRowPaddingStart, mRowPaddingTop, mRowPaddingEnd,
    750                     mRowPaddingBottom);
    751 
    752             // Remove row (but not children) from accessibility node tree.
    753             setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
    754 
    755             if (mItemViewType == ViewTypes.FREQUENT) {
    756                 // ListView handles swiping for this item
    757                 SwipeHelper.setSwipeable(this, true);
    758             } else if (mItemViewType == ViewTypes.TOP) {
    759                 // The contact tile row has its own swipe helpers, that makes each individual
    760                 // tile swipeable.
    761                 final float densityScale = getResources().getDisplayMetrics().density;
    762                 final float pagingTouchSlop = ViewConfiguration.get(context)
    763                         .getScaledPagingTouchSlop();
    764                 mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale,
    765                         pagingTouchSlop);
    766                 // Increase swipe thresholds for square tiles since they are relatively small.
    767                 mSwipeHelper.setChildSwipedFarEnoughFactor(0.9f);
    768                 mSwipeHelper.setChildSwipedFastEnoughFactor(0.1f);
    769                 mOnItemSwipeListener = PhoneFavoritesTileAdapter.this;
    770             }
    771         }
    772 
    773         /**
    774          * Configures the row to add {@link ContactEntry}s information to the views
    775          */
    776         public void configureRow(ArrayList<ContactEntry> list, int position, boolean isLastRow) {
    777             int columnCount = mItemViewType == ViewTypes.FREQUENT ? 1 : mColumnCount;
    778             mPosition = position;
    779 
    780             // Adding tiles to row and filling in contact information
    781             for (int columnCounter = 0; columnCounter < columnCount; columnCounter++) {
    782                 ContactEntry entry =
    783                         columnCounter < list.size() ? list.get(columnCounter) : null;
    784                 addTileFromEntry(entry, columnCounter, isLastRow);
    785             }
    786             if (columnCount == 1) {
    787                 if (list.get(0) == ContactEntry.BLANK_ENTRY) {
    788                     setVisibility(View.INVISIBLE);
    789                 } else {
    790                     setVisibility(View.VISIBLE);
    791                 }
    792             }
    793         }
    794 
    795         private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) {
    796             final PhoneFavoriteTileView contactTile;
    797 
    798             if (getChildCount() <= childIndex) {
    799 
    800                 contactTile = (PhoneFavoriteTileView) inflate(mContext, mLayoutResId, null);
    801                 // Note: the layoutparam set here is only actually used for FREQUENT.
    802                 // We override onMeasure() for STARRED and we don't care the layout param there.
    803                 final Resources resources = mContext.getResources();
    804                 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
    805                         ViewGroup.LayoutParams.WRAP_CONTENT,
    806                         ViewGroup.LayoutParams.WRAP_CONTENT);
    807 
    808                 params.setMargins(
    809                         resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 0,
    810                         resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 0);
    811                 contactTile.setLayoutParams(params);
    812                 contactTile.setPhotoManager(mPhotoManager);
    813                 contactTile.setListener(mListener);
    814                 addView(contactTile);
    815             } else {
    816                 contactTile = (PhoneFavoriteTileView) getChildAt(childIndex);
    817             }
    818             contactTile.loadFromContact(entry);
    819 
    820             int entryIndex = -1;
    821             switch (mItemViewType) {
    822                 case ViewTypes.TOP:
    823                     // Setting divider visibilities
    824                     contactTile.setPaddingRelative(0, 0,
    825                             childIndex >= mColumnCount - 1 ? 0 : mPaddingInPixels, 0);
    826                     entryIndex = getFirstContactEntryIndexForPosition(mPosition) + childIndex;
    827                     SwipeHelper.setSwipeable(contactTile, false);
    828                     break;
    829                 case ViewTypes.FREQUENT:
    830                     contactTile.setHorizontalDividerVisibility(
    831                             isLastRow ? View.GONE : View.VISIBLE);
    832                     entryIndex = getFirstContactEntryIndexForPosition(mPosition);
    833                     SwipeHelper.setSwipeable(this, true);
    834                     break;
    835                 default:
    836                     break;
    837             }
    838             // tag the tile with the index of the contact entry it is associated with
    839             if (entryIndex != -1) {
    840                 contactTile.setTag(CONTACT_ENTRY_INDEX_TAG, entryIndex);
    841             }
    842             contactTile.setupFavoriteContactCard();
    843         }
    844 
    845         @Override
    846         protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    847             switch (mItemViewType) {
    848                 case ViewTypes.TOP:
    849                     onLayoutForTiles();
    850                     return;
    851                 default:
    852                     super.onLayout(changed, left, top, right, bottom);
    853                     return;
    854             }
    855         }
    856 
    857         private void onLayoutForTiles() {
    858             final int count = getChildCount();
    859 
    860             // Just line up children horizontally.
    861             int childLeft = getPaddingStart();
    862             for (int i = 0; i < count; i++) {
    863                 final View child = getChildAt(i);
    864 
    865                 // Note MeasuredWidth includes the padding.
    866                 final int childWidth = child.getMeasuredWidth();
    867                 child.layout(childLeft, getPaddingTop(), childLeft + childWidth,
    868                         getPaddingTop() + child.getMeasuredHeight());
    869                 childLeft += childWidth;
    870             }
    871         }
    872 
    873         @Override
    874         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    875             switch (mItemViewType) {
    876                 case ViewTypes.TOP:
    877                     onMeasureForTiles(widthMeasureSpec);
    878                     return;
    879                 default:
    880                     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    881                     return;
    882             }
    883         }
    884 
    885         private void onMeasureForTiles(int widthMeasureSpec) {
    886             final int width = MeasureSpec.getSize(widthMeasureSpec);
    887 
    888             final int childCount = getChildCount();
    889             if (childCount == 0) {
    890                 // Just in case...
    891                 setMeasuredDimension(width, 0);
    892                 return;
    893             }
    894 
    895             // 1. Calculate image size.
    896             //      = ([total width] - [total padding]) / [child count]
    897             //
    898             // 2. Set it to width/height of each children.
    899             //    If we have a remainder, some tiles will have 1 pixel larger width than its height.
    900             //
    901             // 3. Set the dimensions of itself.
    902             //    Let width = given width.
    903             //    Let height = image size + bottom paddding.
    904 
    905             final int totalPaddingsInPixels = (mColumnCount - 1) * mPaddingInPixels
    906                     + mRowPaddingStart + mRowPaddingEnd;
    907 
    908             // Preferred width / height for images (excluding the padding).
    909             // The actual width may be 1 pixel larger than this if we have a remainder.
    910             final int imageWidth = (width - totalPaddingsInPixels) / mColumnCount;
    911             final int remainder = width - (imageWidth * mColumnCount) - totalPaddingsInPixels;
    912 
    913             final int height = (int) (mHeightToWidthRatio * imageWidth);
    914 
    915             for (int i = 0; i < childCount; i++) {
    916                 final View child = getChildAt(i);
    917                 final int childWidth = imageWidth + child.getPaddingRight()
    918                         // Compensate for the remainder
    919                         + (i < remainder ? 1 : 0);
    920                 child.measure(
    921                         MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
    922                         MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
    923                         );
    924             }
    925             setMeasuredDimension(width, height + getPaddingTop() + getPaddingBottom());
    926         }
    927 
    928         /**
    929          * Gets the index of the item at the specified coordinates.
    930          *
    931          * @param itemX X-coordinate of the selected item.
    932          * @param itemY Y-coordinate of the selected item.
    933          * @return Index of the selected item in the cached array.
    934          */
    935         public int getItemIndex(float itemX, float itemY) {
    936             if (mMaxTiledRows == NO_ROW_LIMIT || mPosition < mMaxTiledRows) {
    937                 if (DEBUG) {
    938                     Log.v(TAG, String.valueOf(itemX) + " " + String.valueOf(itemY));
    939                 }
    940                 for (int i = 0; i < getChildCount(); ++i) {
    941                     /** If the row contains multiple tiles, checks each tile to see if the point
    942                      * is contained in the tile. */
    943                     final View child = getChildAt(i);
    944                     /** The coordinates passed in are based on the ListView,
    945                      * translate for each child first */
    946                     final int xInListView = child.getLeft() + getLeft();
    947                     final int yInListView = child.getTop() + getTop();
    948                     final int distanceX = (int) itemX - xInListView;
    949                     final int distanceY = (int) itemY - yInListView;
    950                     if ((distanceX > 0 && distanceX < child.getWidth()) &&
    951                             (distanceY > 0 && distanceY < child.getHeight())) {
    952                         /** If the point is contained in the rectangle, computes the index of the
    953                          * item in the cached array. */
    954                         return i + (mPosition) * mColumnCount;
    955                     }
    956                 }
    957             } else {
    958                 /** If the selected item is one of the rows, compute the index. */
    959                 return getRegularRowItemIndex();
    960             }
    961             return -1;
    962         }
    963 
    964         /**
    965          * Gets the index of the regular row item.
    966          *
    967          * @return Index of the selected item in the cached array.
    968          */
    969         public int getRegularRowItemIndex() {
    970             return (mPosition - mMaxTiledRows) + mColumnCount * mMaxTiledRows;
    971         }
    972 
    973         public PhoneFavoritesTileAdapter getTileAdapter() {
    974             return PhoneFavoritesTileAdapter.this;
    975         }
    976 
    977         public int getPosition() {
    978             return mPosition;
    979         }
    980 
    981         /**
    982          * Find the view under the pointer.
    983          */
    984         public View getViewAtPosition(int x, int y) {
    985             // find the view under the pointer, accounting for GONE views
    986             final int count = getChildCount();
    987             View view;
    988             for (int childIdx = 0; childIdx < count; childIdx++) {
    989                 view = getChildAt(childIdx);
    990                 if (x >= view.getLeft() && x <= view.getRight()) {
    991                     return view;
    992                 }
    993             }
    994             return null;
    995         }
    996 
    997         @Override
    998         public View getChildAtPosition(MotionEvent ev) {
    999             final View view = getViewAtPosition((int) ev.getX(), (int) ev.getY());
   1000             if (view != null &&
   1001                     SwipeHelper.isSwipeable(view) &&
   1002                     view.getVisibility() != GONE) {
   1003                 // If this view is swipable, then return it. If not, because the removal
   1004                 // dialog is currently showing, then return a null view, which will simply
   1005                 // be ignored by the swipe helper.
   1006                 return view;
   1007             }
   1008             return null;
   1009         }
   1010 
   1011         @Override
   1012         public View getChildContentView(View v) {
   1013             return v.findViewById(R.id.contact_favorite_card);
   1014         }
   1015 
   1016         @Override
   1017         public void onScroll() {}
   1018 
   1019         @Override
   1020         public boolean canChildBeDismissed(View v) {
   1021             return true;
   1022         }
   1023 
   1024         @Override
   1025         public void onBeginDrag(View v) {
   1026             removePendingContactEntry();
   1027             final int index = indexOfChild(v);
   1028 
   1029             /*
   1030             if (index > 0) {
   1031                 detachViewFromParent(index);
   1032                 attachViewToParent(v, 0, v.getLayoutParams());
   1033             }*/
   1034 
   1035             // We do this so the underlying ScrollView knows that it won't get
   1036             // the chance to intercept events anymore
   1037             requestDisallowInterceptTouchEvent(true);
   1038         }
   1039 
   1040         @Override
   1041         public void onChildDismissed(View v) {
   1042             if (v != null) {
   1043                 if (mOnItemSwipeListener != null) {
   1044                     mOnItemSwipeListener.onSwipe(v);
   1045                 }
   1046             }
   1047         }
   1048 
   1049         @Override
   1050         public void onDragCancelled(View v) {}
   1051 
   1052         @Override
   1053         public boolean onInterceptTouchEvent(MotionEvent ev) {
   1054             if (mSwipeHelper != null && isSwipeEnabled()) {
   1055                 return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev);
   1056             } else {
   1057                 return super.onInterceptTouchEvent(ev);
   1058             }
   1059         }
   1060 
   1061         @Override
   1062         public boolean onTouchEvent(MotionEvent ev) {
   1063             if (mSwipeHelper != null && isSwipeEnabled()) {
   1064                 return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev);
   1065             } else {
   1066                 return super.onTouchEvent(ev);
   1067             }
   1068         }
   1069 
   1070         public int getItemViewType() {
   1071             return mItemViewType;
   1072         }
   1073 
   1074         public void setOnItemSwipeListener(OnItemGestureListener listener) {
   1075             mOnItemSwipeListener = listener;
   1076         }
   1077     }
   1078 
   1079     /**
   1080      * Used when a contact is swiped away. This will both unstar and set pinned position of the
   1081      * contact to PinnedPosition.DEMOTED so that it doesn't show up anymore in the favorites list.
   1082      */
   1083     private void unstarAndUnpinContact(Uri contactUri) {
   1084         final ContentValues values = new ContentValues(2);
   1085         values.put(Contacts.STARRED, false);
   1086         values.put(Contacts.PINNED, PinnedPositions.DEMOTED);
   1087         mContext.getContentResolver().update(contactUri, values, null, null);
   1088     }
   1089 
   1090     /**
   1091      * Given a list of contacts that each have pinned positions, rearrange the list (destructive)
   1092      * such that all pinned contacts are in their defined pinned positions, and unpinned contacts
   1093      * take the spaces between those pinned contacts. Demoted contacts should not appear in the
   1094      * resulting list.
   1095      *
   1096      * This method also updates the pinned positions of pinned contacts so that they are all
   1097      * unique positive integers within range from 0 to toArrange.size() - 1. This is because
   1098      * when the contact entries are read from the database, it is possible for them to have
   1099      * overlapping pin positions due to sync or modifications by third party apps.
   1100      */
   1101     @VisibleForTesting
   1102     /* package */ void arrangeContactsByPinnedPosition(ArrayList<ContactEntry> toArrange) {
   1103         final PriorityQueue<ContactEntry> pinnedQueue =
   1104                 new PriorityQueue<ContactEntry>(PIN_LIMIT, mContactEntryComparator);
   1105 
   1106         final List<ContactEntry> unpinnedContacts = new LinkedList<ContactEntry>();
   1107 
   1108         final int length = toArrange.size();
   1109         for (int i = 0; i < length; i++) {
   1110             final ContactEntry contact = toArrange.get(i);
   1111             // Decide whether the contact is hidden(demoted), pinned, or unpinned
   1112             if (contact.pinned > PIN_LIMIT) {
   1113                 unpinnedContacts.add(contact);
   1114             } else if (contact.pinned > PinnedPositions.DEMOTED) {
   1115                 // Demoted or contacts with negative pinned positions are ignored.
   1116                 // Pinned contacts go into a priority queue where they are ranked by pinned
   1117                 // position. This is required because the contacts provider does not return
   1118                 // contacts ordered by pinned position.
   1119                 pinnedQueue.add(contact);
   1120             }
   1121         }
   1122 
   1123         final int maxToPin = Math.min(PIN_LIMIT, pinnedQueue.size() + unpinnedContacts.size());
   1124 
   1125         toArrange.clear();
   1126         for (int i = 0; i < maxToPin; i++) {
   1127             if (!pinnedQueue.isEmpty() && pinnedQueue.peek().pinned <= i) {
   1128                 final ContactEntry toPin = pinnedQueue.poll();
   1129                 toPin.pinned = i;
   1130                 toArrange.add(toPin);
   1131             } else if (!unpinnedContacts.isEmpty()) {
   1132                 toArrange.add(unpinnedContacts.remove(0));
   1133             }
   1134         }
   1135 
   1136         // If there are still contacts in pinnedContacts at this point, it means that the pinned
   1137         // positions of these pinned contacts exceed the actual number of contacts in the list.
   1138         // For example, the user had 10 frequents, starred and pinned one of them at the last spot,
   1139         // and then cleared frequents. Contacts in this situation should become unpinned.
   1140         while (!pinnedQueue.isEmpty()) {
   1141             final ContactEntry entry = pinnedQueue.poll();
   1142             entry.pinned = PinnedPositions.UNPINNED;
   1143             toArrange.add(entry);
   1144         }
   1145 
   1146         // Any remaining unpinned contacts that weren't in the gaps between the pinned contacts
   1147         // now just get appended to the end of the list.
   1148         toArrange.addAll(unpinnedContacts);
   1149     }
   1150 
   1151     /**
   1152      * Given an existing list of contact entries and a single entry that is to be pinned at a
   1153      * particular position, return a ContentValues object that contains new pinned positions for
   1154      * all contacts that are forced to be pinned at new positions, trying as much as possible to
   1155      * keep pinned contacts at their original location.
   1156      *
   1157      * At this point in time the pinned position of each contact in the list has already been
   1158      * updated by {@link #arrangeContactsByPinnedPosition}, so we can assume that all pinned
   1159      * positions(within {@link #PIN_LIMIT} are unique positive integers.
   1160      */
   1161     @VisibleForTesting
   1162     /* package */ ContentValues getReflowedPinnedPositions(ArrayList<ContactEntry> list,
   1163             ContactEntry entryToPin, int oldPos, int newPinPos) {
   1164 
   1165         final ContentValues cv = new ContentValues();
   1166         final int lowerBound = Math.min(oldPos, newPinPos);
   1167         final int upperBound = Math.max(oldPos, newPinPos);
   1168         for (int i = lowerBound; i <= upperBound; i++) {
   1169             final ContactEntry entry = list.get(i);
   1170             if (entry.pinned == i) continue;
   1171             cv.put(String.valueOf(entry.id), i);
   1172         }
   1173         return cv;
   1174     }
   1175 
   1176     protected static class ViewTypes {
   1177         public static final int FREQUENT = 0;
   1178         public static final int TOP = 1;
   1179         public static final int COUNT = 2;
   1180     }
   1181 
   1182     @Override
   1183     public void onSwipe(View view) {
   1184         final PhoneFavoriteTileView tileView = (PhoneFavoriteTileView) view.findViewById(
   1185                 R.id.contact_tile);
   1186         // When the view is in the removal dialog, it should no longer be swipeable
   1187         SwipeHelper.setSwipeable(view, false);
   1188         tileView.displayRemovalDialog();
   1189 
   1190         final Integer entryIndex = (Integer) tileView.getTag(
   1191                 ContactTileRow.CONTACT_ENTRY_INDEX_TAG);
   1192 
   1193         setPotentialRemoveEntryIndex(entryIndex);
   1194     }
   1195 
   1196     @Override
   1197     public void onTouch() {
   1198         removePendingContactEntry();
   1199         return;
   1200     }
   1201 
   1202     @Override
   1203     public boolean isSwipeEnabled() {
   1204         return !mAwaitingRemove;
   1205     }
   1206 
   1207     @Override
   1208     public void onDragStarted(int itemIndex, int x, int y, PhoneFavoriteTileView view) {
   1209         setInDragging(true);
   1210         popContactEntry(itemIndex);
   1211     }
   1212 
   1213     @Override
   1214     public void onDragHovered(int itemIndex, int x, int y) {
   1215         if (mInDragging &&
   1216                 mDragEnteredEntryIndex != itemIndex &&
   1217                 isIndexInBound(itemIndex) &&
   1218                 itemIndex < PIN_LIMIT &&
   1219                 itemIndex >= 0) {
   1220             markDropArea(itemIndex);
   1221         }
   1222     }
   1223 
   1224     @Override
   1225     public void onDragFinished(int x, int y) {
   1226         setInDragging(false);
   1227         // A contact has been dragged to the RemoveView in order to be unstarred,  so simply wait
   1228         // for the new contact cursor which will cause the UI to be refreshed without the unstarred
   1229         // contact.
   1230         if (!mAwaitingRemove) {
   1231             handleDrop();
   1232         }
   1233     }
   1234 
   1235     @Override
   1236     public void onDroppedOnRemove() {
   1237         if (mDraggedEntry != null) {
   1238             unstarAndUnpinContact(mDraggedEntry.lookupUri);
   1239             mAwaitingRemove = true;
   1240         }
   1241     }
   1242 }
   1243