Adds Drag and Drop UI to the Dialer main view.

- Adds drag and drop listner.
- Changes the FavoritesTileAdapter to use an array stored in cache to
  populate the view.
- Adds animation for drag and drop.
- Adds swipe to delete an entry.

Change-Id: I0717fb3d256b2ab2353f86a998de07edb24e9b4c
diff --git a/res/layout/phone_favorite_regular_row_view.xml b/res/layout/phone_favorite_regular_row_view.xml
index eda5a02..532e858 100644
--- a/res/layout/phone_favorite_regular_row_view.xml
+++ b/res/layout/phone_favorite_regular_row_view.xml
@@ -18,14 +18,14 @@
 <view
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/contact_tile_frequent_phone"
-    class="com.android.dialer.list.PhoneFavoriteRegularRowView"
-    android:focusable="true"
-    android:background="?android:attr/selectableItemBackground"
-    android:nextFocusLeft="@+id/contact_tile_quick">
+    class="com.android.dialer.list.PhoneFavoriteRegularRowView">
 
     <RelativeLayout
+        android:id="@+id/contact_favorite_card"
         android:layout_width="match_parent"
-        android:layout_height="match_parent" >
+        android:layout_height="match_parent"
+        android:focusable="true"
+        android:background="?android:attr/selectableItemBackground" >
 
         <com.android.contacts.common.widget.LayoutSuppressingQuickContactBadge
             android:id="@id/contact_tile_quick"
@@ -54,4 +54,46 @@
             android:textAlignment="viewStart" />
     </RelativeLayout>
 
+    <LinearLayout
+        android:id="@+id/favorite_remove_dialogue"
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentStart="true"
+        android:gravity="center_vertical"
+        android:alpha="0.0"
+        android:visibility="gone">
+
+        <TextView
+            android:id="@+id/favorite_remove_dialogue_text"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:text="@string/favorite_hidden"
+            android:ellipsize="marquee"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:layout_marginLeft="8dip"
+            android:layout_marginStart="8dip"
+            android:singleLine="true"
+            android:layout_gravity="center_vertical"
+            android:textDirection="ltr"
+            android:textAlignment="viewStart" />
+
+        <TextView
+            android:id="@+id/favorite_remove_undo_button"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_weight="0.5"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:ellipsize="marquee"
+            android:singleLine="true"
+            android:text="@string/favorite_hidden_undo"
+            android:layout_marginLeft="8dip"
+            android:layout_marginStart="8dip"
+            android:gravity="end"
+            android:layout_gravity="center_vertical"
+            android:clickable="true"/>
+
+    </LinearLayout>
 </view>
diff --git a/res/layout/phone_favorite_tile_view.xml b/res/layout/phone_favorite_tile_view.xml
index e82a4e2..2865c2a 100644
--- a/res/layout/phone_favorite_tile_view.xml
+++ b/res/layout/phone_favorite_tile_view.xml
@@ -16,14 +16,17 @@
 <view
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:background="@null"
-    android:paddingBottom="1dp"
-    android:paddingRight="1dp"
-    android:paddingEnd="1dp"
-    class="com.android.dialer.list.PhoneFavoriteTileView" >
+    android:paddingBottom="1dip"
+    android:paddingRight="1dip"
+    android:paddingEnd="1dip"
+    class="com.android.dialer.list.PhoneFavoriteSquareTileView" >
 
     <RelativeLayout
+        android:id="@+id/contact_tile_favorite_card"
         android:layout_width="match_parent"
-        android:layout_height="match_parent">
+        android:layout_height="match_parent"
+        android:focusable="true"
+        android:background="?android:attr/selectableItemBackground" >>
 
         <com.android.contacts.common.widget.LayoutSuppressingImageView
             android:id="@+id/contact_tile_image"
@@ -83,4 +86,47 @@
 
     </RelativeLayout>
 
+    <LinearLayout
+        android:id="@+id/favorite_tile_remove_dialogue"
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentStart="true"
+        android:gravity="center_horizontal"
+        android:alpha="0.0"
+        android:visibility="gone">
+
+        <TextView
+            android:id="@+id/favorite_tile_remove_dialogue_text"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:text="@string/favorite_hidden"
+            android:ellipsize="marquee"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:layout_marginLeft="8dip"
+            android:layout_marginStart="8dip"
+            android:singleLine="true"
+            android:layout_gravity="center_horizontal"
+            android:textDirection="ltr"
+            android:textAlignment="viewStart" />
+
+        <TextView
+            android:id="@+id/favorite_tile_remove_undo_button"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_weight="0.5"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:ellipsize="marquee"
+            android:singleLine="true"
+            android:text="@string/favorite_hidden_undo"
+            android:layout_marginLeft="8dip"
+            android:layout_marginStart="8dip"
+            android:gravity="end"
+            android:layout_gravity="center_horizontal"
+            android:clickable="true"/>
+
+    </LinearLayout>
+
 </view>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 06d0a1e..307d56b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -606,4 +606,8 @@
     <string name="show_all_contacts_title">All contacts</string>
     <!--  Title of show all contacts button -->
     <string name="show_all_contacts_button_text">All contacts</string>
+    <!--  Text displayed when user swipes out a favorite contact -->
+    <string name="favorite_hidden">Hidden from favorites</string>
+    <!--  Text displayed for the undo button to undo removing a favorite contact -->
+    <string name="favorite_hidden_undo">Undo</string>
 </resources>
