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