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