diff --git a/src/com/android/dialer/list/NewPhoneFavoriteFragment.java b/src/com/android/dialer/list/NewPhoneFavoriteFragment.java
index ba438cf..eba9310 100644
--- a/src/com/android/dialer/list/NewPhoneFavoriteFragment.java
+++ b/src/com/android/dialer/list/NewPhoneFavoriteFragment.java
@@ -26,6 +26,7 @@
 import android.os.Bundle;
 import android.util.Log;
 import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
diff --git a/src/com/android/dialer/list/PhoneFavoriteDragAndDropListeners.java b/src/com/android/dialer/list/PhoneFavoriteDragAndDropListeners.java
new file mode 100644
index 0000000..846b2a7
--- /dev/null
+++ b/src/com/android/dialer/list/PhoneFavoriteDragAndDropListeners.java
@@ -0,0 +1,196 @@
+/*
+ * 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 android.content.ClipData;
+import android.util.Log;
+import android.view.DragEvent;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnDragListener;
+
+import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow;
+
+/**
+ * Implements the OnLongClickListener and OnDragListener for phone's favorite tiles and rows.
+ */
+public class PhoneFavoriteDragAndDropListeners {
+
+    private static final String TAG = PhoneFavoriteDragAndDropListeners.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    private static final float FLING_HEIGHT_PORTION = 1.f / 4.f;
+    private static final float FLING_WIDTH_PORTION = 1.f / 6.f;
+
+    public static class PhoneFavoriteGestureListener extends SimpleOnGestureListener {
+        private static final float FLING_VELOCITY_MINIMUM = 5.0f;
+        private float mFlingHorizontalThreshold;
+        private float mFlingVerticalThreshold;
+        private final PhoneFavoriteTileView mView;
+
+        public PhoneFavoriteGestureListener(View view) {
+            super();
+            mView = (PhoneFavoriteTileView) view;
+        }
+
+        @Override
+        public void onLongPress(MotionEvent event) {
+            final ClipData data = ClipData.newPlainText("", "");
+            final View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(mView);
+            mView.setPressed(false);
+            if (mView instanceof PhoneFavoriteRegularRowView) {
+                // If the view is regular row, start drag the row view.
+                // TODO: move the padding so we can start drag the original view.
+                mView.getParentRow().startDrag(data, shadowBuilder, null, 0);
+            } else {
+                // If the view is a tile view, start drag the tile.
+                mView.startDrag(data, shadowBuilder, null, 0);
+            }
+        }
+
+        @Override
+        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+            final float x1 = e1.getX();
+            final float x2 = e2.getX();
+            // Temporarily disables parents from getting this event so the listview does not scroll.
+            mView.getParent().requestDisallowInterceptTouchEvent(true);
+            mView.setScrollOffset(x2 - x1);
+            return true;
+        }
+
+        @Override
+        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+            // Sets fling trigger threshold.
+            mFlingVerticalThreshold = (float) mView.getHeight() * FLING_HEIGHT_PORTION;
+            mFlingHorizontalThreshold = (float) mView.getWidth() * FLING_WIDTH_PORTION;
+            final float x1 = e1.getX();
+            final float x2 = e2.getX();
+            final float y1 = e1.getY();
+            final float y2 = e2.getY();
+
+            mView.setPressed(false);
+
+            if (Math.abs(y1 - y2) < mFlingVerticalThreshold &&
+                    Math.abs(x2 - x1) > mFlingHorizontalThreshold &&
+                    Math.abs(velocityX) > FLING_VELOCITY_MINIMUM) {
+                // If fling is triggered successfully, end the scroll and setup removal dialogue.
+                final int removeIndex = mView.getParentRow().getItemIndex(x1, y1);
+                mView.setScrollEnd(false);
+                mView.setupRemoveDialogue();
+                mView.getParentRow().getTileAdapter().setPotentialRemoveEntryIndex(removeIndex);
+
+                return true;
+            } else {
+                mView.setScrollEnd(true);
+                return false;
+            }
+        }
+
+        @Override
+        public boolean onDown(MotionEvent e) {
+            mView.setPressed(true);
+            // Signals that the view will accept further events.
+            return true;
+        }
+
+        @Override
+        public boolean onSingleTapUp(MotionEvent e) {
+            mView.performClick();
+            return true;
+        }
+    }
+
+    /**
+     * Implements the OnDragListener to handle drag events.
+     */
+    public static class PhoneFavoriteDragListener implements OnDragListener {
+        /** Location of the drag event. */
+        private float mX = 0;
+        private float mY = 0;
+        private final ContactTileRow mContactTileRow;
+        private final PhoneFavoritesTileAdapter mTileAdapter;
+
+        public PhoneFavoriteDragListener(ContactTileRow contactTileRow,
+                PhoneFavoritesTileAdapter tileAdapter) {
+            super();
+            mContactTileRow = contactTileRow;
+            mTileAdapter = tileAdapter;
+        }
+
+        @Override
+        public boolean onDrag(View v, DragEvent event) {
+            // Handles drag events.
+            switch (event.getAction()) {
+                case DragEvent.ACTION_DRAG_STARTED:
+                    break;
+                case DragEvent.ACTION_DRAG_ENTERED:
+                    break;
+                case DragEvent.ACTION_DRAG_EXITED:
+                    break;
+                case DragEvent.ACTION_DROP:
+                    // Gets the location of the drag with respect to the whole Dialer view.
+                    mX = event.getX() + v.getLeft();
+                    mY = event.getY() + v.getTop();
+
+                    // Indicates a drag has finished.
+                    if (mTileAdapter != null && mContactTileRow != null) {
+                        mTileAdapter.setInDragging(false);
+
+                        // Finds out at which position of the list the Contact is being dropped.
+                        final int dropIndex = mContactTileRow.getItemIndex(mX, mY);
+                        if (DEBUG) {
+                            Log.v(TAG, "Stop dragging " + String.valueOf(dropIndex));
+                        }
+
+                        // Adds the dragged contact to the drop position.
+                        mTileAdapter.dropContactEntry(dropIndex);
+                    }
+                    break;
+                case DragEvent.ACTION_DRAG_ENDED:
+                    break;
+                case DragEvent.ACTION_DRAG_LOCATION:
+                    // Gets the current drag location with respect to the whole Dialer view.
+                    mX = event.getX() + v.getLeft();
+                    mY = event.getY() + v.getTop();
+                    if (DEBUG) {
+                        Log.v(TAG, String.valueOf(mX) + "; " + String.valueOf(mY));
+                    }
+
+                    if (mTileAdapter != null && mContactTileRow != null) {
+                        // If there is no drag in process, initializes the drag.
+                        if (!mTileAdapter.getInDragging()) {
+                            // Finds out which item is being dragged.
+                            final int dragIndex = mContactTileRow.getItemIndex(mX, mY);
+                            if (DEBUG) {
+                                Log.v(TAG, "Start dragging " + String.valueOf(dragIndex));
+                            }
+
+                            // Indicates a drag has started.
+                            mTileAdapter.setInDragging(true);
+
+                            // Temporarily pops out the Contact entry.
+                            mTileAdapter.popContactEntry(dragIndex);
+                        }
+                    }
+                    break;
+                default:
+                    break;
+            }
+            return true;
+        }
+    }
+}
diff --git a/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java b/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java
index 2f5921e..6d9fdcb 100644
--- a/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java
+++ b/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java
@@ -16,29 +16,45 @@
 package com.android.dialer.list;
 
 import android.content.Context;
