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