Add selection handle dragging tests.

- Set an id for each HandleView to access the hanlde.
- Rename DragOnTextViewActions to DragAction to use it for
dragging handles.
- Introduce HandleCoordinates to provide proper coordinates
for handles.

More tests will be added in following CLs.

Bug: 25730231
Change-Id: I9276bf2f983983ec9aae0ddcf674d3dcee566892
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 2fabe33..8cd4de3 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -3494,7 +3494,8 @@
         }
     }
 
-    private abstract class HandleView extends View implements TextViewPositionListener {
+    @VisibleForTesting
+    public abstract class HandleView extends View implements TextViewPositionListener {
         protected Drawable mDrawable;
         protected Drawable mDrawableLtr;
         protected Drawable mDrawableRtl;
@@ -3525,8 +3526,9 @@
         // a different line.
         protected int mPreviousLineTouched = UNSET_LINE;
 
-        public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
+        private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
             super(mTextView.getContext());
+            setId(id);
             mContainer = new PopupWindow(mTextView.getContext(), null,
                     com.android.internal.R.attr.textSelectHandleWindowStyle);
             mContainer.setSplitTouchEnabled(true);
@@ -3888,7 +3890,7 @@
         private Runnable mHider;
 
         public InsertionHandleView(Drawable drawable) {
-            super(drawable, drawable);
+            super(drawable, drawable, com.android.internal.R.id.insertion_handle);
         }
 
         @Override
@@ -4073,7 +4075,7 @@
         private final int[] mTextViewLocation = new int[2];
 
         public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
-            super(drawableLtr, drawableRtl);
+            super(drawableLtr, drawableRtl, com.android.internal.R.id.selection_start_handle);
             ViewConfiguration viewConfiguration = ViewConfiguration.get(
                     mTextView.getContext());
             mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
@@ -4313,7 +4315,7 @@
         private final int[] mTextViewLocation = new int[2];
 
         public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
-            super(drawableLtr, drawableRtl);
+            super(drawableLtr, drawableRtl, com.android.internal.R.id.selection_end_handle);
             ViewConfiguration viewConfiguration = ViewConfiguration.get(
                     mTextView.getContext());
             mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
diff --git a/core/res/res/values/ids.xml b/core/res/res/values/ids.xml
index 695dafa..a348767 100644
--- a/core/res/res/values/ids.xml
+++ b/core/res/res/values/ids.xml
@@ -94,6 +94,9 @@
   <item type="id" name="redo" />
   <item type="id" name="replaceText" />
   <item type="id" name="shareText" />
+  <item type="id" name="selection_start_handle" />
+  <item type="id" name="selection_end_handle" />
+  <item type="id" name="insertion_handle" />
   <item type="id" name="floating_toolbar_menu_item_image_button" />
 
   <!-- Accessibility action identifier for {@link android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_SHOW_ON_SCREEN}. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index cda7faa..80e5668 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -221,6 +221,9 @@
   <java-symbol type="id" name="profile_badge_line2" />
   <java-symbol type="id" name="profile_badge_line3" />
   <java-symbol type="id" name="transitionPosition" />
+  <java-symbol type="id" name="selection_start_handle" />
+  <java-symbol type="id" name="selection_end_handle" />
+  <java-symbol type="id" name="insertion_handle" />
 
   <java-symbol type="attr" name="actionModeShareDrawable" />
   <java-symbol type="attr" name="alertDialogCenterButtons" />
diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
index bb51570..b03552f 100644
--- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java
+++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
@@ -19,6 +19,8 @@
 import static android.widget.espresso.TextViewActions.clickOnTextAtIndex;
 import static android.widget.espresso.TextViewActions.doubleTapAndDragOnText;
 import static android.widget.espresso.TextViewActions.doubleClickOnTextAtIndex;
+import static android.widget.espresso.TextViewActions.dragHandle;
+import static android.widget.espresso.TextViewActions.Handle;
 import static android.widget.espresso.TextViewActions.longPressAndDragOnText;
 import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex;
 import static android.widget.espresso.TextViewAssertions.hasInsertionPointerAtIndex;
@@ -29,11 +31,16 @@
 import static android.support.test.espresso.action.ViewActions.pressKey;
 import static android.support.test.espresso.action.ViewActions.typeTextIntoFocusedView;
 import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
+import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
+import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
 import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.allOf;
 
 import com.android.frameworks.coretests.R;
 
+import android.support.test.espresso.ViewInteraction;
 import android.test.ActivityInstrumentationTestCase2;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.view.KeyEvent;
@@ -157,4 +164,26 @@
         Thread.sleep(100);
         assertFloatingToolbarIsDisplayed(getActivity());
     }
+
+    private static ViewInteraction onHandleView(int id) {
+        return onView(allOf(withId(id), isAssignableFrom(Editor.HandleView.class)))
+                .inRoot(withDecorView(hasDescendant(withId(id))));
+    }
+
+    @SmallTest
+    public void testSelectionHandles() throws Exception {
+        final String text = "abcd efg hijk lmn";
+        onView(withId(R.id.textview)).perform(click());
+        onView(withId(R.id.textview)).perform(typeTextIntoFocusedView(text));
+        onView(withId(R.id.textview)).perform(doubleClickOnTextAtIndex(text.indexOf('f')));
+
+        final TextView textView = (TextView)getActivity().findViewById(R.id.textview);
+        onHandleView(com.android.internal.R.id.selection_start_handle)
+                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
+        onView(withId(R.id.textview)).check(hasSelection("abcd efg"));
+
+        onHandleView(com.android.internal.R.id.selection_end_handle)
+                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('k') + 1));
+        onView(withId(R.id.textview)).check(hasSelection("abcd efg hijk"));
+    }
 }