-import android.text.TextUtils;
 import android.util.AttributeSet;
-import android.view.View;
+import android.view.GestureDetector;
 
-import com.android.contacts.common.MoreContactUtils;
-import com.android.contacts.common.list.ContactEntry;
-import com.android.contacts.common.list.ContactTileView;
 import com.android.contacts.common.util.ViewUtil;
+import com.android.dialer.R;
+import com.android.dialer.list.PhoneFavoriteDragAndDropListeners.PhoneFavoriteDragListener;
+import com.android.dialer.list.PhoneFavoriteDragAndDropListeners.PhoneFavoriteGestureListener;
 
-/**
- * A light version of the {@link com.android.contacts.common.list.ContactTileView} that is used in Dialtacts
- * for frequently called contacts. Slightly different behavior from superclass...
- * when you tap it, you want to call the frequently-called number for the
- * contact, even if that is not the default number for that contact.
- */
-public class PhoneFavoriteRegularRowView extends ContactTileView {
-    private String mPhoneNumberString;
+import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow;
+
+
+public class PhoneFavoriteRegularRowView extends PhoneFavoriteTileView {
+    private static final String TAG = PhoneFavoriteRegularRowView.class.getSimpleName();
+    private static final boolean DEBUG = false;
 
     public PhoneFavoriteRegularRowView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mFavoriteContactCard = findViewById(R.id.contact_favorite_card);
+        mRemovalDialogue = findViewById(R.id.favorite_remove_dialogue);
+        mUndoRemovalButton = findViewById(R.id.favorite_remove_undo_button);
+
+        mGestureDetector = new GestureDetector(getContext(),
+                new PhoneFavoriteGestureListener(this));
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        mParentRow = (ContactTileRow) getParent();
+        mParentRow.setOnDragListener(new PhoneFavoriteDragListener(mParentRow,
+                mParentRow.getTileAdapter()));
+    }
+
+    @Override
     protected boolean isDarkTheme() {
         return false;
     }
@@ -47,36 +63,4 @@
     protected int getApproximateImageSize() {
         return ViewUtil.getConstantPreLayoutWidth(getQuickContact());
     }
-
-    @Override
-    public void loadFromContact(ContactEntry entry) {
-        super.loadFromContact(entry);
-        mPhoneNumberString = null; // ... in case we're reusing the view
-        if (entry != null) {
-            // Grab the phone-number to call directly... see {@link onClick()}
-            mPhoneNumberString = entry.phoneNumber;
-        }
-    }
-
-    @Override
-    protected OnClickListener createClickListener() {
-        return new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                if (mListener == null) return;
-                if (TextUtils.isEmpty(mPhoneNumberString)) {
-                    // Copy "superclass" implementation
-                    mListener.onContactSelected(getLookupUri(), MoreContactUtils
-                            .getTargetRectFromView(
-                                    mContext, PhoneFavoriteRegularRowView.this));
-                } else {
-                    // When you tap a frequently-called contact, you want to
-                    // call them at the number that you usually talk to them
-                    // at (i.e. the one displayed in the UI), regardless of
-                    // whether that's their default number.
-                    mListener.onCallNumberDirectly(mPhoneNumberString);
-                }
-            }
-        };
-    }
 }
