Add drag to remove for favorites in Dialer

* Add remove view in dialtacts_activity.xml, and rearrange layout slightly
so that it takes up the same position in the layout as the search view
container. Contacts that are dragged to this remove view will be unstarred
and unpinned.

* Add drag event logic to the Remove View, so that when the user hovers a
contact over it, the UI will continue to respond. Previously, only the
PhoneFavoritesListView would detect touch events.

* Refactor DragDropController and OnDragDropListener into separate classes.
DragDropController performs the work of receiving drag/drop events from
multiple views, combining them, and then firing off callbacks as appropriate
to OnDragDropListeners. Each OnDragDropListener can then update their UI
or internal data model as necessary in response to the callbacks.

OnDragDropListener <----------------------------------------
       ^                                                    |
       |                                                    |
DialtactsActivity  --------------->   RemoveView            |
       |                                  |                 |
       v                                  |                 | callbacks
PhoneFavoriteListFragment                 |drag events      |
       |                                  |                 |
       v                                  v                 |
PhoneFavoriteListView ------------>   DragController--------
                       drag events        |
                                          |  callbacks
                                          v
PhoneFavoritesTileAdapter --> OnDragDropListener

* While in here, add a content description for the clear search button

Bug: 13083459
Change-Id: I044ad1c5aa42c7686bde6bf5074095a4fe879bde
(cherry picked from commit 3cefcc69c10ab12bd782a0b53779dbd1cc0e2aa1)
diff --git a/res/drawable-hdpi/ic_remove.png b/res/drawable-hdpi/ic_remove.png
new file mode 100644
index 0000000..1ee6adf
--- /dev/null
+++ b/res/drawable-hdpi/ic_remove.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_remove_highlight.png b/res/drawable-hdpi/ic_remove_highlight.png
new file mode 100644
index 0000000..435ee36
--- /dev/null
+++ b/res/drawable-hdpi/ic_remove_highlight.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_remove.png b/res/drawable-mdpi/ic_remove.png
new file mode 100644
index 0000000..2c134ea
--- /dev/null
+++ b/res/drawable-mdpi/ic_remove.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_remove_highlight.png b/res/drawable-mdpi/ic_remove_highlight.png
new file mode 100644
index 0000000..6a961cb
--- /dev/null
+++ b/res/drawable-mdpi/ic_remove_highlight.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_remove.png b/res/drawable-xhdpi/ic_remove.png
new file mode 100644
index 0000000..be81592
--- /dev/null
+++ b/res/drawable-xhdpi/ic_remove.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_remove_highlight.png b/res/drawable-xhdpi/ic_remove_highlight.png
new file mode 100644
index 0000000..57949e3
--- /dev/null
+++ b/res/drawable-xhdpi/ic_remove_highlight.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_remove.png b/res/drawable-xxhdpi/ic_remove.png
new file mode 100644
index 0000000..2722f23
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_remove.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_remove_highlight.png b/res/drawable-xxhdpi/ic_remove_highlight.png
new file mode 100644
index 0000000..23ee8f6
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_remove_highlight.png
Binary files differ
diff --git a/res/layout/dialtacts_activity.xml b/res/layout/dialtacts_activity.xml
index e2c3853..5a886e2 100644
--- a/res/layout/dialtacts_activity.xml
+++ b/res/layout/dialtacts_activity.xml
@@ -28,20 +28,20 @@
         android:layout_height="match_parent"
         android:clipChildren="false"
         android:orientation="vertical" >
-        <LinearLayout
+        <FrameLayout
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:id="@+id/search_view_container"
-            android:orientation="vertical"
             >
             <LinearLayout
                 android:layout_width="match_parent"
-                android:layout_height="match_parent"
+                android:layout_height="wrap_content"
+                android:id="@+id/search_view_container"
                 android:orientation="horizontal"
                 android:paddingLeft="16dp"
                 android:paddingRight="23dp"
                 android:background="@color/searchbox_background_color"
-                android:gravity="center_vertical">
+                android:gravity="center_vertical"
+                >
                 <EditText
                     android:id="@+id/search_view"
                     android:layout_width="0dp"
@@ -57,6 +57,7 @@
                     android:src="@drawable/ic_close_dk"
                     android:clickable="true"
                     android:background="?android:attr/selectableItemBackground"
+                    android:contentDescription="@string/description_clear_search"
                     android:visibility="gone" />
                 <ImageView
                     android:id="@+id/voice_search_button"