diff --git a/core/tests/coretests/src/android/widget/espresso/DragOnTextViewActions.java b/core/tests/coretests/src/android/widget/espresso/DragAction.java
similarity index 87%
rename from core/tests/coretests/src/android/widget/espresso/DragOnTextViewActions.java
rename to core/tests/coretests/src/android/widget/espresso/DragAction.java
index 9ff8e82..07a2067 100644
--- a/core/tests/coretests/src/android/widget/espresso/DragOnTextViewActions.java
+++ b/core/tests/coretests/src/android/widget/espresso/DragAction.java
@@ -34,26 +34,25 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
-import android.widget.TextView;
 
 import org.hamcrest.Matcher;
 
 
 /**
- * Drags on text in a TextView using touch events.<br>
+ * Drags on a View using touch events.<br>
  * <br>
  * View constraints:
  * <ul>
- * <li>must be a TextView displayed on screen
+ * <li>must be displayed on screen
  * <ul>
  */
-public final class DragOnTextViewActions implements ViewAction {
+public final class DragAction implements ViewAction {
     public interface Dragger extends Swiper {
         UiController wrapUiController(UiController uiController);
     }
 
     /**
-     * Executes different "drag on text" types to given positions.
+     * Executes different drag types to given positions.
      */
     public enum Drag implements Dragger {
 
@@ -82,7 +81,7 @@
 
             @Override
             public String toString() {
-                return "mouse down and drag to select";
+                return "mouse down and drag";
             }
 
             @Override
@@ -92,6 +91,35 @@
         },
 
         /**
+         * Starts a drag with a tap.
+         */
+        TAP {
+            private DownMotionPerformer downMotion = new DownMotionPerformer() {
+                @Override
+                public MotionEvent perform(
+                        UiController uiController, float[] coordinates, float[] precision) {
+                    MotionEvent downEvent = MotionEvents.sendDown(
+                            uiController, coordinates, precision)
+                            .down;
+                    return downEvent;
+                }
+            };
+
+            @Override
+            public Status sendSwipe(
+                    UiController uiController,
+                    float[] startCoordinates, float[] endCoordinates, float[] precision) {
+                return sendLinearDrag(
+                        uiController, downMotion, startCoordinates, endCoordinates, precision);
+            }
+
+            @Override
+            public String toString() {
+                return "tap and drag";
+            }
+        },
+
+        /**
          * Starts a drag with a long-press.
          */
         LONG_PRESS {
@@ -121,7 +149,7 @@
 
             @Override
             public String toString() {
-                return "long press and drag to select";
+                return "long press and drag";
             }
         },
 
@@ -166,7 +194,7 @@
 
             @Override
             public String toString() {
-                return "double-tap and drag to select";
+                return "double-tap and drag";
             }
         };
 
@@ -258,22 +286,25 @@
     private final CoordinatesProvider mStartCoordinatesProvider;
     private final CoordinatesProvider mEndCoordinatesProvider;
     private final PrecisionDescriber mPrecisionDescriber;
+    private final Class<? extends View> mViewClass;
 
-    public DragOnTextViewActions(
+    public DragAction(
             Dragger dragger,
             CoordinatesProvider startCoordinatesProvider,
             CoordinatesProvider endCoordinatesProvider,
-            PrecisionDescriber precisionDescriber) {
+            PrecisionDescriber precisionDescriber,
+            Class<? extends View> viewClass) {
         mDragger = checkNotNull(dragger);
         mStartCoordinatesProvider = checkNotNull(startCoordinatesProvider);
         mEndCoordinatesProvider = checkNotNull(endCoordinatesProvider);
         mPrecisionDescriber = checkNotNull(precisionDescriber);
+        mViewClass = viewClass;
     }
 
     @Override
     @SuppressWarnings("unchecked")
     public Matcher<View> getConstraints() {
-        return allOf(isCompletelyDisplayed(), isAssignableFrom(TextView.class));
+        return allOf(isCompletelyDisplayed(), isAssignableFrom(mViewClass));
     }
 
     @Override
diff --git a/core/tests/coretests/src/android/widget/espresso/TextViewActions.java b/core/tests/coretests/src/android/widget/espresso/TextViewActions.java
index 4f5a72b..b84afff 100644
--- a/core/tests/coretests/src/android/widget/espresso/TextViewActions.java
+++ b/core/tests/coretests/src/android/widget/espresso/TextViewActions.java
@@ -17,7 +17,7 @@
 package android.widget.espresso;
 
 import static android.support.test.espresso.action.ViewActions.actionWithAssertions;
-
+import android.graphics.Rect;
 import android.support.test.espresso.PerformException;
 import android.support.test.espresso.ViewAction;
 import android.support.test.espresso.action.CoordinatesProvider;
@@ -27,6 +27,7 @@
 import android.support.test.espresso.util.HumanReadables;
 import android.text.Layout;
 import android.view.View;
+import android.widget.Editor;
 import android.widget.TextView;
 
 /**
@@ -95,11 +96,12 @@
      */
     public static ViewAction longPressAndDragOnText(int startIndex, int endIndex) {
         return actionWithAssertions(
-                new DragOnTextViewActions(
-                        DragOnTextViewActions.Drag.LONG_PRESS,
+                new DragAction(
+                        DragAction.Drag.LONG_PRESS,
                         new TextCoordinates(startIndex),
                         new TextCoordinates(endIndex),
-                        Press.FINGER));
+                        Press.FINGER,
+                        TextView.class));
     }
 
     /**
@@ -116,11 +118,12 @@
      */
     public static ViewAction doubleTapAndDragOnText(int startIndex, int endIndex) {
         return actionWithAssertions(
-                new DragOnTextViewActions(
-                        DragOnTextViewActions.Drag.DOUBLE_TAP,
+                new DragAction(
+                        DragAction.Drag.DOUBLE_TAP,
                         new TextCoordinates(startIndex),
                         new TextCoordinates(endIndex),
-                        Press.FINGER));
+                        Press.FINGER,
+                        TextView.class));
     }
 
     /**
@@ -137,11 +140,89 @@
      */
     public static ViewAction mouseDragOnText(int startIndex, int endIndex) {
         return actionWithAssertions(
-                new DragOnTextViewActions(
-                        DragOnTextViewActions.Drag.MOUSE_DOWN,
+                new DragAction(
+                        DragAction.Drag.MOUSE_DOWN,
                         new TextCoordinates(startIndex),
                         new TextCoordinates(endIndex),
-                        Press.PINPOINT));
+                        Press.PINPOINT,
+                        TextView.class));
+    }
+
+    public enum Handle {
+        SELECTION_START,
+        SELECTION_END,
+        INSERTION
+    };
+
+    /**
+     * Returns an action that tap then drags on the handle from the current position to endIndex on
+     * the TextView.<br>
+     * <br>
+     * View constraints:
+     * <ul>
+     * <li>must be a TextView's drag-handle displayed on screen
+     * <ul>
+     *
+     * @param textView TextView the handle is on
+     * @param handleType Type of the handle
+     * @param endIndex The index of the TextView's text to end the drag at
+     */
+    public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex) {
+        final int currentOffset = handleType == Handle.SELECTION_START ?
+                textView.getSelectionStart() : textView.getSelectionEnd();
+        return actionWithAssertions(
+                new DragAction(
+                        DragAction.Drag.TAP,
+                        new HandleCoordinates(textView, handleType, currentOffset),
+                        new HandleCoordinates(textView, handleType, endIndex),
+                        Press.FINGER,
+                        Editor.HandleView.class));
+    }
+
+    /**
+     * A provider of the x, y coordinates of the handle that points the specified text index in a
+     * text view.
+     */
+    private static final class HandleCoordinates implements CoordinatesProvider {
+        private final TextView mTextView;
+        private final Handle mHandleType;
+        private final int mIndex;
+        private final String mActionDescription;
+
+        public HandleCoordinates(TextView textView, Handle handleType, int index) {
+            mTextView = textView;
+            mHandleType = handleType;
+            mIndex = index;
+            mActionDescription = "Could not locate " + handleType.toString()
+                    + " handle that points text index: " + index;
+        }
+
+        @Override
+        public float[] calculateCoordinates(View view) {
+            try {
+                return locateHandlePointsTextIndex(view);
+            } catch (StringIndexOutOfBoundsException e) {
+                throw new PerformException.Builder()
+                        .withActionDescription(mActionDescription)
+                        .withViewDescription(HumanReadables.describe(view))
+                        .withCause(e)
+                        .build();
+            }
+        }
+
+        private float[] locateHandlePointsTextIndex(View view) {
+            final int currentOffset = mHandleType == Handle.SELECTION_START ?
+                    mTextView.getSelectionStart() : mTextView.getSelectionEnd();
+            final float[] currentCoordinates =
+                    (new TextCoordinates(currentOffset)).calculateCoordinates(mTextView);
+            final float[] targetCoordinates =
+                    (new TextCoordinates(mIndex)).calculateCoordinates(mTextView);
+            final Rect bounds = new Rect();
+            view.getBoundsOnScreen(bounds);
+            final float diffX = bounds.centerX() - currentCoordinates[0];
+            final float diffY = bounds.centerY() - currentCoordinates[1];
+            return new float[] {targetCoordinates[0] + diffX, targetCoordinates[1] + diffY};
+        }
     }
 
     /**