diff --git a/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java b/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java
new file mode 100644
index 0000000..fe07d18
--- /dev/null
+++ b/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java
@@ -0,0 +1,82 @@
+/*
+
+ * Copyright (C) 2011 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 android.content.Context;
+import android.content.Intent;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.View;
+import android.widget.ImageButton;
+
+import com.android.contacts.common.R;
+import com.android.dialer.list.PhoneFavoriteDragAndDropListeners.PhoneFavoriteDragListener;
+import com.android.dialer.list.PhoneFavoriteDragAndDropListeners.PhoneFavoriteGestureListener;
+import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow;
+
+/**
+ * Displays the contact's picture overlayed with their name
+ * in a perfect square. It also has an additional touch target for a secondary action.
+ */
+public class PhoneFavoriteSquareTileView extends PhoneFavoriteTileView {
+    private static final String TAG = PhoneFavoriteSquareTileView.class.getSimpleName();
+    private ImageButton mSecondaryButton;
+
+    public PhoneFavoriteSquareTileView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mFavoriteContactCard = findViewById(com.android.dialer.R.id.contact_tile_favorite_card);
+        mRemovalDialogue = findViewById(com.android.dialer.R.id.favorite_tile_remove_dialogue);
+        mUndoRemovalButton = findViewById(com.android.dialer.R.id.favorite_tile_remove_undo_button);
+        mSecondaryButton = (ImageButton) findViewById(R.id.contact_tile_secondary_button);
+        mSecondaryButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent intent = new Intent(Intent.ACTION_VIEW, getLookupUri());
+                // Secondary target will be visible only from phone's favorite screen, then
+                // we want to launch it as a separate People task.
+                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                getContext().startActivity(intent);
+            }
+        });
+
+        mGestureDetector = new GestureDetector(getContext(),
+                new PhoneFavoriteGestureListener(this));
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        mParentRow = (ContactTileRow) getParent();
+        setOnDragListener(new PhoneFavoriteDragListener(mParentRow, mParentRow.getTileAdapter()));
+    }
+
+    @Override
+    protected boolean isDarkTheme() {
+        return false;
+    }
+
+    @Override
+    protected int getApproximateImageSize() {
+        // The picture is the full size of the tile (minus some padding, but we can be generous)
+        return mListener.getApproximateTileWidth();
+    }
+}
diff --git a/src/com/android/dialer/list/PhoneFavoriteTileView.java b/src/com/android/dialer/list/PhoneFavoriteTileView.java
index d87e2a8..9a1577a 100644
--- a/src/com/android/dialer/list/PhoneFavoriteTileView.java
+++ b/src/com/android/dialer/list/PhoneFavoriteTileView.java
@@ -1,4 +1,5 @@
 /*
+
  * Copyright (C) 2011 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,51 +16,239 @@
  */
 package com.android.dialer.list;
 
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.content.Context;
-import android.content.Intent;
+import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
 import android.view.View;
-import android.widget.ImageButton;
 
-import com.android.contacts.common.R;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.list.ContactEntry;
 import com.android.contacts.common.list.ContactTileView;
+import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow;
 
 /**
- * Displays the contact's picture overlayed with their name
- * in a perfect square. It also has an additional touch target for a secondary action.
+ * A light version of the {@link com.android.contacts.common.list.ContactTileView} that is used in
+ * Dialtacts for frequently called contacts. Slightly different behavior from superclass when you
+ * tap it, you want to call the frequently-called number for the contact, even if that is not the
+ * default number for that contact. This abstract class is the super class to both the row and tile
+ * view.
  */