@@ -68,12 +69,35 @@
                     android:contentDescription="@string/description_start_voice_search"
                     android:background="?android:attr/selectableItemBackground" />
             </LinearLayout>
-            <View
-                android:id="@+id/searchbox_divider"
-                android:layout_height="1dp"
+            <com.android.dialer.list.RemoveView
                 android:layout_width="match_parent"
-                android:background="@color/background_dialer_light" />
-        </LinearLayout>
+                android:layout_height="56dp"
+                android:id="@+id/remove_view_container"
+                android:orientation="horizontal"
+                android:gravity="center"
+                android:visibility="gone">
+                <ImageView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:id="@+id/remove_view_icon"
+                    android:src="@drawable/ic_remove"
+                    android:contentDescription="@string/remove_contact"
+                />
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:id="@+id/remove_view_text"
+                    android:textSize="@dimen/remove_text_size"
+                    android:textColor="@color/remove_text_color"
+                    android:text="@string/remove_contact"
+                />
+            </com.android.dialer.list.RemoveView>
+        </FrameLayout>
+        <View
+            android:id="@+id/searchbox_divider"
+            android:layout_height="1dp"
+            android:layout_width="match_parent"
+            android:background="@color/background_dialer_light" />
         <FrameLayout
             android:layout_height="0dp"
             android:layout_weight="1"
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 370bdfe..fc8e847 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -12,7 +12,7 @@
   ~ 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
-  -->
+-->
 
 <resources>
 
@@ -27,8 +27,10 @@
     <!-- Color of the text describing an unconsumed voicemail. -->
     <color name="call_log_voicemail_highlight_color">#33b5e5</color>
 
-    <!-- Colour of voicemail progress bar to the right of position indicator.
-         Same as the background color of the dialer -->
+    <!--
+         Colour of voicemail progress bar to the right of position indicator.
+         Same as the background color of the dialer
+    -->
     <color name="voicemail_playback_seek_bar_yet_to_play">#cecece</color>
 
     <!-- Colour of voicemail progress bar to the left of position indicator. -->
@@ -38,7 +40,7 @@
     <color name="item_selected">#660099cc</color>
 
     <!-- Background color of new dialer activity -->
-    <color name="background_dialer_light">#cecece</color>
+    <color name="background_dialer_light">#eeeeee</color>
 
     <!-- Background color of dialer list items (contacts, call log entries) -->
     <color name="background_dialer_list_items">#eeeeee</color>
@@ -71,6 +73,12 @@
     <!-- Text color for no favorites message -->
     <color name="nofavorite_text_color">#777777</color>
 
+    <!-- Text color for the "Remove" text in its regular state -->
+    <color name="remove_text_color">#555555</color>
+
+    <!-- Text color for the "Remove" text when a contact is dragged on top of the remove view -->
+    <color name="remove_highlighted_text_color">#FF3F3B</color>
+
     <!-- Text color for the "speed dial" label in the favorites menu. -->
     <color name="speed_dial_text_color">#555555</color>
 
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3c856d2..fb74e97 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -13,7 +13,7 @@
   ~ 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
-  -->
+-->
 <resources>
 
     <!-- Height of edit text in dialpad fragment -->
@@ -29,9 +29,15 @@
     <!-- Match call_button_height to Phone's dimens/in_call_end_button_height -->
     <dimen name="call_button_height">74dp</dimen>
 
-    <!--  Search View -->
+    <!-- Search View -->
     <dimen name="search_text_size">14sp</dimen>
 
