Move mouse selection handling logic to Editor.

- Remove mouse selection related logic from
ArrowKeyMovementMethod and Touch.
- Extend drag accelerator to support character based
selection by dragging and use it for mouse drag selection.
- Enable word based drag accelerator even if the clicked
position is not on a word.
- Disable y-coordinate adjustment for word based drag
accelerator when the user is using a mouse.

Bug: 19544351

Change-Id: I1b29a65be3d321ac472f8c5b70ab6fee4e895964
diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java
index 2459cfa..57fe131 100644
--- a/core/java/android/text/method/ArrowKeyMovementMethod.java
+++ b/core/java/android/text/method/ArrowKeyMovementMethod.java
@@ -20,7 +20,6 @@
 import android.text.Layout;
 import android.text.Selection;
 import android.text.Spannable;
-import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
@@ -222,34 +221,26 @@
         return lineEnd(widget, buffer);
     }
 
-    private static boolean isTouchSelecting(boolean isMouse, Spannable buffer) {
-        return isMouse ? Touch.isActivelySelecting(buffer) : isSelecting(buffer);
-    }
-
     @Override
     public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
         int initialScrollX = -1;
         int initialScrollY = -1;
         final int action = event.getAction();
-        final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
 
         if (action == MotionEvent.ACTION_UP) {
             initialScrollX = Touch.getInitialScrollX(widget, buffer);
             initialScrollY = Touch.getInitialScrollY(widget, buffer);
         }
 
-        boolean wasTouchSelecting = isTouchSelecting(isMouse, buffer);
+        boolean wasTouchSelecting = isSelecting(buffer);
         boolean handled = Touch.onTouchEvent(widget, buffer, event);
 