-public class PhoneFavoriteTileView extends ContactTileView {
-    private ImageButton mSecondaryButton;
+public abstract class PhoneFavoriteTileView extends ContactTileView {
+
+    private static final String TAG = PhoneFavoriteTileView.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    /** Length of all animations in miniseconds. */
+    private static final int ANIMATION_LENGTH = 300;
+
+    /** The view that holds the front layer of the favorite contact card. */
+    protected View mFavoriteContactCard;
+    /** The view that holds the background layer of the removal dialogue. */
+    protected View mRemovalDialogue;
+    /** Undo button for undoing favorite removal. */
+    protected View mUndoRemovalButton;
+    /** The view that holds the list view row. */
+    protected ContactTileRow mParentRow;
+
+    /** Users' most frequent phone number. */
+    private String mPhoneNumberString;
+
+    /** Custom gesture detector.*/
+    protected GestureDetector mGestureDetector;
+    /** Indicator of whether a scroll has started. */
+    private boolean mInScroll;
 
     public PhoneFavoriteTileView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
+    public ContactTileRow getParentRow() {
+        return mParentRow;
+    }
 
-        mSecondaryButton = (ImageButton) findViewById(R.id.contact_tile_secondary_button);
-        mSecondaryButton.setOnClickListener(new OnClickListener() {
+    @Override
+    public void loadFromContact(ContactEntry entry) {
+        super.loadFromContact(entry);
+        mPhoneNumberString = null; // ... in case we're reusing the view
+        if (entry != null) {
+            // Grab the phone-number to call directly... see {@link onClick()}
+            mPhoneNumberString = entry.phoneNumber;
+        }
+    }
+
+    /**
+     * Gets the latest scroll gesture offset.
+     */
+    public void setScrollOffset(float offset) {
+        // Sets the mInScroll variable to indicate a scroll is in progress.
+        if (!mInScroll) {
+            mInScroll = true;
+        }
+
+        // Changes the view to follow user's scroll position.
+        shiftViewWithScroll(offset);
+    }
+
+    /**
+     * Shifts the view to follow user's scroll position.
+     */
+    private void shiftViewWithScroll(float offset) {
+       if (mInScroll) {
+           // Shifts the foreground card to follow users' scroll gesture.
+           mFavoriteContactCard.setTranslationX(offset);
+
+           // Changes transparency of the foreground and background color
+           final float alpha = 1.f - Math.abs((offset)) / getWidth();
+           final float cappedAlpha = Math.min(Math.max(alpha, 0.f), 1.f);
+           mFavoriteContactCard.setAlpha(cappedAlpha);
+       }
+    }
+
+    /**
+     * Sets the scroll has finished.
+     *
+     * @param isUnfinishedFling True if it is triggered from the onFling method, but the fling was
+     * too short or too slow, or from the scroll that does not trigger fling.
+     */
+    public void setScrollEnd(boolean isUnfinishedFling) {
+        mInScroll = false;
+
+        if (isUnfinishedFling) {
+            // If the fling is too short or too slow, or it is from a scroll, bring back the
+            // favorite contact card.
+            final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(mFavoriteContactCard, "alpha",
+                    1.f).setDuration(ANIMATION_LENGTH);
+            final ObjectAnimator moveBack = ObjectAnimator.ofFloat(mFavoriteContactCard,
+                    "translationX", 0.f).setDuration(ANIMATION_LENGTH);
+            final ObjectAnimator backgroundFadeOut = ObjectAnimator.ofInt(
+                    mParentRow.getBackground(), "alpha", 255).setDuration(ANIMATION_LENGTH);
+            final AnimatorSet animSet = new AnimatorSet();
+            animSet.playTogether(fadeIn, moveBack, backgroundFadeOut);
+            animSet.start();
+        } else {
+            // If the fling is fast and far enough, move away the favorite contact card, bring the
+            // favorite removal view to the foreground to ask user to confirm removal.
+            int animationLength = (int) ((1 - Math.abs(mFavoriteContactCard.getTranslationX()) /
+                    getWidth()) * ANIMATION_LENGTH);
+            final ObjectAnimator fadeOut = ObjectAnimator.ofFloat(mFavoriteContactCard, "alpha",
+                    0.f).setDuration(animationLength);
+            final ObjectAnimator moveAway = ObjectAnimator.ofFloat(mFavoriteContactCard,
+                    "translationX", getWidth()).setDuration(animationLength);
+            final ObjectAnimator backgroundFadeIn = ObjectAnimator.ofInt(
+                    mParentRow.getBackground(), "alpha", 0).setDuration(animationLength);
+            if (mFavoriteContactCard.getTranslationX() < 0) {
+                moveAway.setFloatValues(-getWidth());
+            }
+            final AnimatorSet animSet = new AnimatorSet();
+            animSet.playTogether(fadeOut, moveAway, backgroundFadeIn);
+            animSet.start();
+        }
+    }
+
+    /**
+     * Signals the user wants to undo removing the favorite contact.
+     */
+    public void undoRemove() {
+        // Makes the removal dialogue invisible.
+        mRemovalDialogue.setAlpha(0.0f);
+        mRemovalDialogue.setVisibility(GONE);
+
+        // Animates back the favorite contact card.
+        final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(mFavoriteContactCard, "alpha", 1.f).
+                setDuration(ANIMATION_LENGTH);
+        final ObjectAnimator moveBack = ObjectAnimator.ofFloat(mFavoriteContactCard, "translationX",
+                0.f).setDuration(ANIMATION_LENGTH);
+        final ObjectAnimator backgroundFadeOut = ObjectAnimator.ofInt(mParentRow.getBackground(),
+                "alpha", 255).setDuration(ANIMATION_LENGTH);
+        final AnimatorSet animSet = new AnimatorSet();
+        animSet.playTogether(fadeIn, moveBack, backgroundFadeOut);
+        animSet.start();
+
+        // Signals the PhoneFavoritesTileAdapter to undo the potential delete.
+        mParentRow.getTileAdapter().undoPotentialRemoveEntryIndex();
+    }
+
+    /**
+     * Sets up the removal dialogue.
+     */
+    public void setupRemoveDialogue() {
+        mRemovalDialogue.setVisibility(VISIBLE);
+        mRemovalDialogue.setAlpha(1.0f);
+
+        mUndoRemovalButton.setOnClickListener(new OnClickListener() {
             @Override
-            public void onClick(View v) {
-                Intent intent = new Intent(Intent.ACTION_VIEW, getLookupUri());
-                // Secondary target will be visible only from phone's favorite screen, then
-                // we want to launch it as a separate People task.
-                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-                getContext().startActivity(intent);
+            public void onClick(View view) {
+                undoRemove();
             }
         });
     }
 
-    @Override
-    protected boolean isDarkTheme() {
-        return false;
+    /**
+     * Sets up the favorite contact card.
+     */
+    public void setupFavoriteContactCard() {
+        if (mRemovalDialogue != null) {
+            mRemovalDialogue.setVisibility(GONE);
+            mRemovalDialogue.setAlpha(0.f);
+        }
+        mFavoriteContactCard.setAlpha(1.0f);
+        mFavoriteContactCard.setTranslationX(0.f);
     }
 
     @Override
-    protected int getApproximateImageSize() {
-        // The picture is the full size of the tile (minus some padding, but we can be generous)
-        return mListener.getApproximateTileWidth();
+    protected OnClickListener createClickListener() {
+        return new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mListener == null) return;
+                if (TextUtils.isEmpty(mPhoneNumberString)) {
+                    // Copy "superclass" implementation
+                    mListener.onContactSelected(getLookupUri(), MoreContactUtils
+                            .getTargetRectFromView(
+                                    mContext, PhoneFavoriteTileView.this));
+                } else {
+                    // When you tap a frequently-called contact, you want to
+                    // call them at the number that you usually talk to them
+                    // at (i.e. the one displayed in the UI), regardless of
+                    // whether that's their default number.
+                    mListener.onCallNumberDirectly(mPhoneNumberString);
+                }
+            }
+        };
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (DEBUG) {
+            Log.v(TAG, event.toString());
+        }
+        switch (event.getAction()) {
+            // If the scroll has finished without triggering a fling, handles it here.
+            case MotionEvent.ACTION_UP:
+                setPressed(false);
+                if (mInScroll) {
+                    if (!mGestureDetector.onTouchEvent(event)) {
+                        setScrollEnd(true);
+                    }
+                    return true;
+                }
+                break;
+            // When user starts a new gesture, clean up the pending removals.
+            case MotionEvent.ACTION_DOWN:
+                mParentRow.getTileAdapter().removeContactEntry();
+                break;
+            // When user continues with a new gesture, cleans up all the temp variables.
+            case MotionEvent.ACTION_CANCEL:
+                mParentRow.getTileAdapter().cleanTempVariables();
+                break;
+            default:
+                break;
+        }
+        return mGestureDetector.onTouchEvent(event);
     }
 }
diff --git a/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java
index 6a4476d..0a08f2c 100644
--- a/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java
+++ b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java
@@ -15,11 +15,13 @@
  */
 package com.android.dialer.list;
 
+import android.animation.ObjectAnimator;
 import android.content.ContentUris;
 import android.content.Context;
 import android.content.res.Resources;
 import android.database.Cursor;
-import android.graphics.drawable.Drawable;
+import android.graphics.Color;
+import android.graphics.Rect;
 import android.net.Uri;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.Contacts;
@@ -33,7 +35,6 @@
 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;
 import com.android.contacts.common.list.ContactTileView;
 
 import java.util.ArrayList;
@@ -45,14 +46,33 @@
  *
  */
 public class PhoneFavoritesTileAdapter extends BaseAdapter {
-    private static final String TAG = ContactTileAdapter.class.getSimpleName();
+    private static final String TAG = PhoneFavoritesTileAdapter.class.getSimpleName();
+    private static final boolean DEBUG = false;
 
     public static final int ROW_LIMIT_DEFAULT = 1;
 
+    /** Time period for an animation. */
+    private static final int ANIMATION_LENGTH = 300;
+
+    private final ObjectAnimator mTranslateHorizontalAnimation;
+    private final ObjectAnimator mTranslateVerticalAnimation;
+    private final ObjectAnimator mAlphaAnimation;
+
     private ContactTileView.Listener mListener;
     private Context mContext;
     private Resources mResources;
-    protected Cursor mContactCursor = null;
+
+    /** 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;
+    /** Position of the contact pending removal. */
+    private int mPotentialRemoveEntryIndex = -1;
+
     private ContactPhotoManager mPhotoManager;
     protected int mNumFrequents;
     protected int mNumStarred;
@@ -78,22 +98,36 @@
     private boolean mIsQuickContactEnabled = false;
     private final int mPaddingInPixels;
 
-    public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, int numCols) {
+    /** Indicates whether a drag is in process. */
+    private boolean mInDragging = false;
+
+    public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, 
+            int numCols) {
         this(context, listener, numCols, ROW_LIMIT_DEFAULT);
     }
 
-    public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, int numCols,
-            int maxTiledRows) {
+    public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener,
+            int numCols, int maxTiledRows) {
         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_padding);
+
+        // Initiates all animations.
+        mAlphaAnimation = ObjectAnimator.ofFloat(null, "alpha", 1.f).setDuration(ANIMATION_LENGTH);
+
+        mTranslateHorizontalAnimation = ObjectAnimator.ofFloat(null, "translationX", 0.f).
+                setDuration(ANIMATION_LENGTH);
+
+        mTranslateVerticalAnimation = ObjectAnimator.ofFloat(null, "translationY", 0.f).setDuration(
+                ANIMATION_LENGTH);
+
         bindColumnIndices();
     }
 
@@ -114,6 +148,20 @@
     }
 
     /**
+     * Indicates whether a drag is in process.
+     *
+     * @param inDragging Boolean variable indicating whether there is a drag in process.
+     */
+    public void setInDragging(boolean 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}.
      */
