blob: 45cc5a371e0dd1372a64d17a97cb4ad128c91ff0 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dialer.list;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ComparisonChain;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.PinnedPositions;
import android.text.TextUtils;
import android.util.Log;
import android.util.LongSparseArray;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import com.android.contacts.common.ContactPhotoManager;
import com.android.contacts.common.ContactTileLoaderFactory;
import com.android.contacts.common.R;
import com.android.contacts.common.list.ContactEntry;
import com.android.contacts.common.list.ContactTileAdapter.DisplayType;
import com.android.contacts.common.list.ContactTileView;
import com.android.dialer.list.SwipeHelper.OnItemGestureListener;
import com.android.dialer.list.SwipeHelper.SwipeHelperCallback;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;
/**
* Also allows for a configurable number of columns as well as a maximum row of tiled contacts.
*
* This adapter has been rewritten to only support a maximum of one row for favorites.
*
*/
public class PhoneFavoritesTileAdapter extends BaseAdapter implements
SwipeHelper.OnItemGestureListener, OnDragDropListener {
private static final String TAG = PhoneFavoritesTileAdapter.class.getSimpleName();
private static final boolean DEBUG = false;
public static final int NO_ROW_LIMIT = -1;
public static final int ROW_LIMIT_DEFAULT = NO_ROW_LIMIT;
private ContactTileView.Listener mListener;
private OnDataSetChangedForAnimationListener mDataSetChangedListener;
private Context mContext;
private Resources mResources;
/** Contact data stored in cache. This is used to populate the associated view. */
protected ArrayList<ContactEntry> mContactEntries = null;
/** Back up of the temporarily removed Contact during dragging. */
private ContactEntry mDraggedEntry = null;
/** Position of the temporarily removed contact in the cache. */
private int mDraggedEntryIndex = -1;
/** New position of the temporarily removed contact in the cache. */
private int mDropEntryIndex = -1;
/** New position of the temporarily entered contact in the cache. */
private int mDragEnteredEntryIndex = -1;
/** Position of the contact pending removal. */
private int mPotentialRemoveEntryIndex = -1;
private long mIdToKeepInPlace = -1;
private boolean mAwaitingRemove = false;
private boolean mDelayCursorUpdates = false;
private ContactPhotoManager mPhotoManager;
protected int mNumFrequents;
protected int mNumStarred;
protected int mColumnCount;
private int mMaxTiledRows = ROW_LIMIT_DEFAULT;
private int mStarredIndex;
protected int mIdIndex;
protected int mLookupIndex;
protected int mPhotoUriIndex;
protected int mNameIndex;
protected int mPresenceIndex;
protected int mStatusIndex;
private int mPhoneNumberIndex;
private int mPhoneNumberTypeIndex;
private int mPhoneNumberLabelIndex;
private int mIsDefaultNumberIndex;
protected int mPinnedIndex;
protected int mContactIdIndex;
private final int mPaddingInPixels;
/** Indicates whether a drag is in process. */
private boolean mInDragging = false;
public static final int PIN_LIMIT = 20;
/**
* The soft limit on how many contact tiles to show.
* NOTE This soft limit would not restrict the number of starred contacts to show, rather
* 1. If the count of starred contacts is less than this limit, show 20 tiles total.
* 2. If the count of starred contacts is more than or equal to this limit,
* show all starred tiles and no frequents.
*/
private static final int TILES_SOFT_LIMIT = 20;
final Comparator<ContactEntry> mContactEntryComparator = new Comparator<ContactEntry>() {
@Override
public int compare(ContactEntry lhs, ContactEntry rhs) {
return ComparisonChain.start()
.compare(lhs.pinned, rhs.pinned)
.compare(lhs.name, rhs.name)
.result();
}
};
public interface OnDataSetChangedForAnimationListener {
public void onDataSetChangedForAnimation(long... idsInPlace);
public void cacheOffsetsForDatasetChange();
};
public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener,
OnDataSetChangedForAnimationListener dataSetChangedListener,
int numCols, int maxTiledRows) {
mDataSetChangedListener = dataSetChangedListener;
mListener = listener;
mContext = context;
mResources = context.getResources();
mColumnCount = numCols;
mNumFrequents = 0;
mMaxTiledRows = maxTiledRows;
mContactEntries = new ArrayList<ContactEntry>();
// Converting padding in dips to padding in pixels
mPaddingInPixels = mContext.getResources()
.getDimensionPixelSize(R.dimen.contact_tile_divider_width);
bindColumnIndices();
}
public void setPhotoLoader(ContactPhotoManager photoLoader) {
mPhotoManager = photoLoader;
}
public void setMaxRowCount(int maxRows) {
mMaxTiledRows = maxRows;
}
public void setColumnCount(int columnCount) {
mColumnCount = columnCount;
}
/**
* Indicates whether a drag is in process.
*
* @param inDragging Boolean variable indicating whether there is a drag in process.
*/
public void setInDragging(boolean inDragging) {
mDelayCursorUpdates = inDragging;
mInDragging = inDragging;
}
/** Gets whether the drag is in process. */
public boolean getInDragging() {
return mInDragging;
}
/**
* Sets the column indices for expected {@link Cursor}
* based on {@link DisplayType}.
*/
protected void bindColumnIndices() {
mIdIndex = ContactTileLoaderFactory.CONTACT_ID;
mLookupIndex = ContactTileLoaderFactory.LOOKUP_KEY;
mPhotoUriIndex = ContactTileLoaderFactory.PHOTO_URI;
mNameIndex = ContactTileLoaderFactory.DISPLAY_NAME;
mStarredIndex = ContactTileLoaderFactory.STARRED;
mPresenceIndex = ContactTileLoaderFactory.CONTACT_PRESENCE;
mStatusIndex = ContactTileLoaderFactory.CONTACT_STATUS;
mPhoneNumberIndex = ContactTileLoaderFactory.PHONE_NUMBER;
mPhoneNumberTypeIndex = ContactTileLoaderFactory.PHONE_NUMBER_TYPE;
mPhoneNumberLabelIndex = ContactTileLoaderFactory.PHONE_NUMBER_LABEL;
mIsDefaultNumberIndex = ContactTileLoaderFactory.IS_DEFAULT_NUMBER;
mPinnedIndex = ContactTileLoaderFactory.PINNED;
mContactIdIndex = ContactTileLoaderFactory.CONTACT_ID_FOR_DATA;
}
/**
* Gets the number of frequents from the passed in cursor.
*
* This methods is needed so the GroupMemberTileAdapter can override this.
*
* @param cursor The cursor to get number of frequents from.
*/
protected void saveNumFrequentsFromCursor(Cursor cursor) {
mNumFrequents = cursor.getCount() - mNumStarred;
}
/**
* Creates {@link ContactTileView}s for each item in {@link Cursor}.
*
* Else use {@link ContactTileLoaderFactory}
*/
public void setContactCursor(Cursor cursor) {
if (!mDelayCursorUpdates && cursor != null && !cursor.isClosed()) {
mNumStarred = getNumStarredContacts(cursor);
if (mAwaitingRemove) {
mDataSetChangedListener.cacheOffsetsForDatasetChange();
}
saveNumFrequentsFromCursor(cursor);
saveCursorToCache(cursor);
// cause a refresh of any views that rely on this data
notifyDataSetChanged();
// about to start redraw
if (mIdToKeepInPlace != -1) {
mDataSetChangedListener.onDataSetChangedForAnimation(mIdToKeepInPlace);
} else {
mDataSetChangedListener.onDataSetChangedForAnimation();
}
mIdToKeepInPlace = -1;
}
}
/**
* Saves the cursor data to the cache, to speed up UI changes.
*
* @param cursor Returned cursor with data to populate the view.
*/
private void saveCursorToCache(Cursor cursor) {
mContactEntries.clear();
cursor.moveToPosition(-1);
final LongSparseArray<Object> duplicates = new LongSparseArray<Object>(cursor.getCount());
// Track the length of {@link #mContactEntries} and compare to {@link #TILES_SOFT_LIMIT}.
int counter = 0;
while (cursor.moveToNext()) {
final int starred = cursor.getInt(mStarredIndex);
final long id;
// We display a maximum of TILES_SOFT_LIMIT contacts, or the total number of starred
// whichever is greater.
if (starred < 1 && counter >= TILES_SOFT_LIMIT) {
break;
} else {
id = cursor.getLong(mContactIdIndex);
}
final ContactEntry existing = (ContactEntry) duplicates.get(id);
if (existing != null) {
// Check if the existing number is a default number. If not, clear the phone number
// and label fields so that the disambiguation dialog will show up.
if (!existing.isDefaultNumber) {
existing.phoneLabel = null;
existing.phoneNumber = null;
}
continue;
}
final String photoUri = cursor.getString(mPhotoUriIndex);
final String lookupKey = cursor.getString(mLookupIndex);
final int pinned = cursor.getInt(mPinnedIndex);
final String name = cursor.getString(mNameIndex);
final boolean isStarred = cursor.getInt(mStarredIndex) > 0;
final boolean isDefaultNumber = cursor.getInt(mIsDefaultNumberIndex) > 0;
final ContactEntry contact = new ContactEntry();
contact.id = id;
contact.name = (!TextUtils.isEmpty(name)) ? name :
mResources.getString(R.string.missing_name);
contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null);
contact.lookupKey = ContentUris.withAppendedId(
Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id);
contact.isFavorite = isStarred;
contact.isDefaultNumber = isDefaultNumber;
// Set phone number and label
final int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex);
final String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex);
contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType,
phoneNumberCustomLabel);
contact.phoneNumber = cursor.getString(mPhoneNumberIndex);
contact.pinned = pinned;
mContactEntries.add(contact);
duplicates.put(id, contact);
counter++;
}
mAwaitingRemove = false;
arrangeContactsByPinnedPosition(mContactEntries);
notifyDataSetChanged();
}
/**
* Iterates over the {@link Cursor}
* Returns position of the first NON Starred Contact
* Returns -1 if {@link DisplayType#STARRED_ONLY}
* Returns 0 if {@link DisplayType#FREQUENT_ONLY}
*/
protected int getNumStarredContacts(Cursor cursor) {
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
if (cursor.getInt(mStarredIndex) == 0) {
return cursor.getPosition();
}
}
// There are not NON Starred contacts in cursor
// Set divider positon to end
return cursor.getCount();
}
/**
* Loads a contact from the cached list.
*
* @param position Position of the Contact.
* @return Contact at the requested position.
*/
protected ContactEntry getContactEntryFromCache(int position) {
if (mContactEntries.size() <= position) return null;
return mContactEntries.get(position);
}
/**
* Returns the number of frequents that will be displayed in the list.
*/
public int getNumFrequents() {
return mNumFrequents;
}
@Override
public int getCount() {
if (mContactEntries == null || mContactEntries.isEmpty()) {
return 0;
}
int total = mContactEntries.size();
// The number of contacts that don't show up as tiles
final int nonTiledRows = Math.max(0, total - getMaxContactsInTiles());
// The number of tiled rows
final int tiledRows = getRowCount(total - nonTiledRows);
return nonTiledRows + tiledRows;
}
public int getMaxTiledRows() {
return mMaxTiledRows;
}
/**
* Returns the number of rows required to show the provided number of entries
* with the current number of columns.
*/
protected int getRowCount(int entryCount) {
if (entryCount == 0) return 0;
final int nonLimitedRows = ((entryCount - 1) / mColumnCount) + 1;
if (mMaxTiledRows == NO_ROW_LIMIT) {
return nonLimitedRows;
}
return Math.min(mMaxTiledRows, nonLimitedRows);
}
private int getMaxContactsInTiles() {
if (mMaxTiledRows == NO_ROW_LIMIT) {
return Integer.MAX_VALUE;
}
return mColumnCount * mMaxTiledRows;
}
public int getRowIndex(int entryIndex) {
if (entryIndex < getMaxContactsInTiles()) {
return entryIndex / mColumnCount;
} else {
return entryIndex - mMaxTiledRows * (mColumnCount + 1);
}
}
public int getColumnCount() {
return mColumnCount;
}
/**
* Returns an ArrayList of the {@link ContactEntry}s that are to appear
* on the row for the given position.
*/
@Override
public ArrayList<ContactEntry> getItem(int position) {
ArrayList<ContactEntry> resultList = new ArrayList<ContactEntry>(mColumnCount);
final int entryIndex = getFirstContactEntryIndexForPosition(position);
final int viewType = getItemViewType(position);
final int columnCount;
if (viewType == ViewTypes.TOP) {
columnCount = mColumnCount;
} else {
columnCount = 1;
}
for (int i = 0; i < columnCount; i++) {
final ContactEntry entry = getContactEntryFromCache(entryIndex + i);
if (entry == null) break; // less than mColumnCount contacts
resultList.add(entry);
}
return resultList;
}
/*
* Given a position in the adapter, returns the index of the first contact entry that is to be
* in that row.
*/
private int getFirstContactEntryIndexForPosition(int position) {
final int maxContactsInTiles = getMaxContactsInTiles();
if (position < getRowCount(maxContactsInTiles)) {
// Contacts that appear as tiles
return position * mColumnCount;
} else {
// Contacts that appear as rows
// The actual position of the contact in the cursor is simply total the number of
// tiled contacts + the given position
return maxContactsInTiles + position - mMaxTiledRows;
}
}
/**
* For the top row of tiled contacts, the item id is the position of the row of
* contacts.
* For frequent contacts, the item id is the maximum number of rows of tiled contacts +
* the actual contact id. Since contact ids are always greater than 0, this guarantees that
* all items within this adapter will always have unique ids.
*/
@Override
public long getItemId(int position) {
if (getItemViewType(position) == ViewTypes.FREQUENT) {
return getAdjustedItemId(getItem(position).get(0).id);
} else {
return position;
}
}
/**
* Calculates the stable itemId for a particular entry based on the entry's contact ID. This
* stable itemId is used for animation purposes.
*/
public long getAdjustedItemId(long id) {
if (mMaxTiledRows == NO_ROW_LIMIT) {
return id;
}
return mMaxTiledRows + id;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean areAllItemsEnabled() {
// No dividers, so all items are enabled.
return true;
}
@Override
public boolean isEnabled(int position) {
return getCount() > 0;
}
@Override
public void notifyDataSetChanged() {
if (DEBUG) {
Log.v(TAG, "notifyDataSetChanged");
}
super.notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (DEBUG) {
Log.v(TAG, "get view for " + String.valueOf(position));
}
int itemViewType = getItemViewType(position);
ContactTileRow contactTileRowView = null;
if (convertView instanceof ContactTileRow) {
contactTileRowView = (ContactTileRow) convertView;
}
ArrayList<ContactEntry> contactList = getItem(position);
if (contactTileRowView == null) {
// Creating new row if needed
contactTileRowView = new ContactTileRow(mContext, itemViewType, position);
}
contactTileRowView.configureRow(contactList, position, position == getCount() - 1);
return contactTileRowView;
}
private int getLayoutResourceId(int viewType) {
switch (viewType) {
case ViewTypes.FREQUENT:
return R.layout.phone_favorite_regular_row_view;
case ViewTypes.TOP:
return R.layout.phone_favorite_tile_view;
default:
throw new IllegalArgumentException("Unrecognized viewType " + viewType);
}
}
@Override
public int getViewTypeCount() {
return ViewTypes.COUNT;
}
@Override
public int getItemViewType(int position) {
if (position < getMaxContactsInTiles()) {
return ViewTypes.TOP;
} else {
return ViewTypes.FREQUENT;
}
}
/**
* Temporarily removes a contact from the list for UI refresh. Stores data for this contact
* in the back-up variable.
*
* @param index Position of the contact to be removed.
*/
public void popContactEntry(int index) {
if (isIndexInBound(index)) {
mDraggedEntry = mContactEntries.get(index);
mDraggedEntryIndex = index;
mDragEnteredEntryIndex = index;
markDropArea(mDragEnteredEntryIndex);
}
}
/**
* @param itemIndex Position of the contact in {@link #mContactEntries}.
* @return True if the given index is valid for {@link #mContactEntries}.
*/
private boolean isIndexInBound(int itemIndex) {
return itemIndex >= 0 && itemIndex < mContactEntries.size();
}
/**
* Mark the tile as drop area by given the item index in {@link #mContactEntries}.
*
* @param itemIndex Position of the contact in {@link #mContactEntries}.
*/
private void markDropArea(int itemIndex) {
if (isIndexInBound(mDragEnteredEntryIndex) && isIndexInBound(itemIndex)) {
mDataSetChangedListener.cacheOffsetsForDatasetChange();
// Remove the old placeholder item and place the new placeholder item.
final int oldIndex = mDragEnteredEntryIndex;
mContactEntries.remove(mDragEnteredEntryIndex);
mDragEnteredEntryIndex = itemIndex;
mContactEntries.add(mDragEnteredEntryIndex, ContactEntry.BLANK_ENTRY);
ContactEntry.BLANK_ENTRY.id = mDraggedEntry.id;
mDataSetChangedListener.onDataSetChangedForAnimation();
notifyDataSetChanged();
}
}
/**
* Drops the temporarily removed contact to the desired location in the list.
*/
public void handleDrop() {
boolean changed = false;
if (mDraggedEntry != null) {
if (isIndexInBound(mDragEnteredEntryIndex) &&
mDragEnteredEntryIndex != mDraggedEntryIndex) {
// Don't add the ContactEntry here (to prevent a double animation from occuring).
// When we receive a new cursor the list of contact entries will automatically be
// populated with the dragged ContactEntry at the correct spot.
mDropEntryIndex = mDragEnteredEntryIndex;
mContactEntries.set(mDropEntryIndex, mDraggedEntry);
mIdToKeepInPlace = getAdjustedItemId(mDraggedEntry.id);
mDataSetChangedListener.cacheOffsetsForDatasetChange();
changed = true;
} else if (isIndexInBound(mDraggedEntryIndex)) {
// If {@link #mDragEnteredEntryIndex} is invalid,
// falls back to the original position of the contact.
mContactEntries.remove(mDragEnteredEntryIndex);
mContactEntries.add(mDraggedEntryIndex, mDraggedEntry);
mDropEntryIndex = mDraggedEntryIndex;
notifyDataSetChanged();
}
if (changed && mDropEntryIndex < PIN_LIMIT) {
final ContentValues cv = getReflowedPinnedPositions(mContactEntries, mDraggedEntry,
mDraggedEntryIndex, mDropEntryIndex);
final Uri pinUri = PinnedPositions.UPDATE_URI.buildUpon().build();
// update the database here with the new pinned positions
mContext.getContentResolver().update(pinUri, cv, null, null);
}
mDraggedEntry = null;
}
}
/**
* Invoked when the dragged item is dropped to unsupported location. We will then move the
* contact back to where it was dragged from.
*/
public void dropToUnsupportedView() {
if (isIndexInBound(mDragEnteredEntryIndex)) {
mContactEntries.remove(mDragEnteredEntryIndex);
mContactEntries.add(mDraggedEntryIndex, mDraggedEntry);
notifyDataSetChanged();
}
}
/**
* Sets an item to for pending removal. If the user does not click the undo button, the item
* will be removed at the next interaction.
*
* @param index Index of the item to be removed.
*/
public void setPotentialRemoveEntryIndex(int index) {
mPotentialRemoveEntryIndex = index;
}
/**
* Removes a contact entry from the list.
*
* @return True is an item is removed. False is there is no item to be removed.
*/
public boolean removePendingContactEntry() {
boolean removed = false;
if (isIndexInBound(mPotentialRemoveEntryIndex)) {
final ContactEntry entry = mContactEntries.get(mPotentialRemoveEntryIndex);
unstarAndUnpinContact(entry.lookupKey);
removed = true;
mAwaitingRemove = true;
}
cleanTempVariables();
return removed;
}
/**
* Resets the item for pending removal.
*/
public void undoPotentialRemoveEntryIndex() {
mPotentialRemoveEntryIndex = -1;
}
public boolean hasPotentialRemoveEntryIndex() {
return isIndexInBound(mPotentialRemoveEntryIndex);
}
/**
* Clears all temporary variables at a new interaction.
*/
public void cleanTempVariables() {
mDraggedEntryIndex = -1;
mDropEntryIndex = -1;
mDragEnteredEntryIndex = -1;
mDraggedEntry = null;
mPotentialRemoveEntryIndex = -1;
}
/**
* Acts as a row item composed of {@link ContactTileView}
*
*/
public class ContactTileRow extends FrameLayout implements SwipeHelperCallback {
public static final int CONTACT_ENTRY_INDEX_TAG = R.id.contact_entry_index_tag;
private int mItemViewType;
private int mLayoutResId;
private final int mRowPaddingStart;
private final int mRowPaddingEnd;
private final int mRowPaddingTop;
private final int mRowPaddingBottom;
private final float mHeightToWidthRatio;
private int mPosition;
private SwipeHelper mSwipeHelper;
private OnItemGestureListener mOnItemSwipeListener;
public ContactTileRow(Context context, int itemViewType, int position) {
super(context);
mItemViewType = itemViewType;
mLayoutResId = getLayoutResourceId(mItemViewType);
mPosition = position;
final Resources resources = mContext.getResources();
mHeightToWidthRatio = getResources().getFraction(
R.dimen.contact_tile_height_to_width_ratio, 1, 1);
if (mItemViewType == ViewTypes.TOP) {
// For tiled views, we still want padding to be set on the ContactTileRow.
// Otherwise the padding would be set around each of the tiles, which we don't want
mRowPaddingTop = resources.getDimensionPixelSize(
R.dimen.favorites_row_top_padding);
mRowPaddingBottom = resources.getDimensionPixelSize(
R.dimen.favorites_row_bottom_padding);
mRowPaddingStart = resources.getDimensionPixelSize(
R.dimen.favorites_row_start_padding);
mRowPaddingEnd = resources.getDimensionPixelSize(
R.dimen.favorites_row_end_padding);
} else {
// For row views, padding is set on the view itself.
mRowPaddingTop = 0;
mRowPaddingBottom = 0;
mRowPaddingStart = 0;
mRowPaddingEnd = 0;
}
setPaddingRelative(mRowPaddingStart, mRowPaddingTop, mRowPaddingEnd,
mRowPaddingBottom);
// Remove row (but not children) from accessibility node tree.
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
if (mItemViewType == ViewTypes.FREQUENT) {
// ListView handles swiping for this item
SwipeHelper.setSwipeable(this, true);
} else if (mItemViewType == ViewTypes.TOP) {
// The contact tile row has its own swipe helpers, that makes each individual
// tile swipeable.
final float densityScale = getResources().getDisplayMetrics().density;
final float pagingTouchSlop = ViewConfiguration.get(context)
.getScaledPagingTouchSlop();
mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale,
pagingTouchSlop);
// Increase swipe thresholds for square tiles since they are relatively small.
mSwipeHelper.setChildSwipedFarEnoughFactor(0.9f);
mSwipeHelper.setChildSwipedFastEnoughFactor(0.1f);
mOnItemSwipeListener = PhoneFavoritesTileAdapter.this;
}
}
/**
* Configures the row to add {@link ContactEntry}s information to the views
*/
public void configureRow(ArrayList<ContactEntry> list, int position, boolean isLastRow) {
int columnCount = mItemViewType == ViewTypes.FREQUENT ? 1 : mColumnCount;
mPosition = position;
// Adding tiles to row and filling in contact information
for (int columnCounter = 0; columnCounter < columnCount; columnCounter++) {
ContactEntry entry =
columnCounter < list.size() ? list.get(columnCounter) : null;
addTileFromEntry(entry, columnCounter, isLastRow);
}
if (columnCount == 1) {
if (list.get(0) == ContactEntry.BLANK_ENTRY) {
setVisibility(View.INVISIBLE);
} else {
setVisibility(View.VISIBLE);
}
}
}
private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) {
final PhoneFavoriteTileView contactTile;
if (getChildCount() <= childIndex) {
contactTile = (PhoneFavoriteTileView) inflate(mContext, mLayoutResId, null);
// Note: the layoutparam set here is only actually used for FREQUENT.
// We override onMeasure() for STARRED and we don't care the layout param there.
final Resources resources = mContext.getResources();
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(
resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 0,
resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 0);
contactTile.setLayoutParams(params);
contactTile.setPhotoManager(mPhotoManager);
contactTile.setListener(mListener);
addView(contactTile);
} else {
contactTile = (PhoneFavoriteTileView) getChildAt(childIndex);
}
contactTile.loadFromContact(entry);
int entryIndex = -1;
switch (mItemViewType) {
case ViewTypes.TOP:
// Setting divider visibilities
contactTile.setPaddingRelative(0, 0,
childIndex >= mColumnCount - 1 ? 0 : mPaddingInPixels, 0);
entryIndex = getFirstContactEntryIndexForPosition(mPosition) + childIndex;
SwipeHelper.setSwipeable(contactTile, false);
break;
case ViewTypes.FREQUENT:
contactTile.setHorizontalDividerVisibility(
isLastRow ? View.GONE : View.VISIBLE);
entryIndex = getFirstContactEntryIndexForPosition(mPosition);
SwipeHelper.setSwipeable(this, true);
break;
default:
break;
}
// tag the tile with the index of the contact entry it is associated with
if (entryIndex != -1) {
contactTile.setTag(CONTACT_ENTRY_INDEX_TAG, entryIndex);
}
contactTile.setupFavoriteContactCard();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
switch (mItemViewType) {
case ViewTypes.TOP:
onLayoutForTiles();
return;
default:
super.onLayout(changed, left, top, right, bottom);
return;
}
}
private void onLayoutForTiles() {
final int count = getChildCount();
// Just line up children horizontally.
int childLeft = getPaddingStart();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
// Note MeasuredWidth includes the padding.
final int childWidth = child.getMeasuredWidth();
child.layout(childLeft, getPaddingTop(), childLeft + childWidth,
getPaddingTop() + child.getMeasuredHeight());
childLeft += childWidth;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
switch (mItemViewType) {
case ViewTypes.TOP:
onMeasureForTiles(widthMeasureSpec);
return;
default:
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
}
private void onMeasureForTiles(int widthMeasureSpec) {
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int childCount = getChildCount();
if (childCount == 0) {
// Just in case...
setMeasuredDimension(width, 0);
return;
}
// 1. Calculate image size.
// = ([total width] - [total padding]) / [child count]
//
// 2. Set it to width/height of each children.
// If we have a remainder, some tiles will have 1 pixel larger width than its height.
//
// 3. Set the dimensions of itself.
// Let width = given width.
// Let height = image size + bottom paddding.
final int totalPaddingsInPixels = (mColumnCount - 1) * mPaddingInPixels
+ mRowPaddingStart + mRowPaddingEnd;
// Preferred width / height for images (excluding the padding).
// The actual width may be 1 pixel larger than this if we have a remainder.
final int imageWidth = (width - totalPaddingsInPixels) / mColumnCount;
final int remainder = width - (imageWidth * mColumnCount) - totalPaddingsInPixels;
final int height = (int) (mHeightToWidthRatio * imageWidth);
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final int childWidth = imageWidth + child.getPaddingRight()
// Compensate for the remainder
+ (i < remainder ? 1 : 0);
child.measure(
MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
);
}
setMeasuredDimension(width, height + getPaddingTop() + getPaddingBottom());
}
/**
* Gets the index of the item at the specified coordinates.
*
* @param itemX X-coordinate of the selected item.
* @param itemY Y-coordinate of the selected item.
* @return Index of the selected item in the cached array.
*/
public int getItemIndex(float itemX, float itemY) {
if (mMaxTiledRows == NO_ROW_LIMIT || mPosition < mMaxTiledRows) {
if (DEBUG) {
Log.v(TAG, String.valueOf(itemX) + " " + String.valueOf(itemY));
}
for (int i = 0; i < getChildCount(); ++i) {
/** If the row contains multiple tiles, checks each tile to see if the point
* is contained in the tile. */
final View child = getChildAt(i);
/** The coordinates passed in are based on the ListView,
* translate for each child first */
final int xInListView = child.getLeft() + getLeft();
final int yInListView = child.getTop() + getTop();
final int distanceX = (int) itemX - xInListView;
final int distanceY = (int) itemY - yInListView;
if ((distanceX > 0 && distanceX < child.getWidth()) &&
(distanceY > 0 && distanceY < child.getHeight())) {
/** If the point is contained in the rectangle, computes the index of the
* item in the cached array. */
return i + (mPosition) * mColumnCount;
}
}
} else {
/** If the selected item is one of the rows, compute the index. */
return getRegularRowItemIndex();
}
return -1;
}
/**
* Gets the index of the regular row item.
*
* @return Index of the selected item in the cached array.
*/
public int getRegularRowItemIndex() {
return (mPosition - mMaxTiledRows) + mColumnCount * mMaxTiledRows;
}
public PhoneFavoritesTileAdapter getTileAdapter() {
return PhoneFavoritesTileAdapter.this;
}
public int getPosition() {
return mPosition;
}
/**
* Find the view under the pointer.
*/
public View getViewAtPosition(int x, int y) {
// find the view under the pointer, accounting for GONE views
final int count = getChildCount();
View view;
for (int childIdx = 0; childIdx < count; childIdx++) {
view = getChildAt(childIdx);
if (x >= view.getLeft() && x <= view.getRight()) {
return view;
}
}
return null;
}
@Override
public View getChildAtPosition(MotionEvent ev) {
final View view = getViewAtPosition((int) ev.getX(), (int) ev.getY());
if (view != null &&
SwipeHelper.isSwipeable(view) &&
view.getVisibility() != GONE) {
// If this view is swipable, then return it. If not, because the removal
// dialog is currently showing, then return a null view, which will simply
// be ignored by the swipe helper.
return view;
}
return null;
}
@Override
public View getChildContentView(View v) {
return v.findViewById(R.id.contact_favorite_card);
}
@Override
public void onScroll() {}
@Override
public boolean canChildBeDismissed(View v) {
return true;
}
@Override
public void onBeginDrag(View v) {
removePendingContactEntry();
final int index = indexOfChild(v);
/*
if (index > 0) {
detachViewFromParent(index);
attachViewToParent(v, 0, v.getLayoutParams());
}*/
// We do this so the underlying ScrollView knows that it won't get
// the chance to intercept events anymore
requestDisallowInterceptTouchEvent(true);
}
@Override
public void onChildDismissed(View v) {
if (v != null) {
if (mOnItemSwipeListener != null) {
mOnItemSwipeListener.onSwipe(v);
}
}
}
@Override
public void onDragCancelled(View v) {}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mSwipeHelper != null && isSwipeEnabled()) {
return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev);
} else {
return super.onInterceptTouchEvent(ev);
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mSwipeHelper != null && isSwipeEnabled()) {
return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev);
} else {
return super.onTouchEvent(ev);
}
}
public int getItemViewType() {
return mItemViewType;
}
public void setOnItemSwipeListener(OnItemGestureListener listener) {
mOnItemSwipeListener = listener;
}
}
/**
* Used when a contact is swiped away. This will both unstar and set pinned position of the
* contact to PinnedPosition.DEMOTED so that it doesn't show up anymore in the favorites list.
*/
private void unstarAndUnpinContact(Uri contactUri) {
final ContentValues values = new ContentValues(2);
values.put(Contacts.STARRED, false);
values.put(Contacts.PINNED, PinnedPositions.DEMOTED);
mContext.getContentResolver().update(contactUri, values, null, null);
}
/**
* Given a list of contacts that each have pinned positions, rearrange the list (destructive)
* such that all pinned contacts are in their defined pinned positions, and unpinned contacts
* take the spaces between those pinned contacts. Demoted contacts should not appear in the
* resulting list.
*
* This method also updates the pinned positions of pinned contacts so that they are all
* unique positive integers within range from 0 to toArrange.size() - 1. This is because
* when the contact entries are read from the database, it is possible for them to have
* overlapping pin positions due to sync or modifications by third party apps.
*/
@VisibleForTesting
/* package */ void arrangeContactsByPinnedPosition(ArrayList<ContactEntry> toArrange) {
final PriorityQueue<ContactEntry> pinnedQueue =
new PriorityQueue<ContactEntry>(PIN_LIMIT, mContactEntryComparator);
final List<ContactEntry> unpinnedContacts = new LinkedList<ContactEntry>();
final int length = toArrange.size();
for (int i = 0; i < length; i++) {
final ContactEntry contact = toArrange.get(i);
// Decide whether the contact is hidden(demoted), pinned, or unpinned
if (contact.pinned > PIN_LIMIT) {
unpinnedContacts.add(contact);
} else if (contact.pinned > PinnedPositions.DEMOTED) {
// Demoted or contacts with negative pinned positions are ignored.
// Pinned contacts go into a priority queue where they are ranked by pinned
// position. This is required because the contacts provider does not return
// contacts ordered by pinned position.
pinnedQueue.add(contact);
}
}
final int maxToPin = Math.min(PIN_LIMIT, pinnedQueue.size() + unpinnedContacts.size());
toArrange.clear();
for (int i = 0; i < maxToPin; i++) {
if (!pinnedQueue.isEmpty() && pinnedQueue.peek().pinned <= i) {
final ContactEntry toPin = pinnedQueue.poll();
toPin.pinned = i;
toArrange.add(toPin);
} else if (!unpinnedContacts.isEmpty()) {
toArrange.add(unpinnedContacts.remove(0));
}
}
// If there are still contacts in pinnedContacts at this point, it means that the pinned
// positions of these pinned contacts exceed the actual number of contacts in the list.
// For example, the user had 10 frequents, starred and pinned one of them at the last spot,
// and then cleared frequents. Contacts in this situation should become unpinned.
while (!pinnedQueue.isEmpty()) {
final ContactEntry entry = pinnedQueue.poll();
entry.pinned = PinnedPositions.UNPINNED;
toArrange.add(entry);
}
// Any remaining unpinned contacts that weren't in the gaps between the pinned contacts
// now just get appended to the end of the list.
toArrange.addAll(unpinnedContacts);
}
/**
* Given an existing list of contact entries and a single entry that is to be pinned at a
* particular position, return a ContentValues object that contains new pinned positions for
* all contacts that are forced to be pinned at new positions, trying as much as possible to
* keep pinned contacts at their original location.
*
* At this point in time the pinned position of each contact in the list has already been
* updated by {@link #arrangeContactsByPinnedPosition}, so we can assume that all pinned
* positions(within {@link #PIN_LIMIT} are unique positive integers.
*/
@VisibleForTesting
/* package */ ContentValues getReflowedPinnedPositions(ArrayList<ContactEntry> list,
ContactEntry entryToPin, int oldPos, int newPinPos) {
final ContentValues cv = new ContentValues();
final int lowerBound = Math.min(oldPos, newPinPos);
final int upperBound = Math.max(oldPos, newPinPos);
for (int i = lowerBound; i <= upperBound; i++) {
final ContactEntry entry = list.get(i);
if (entry.pinned == i) continue;
cv.put(String.valueOf(entry.id), i);
}
return cv;
}
protected static class ViewTypes {
public static final int FREQUENT = 0;
public static final int TOP = 1;
public static final int COUNT = 2;
}
@Override
public void onSwipe(View view) {
final PhoneFavoriteTileView tileView = (PhoneFavoriteTileView) view.findViewById(
R.id.contact_tile);
// When the view is in the removal dialog, it should no longer be swipeable
SwipeHelper.setSwipeable(view, false);
tileView.displayRemovalDialog();
final Integer entryIndex = (Integer) tileView.getTag(
ContactTileRow.CONTACT_ENTRY_INDEX_TAG);
setPotentialRemoveEntryIndex(entryIndex);
}
@Override
public void onTouch() {
removePendingContactEntry();
return;
}
@Override
public boolean isSwipeEnabled() {
return !mAwaitingRemove;
}
@Override
public void onDragStarted(int itemIndex, int x, int y, PhoneFavoriteTileView view) {
setInDragging(true);
popContactEntry(itemIndex);
}
@Override
public void onDragHovered(int itemIndex, int x, int y) {
if (mInDragging &&
mDragEnteredEntryIndex != itemIndex &&
isIndexInBound(itemIndex) &&
itemIndex < PIN_LIMIT &&
itemIndex >= 0) {
markDropArea(itemIndex);
}
}
@Override
public void onDragFinished(int x, int y) {
setInDragging(false);
// A contact has been dragged to the RemoveView in order to be unstarred, so simply wait
// for the new contact cursor which will cause the UI to be refreshed without the unstarred
// contact.
if (!mAwaitingRemove) {
handleDrop();
}
}
@Override
public void onDroppedOnRemove() {
if (mDraggedEntry != null) {
unstarAndUnpinContact(mDraggedEntry.lookupKey);
mAwaitingRemove = true;
}
}
}