+    <!--
+          Drag to remove view (in dp because it is used in conjunction with a statically
+          sized icon
+    -->
+    <dimen name="remove_text_size">16dp</dimen>
+
     <!-- Call Log -->
     <dimen name="call_log_call_action_size">32dip</dimen>
     <dimen name="call_log_call_action_width">48dip</dimen>
@@ -51,9 +57,11 @@
          the main area of a call log entry and the secondary action button. -->
     <dimen name="call_log_list_item_vertical_divider_width">1dp</dimen>
 
-    <!-- Layout weight values for dialpad screen. These layouts will be used in one
+    <!--
+         Layout weight values for dialpad screen. These layouts will be used in one
          LinearLayout (dialpad_fragment.xml), configuring dialpad screen's vertical
-         ratio. -->
+         ratio.
+    -->
     <integer name="dialpad_layout_weight_digits">15</integer>
     <integer name="dialpad_layout_weight_dialpad">65</integer>
 
@@ -63,14 +71,14 @@
     <dimen name="dialpad_key_plus_size">15dp</dimen>
     <dimen name="dialpad_key_special_characters_size">25dp</dimen>
     <dimen name="dialpad_key_letters_width">41dp</dimen>
-
-
     <dimen name="fake_action_bar_height">60dp</dimen>
     <!-- Min with of fake menu buttons, which should be same as ActionBar's one -->
     <dimen name="fake_menu_button_min_width">56dp</dimen>
 
     <!--  Favorites tile and recent call log padding -->
     <dimen name="contact_tile_divider_width">12dp</dimen>
+    <!-- Favorites tile and recent call log padding -->
+    <dimen name="contact_tile_divider_padding">3dp</dimen>
     <dimen name="contact_tile_info_button_height_and_width">36dp</dimen>
     <item name="contact_tile_height_to_width_ratio" type="dimen">67%</item>
     <dimen name="favorites_row_top_padding">6dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 2564cd7..20a73c1 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -533,6 +533,9 @@
     -->
     <string name="description_call_log_unheard_voicemail">Unheard voicemail</string>
 
+    <!--  String describing the icon used to clear the search field -->
+    <string name="description_clear_search">Clear search</string>
+
     <!-- String describing the icon used to start a voice search -->
     <string name="description_start_voice_search">Start voice search</string>
 
@@ -720,6 +723,8 @@
     <!-- Content description for dismiss button on badge. [CHAR LIMIT=NONE] -->
     <string name="description_dismiss">Dismiss</string>
 
+    <!-- Remove button that shows up when contact is long-pressed. [CHAR LIMIT=NONE] -->
+    <string name="remove_contact">Remove</string>
     <!-- Header text displayed on the main dialer screen above the list of favorite phone numbers.
          [CHAR LIMIT=21] -->
     <string name="favorites_menu_speed_dial">Speed Dial</string>
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index 0c0fcda..227cd8e 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -22,7 +22,6 @@
 import android.app.Activity;
 import android.app.Fragment;
 import android.app.FragmentManager;
-import android.app.FragmentManager.BackStackEntry;
 import android.app.FragmentTransaction;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
@@ -50,8 +49,6 @@
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.View.OnFocusChangeListener;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.AbsListView.OnScrollListener;
 import android.widget.EditText;
@@ -70,9 +67,13 @@
 import com.android.dialer.dialpad.SmartDialPrefix;
 import com.android.dialer.interactions.PhoneNumberInteraction;
 import com.android.dialer.list.AllContactsActivity;
+import com.android.dialer.list.DragDropController;
+import com.android.dialer.list.OnDragDropListener;
 import com.android.dialer.list.OnListFragmentScrolledListener;
 import com.android.dialer.list.PhoneFavoriteFragment;
+import com.android.dialer.list.PhoneFavoriteTileView;
 import com.android.dialer.list.RegularSearchFragment;
+import com.android.dialer.list.RemoveView;
 import com.android.dialer.list.SearchFragment;
 import com.android.dialer.list.SmartDialSearchFragment;
 import com.android.dialerbind.DatabaseHelperManager;
@@ -88,7 +89,9 @@
         DialpadFragment.OnDialpadQueryChangedListener, PopupMenu.OnMenuItemClickListener,
         OnListFragmentScrolledListener,
         DialpadFragment.OnDialpadFragmentStartedListener,
-        PhoneFavoriteFragment.OnShowAllContactsListener {
+        PhoneFavoriteFragment.OnShowAllContactsListener,
+        PhoneFavoriteFragment.HostInterface,
+        OnDragDropListener {
     private static final String TAG = "DialtactsActivity";
 
     public static final boolean DEBUG = false;
@@ -122,6 +125,8 @@
 
     private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1;
 
+    private static final int FADE_ANIMATION_DURATION = 200;
+
     private String mFilterText;
 
     /**
@@ -168,6 +173,7 @@
      */
     private boolean mFirstLaunch;
     private View mSearchViewContainer;
+    private RemoveView mRemoveViewContainer;
     private View mSearchViewCloseButton;
     private View mVoiceSearchButton;
     private EditText mSearchView;
@@ -312,6 +318,7 @@
         mBottomPaddingView = findViewById(R.id.dialtacts_bottom_padding);
         mFragmentsFrame = findViewById(R.id.dialtacts_frame);
         mActionBar = findViewById(R.id.fake_action_bar);
+        mRemoveViewContainer = (RemoveView) findViewById(R.id.remove_view_container);
         prepareSearchView();
 
         if (UI.FILTER_CONTACTS_ACTION.equals(intent.getAction())
@@ -977,4 +984,48 @@
                 PackageManager.MATCH_DEFAULT_ONLY);
         return resolveInfo != null && resolveInfo.size() > 0;
     }
+
+    @Override
+    public void onDragStarted(int itemIndex, int x, int y, PhoneFavoriteTileView view) {
+        crossfadeViews(mRemoveViewContainer, mSearchViewContainer, FADE_ANIMATION_DURATION);
+    }
+
+    @Override
+    public void onDragHovered(int itemIndex, int x, int y) {}
+
+    @Override
+    public void onDragFinished(int x, int y) {
+        crossfadeViews(mSearchViewContainer, mRemoveViewContainer, FADE_ANIMATION_DURATION);
+    }
+
+    @Override
+    public void onDroppedOnRemove() {}
+
+    /**
+     * Allows the PhoneFavoriteFragment to attach the drag controller to mRemoveViewContainer
+     * once it has been attached to the activity.
+     */
+    @Override
+    public void setDragDropController(DragDropController dragController) {
+        mRemoveViewContainer.setDragDropController(dragController);
+    }
+
+    /**
+     * Crossfades two views so that the first one appears while the other one is fading
+     * out of view.
+     */
+    private void crossfadeViews(final View fadeIn, final View fadeOut, int duration) {
+        fadeOut.animate().alpha(0).setDuration(duration)
+        .setListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                fadeOut.setVisibility(View.GONE);
+            }
+        });
+
+        fadeIn.setVisibility(View.VISIBLE);
+        fadeIn.setAlpha(0);
+        fadeIn.animate().alpha(1).setDuration(FADE_ANIMATION_DURATION)
+                .setListener(null);
+    }
 }
