summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorke Lee <yorkelee@google.com>2013-07-31 16:05:46 -0700
committerYorke Lee <yorkelee@google.com>2013-08-07 14:10:44 -0700
commit55f3ec9f882b2389e498ea68f68ce8670c5d4c73 (patch)
tree8c40696ca3cd739bb606f7e90ecc23d4e2ae8662
parentfb585079cc4c522c27f6dd6bf03fd296535960f3 (diff)
Integrate pinning support with drag and drop UI
* Added the concept of a blank entry. This is used to keep the rest of the list in place while a contact is being dragged. Blank entries appear as invisible views for now. * Added the concept of a DEMOTED contact, which is used to indicate that a contact should never be displayed in the favorites list. * Pinned position management methods and tests Added two methods to handle assigning of contact positions based on their pinned positions, as well as reflowing of contacts when a contact is pinned at a new position. The assigning of contact positions works as defined below: 1) All contacts with pinned positions are assigned their defined pinned positions as necessary. 2) If two contacts have been given the same pinned position, they are ranked based on the order returned from the ContactsProvider ( alphabetical by display name). The second contact is then pushed to the next position, bumping all corresponding pinned contacts down by one spot as necessary. 3) If there is a contact with a pinned position that is greater than the number of starred + frequent contacts (this can happen if user clears frequents, for example), then it is simply treated as an unpinned contacts. 4) All unpinned contacts flow to fill in the gaps between pinned contacts. The reflowing of contacts works as defined below: 1) Pinning a contact at a new spot replacing an unpinned contact simply pins it at that position. 2) Pinning a contact at a new spot where there was already an existing pinned contact bumps that replaced pinned contact down by one spot alongside all subsequent pinned contacts as necessary. I.e. contact A is pinned at position 2. Before: [A][1][P][P][4][P] After: [0][1][A][P][P][P] 3) If there is no more space at the end of the list to bump the pinned contacts one spot further, then the replaced pinned contacts will shift to the left to take the space of the original moved contact. I.e. contact A is pinned at position 3. Before: [A][1][P][P][P][P] After: [0][P][P][A][P][P] * We no longer perform dynamic updates of the in-memory list. Instead, when a contact is dropped at a new location, the new positions are sent to the ContactsProvider, which updates the database and notifies the adapter that there is a new cursor with updated data. The new list of ContactEntries is then reconstructed from this new cursor. Note that animation behavior with the newly introduced pinned contacts is pretty janky looking - Instead of flowing together with the rest of the unpinned contacts, pinned contacts should remain in position and not animate on a dataset change. Likewise for empty contacts. In order to support this, the animation logic will have to be rewritten to take into account not just soley where the dragged contact was dropped, but also the old and new position of each and every single contact in the list, before and after the database refresh. Change-Id: I5c9542a7fec529f5d965eebe76acbebe1fc10357
-rw-r--r--src/com/android/dialer/DialtactsActivity.java5
-rw-r--r--src/com/android/dialer/list/PhoneFavoriteTileView.java8
-rw-r--r--src/com/android/dialer/list/PhoneFavoritesTileAdapter.java218
-rw-r--r--tests/src/com/android/dialer/list/PhoneFavoritesTileAdapterTest.java46
4 files changed, 248 insertions, 29 deletions
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index a0153ea8b..16fdb042b 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -183,6 +183,11 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O
public void onHomeInActionBarSelected() {
exitSearchUi();
}
+
+ @Override
+ public void onCallNumberDirectly(String phoneNumber) {
+
+ }
};
/**
diff --git a/src/com/android/dialer/list/PhoneFavoriteTileView.java b/src/com/android/dialer/list/PhoneFavoriteTileView.java
index 4efc4dbb9..8903e4b9b 100644
--- a/src/com/android/dialer/list/PhoneFavoriteTileView.java
+++ b/src/com/android/dialer/list/PhoneFavoriteTileView.java
@@ -78,6 +78,14 @@ public abstract class PhoneFavoriteTileView extends ContactTileView {
if (entry != null) {
// Grab the phone-number to call directly... see {@link onClick()}
mPhoneNumberString = entry.phoneNumber;
+ // If this is a blank entry, don't show anything.
+ // TODO krelease:Just hide the view for now. For this to truly look like an empty row
+ // the entire ContactTileRow needs to be hidden.
+ if (entry == ContactEntry.BLANK_ENTRY) {
+ setVisibility(View.INVISIBLE);
+ } else {
+ setVisibility(View.VISIBLE);
+ }
}
}
diff --git a/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java
index 992cb1f08..614a0d319 100644
--- a/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java
+++ b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java
@@ -17,14 +17,15 @@ package com.android.dialer.list;
import android.animation.ObjectAnimator;
import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
-import android.graphics.Color;
import android.graphics.Rect;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.PinnedPositions;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -35,9 +36,17 @@ 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.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ComparisonChain;
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.
@@ -94,13 +103,26 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter {
private int mPhoneNumberIndex;
private int mPhoneNumberTypeIndex;
private int mPhoneNumberLabelIndex;
+ protected int mPinnedIndex;
+ protected int mContactIdForFrequentIndex;
- private boolean mIsQuickContactEnabled = false;
private final int mPaddingInPixels;
/** Indicates whether a drag is in process. */
private boolean mInDragging = false;
+ private static final int PIN_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 PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener,
int numCols) {
this(context, listener, numCols, ROW_LIMIT_DEFAULT);
@@ -143,10 +165,6 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter {
mColumnCount = columnCount;
}
- public void enableQuickContact(boolean enableQuickContact) {
- mIsQuickContactEnabled = enableQuickContact;
- }
-
/**
* Indicates whether a drag is in process.
*
@@ -177,6 +195,8 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter {
mPhoneNumberIndex = ContactTileLoaderFactory.PHONE_NUMBER;
mPhoneNumberTypeIndex = ContactTileLoaderFactory.PHONE_NUMBER_TYPE;
mPhoneNumberLabelIndex = ContactTileLoaderFactory.PHONE_NUMBER_LABEL;
+ mPinnedIndex = ContactTileLoaderFactory.PINNED;
+ mContactIdForFrequentIndex = ContactTileLoaderFactory.CONTACT_ID_FOR_FREQUENT;
}
/**
@@ -213,14 +233,28 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter {
*/
private void saveCursorToCache(Cursor cursor) {
mContactEntries.clear();
+
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
final long id = cursor.getLong(mIdIndex);
+
final String photoUri = cursor.getString(mPhotoUriIndex);
final String lookupKey = cursor.getString(mLookupIndex);
final ContactEntry contact = new ContactEntry();
+
+ final int pinned = cursor.getInt(mPinnedIndex);
+ final int starred = cursor.getInt(mStarredIndex);
+
final String name = cursor.getString(mNameIndex);
+
+ if (starred > 0) {
+ contact.id = id;
+ } else {
+ // The contact id for frequent contacts is stored in the .contact_id field rather
+ // than the _id field
+ contact.id = cursor.getLong(mContactIdForFrequentIndex);
+ }
contact.name = (name != null) ? name : mResources.getString(R.string.missing_name);
contact.status = cursor.getString(mStatusIndex);
contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null);
@@ -233,8 +267,14 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter {
contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType,
phoneNumberCustomLabel);
contact.phoneNumber = cursor.getString(mPhoneNumberIndex);
+
+ contact.pinned = pinned;
mContactEntries.add(contact);
}
+
+ arrangeContactsByPinnedPosition(mContactEntries);
+
+ notifyDataSetChanged();
}
/**
@@ -367,7 +407,7 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter {
@Override
public void notifyDataSetChanged() {
if (DEBUG) {
- Log.v(TAG, "nofigyDataSetChanged");
+ Log.v(TAG, "notifyDataSetChanged");
}
super.notifyDataSetChanged();
}
@@ -381,20 +421,10 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter {
*/
private void configureAnimationToView(ContactTileRow contactTileRowView, int position,
int itemViewType) {
- if (mInDragging) {
- // If the one item above the row is being dragged, animates all following items to
- // move up. If the item is a favorite tile, animate it to appear from right.
- if (position >= getRowIndex(mDraggedEntryIndex)) {
- if (itemViewType == ViewTypes.FREQUENT) {
- mTranslateVerticalAnimation.setTarget(contactTileRowView);
- mTranslateVerticalAnimation.setFloatValues(contactTileRowView.getHeight(), 0);
- mTranslateVerticalAnimation.clone().start();
- } else {
- contactTileRowView.animateTilesAppearLeft(mDraggedEntryIndex -
- position * mColumnCount);
- }
- }
- } else if (mDropEntryIndex != -1) {
+ // No need to animate anything if we are just entering a drag, because the blank
+ // entry takes the place of the dragged entry anyway.
+ if (mInDragging) return;
+ if (mDropEntryIndex != -1) {
// If one item is dropped in front the row, animate all following rows to shift down.
// If the item is a favorite tile, animate it to appear from left.
if (position >= getRowIndex(mDropEntryIndex)) {
@@ -497,8 +527,8 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter {
public void popContactEntry(int index) {
if (index >= 0 && index < mContactEntries.size()) {
mDraggedEntry = mContactEntries.get(index);
+ mContactEntries.set(index, ContactEntry.BLANK_ENTRY);
mDraggedEntryIndex = index;
- mContactEntries.remove(index);
notifyDataSetChanged();
}
}
@@ -509,17 +539,30 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter {
* @param index Location where the contact will be dropped.
*/
public void dropContactEntry(int index) {
+ boolean changed = false;
if (mDraggedEntry != null) {
if (index >= 0 && index <= mContactEntries.size()) {
- mContactEntries.add(index, mDraggedEntry);
+ // 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 = index;
+ changed = true;
} else if (mDraggedEntryIndex >= 0 && mDraggedEntryIndex <= mContactEntries.size()) {
/** If the index is invalid, falls back to the original position of the contact. */
- mContactEntries.add(mDraggedEntryIndex, mDraggedEntry);
+ mContactEntries.set(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().appendQueryParameter(
+ PinnedPositions.STAR_WHEN_PINNING, "true").build();
+ // update the database here with the new pinned positions
+ mContext.getContentResolver().update(pinUri, cv, null, null);
}
mDraggedEntry = null;
- notifyDataSetChanged();
}
}
@@ -542,14 +585,14 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter {
}
/**
- * Removes a contact entry from the cache.
+ * 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 removeContactEntry() {
if (mPotentialRemoveEntryIndex >= 0 && mPotentialRemoveEntryIndex < mContactEntries.size()) {
- mContactEntries.remove(mPotentialRemoveEntryIndex);
- notifyDataSetChanged();
+ final ContactEntry entry = mContactEntries.get(mPotentialRemoveEntryIndex);
+ unstarAndUnpinContact(entry.lookupKey);
return true;
}
return false;
@@ -575,7 +618,6 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter {
/**
* Acts as a row item composed of {@link ContactTileView}
*
- * TODO: FREQUENT doesn't really need it. Just let {@link #getView} return
*/
public class ContactTileRow extends FrameLayout {
private int mItemViewType;
@@ -810,6 +852,124 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter {
}
}
+ /**
+ * 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();
+
+ // Add the dragged contact at the user-requested spot.
+ cv.put(String.valueOf(entryToPin.id), newPinPos);
+
+ final int listSize = list.size();
+ if (oldPos < newPinPos && list.get(listSize - 1).pinned == (listSize - 1)) {
+ // The only time we should get here is it we are completely full - i.e. starting
+ // from the newly pinned contact to the end of the list, every single contact
+ // thereafter is pinned, and a contact is being shifted to the right by the user.
+ // Instead of trying to make room to the right, we should thus try to shift contacts
+ // to the left instead, working backwards through the list, starting from the contact
+ // which just got bumped.
+ for (int i = newPinPos; i >= 0; i--) {
+ final ContactEntry entry = list.get(i);
+ // Once we find an unpinned spot(or a blank entry), we can stop pushing contacts
+ // to the left.
+ if (entry.pinned > PIN_LIMIT) break;
+ cv.put(String.valueOf(entry.id), entry.pinned - 1);
+ }
+ } else {
+ // Shift any pinned contacts to the right as necessary, until an unpinned
+ // spot is found
+ for (int i = newPinPos; i < PIN_LIMIT && i < list.size(); i++) {
+ final ContactEntry entry = list.get(i);
+ if (entry.pinned > PIN_LIMIT) break;
+ cv.put(String.valueOf(entry.id), entry.pinned + 1);
+ }
+ }
+ return cv;
+ }
+
protected static class ViewTypes {
public static final int COUNT = 2;
public static final int FREQUENT = 0;
diff --git a/tests/src/com/android/dialer/list/PhoneFavoritesTileAdapterTest.java b/tests/src/com/android/dialer/list/PhoneFavoritesTileAdapterTest.java
new file mode 100644
index 000000000..40d171c8c
--- /dev/null
+++ b/tests/src/com/android/dialer/list/PhoneFavoritesTileAdapterTest.java
@@ -0,0 +1,46 @@
+package com.android.dialer.list;
+
+import android.test.AndroidTestCase;
+
+public class PhoneFavoritesTileAdapterTest extends AndroidTestCase {
+ private PhoneFavoritesTileAdapter mAdapter;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mAdapter = new PhoneFavoritesTileAdapter(getContext(), null, 3, 1);
+ }
+
+ /**
+ * TODO Krelease: Add tests
+ *
+ * Test cases (various combinations of):
+ * No pinned contacts
+ * One pinned contact
+ * Multiple pinned contacts with differing pinned positions
+ * Multiple pinned contacts with conflicting pinned positions
+ * Pinned contacts with pinned positions at the start, middle, end, and outside the list
+ */
+ public void testArrangeContactsByPinnedPosition() {
+
+ }
+
+ /**
+ * TODO Krelease: Add tests
+ *
+ * This method assumes that contacts have already been reordered by
+ * arrangeContactsByPinnedPosition, so we can test it with a less expansive set of test data.
+ *
+ * Test cases:
+ * Pin a single contact at the start, middle and end of a completely unpinned list
+ * Pin a single contact at the start, middle and end of a list with various numbers of
+ * pinned contacts
+ * Pin a single contact at the start, middle and end of a list where all contacts are pinned
+ * such that contacts are forced to the left as necessary.
+ */
+ public void testGetReflowedPinnedPositions() {
+
+ }
+
+
+}