@@ -148,13 +196,49 @@
      * Else use {@link ContactTileLoaderFactory}
      */
     public void setContactCursor(Cursor cursor) {
-        mContactCursor = cursor;
-        mNumStarred = getNumStarredContacts(cursor);
+        if (cursor != null && !cursor.isClosed()) {
+            mNumStarred = getNumStarredContacts(cursor);
+            saveNumFrequentsFromCursor(cursor);
+            saveCursorToCache(cursor);
 
-        saveNumFrequentsFromCursor(cursor);
+            // cause a refresh of any views that rely on this data
+            notifyDataSetChanged();
+        }
+    }
 
-        // cause a refresh of any views that rely on this data
-        notifyDataSetChanged();
+    /**
+     * 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();
+        try {
+            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 String name = cursor.getString(mNameIndex);
+                contact.name = (name != null) ? name : mResources.getString(R.string.missing_name);
+                contact.status = cursor.getString(mStatusIndex);
+                contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null);
+                contact.lookupKey = ContentUris.withAppendedId(
+                        Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id);
+
+                // 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);
+                mContactEntries.add(contact);
+            }
+        } finally {
+            cursor.close();
+        }
     }
 
     /**
@@ -164,10 +248,6 @@
      * Returns 0 if {@link DisplayType#FREQUENT_ONLY}
      */
     protected int getNumStarredContacts(Cursor cursor) {
-        if (cursor == null || cursor.isClosed()) {
-            throw new IllegalStateException("Unable to access cursor");
-        }
-
         cursor.moveToPosition(-1);
         while (cursor.moveToNext()) {
             if (cursor.getInt(mStarredIndex) == 0) {
@@ -180,32 +260,15 @@
         return cursor.getCount();
     }
 
-    protected ContactEntry createContactEntryFromCursor(Cursor cursor, int position) {
-        // If the loader was canceled we will be given a null cursor.
-        // In that case, show an empty list of contacts.
-        if (cursor == null || cursor.isClosed() || cursor.getCount() <= position) return null;
-
-        cursor.moveToPosition(position);
-        long id = cursor.getLong(mIdIndex);
-        String photoUri = cursor.getString(mPhotoUriIndex);
-        String lookupKey = cursor.getString(mLookupIndex);
-
-        ContactEntry contact = new ContactEntry();
-        String name = cursor.getString(mNameIndex);
-        contact.name = (name != null) ? name : mResources.getString(R.string.missing_name);
-        contact.status = cursor.getString(mStatusIndex);
-        contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null);
-        contact.lookupKey = ContentUris.withAppendedId(
-                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id);
-
-        // Set phone number and label
-        int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex);
-        String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex);
-        contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType,
-                phoneNumberCustomLabel);
-        contact.phoneNumber = cursor.getString(mPhoneNumberIndex);
-
-        return contact;
+    /**
+     * 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);
     }
 
     /**
@@ -217,7 +280,7 @@
 
     @Override
     public int getCount() {
-        if (mContactCursor == null || mContactCursor.isClosed()) {
+        if (mContactEntries == null) {
             return 0;
         }
 
@@ -244,6 +307,14 @@
         return mColumnCount * mMaxTiledRows;
     }
 
+    protected int getRowIndex(int entryIndex) {
+        if (entryIndex < mMaxTiledRows * mColumnCount) {
+            return entryIndex / mColumnCount;
+        } else {
+            return entryIndex - mMaxTiledRows * mColumnCount + mMaxTiledRows;
+        }
+    }
+
     public int getColumnCount() {
         return mColumnCount;
     }
@@ -261,7 +332,7 @@
             // Contacts that appear as tiles
             for (int columnCounter = 0; columnCounter < mColumnCount &&
                     contactIndex != maxContactsInTiles; columnCounter++) {
-                resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex));
+                resultList.add(getContactEntryFromCache(contactIndex));
                 contactIndex++;
             }
         } else {
@@ -269,7 +340,7 @@
             // The actual position of the contact in the cursor is simply total the number of
             // tiled contacts + the given position
             contactIndex = maxContactsInTiles + position - 1;
-            resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex));
+            resultList.add(getContactEntryFromCache(contactIndex));
         }
 
         return resultList;
@@ -295,7 +366,74 @@
     }
 
     @Override
+    public void notifyDataSetChanged() {
+        if (DEBUG) {
+            Log.v(TAG, "nofigyDataSetChanged");
+        }
+        super.notifyDataSetChanged();
+    }
+
+    /**
+     * Configures the animation for each view.
+     *
+     * @param contactTileRowView The row to be animated.
+     * @param position The position of the row.
+     * @param itemViewType The type of the row.
+     */
+    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) {
+            // 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)) {
+                if (itemViewType == ViewTypes.FREQUENT) {
+                    if (position == getRowIndex(mDropEntryIndex) || position == mMaxTiledRows) {
+                        contactTileRowView.setVisibility(View.VISIBLE);
+                        mAlphaAnimation.setTarget(contactTileRowView);
+                        mAlphaAnimation.clone().start();
+                    } else {
+                        mTranslateVerticalAnimation.setTarget(contactTileRowView);
+                        mTranslateVerticalAnimation.setFloatValues(-contactTileRowView.getHeight(),
+                                0);
+                        mTranslateVerticalAnimation.clone().start();
+                    }
+                } else {
+                    contactTileRowView.animateTilesAppearRight(mDropEntryIndex + 1 -
+                            position * mColumnCount);
+                }
+            }
+        } else if (mPotentialRemoveEntryIndex != -1) {
+            // If one item is to be removed above this row, animate the row to shift up. If it is
+            // a favorite contact tile, animate it to appear from right.
+            if (position >= getRowIndex(mPotentialRemoveEntryIndex)) {
+                if (itemViewType == ViewTypes.FREQUENT) {
+                    mTranslateVerticalAnimation.setTarget(contactTileRowView);
+                    mTranslateVerticalAnimation.setFloatValues(contactTileRowView.getHeight(), 0);
+                    mTranslateVerticalAnimation.clone().start();
+                } else {
+                    contactTileRowView.animateTilesAppearLeft(
+                            mPotentialRemoveEntryIndex - position * mColumnCount);
+                }
+            }
+        }
+    }
+
+    @Override
     public View getView(int position, View convertView, ViewGroup parent) {
+        Log.v(TAG, "get view for " + String.valueOf(position));
         int itemViewType = getItemViewType(position);
 
         ContactTileRow contactTileRowView  = (ContactTileRow) convertView;
@@ -304,10 +442,13 @@
 
         if (contactTileRowView == null) {
             // Creating new row if needed
-            contactTileRowView = new ContactTileRow(mContext, itemViewType);
+            contactTileRowView = new ContactTileRow(mContext, itemViewType, position);
         }
 
-        contactTileRowView.configureRow(contactList, position == getCount() - 1);
+        contactTileRowView.configureRow(contactList, position, position == getCount() - 1);
+
+        configureAnimationToView(contactTileRowView, position, itemViewType);
+
         return contactTileRowView;
     }
 