diff --git a/src/com/android/dialer/list/DragDropController.java b/src/com/android/dialer/list/DragDropController.java
new file mode 100644
index 0000000..399cd09
--- /dev/null
+++ b/src/com/android/dialer/list/DragDropController.java
@@ -0,0 +1,71 @@
+package com.android.dialer.list;
+
+import android.view.View;
+
+import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class that handles and combines drag events generated from multiple views, and then fires
+ * off events to any OnDragDropListeners that have registered for callbacks.
+ */
+public class DragDropController {
+    private List<OnDragDropListener> mOnDragDropListeners = new ArrayList<OnDragDropListener>();
+
+    /**
+     * @return True if the drag is started, false if the drag is cancelled for some reason.
+     */
+    boolean handleDragStarted(int x, int y, ContactTileRow tileRow) {
+        final PhoneFavoriteTileView tileView =
+                (PhoneFavoriteTileView) tileRow.getViewAtPosition(x, y);
+
+        final int itemIndex = tileRow.getItemIndex(x, y);
+        if (itemIndex != -1 && !mOnDragDropListeners.isEmpty()) {
+            for (int i = 0; i < mOnDragDropListeners.size(); i++) {
+                mOnDragDropListeners.get(i).onDragStarted(itemIndex, x, y, tileView);
+            }
+        }
+
+        return true;
+    }
+
+    public void handleDragHovered(int x, int y, View view) {
+        int itemIndex;
+        if (!(view instanceof ContactTileRow)) {
+            itemIndex = -1;
+        } else {
+            final ContactTileRow tile = (ContactTileRow) view;
+            itemIndex = tile.getItemIndex(x, y);
+        }
+        for (int i = 0; i < mOnDragDropListeners.size(); i++) {
+            mOnDragDropListeners.get(i).onDragHovered(itemIndex, x, y);
+        }
+    }
+
+    public void handleDragFinished(int x, int y, boolean isRemoveView) {
+        if (isRemoveView) {
+            for (int i = 0; i < mOnDragDropListeners.size(); i++) {
+                mOnDragDropListeners.get(i).onDroppedOnRemove();
+            }
+        }
+
+        for (int i = 0; i < mOnDragDropListeners.size(); i++) {
+            mOnDragDropListeners.get(i).onDragFinished(x, y);
+        }
+    }
+
+    public void addOnDragDropListener(OnDragDropListener listener) {
+        if (!mOnDragDropListeners.contains(listener)) {
+            mOnDragDropListeners.add(listener);
+        }
+    }
+
+    public void removeOnDragDropListener(OnDragDropListener listener) {
+        if (mOnDragDropListeners.contains(listener)) {
+            mOnDragDropListeners.remove(listener);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/dialer/list/OnDragDropListener.java b/src/com/android/dialer/list/OnDragDropListener.java
new file mode 100644
index 0000000..7f6d179
--- /dev/null
+++ b/src/com/android/dialer/list/OnDragDropListener.java
@@ -0,0 +1,41 @@
+package com.android.dialer.list;
+
+
+/**
+ * Classes that want to receive callbacks in response to drag events should implement this
+ * interface.
+ */
+public interface OnDragDropListener {
+    /**
+     * Called when a drag is started.
+     * @param itemIndex Index of the contact on which the drag was triggered
+     * @param x X-coordinate of the drag event
+     * @param y Y-coordinate of the drag event
+     * @param view The contact tile which the drag was started on
+     */
+    public void onDragStarted(int itemIndex, int x, int y, PhoneFavoriteTileView view);
+
+    /**
+     * Called when a drag is in progress and the user moves the dragged contact to a
+     * location.
+     * @param itemIndex Index of the contact in the ListView which is currently being displaced
+     * by the dragged contact
+     * @param x X-coordinate of the drag event
+     * @param y Y-coordinate of the drag event
+     */
+    public void onDragHovered(int itemIndex, int x, int y);
+
+    /**
+     * Called when a drag is completed (whether by dropping it somewhere or simply by dragging
+     * the contact off the screen)
+     * @param x X-coordinate of the drag event
+     * @param y Y-coordinate of the drag event
+     */
+    public void onDragFinished(int x, int y);
+
+    /**
+     * Called when a contact has been dropped on the remove view, indicating that the user
+     * wants to remove this contact.
+     */
+    public void onDroppedOnRemove();
+}
\ No newline at end of file
diff --git a/src/com/android/dialer/list/PhoneFavoriteFragment.java b/src/com/android/dialer/list/PhoneFavoriteFragment.java
index 860f9dc..79dbe8c 100644
--- a/src/com/android/dialer/list/PhoneFavoriteFragment.java
+++ b/src/com/android/dialer/list/PhoneFavoriteFragment.java
@@ -107,6 +107,10 @@
         public void onCallNumberDirectly(String phoneNumber);
     }
 
+    public interface HostInterface {
+        public void setDragDropController(DragDropController controller);
+    }
+
     private class MissedCallLogLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
 
         @Override
@@ -279,7 +283,7 @@
         mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT);
         mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
         mListView.setOnItemSwipeListener(mContactTileAdapter);
-        mListView.setOnDragDropListener(mContactTileAdapter);
+        mListView.getDragDropController().addOnDragDropListener(mContactTileAdapter);
 
         final ImageView dragShadowOverlay =
                 (ImageView) mParentView.findViewById(R.id.contact_tile_drag_shadow_overlay);
@@ -347,6 +351,15 @@
                     + " must implement OnShowAllContactsListener");
         }
 