-        if (widget.didTouchFocusSelect() && !isMouse) {
+        if (widget.didTouchFocusSelect()) {
             return handled;
         }
         if (action == MotionEvent.ACTION_DOWN) {
-            // Capture the mouse pointer down location to ensure selection starts
-            // right under the mouse (and is not influenced by cursor location).
-            // The code below needs to run for mouse events.
             // For touch events, the code should run only when selection is active.
-            if (isMouse || isTouchSelecting(isMouse, buffer)) {
+            if (isSelecting(buffer)) {
                 if (!widget.isFocused()) {
                     if (!widget.requestFocus()) {
                         return handled;
@@ -265,15 +256,8 @@
             }
         } else if (widget.isFocused()) {
             if (action == MotionEvent.ACTION_MOVE) {
-                // Cursor can be active at any location in the text while mouse pointer can start
-                // selection from a totally different location. Use LAST_TAP_DOWN span to ensure
-                // text selection will start from mouse pointer location.
-                final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN);
-                if (isMouse && Touch.isSelectionStarted(buffer)) {
-                    Selection.setSelection(buffer, startOffset);
-                }
-
-                if (isTouchSelecting(isMouse, buffer) && handled) {
+                if (isSelecting(buffer) && handled) {
+                    final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN);
                     // Before selecting, make sure we've moved out of the "slop".
                     // handled will be true, if we're in select mode AND we're
                     // OUT of the slop
diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java
index fee7377..d9068dc 100644
--- a/core/java/android/text/method/Touch.java
+++ b/core/java/android/text/method/Touch.java
@@ -119,18 +119,12 @@
             ds = buffer.getSpans(0, buffer.length(), DragState.class);
 
             if (ds.length > 0) {
-                ds[0].mIsSelectionStarted = false;
-
                 if (ds[0].mFarEnough == false) {
                     int slop = ViewConfiguration.get(widget.getContext()).getScaledTouchSlop();
 
                     if (Math.abs(event.getX() - ds[0].mX) >= slop ||
                         Math.abs(event.getY() - ds[0].mY) >= slop) {
                         ds[0].mFarEnough = true;
-                        if (event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
-                            ds[0].mIsActivelySelecting = true;
-                            ds[0].mIsSelectionStarted = true;
-                        }
                     }
                 }
 
@@ -142,13 +136,9 @@
                             || MetaKeyKeyListener.getMetaState(buffer,
                                     MetaKeyKeyListener.META_SELECTING) != 0;
 
-                    if (!event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
-                        ds[0].mIsActivelySelecting = false;
-                    }
-
                     float dx;
                     float dy;
-                    if (cap && event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
+                    if (cap) {
                         // if we're selecting, we want the scroll to go in
                         // the direction of the drag
                         dx = event.getX() - ds[0].mX;
@@ -160,7 +150,6 @@
                     ds[0].mX = event.getX();
                     ds[0].mY = event.getY();
 
-                    int nx = widget.getScrollX() + (int) dx;
                     int ny = widget.getScrollY() + (int) dy;
 
                     int padding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom();
@@ -172,10 +161,6 @@
                     int oldX = widget.getScrollX();
                     int oldY = widget.getScrollY();
 
-                    if (!event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
-                        scrollTo(widget, layout, nx, ny);
-                    }
-
                     // If we actually scrolled, then cancel the up action.
                     if (oldX != widget.getScrollX() || oldY != widget.getScrollY()) {
                         widget.cancelLongPress();
@@ -207,37 +192,6 @@
         return ds.length > 0 ? ds[0].mScrollY : -1;
     }
 
-    /**
-     * Checks if selection is still active.
-     * This is useful for extending Selection span on buffer.
-     * @param buffer The text buffer.
-     * @return true if buffer has been marked for selection.
-     *
-     * @hide
-     */
-    static boolean isActivelySelecting(Spannable buffer) {
-        DragState[] ds;
-        ds = buffer.getSpans(0, buffer.length(), DragState.class);
-
-        return ds.length > 0 && ds[0].mIsActivelySelecting;
-    }
-
-    /**
-     * Checks if selection has begun (are we out of slop?).
-     * Note: DragState.mIsSelectionStarted goes back to false with the very next event.
-     * This is useful for starting Selection span on buffer.
-     * @param buffer The text buffer.
-     * @return true if selection has started on the buffer.
-     *
-     * @hide
-     */
-    static boolean isSelectionStarted(Spannable buffer) {
-        DragState[] ds;
-        ds = buffer.getSpans(0, buffer.length(), DragState.class);
-
-        return ds.length > 0 && ds[0].mIsSelectionStarted;
-    }
-
     private static class DragState implements NoCopySpan {
         public float mX;
         public float mY;
@@ -245,8 +199,6 @@
         public int mScrollY;
         public boolean mFarEnough;
         public boolean mUsed;
-        public boolean mIsActivelySelecting;
-        public boolean mIsSelectionStarted;
 
         public DragState(float x, float y, int scrollX, int scrollY) {
             mX = x;
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 5146bc6..6b5d87a 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -75,6 +75,7 @@
 import android.view.DisplayListCanvas;
 import android.view.DragEvent;
 import android.view.Gravity;
+import android.view.InputDevice;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -805,8 +806,8 @@
         final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
 
         // Safety check in case standard touch event handling has been bypassed
-        if (minOffset < 0 || minOffset >= mTextView.getText().length()) return false;
-        if (maxOffset < 0 || maxOffset >= mTextView.getText().length()) return false;
+        if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
+        if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
 
         int selectionStart, selectionEnd;
 
@@ -1773,7 +1774,8 @@
         stopTextActionMode();
         mPreserveDetachedSelection = false;
 
-        getSelectionController().enterDrag();
+        getSelectionController().enterDrag(
+                SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
         return true;
     }
 
@@ -4492,14 +4494,22 @@
 
         // Where the user first starts the drag motion.
         private int mStartOffset = -1;
-        // Indicates whether the user is selecting text and using the drag accelerator.
-        private boolean mDragAcceleratorActive;
+
         private boolean mHaventMovedEnoughToStartDrag;
         // The line that a selection happened most recently with the drag accelerator.
         private int mLineSelectionIsOn = -1;
         // Whether the drag accelerator has selected past the initial line.
         private boolean mSwitchedLines = false;
 
+        // Indicates the drag accelerator mode that the user is currently using.
+        private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
+        // Drag accelerator is inactive.
+        private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
+        // Character based selection by dragging. Only for mouse.
+        private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
+        // Word based selection by dragging. Enabled after long pressing or double tapping.
+        private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
+
         SelectionModifierCursorController() {
             resetTouchOffsets();
         }
@@ -4510,7 +4520,6 @@
             }
             initDrawables();
             initHandles();
-            hideInsertionPointCursorController();
         }
 
         private void initDrawables() {
@@ -4548,10 +4557,10 @@
             if (mEndHandle != null) mEndHandle.hide();
         }
 
-        public void enterDrag() {
+        public void enterDrag(int dragAcceleratorMode) {
             // Just need to init the handles / hide insertion cursor.
             show();
-            mDragAcceleratorActive = true;
+            mDragAcceleratorMode = dragAcceleratorMode;
             // Start location of selection.
             mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
                     mLastDownPositionY);
@@ -4563,6 +4572,7 @@
             // the user to continue dragging across the screen to select text; TextView will
             // scroll as necessary.
             mTextView.getParent().requestDisallowInterceptTouchEvent(true);
+            mTextView.cancelLongPress();
         }
 
         public void onTouchEvent(MotionEvent event) {
@@ -4570,6 +4580,7 @@
             // selection and tap can move cursor from this tap position.
             final float eventX = event.getX();
             final float eventY = event.getY();
+            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
             switch (event.getActionMasked()) {
                 case MotionEvent.ACTION_DOWN:
                     if (extractedTextModeWillBeStarted()) {
@@ -4593,7 +4604,7 @@
                                 boolean stayedInArea =
                                         distanceSquared < doubleTapSlop * doubleTapSlop;
 
-                                if (stayedInArea && isPositionOnText(eventX, eventY)) {
+                                if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
                                     selectCurrentWordAndStartDrag();
                                     mDiscardNextActionUp = true;
                                 }
@@ -4639,94 +4650,136 @@
                         }
                     }
 
+                    if (isMouse && !isDragAcceleratorActive()) {
+                        final int offset = mTextView.getOffsetForPosition(eventX, eventY);
+                        if (mStartOffset != offset) {
+                            // Start character based drag accelerator.
+                            if (mTextActionMode != null) {
+                                mTextActionMode.finish();
+                            }
+                            enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
+                            mDiscardNextActionUp = true;
+                            mHaventMovedEnoughToStartDrag = false;
+                        }
+                    }
+
                     if (mStartHandle != null && mStartHandle.isShowing()) {
                         // Don't do the drag if the handles are showing already.
                         break;
                     }
 
-                    if (mStartOffset != -1 && mTextView.getLayout() != null) {
-                        if (!mHaventMovedEnoughToStartDrag) {
-
-                            float y = eventY;
-                            if (mSwitchedLines) {
-                                // Offset the finger by the same vertical offset as the handles.
-                                // This improves visibility of the content being selected by
-                                // shifting the finger below the content, this is applied once
-                                // the user has switched lines.
-                                final float fingerOffset = (mStartHandle != null)
-                                        ? mStartHandle.getIdealVerticalOffset()
-                                        : touchSlop;
-                                y = eventY - fingerOffset;
-                            }
-
-                            final int currLine = getCurrentLineAdjustedForSlop(
-                                    mTextView.getLayout(),
-                                    mLineSelectionIsOn, y);
-                            if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
-                                // Break early here, we want to offset the finger position from
-                                // the selection highlight, once the user moved their finger
-                                // to a different line we should apply the offset and *not* switch
-                                // lines until recomputing the position with the finger offset.
-                                mSwitchedLines = true;
-                                break;
-                            }
-
-                            int startOffset;
-                            int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
-                            // Snap to word boundaries.
-                            if (mStartOffset < offset) {
-                                // Expanding with end handle.
-                                offset = getWordEnd(offset);
-                                startOffset = getWordStart(mStartOffset);
-                            } else {
-                                // Expanding with start handle.
-                                offset = getWordStart(offset);
-                                startOffset = getWordEnd(mStartOffset);
-                            }
-                            mLineSelectionIsOn = currLine;
-                            Selection.setSelection((Spannable) mTextView.getText(),
-                                    startOffset, offset);
-                        }
-                    }
+                    updateSelection(event);
                     break;
 
                 case MotionEvent.ACTION_UP:
-                    if (mDragAcceleratorActive) {
-                        // No longer dragging to select text, let the parent intercept events.
-                        mTextView.getParent().requestDisallowInterceptTouchEvent(false);
+                    if (!isDragAcceleratorActive()) {
+                        break;
+                    }
+                    updateSelection(event);
 
-                        show();
-                        int startOffset = mTextView.getSelectionStart();
-                        int endOffset = mTextView.getSelectionEnd();
+                    // No longer dragging to select text, let the parent intercept events.
+                    mTextView.getParent().requestDisallowInterceptTouchEvent(false);
 
-                        // Since we don't let drag handles pass once they're visible, we need to
-                        // make sure the start / end locations are correct because the user *can*
-                        // switch directions during the initial drag.
-                        if (endOffset < startOffset) {
-                            int tmp = endOffset;
-                            endOffset = startOffset;
-                            startOffset = tmp;
+                    int startOffset = mTextView.getSelectionStart();
+                    int endOffset = mTextView.getSelectionEnd();
 
-                            // Also update the selection with the right offsets in this case.
-                            Selection.setSelection((Spannable) mTextView.getText(),
-                                    startOffset, endOffset);
-                        }
+                    // Since we don't let drag handles pass once they're visible, we need to
+                    // make sure the start / end locations are correct because the user *can*
+                    // switch directions during the initial drag.
+                    if (endOffset < startOffset) {
+                        int tmp = endOffset;
+                        endOffset = startOffset;
+                        startOffset = tmp;
 
-                        // Need to do this to display the handles.
+                        // Also update the selection with the right offsets in this case.
+                        Selection.setSelection((Spannable) mTextView.getText(),
+                                startOffset, endOffset);
+                    }
+                    if (startOffset != endOffset) {
                         mStartHandle.showAtLocation(startOffset);
                         mEndHandle.showAtLocation(endOffset);
-
-                        // No longer the first dragging motion, reset.
                         startSelectionActionMode();
-
-                        mDragAcceleratorActive = false;
-                        mStartOffset = -1;
-                        mSwitchedLines = false;
                     }
+
+                    // No longer the first dragging motion, reset.
+                    resetDragAcceleratorState();
                     break;
             }
         }
 
+        private void updateSelection(MotionEvent event) {
+            if (mTextView.getLayout() != null) {
+                switch (mDragAcceleratorMode) {
+                    case DRAG_ACCELERATOR_MODE_CHARACTER:
+                        updateCharacterBasedSelection(event);
+                        break;
+                    case DRAG_ACCELERATOR_MODE_WORD:
+                        updateWordBasedSelection(event);
+                        break;
+                }
+            }
+        }
+
+        private void updateCharacterBasedSelection(MotionEvent event) {
+            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+            Selection.setSelection((Spannable) mTextView.getText(), mStartOffset, offset);
+        }
+
+        private void updateWordBasedSelection(MotionEvent event) {
+            if (mHaventMovedEnoughToStartDrag) {
+                return;
+            }
+            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
+            final ViewConfiguration viewConfig = ViewConfiguration.get(
+                    mTextView.getContext());
+            final float eventX = event.getX();
+            final float eventY = event.getY();
+            final int currLine;
+            if (isMouse) {
+                // No need to offset the y coordinate for mouse input.
+                currLine = mTextView.getLineAtCoordinate(eventY);
+            } else {
+                float y = eventY;
+                if (mSwitchedLines) {
+                    // Offset the finger by the same vertical offset as the handles.
+                    // This improves visibility of the content being selected by
+                    // shifting the finger below the content, this is applied once
+                    // the user has switched lines.
+                    final int touchSlop = viewConfig.getScaledTouchSlop();
+                    final float fingerOffset = (mStartHandle != null)
+                            ? mStartHandle.getIdealVerticalOffset()
+                            : touchSlop;
+                    y = eventY - fingerOffset;
+                }
+
+                currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
+                        y);
+                if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
+                    // Break early here, we want to offset the finger position from
+                    // the selection highlight, once the user moved their finger
+                    // to a different line we should apply the offset and *not* switch
+                    // lines until recomputing the position with the finger offset.
+                    mSwitchedLines = true;
+                    return;
+                }
+            }
+
+            int startOffset;
+            int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
+            // Snap to word boundaries.
+            if (mStartOffset < offset) {
+                // Expanding with end handle.
+                offset = getWordEnd(offset);
+                startOffset = getWordStart(mStartOffset);
+            } else {
+                // Expanding with start handle.
+                offset = getWordStart(offset);
+                startOffset = getWordEnd(mStartOffset);
+            }
+            mLineSelectionIsOn = currLine;
+            Selection.setSelection((Spannable) mTextView.getText(),
+                    startOffset, offset);
+        }
         /**
          * @param event
          */
@@ -4749,8 +4802,12 @@
 
         public void resetTouchOffsets() {
             mMinTouchOffset = mMaxTouchOffset = -1;
+            resetDragAcceleratorState();
+        }
+
+        private void resetDragAcceleratorState() {
             mStartOffset = -1;
-            mDragAcceleratorActive = false;
+            mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
             mSwitchedLines = false;
         }
 
@@ -4765,7 +4822,7 @@
          * @return true if the user is selecting text using the drag accelerator.
          */
         public boolean isDragAcceleratorActive() {
-            return mDragAcceleratorActive;
+            return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
         }
 
         public void onTouchModeChanged(boolean isInTouchMode) {