@@ -347,22 +488,100 @@
     }
 
     /**
+     * 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 (index >= 0 && index < mContactEntries.size()) {
+            mDraggedEntry = mContactEntries.get(index);
+            mDraggedEntryIndex = index;
+            mContactEntries.remove(index);
+            notifyDataSetChanged();
+        }
+    }
+
+    /**
+     * Drops the temporarily removed contact to the desired location in the list.
+     *
+     * @param index Location where the contact will be dropped.
+     */
+    public void dropContactEntry(int index) {
+        if (mDraggedEntry != null) {
+            if (index >= 0 && index <= mContactEntries.size()) {
+                mContactEntries.add(index, mDraggedEntry);
+                mDropEntryIndex = index;
+            } 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);
+                mDropEntryIndex = mDraggedEntryIndex;
+            }
+            mDraggedEntry = null;
+            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 cache.
+     *
+     * @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();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Resets the item for pending removal.
+     */
+    public void undoPotentialRemoveEntryIndex() {
+        mPotentialRemoveEntryIndex = -1;
+    }
+
+    /**
+     * Clears all temporary variables at a new interaction.
+     */
+    public void cleanTempVariables() {
+        mDraggedEntryIndex = -1;
+        mDropEntryIndex = -1;
+        mDraggedEntry = null;
+        mPotentialRemoveEntryIndex = -1;
+    }
+
+    /**
      * Acts as a row item composed of {@link ContactTileView}
      *
      * TODO: FREQUENT doesn't really need it.  Just let {@link #getView} return
      */