+        try {
+            OnDragDropListener listener = (OnDragDropListener) activity;
+            mListView.getDragDropController().addOnDragDropListener(listener);
+            ((HostInterface) activity).setDragDropController(mListView.getDragDropController());
+        } catch (ClassCastException e) {
+            throw new ClassCastException(activity.toString()
+                    + " must implement OnDragDropListener and HostInterface");
+        }
+
         // Use initLoader() instead of restartLoader() to refraining unnecessary reload.
         // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will
         // be called, on which we'll check if "all" contacts should be reloaded again or not.
diff --git a/src/com/android/dialer/list/PhoneFavoriteListView.java b/src/com/android/dialer/list/PhoneFavoriteListView.java
index 99979dd..adda3cf 100644
--- a/src/com/android/dialer/list/PhoneFavoriteListView.java
+++ b/src/com/android/dialer/list/PhoneFavoriteListView.java
@@ -29,7 +29,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
-import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.ListView;
 
@@ -44,7 +43,8 @@
  * - Swiping, which is borrowed from packages/apps/UnifiedEmail (com.android.mail.ui.Swipeable)
  * - Drag and drop
  */
-public class PhoneFavoriteListView extends ListView implements SwipeHelperCallback {
+public class PhoneFavoriteListView extends ListView implements SwipeHelperCallback,
+        OnDragDropListener {
 
     public static final String LOG_TAG = PhoneFavoriteListView.class.getSimpleName();
 
@@ -52,7 +52,6 @@
     private boolean mEnableSwipe = true;
 
     private OnItemGestureListener mOnItemGestureListener;
-    private OnDragDropListener mOnDragDropListener;
 
     private float mDensityScale;
     private float mTouchSlop;
@@ -81,6 +80,8 @@
     private int mDragShadowLeft;
     private int mDragShadowTop;
 
+    private DragDropController mDragDropController = new DragDropController();
+
     private final float DRAG_SHADOW_ALPHA = 0.7f;
 
     /**
@@ -130,6 +131,7 @@
         mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this,
                 mDensityScale, mTouchSlop);
         setItemsCanFocus(true);
+        mDragDropController.addOnDragDropListener(this);
     }
 
     @Override
@@ -156,10 +158,10 @@
         mOnItemGestureListener = listener;
     }
 
-    public void setOnDragDropListener(OnDragDropListener listener) {
-        mOnDragDropListener = listener;
-    }
-
+    /**
+     * TODO: This is all swipe to remove code (nothing to do with drag to remove). This should
+     * be cleaned up and removed once drag to remove becomes the only way to remove contacts.
+     */
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
@@ -232,6 +234,10 @@
         requestDisallowInterceptTouchEvent(true);
     }
 
+    /**
+     * End of swipe-to-remove code
+     */
+
     @Override
     public boolean dispatchDragEvent(DragEvent event) {
         final int action = event.getAction();
@@ -239,13 +245,34 @@
         final int eY = (int) event.getY();
         switch (action) {
             case DragEvent.ACTION_DRAG_STARTED:
-                if (!handleDragStarted(mTouchDownForDragStartX, mTouchDownForDragStartY)) {
+                final int[] coordinates = new int[2];
+                getLocationOnScreen(coordinates);
+                // Calculate the X and Y coordinates of the drag event relative to the view
+                final int viewX = eX - coordinates[0];
+                final int viewY = eY - coordinates[1];
+                final View child = getViewAtPosition(viewX, viewY);
+
+                if (!(child instanceof ContactTileRow)) {
+                    // Bail early.
                     return false;
-                };
+                }
+
+                final ContactTileRow tile = (ContactTileRow) child;
+
+                // Disable drag and drop if there is a contact that has been swiped and is currently
+                // in the pending remove state
+                if (tile.getTileAdapter().hasPotentialRemoveEntryIndex()) {
+                    return false;
+                }
+
+                if (!mDragDropController.handleDragStarted(viewX, viewY, tile)) {
+                    return false;
+                }
                 break;
             case DragEvent.ACTION_DRAG_LOCATION:
                 mLastDragY = eY;
-                handleDragHovered(eX, eY);
+                final View view = getViewAtPosition(eX, eY);
+                mDragDropController.handleDragHovered(eX, eY, view);
                 // Kick off {@link #mScrollHandler} if it's not started yet.
                 if (!mIsDragScrollerRunning &&
                         // And if the distance traveled while dragging exceeds the touch slop
@@ -268,13 +295,13 @@
                 mIsDragScrollerRunning = false;
                 // Either a successful drop or it's ended with out drop.
                 if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) {
-                    handleDragFinished(eX, eY);
+                    mDragDropController.handleDragFinished(eX, eY, false);
                 }
                 break;
             default:
                 break;
         }
-        // This ListView will consumer the drag events on behalf of its children.
+        // This ListView will consume the drag events on behalf of its children.
         return true;
     }
 
@@ -303,69 +330,48 @@
         }
     }
 
