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 */
16package com.android.dialer.list;
17
18import com.google.common.annotations.VisibleForTesting;
19import com.google.common.collect.ComparisonChain;
20import com.google.common.collect.Lists;
21
22import android.content.ContentProviderOperation;
23import android.content.ContentUris;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.OperationApplicationException;
27import android.content.res.Resources;
28import android.database.Cursor;
29import android.net.Uri;
30import android.os.RemoteException;
31import android.provider.ContactsContract;
32import android.provider.ContactsContract.CommonDataKinds.Phone;
33import android.provider.ContactsContract.Contacts;
34import android.provider.ContactsContract.PinnedPositions;
35import android.text.TextUtils;
36import android.util.Log;
37import android.util.LongSparseArray;
38import android.view.View;
39import android.view.ViewGroup;
40import android.widget.BaseAdapter;
41import android.widget.FrameLayout;
42
43import com.android.contacts.common.ContactPhotoManager;
44import com.android.contacts.common.ContactTileLoaderFactory;
45import com.android.contacts.common.list.ContactEntry;
46import com.android.contacts.common.list.ContactTileAdapter.DisplayType;
47import com.android.contacts.common.list.ContactTileView;
48import com.android.dialer.R;
49
50import java.util.ArrayList;
51import java.util.Comparator;
52import java.util.LinkedList;
53import java.util.List;
54import java.util.PriorityQueue;
55
56/**
57 * Also allows for a configurable number of columns as well as a maximum row of tiled contacts.
58 */
59public 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