-    private class ContactTileRow extends FrameLayout {
+    public class ContactTileRow extends FrameLayout {
         private int mItemViewType;
         private int mLayoutResId;
         private final int mRowPaddingStart;
         private final int mRowPaddingEnd;
         private final int mRowPaddingTop;
         private final int mRowPaddingBottom;
+        private int mPosition;
 
-        public ContactTileRow(Context context, int itemViewType) {
+        public ContactTileRow(Context context, int itemViewType, int position) {
             super(context);
             mItemViewType = itemViewType;
             mLayoutResId = getLayoutResourceId(mItemViewType);
+            mPosition = position;
 
             final Resources resources = mContext.getResources();
             mRowPaddingStart = resources.getDimensionPixelSize(
@@ -386,8 +605,9 @@
         /**
          * Configures the row to add {@link ContactEntry}s information to the views
          */
-        public void configureRow(ArrayList<ContactEntry> list, boolean isLastRow) {
+        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++) {
@@ -398,11 +618,11 @@
         }
 
         private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) {
-            final ContactTileView contactTile;
+            final PhoneFavoriteTileView contactTile;
 
             if (getChildCount() <= childIndex) {
 
-                contactTile = (ContactTileView) inflate(mContext, mLayoutResId, null);
+                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();
@@ -411,18 +631,17 @@
                         ViewGroup.LayoutParams.WRAP_CONTENT);
 
                 params.setMargins(
-                        resources.getDimensionPixelSize(R.dimen.detail_item_side_margin),
-                        0,
-                        resources.getDimensionPixelSize(R.dimen.detail_item_side_margin),
-                        0);
+                        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 = (ContactTileView) getChildAt(childIndex);
+                contactTile = (PhoneFavoriteTileView) getChildAt(childIndex);
             }
             contactTile.loadFromContact(entry);
+            contactTile.setId(childIndex);
             switch (mItemViewType) {
                 case ViewTypes.TOP:
                     // Setting divider visibilities
@@ -436,6 +655,7 @@
                 default:
                     break;
             }
+            contactTile.setupFavoriteContactCard();
         }
 
         @Override
@@ -518,6 +738,65 @@
             }
             setMeasuredDimension(width, imageSize + 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 (mPosition < mMaxTiledRows) {
+                final Rect childRect = new Rect();
+                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. */
+                    getChildAt(i).getHitRect(childRect);
+                    if (DEBUG) {
+                        Log.v(TAG, childRect.toString());
+                    }
+                    if (childRect.contains((int)itemX, (int)itemY)) {
+                        /** 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 (mPosition - mMaxTiledRows) + mColumnCount * mMaxTiledRows;
+            }
+            return -1;
+        }
+
+        public PhoneFavoritesTileAdapter getTileAdapter() {
+            return PhoneFavoritesTileAdapter.this;
+        }
+
+        public void animateTilesAppearLeft(int index) {
+            for (int i = index; i < getChildCount(); ++i) {
+                View childView = getChildAt(i);
+                mTranslateHorizontalAnimation.setTarget(childView);
+                mTranslateHorizontalAnimation.setFloatValues(childView.getWidth(), 0);
+                mTranslateHorizontalAnimation.clone().start();
+            }
+        }
+
+        public void animateTilesAppearRight(int index) {
+            for (int i = index; i < getChildCount(); ++i) {
+                View childView = getChildAt(i);
+                mTranslateHorizontalAnimation.setTarget(childView);
+                mTranslateHorizontalAnimation.setFloatValues(-childView.getWidth(), 0);
+                mTranslateHorizontalAnimation.clone().start();
+            }
+        }
+
+        public int getPosition() {
+            return mPosition;
+        }
     }
 
     protected static class ViewTypes {