-    /**
-     * @return True if the drag is started.
-     */
-    private boolean handleDragStarted(int x, int y) {
-        final View child = getViewAtPosition(x, y);
-        if (!(child instanceof ContactTileRow)) {
-            // Bail early.
-            return false;
-        }
-
-        final ContactTileRow tile = (ContactTileRow) child;
-
-        if (tile.getTileAdapter().hasPotentialRemoveEntryIndex()) {
-            return false;
-        }
-
-        final int itemIndex = tile.getItemIndex(x, y);
-        if (itemIndex != -1 && mOnDragDropListener != null) {
-            final PhoneFavoriteTileView tileView =
-                    (PhoneFavoriteTileView) tile.getViewAtPosition(x, y);
-            if (mDragShadowOverlay == null) {
-                return false;
-            }
-
-            mDragShadowOverlay.clearAnimation();
-            mDragShadowBitmap = createDraggedChildBitmap(tileView);
-            if (mDragShadowBitmap == null) {
-                return false;
-            }
-
-            if (tileView instanceof PhoneFavoriteRegularRowView) {
-                mDragShadowLeft = tile.getLeft();
-                mDragShadowTop = tile.getTop();
-            } else {
-                // Square tile is relative to the contact tile,
-                // and contact tile is relative to this list view.
-                mDragShadowLeft = tileView.getLeft() + tileView.getParentRow().getLeft();
-                mDragShadowTop = tileView.getTop() + tileView.getParentRow().getTop();
-            }
-
-            mDragShadowOverlay.setImageBitmap(mDragShadowBitmap);
-            mDragShadowOverlay.setVisibility(VISIBLE);
-            mDragShadowOverlay.setAlpha(DRAG_SHADOW_ALPHA);
-
-            mDragShadowOverlay.setX(mDragShadowLeft);
-            mDragShadowOverlay.setY(mDragShadowTop);
-
-            // x and y passed in are the coordinates of where the user has touched down, calculate
-            // the offset to the top left coordinate of the dragged child.  This will be used for
-            // drawing the drag shadow.
-            mTouchOffsetToChildLeft = x - mDragShadowLeft;
-            mTouchOffsetToChildTop = y - mDragShadowTop;
-
-            // invalidate to trigger a redraw of the drag shadow.
-            invalidate();
-
-            mOnDragDropListener.onDragStarted(itemIndex);
-        }
-
-        return true;
+    public DragDropController getDragDropController() {
+        return mDragDropController;
     }
 
-    private void handleDragHovered(int x, int y) {
+    @Override
+    public void onDragStarted(int itemIndex, int x, int y, PhoneFavoriteTileView tileView) {
+        if (mDragShadowOverlay == null) {
+            return;
+        }
+
+        mDragShadowOverlay.clearAnimation();
+        mDragShadowBitmap = createDraggedChildBitmap(tileView);
+        if (mDragShadowBitmap == null) {
+            return;
+        }
+
+        if (tileView instanceof PhoneFavoriteRegularRowView) {
+            mDragShadowLeft = tileView.getParentRow().getLeft();
+            mDragShadowTop = tileView.getParentRow().getTop();
+        } else {
+            // Square tile is relative to the contact tile,
+            // and contact tile is relative to this list view.
+            mDragShadowLeft = tileView.getLeft() + tileView.getParentRow().getLeft();
+            mDragShadowTop = tileView.getTop() + tileView.getParentRow().getTop();
+        }
+
+        mDragShadowOverlay.setImageBitmap(mDragShadowBitmap);
+        mDragShadowOverlay.setVisibility(VISIBLE);
+        mDragShadowOverlay.setAlpha(DRAG_SHADOW_ALPHA);
+
+        mDragShadowOverlay.setX(mDragShadowLeft);
+        mDragShadowOverlay.setY(mDragShadowTop);
+
+        // x and y passed in are the coordinates of where the user has touched down,
+        // calculate the offset to the top left coordinate of the dragged child.  This
+        // will be used for drawing the drag shadow.
+        mTouchOffsetToChildLeft = x - mDragShadowLeft;
+        mTouchOffsetToChildTop = y - mDragShadowTop;
+    }
+
+    @Override
+    public void onDragHovered(int itemIndex, int x, int y) {
         // Update the drag shadow location.
         mDragShadowLeft = x - mTouchOffsetToChildLeft;
         mDragShadowTop = y - mTouchOffsetToChildTop;
@@ -374,21 +380,10 @@
             mDragShadowOverlay.setX(mDragShadowLeft);
             mDragShadowOverlay.setY(mDragShadowTop);
         }
-
-        final View child = getViewAtPosition(x, y);
-        if (!(child instanceof ContactTileRow)) {
-            // Bail early.
-            return;
-        }
-
-        final ContactTileRow tile = (ContactTileRow) child;
-        final int itemIndex = tile.getItemIndex(x, y);
-        if (itemIndex != -1 && mOnDragDropListener != null) {
-            mOnDragDropListener.onDragHovered(itemIndex);
-        }
     }
 
-    private void handleDragFinished(int x, int y) {
+    @Override
+    public void onDragFinished(int x, int y) {
         // Update the drag shadow location.
         mDragShadowLeft = x - mTouchOffsetToChildLeft;
         mDragShadowTop = y - mTouchOffsetToChildTop;
@@ -400,12 +395,11 @@
                     .setListener(mDragShadowOverAnimatorListener)
                     .start();
         }
-
-        if (mOnDragDropListener != null) {
-            mOnDragDropListener.onDragFinished();
-        }
     }
 
+    @Override
+    public void onDroppedOnRemove() {}
+
     private Bitmap createDraggedChildBitmap(View view) {
         view.setDrawingCacheEnabled(true);
         final Bitmap cache = view.getDrawingCache();
@@ -425,10 +419,4 @@
 
         return bitmap;
     }
-
-    public interface OnDragDropListener {
-        public void onDragStarted(int itemIndex);
-        public void onDragHovered(int itemIndex);
-        public void onDragFinished();
-    }
 }
diff --git a/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java
index dff68b2..45cc5a3 100644
--- a/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java
+++ b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java
@@ -59,7 +59,7 @@
  *
  */
 public class PhoneFavoritesTileAdapter extends BaseAdapter implements
-        SwipeHelper.OnItemGestureListener, PhoneFavoriteListView.OnDragDropListener {
+        SwipeHelper.OnItemGestureListener, OnDragDropListener {
     private static final String TAG = PhoneFavoritesTileAdapter.class.getSimpleName();
     private static final boolean DEBUG = false;
 
@@ -1204,24 +1204,38 @@
     }
 
     @Override
-    public void onDragStarted(int itemIndex) {
+    public void onDragStarted(int itemIndex, int x, int y, PhoneFavoriteTileView view) {
         setInDragging(true);
         popContactEntry(itemIndex);
     }
 
     @Override
-    public void onDragHovered(int itemIndex) {
+    public void onDragHovered(int itemIndex, int x, int y) {
         if (mInDragging &&
                 mDragEnteredEntryIndex != itemIndex &&
                 isIndexInBound(itemIndex) &&
-                itemIndex < PIN_LIMIT) {
+                itemIndex < PIN_LIMIT &&
+                itemIndex >= 0) {
             markDropArea(itemIndex);
         }
     }
 
     @Override
-    public void onDragFinished() {
+    public void onDragFinished(int x, int y) {
         setInDragging(false);
-        handleDrop();
+        // 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;
+        }
     }
 }
diff --git a/src/com/android/dialer/list/RemoveView.java b/src/com/android/dialer/list/RemoveView.java
new file mode 100644
index 0000000..16942fe
--- /dev/null
+++ b/src/com/android/dialer/list/RemoveView.java
@@ -0,0 +1,90 @@
+package com.android.dialer.list;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.DragEvent;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+
+public class RemoveView extends LinearLayout {
+
+    DragDropController mDragDropController;
+    TextView mRemoveText;
+    ImageView mRemoveIcon;
+    int mUnhighlightedColor;
+    int mHighlightedColor;
+    Drawable mRemoveDrawable;
+    Drawable mRemoveHighlightedDrawable;
+
+    public RemoveView(Context context) {
+      super(context);
+    }
+
+    public RemoveView(Context context, AttributeSet attrs) {
+        this(context, attrs, -1);
+    }
+
+    public RemoveView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        mRemoveText = (TextView) findViewById(R.id.remove_view_text);
+        mRemoveIcon = (ImageView) findViewById(R.id.remove_view_icon);
+        final Resources r = getResources();
+        mUnhighlightedColor = r.getColor(R.color.remove_text_color);
+        mHighlightedColor = r.getColor(R.color.remove_highlighted_text_color);
+        mRemoveDrawable = r.getDrawable(R.drawable.ic_remove);
+        mRemoveHighlightedDrawable = r.getDrawable(R.drawable.ic_remove_highlight);
+    }
+
+    public void setDragDropController(DragDropController controller) {
+        mDragDropController = controller;
+    }
+
+    @Override
+    public boolean dispatchDragEvent(DragEvent event) {
+      final int action = event.getAction();
+      switch (action) {
+        case DragEvent.ACTION_DRAG_ENTERED:
+            setAppearanceHighlighted();
+            break;
+        case DragEvent.ACTION_DRAG_EXITED:
+            setAppearanceNormal();
+            break;
+        case DragEvent.ACTION_DRAG_LOCATION:
+            if (mDragDropController != null) {
+                mDragDropController.handleDragHovered((int) event.getX(),
+                        // the true y-coordinate of the event with respect to the listview is
+                        // offset by the height of the remove view
+                        (int) event.getY() - getHeight(), null);
+            }
+            break;
+        case DragEvent.ACTION_DROP:
+            if (mDragDropController != null) {
+                mDragDropController.handleDragFinished((int) event.getX(), (int) event.getY(), true);
+            }
+            setAppearanceNormal();
+            break;
+      }
+      return true;
+    }
+
+    private void setAppearanceNormal() {
+        mRemoveText.setTextColor(mUnhighlightedColor);
+        mRemoveIcon.setImageDrawable(mRemoveDrawable);
+        invalidate();
+    }
+
+    private void setAppearanceHighlighted() {
+        mRemoveText.setTextColor(mHighlightedColor);
+        mRemoveIcon.setImageDrawable(mRemoveHighlightedDrawable);
+        invalidate();
+    }
+}