Merge "Text selection changes - smart handles & drag accelerator"
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index f8e207f..5d519ed 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -217,6 +217,12 @@
     WordIterator mWordIterator;
     SpellChecker mSpellChecker;
 
+    // This word iterator is set with text and used to determine word boundaries
+    // when a user is selecting text.
+    private WordIterator mWordIteratorWithText;
+    // Indicate that the text in the word iterator needs to be updated.
+    private boolean mUpdateWordIteratorText;
+
     private Rect mTempRect;
 
     private TextView mTextView;
@@ -689,9 +695,52 @@
         return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod;
     }
 
+    private int getWordStart(int offset) {
+        // FIXME - For this and similar methods we're not doing anything to check if there's
+        // a LocaleSpan in the text, this may be something we should try handling or checking for.
+        int retOffset = getWordIteratorWithText().getBeginning(offset);
+        if (retOffset == BreakIterator.DONE) retOffset = offset;
+        return retOffset;
+    }
+
+    private int getWordEnd(int offset, boolean includePunctuation) {
+        int retOffset = getWordIteratorWithText().getEnd(offset);
+        if (retOffset == BreakIterator.DONE) {
+            retOffset = offset;
+        } else if (includePunctuation) {
+            retOffset = handlePunctuation(retOffset);
+        }
+        return retOffset;
+    }
+
+    private boolean isEndBoundary(int offset) {
+        int thisEnd = getWordEnd(offset, false);
+        return offset == thisEnd;
+    }
+
+    private boolean isStartBoundary(int offset) {
+        int thisStart = getWordStart(offset);
+        return thisStart == offset;
+    }
+
+    private int handlePunctuation(int offset) {
+        // FIXME - Check with UX how repeated ending punctuation should be handled.
+        // FIXME - Check with UX if / how we would handle non sentence ending characters.
+        // FIXME - Consider punctuation in different languages.
+        CharSequence text = mTextView.getText();
+        if (offset < text.length()) {
+            int c = Character.codePointAt(text, offset);
+            if (c == 0x002e /* period */|| c == 0x003f /* question mark */
+                    || c == 0x0021 /* exclamation mark */) {
+                offset = Character.offsetByCodePoints(text, offset, 1);
+            }
+        }
+        return offset;
+    }
+
     /**
-     * Adjusts selection to the word under last touch offset.
-     * Return true if the operation was successfully performed.
+     * Adjusts selection to the word under last touch offset. Return true if the operation was
+     * successfully performed.
      */
     private boolean selectCurrentWord() {
         if (!canSelectText()) {
@@ -738,6 +787,8 @@
             selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
             selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
         } else {
+            // FIXME - We should check if there's a LocaleSpan in the text, this may be
+            // something we should try handling or checking for.
             final WordIterator wordIterator = getWordIterator();
             wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
 
@@ -760,6 +811,7 @@
     void onLocaleChanged() {
         // Will be re-created on demand in getWordIterator with the proper new locale
         mWordIterator = null;
+        mWordIteratorWithText = null;
     }
 
     /**
@@ -772,6 +824,23 @@
         return mWordIterator;
     }
 
+    private WordIterator getWordIteratorWithText() {
+        if (mWordIteratorWithText == null) {
+            mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
+            mUpdateWordIteratorText = true;
+        }
+        if (mUpdateWordIteratorText) {
+            // FIXME - Shouldn't copy all of the text as only the area of the text relevant
+            // to the user's selection is needed. A possible solution would be to
+            // copy some number N of characters near the selection and then when the
+            // user approaches N then we'd do another copy of the next N characters.
+            CharSequence text = mTextView.getText();
+            mWordIteratorWithText.setCharSequence(text, 0, text.length());
+            mUpdateWordIteratorText = false;
+        }
+        return mWordIteratorWithText;
+    }
+
     private long getCharRange(int offset) {
         final int textLength = mTextView.getText().length();
         if (offset + 1 < textLength) {
@@ -920,9 +989,8 @@
                 mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
                 stopSelectionActionMode();
             } else {
-                getSelectionController().hide();
-                selectCurrentWord();
-                getSelectionController().show();
+                stopSelectionActionMode();
+                startSelectionActionMode();
             }
             handled = true;
         }
@@ -1058,6 +1126,9 @@
     void sendOnTextChanged(int start, int after) {
         updateSpellCheckSpans(start, start + after, false);
 
+        // Flip flag to indicate the word iterator needs to have the text reset.
+        mUpdateWordIteratorText = true;
+
         // Hide the controllers as soon as text is modified (typing, procedural...)
         // We do not hide the span controllers, since they can be added when a new text is
         // inserted into the text view (voice IME).
@@ -1613,6 +1684,9 @@
             }
         }
 
+        if (selectionStarted) {
+            getSelectionController().enterDrag();
+        }
         return selectionStarted;
     }
 
@@ -2894,7 +2968,6 @@
             }
 
             if (menu.hasVisibleItems() || mode.getCustomView() != null) {
-                getSelectionController().show();
                 mTextView.setHasTransientState(true);
                 return true;
             } else {
@@ -3232,6 +3305,8 @@
         private Runnable mActionPopupShower;
         // Minimum touch target size for handles
         private int mMinSize;
+        // Indicates the line of text that the handle is on.
+        protected int mLine = -1;
 
         public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
             super(mTextView.getContext());
@@ -3407,6 +3482,7 @@
                     addPositionToTouchUpFilter(offset);
                 }
                 final int line = layout.getLineForOffset(offset);
+                mLine = line;
 
                 mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX -
                         getHorizontalOffset() + getCursorOffset());
@@ -3456,6 +3532,30 @@
             }
         }
 
+        public void showAtLocation(int offset) {
+            // TODO - investigate if there's a better way to show the handles
+            // after the drag accelerator has occured.
+            int[] tmpCords = new int[2];
+            mTextView.getLocationInWindow(tmpCords);
+
+            Layout layout = mTextView.getLayout();
+            int posX = tmpCords[0];
+            int posY = tmpCords[1];
+
+            final int line = layout.getLineForOffset(offset);
+
+            int startX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f
+                    - mHotspotX - getHorizontalOffset() + getCursorOffset());
+            int startY = layout.getLineBottom(line);
+
+            // Take TextView's padding and scroll into account.
+            startX += mTextView.viewportToContentHorizontalOffset();
+            startY += mTextView.viewportToContentVerticalOffset();
+
+            mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
+                    startX + posX, startY + posY);
+        }
+
         @Override
         protected void onDraw(Canvas c) {
             final int drawWidth = mDrawable.getIntrinsicWidth();
@@ -3694,6 +3794,12 @@
     }
 
     private class SelectionStartHandleView extends HandleView {
+        // The previous offset this handle was at.
+        private int mPrevOffset;
+        // Indicates whether the cursor is making adjustments within a word.
+        private boolean mInWord = false;
+        // Offset to track difference between touch and word boundary.
+        protected int mTouchWordOffset;
 
         public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
             super(drawableLtr, drawableRtl);
@@ -3701,11 +3807,7 @@
 
         @Override
         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
-            if (isRtlRun) {
-                return drawable.getIntrinsicWidth() / 4;
-            } else {
-                return (drawable.getIntrinsicWidth() * 3) / 4;
-            }
+            return isRtlRun ? 0 : drawable.getIntrinsicWidth();
         }
 
         @Override
@@ -3727,21 +3829,81 @@
 
         @Override
         public void updatePosition(float x, float y) {
-            int offset = mTextView.getOffsetForPosition(x, y);
+            final int trueOffset = mTextView.getOffsetForPosition(x, y);
+            final int currLine = mTextView.getLineAtCoordinate(y);
+            int offset = trueOffset;
+            boolean positionCursor = false;
 
-            // Handles can not cross and selection is at least one character
-            final int selectionEnd = mTextView.getSelectionEnd();
-            if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
+            int end = getWordEnd(offset, true);
+            int start = getWordStart(offset);
 
-            positionAtCursorOffset(offset, false);
+            if (offset < mPrevOffset) {
+                // User is increasing the selection.
+                if (!mInWord || currLine < mLine) {
+                    // We're not in a word, or we're on a different line so we'll expand by
+                    // word. First ensure the user has at least entered the next word.
+                    int offsetToWord = Math.min((end - start) / 2, 2);
+                    if (offset <= end - offsetToWord || currLine < mLine) {
+                        offset = start;
+                    } else {
+                        offset = mPrevOffset;
+                    }
+                }
+                mPrevOffset = offset;
+                mTouchWordOffset = trueOffset - offset;
+                mInWord = !isStartBoundary(offset);
+                positionCursor = true;
+            } else if (offset - mTouchWordOffset > mPrevOffset) {
+                // User is shrinking the selection.
+                if (currLine > mLine) {
+                    // We're on a different line, so we'll snap to word boundaries.
+                    offset = end;
+                }
+                offset -= mTouchWordOffset;
+                mPrevOffset = offset;
+                mInWord = !isEndBoundary(offset);
+                positionCursor = true;
+            }
+
+            // Handles can not cross and selection is at least one character.
+            if (positionCursor) {
+                final int selectionEnd = mTextView.getSelectionEnd();
+                if (offset >= selectionEnd) {
+                    // We can't cross the handles so let's just constrain the Y value.
+                    int alteredOffset = mTextView.getOffsetAtCoordinate(mLine, x);
+                    if (alteredOffset >= selectionEnd) {
+                        // Can't pass the other drag handle.
+                        offset = Math.max(0, selectionEnd - 1);
+                    } else {
+                        offset = alteredOffset;
+                    }
+                }
+                positionAtCursorOffset(offset, false);
+            }
         }
 
         public ActionPopupWindow getActionPopupWindow() {
             return mActionPopupWindow;
         }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent event) {
+            boolean superResult = super.onTouchEvent(event);
+            if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+                // Reset the touch word offset when the user has lifted their finger.
+                mTouchWordOffset = 0;
+            }
+            return superResult;
+        }
     }
 
     private class SelectionEndHandleView extends HandleView {
+        // The previous offset this handle was at.
+        private int mPrevOffset;
+        // Indicates whether the cursor is making adjustments within a word.
+        private boolean mInWord = false;
+        // Offset to track difference between touch and word boundary.
+        protected int mTouchWordOffset;
 
         public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
             super(drawableLtr, drawableRtl);
@@ -3749,11 +3911,7 @@
 
         @Override
         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
-            if (isRtlRun) {
-                return (drawable.getIntrinsicWidth() * 3) / 4;
-            } else {
-                return drawable.getIntrinsicWidth() / 4;
-            }
+            return isRtlRun ? drawable.getIntrinsicWidth() : 0;
         }
 
         @Override
@@ -3775,20 +3933,72 @@
 
         @Override
         public void updatePosition(float x, float y) {
-            int offset = mTextView.getOffsetForPosition(x, y);
+            final int trueOffset = mTextView.getOffsetForPosition(x, y);
+            final int currLine = mTextView.getLineAtCoordinate(y);
+            int offset = trueOffset;
+            boolean positionCursor = false;
 
-            // Handles can not cross and selection is at least one character
-            final int selectionStart = mTextView.getSelectionStart();
-            if (offset <= selectionStart) {
-                offset = Math.min(selectionStart + 1, mTextView.getText().length());
+            int end = getWordEnd(offset, true);
+            int start = getWordStart(offset);
+
+            if (offset > mPrevOffset) {
+                // User is increasing the selection.
+                if (!mInWord || currLine > mLine) {
+                    // We're not in a word, or we're on a different line so we'll expand by
+                    // word. First ensure the user has at least entered the next word.
+                    int midPoint = Math.min((end - start) / 2, 2);
+                    if (offset >= start + midPoint || currLine > mLine) {
+                        offset = end;
+                    } else {
+                        offset = mPrevOffset;
+                    }
+                }
+                mPrevOffset = offset;
+                mTouchWordOffset = offset - trueOffset;
+                mInWord = !isEndBoundary(offset);
+                positionCursor = true;
+            } else if (offset + mTouchWordOffset < mPrevOffset) {
+                // User is shrinking the selection.
+                if (currLine > mLine) {
+                    // We're on a different line, so we'll snap to word boundaries.
+                    offset = getWordStart(offset);
+                }
+                offset += mTouchWordOffset;
+                mPrevOffset = offset;
+                positionCursor = true;
+                mInWord = !isStartBoundary(offset);
             }
 
-            positionAtCursorOffset(offset, false);
+            if (positionCursor) {
+                final int selectionStart = mTextView.getSelectionStart();
+                if (offset <= selectionStart) {
+                    // We can't cross the handles so let's just constrain the Y value.
+                    int alteredOffset = mTextView.getOffsetAtCoordinate(mLine, x);
+                    int length = mTextView.getText().length();
+                    if (alteredOffset <= selectionStart) {
+                        // Can't pass the other drag handle.
+                        offset = Math.min(selectionStart + 1, length);
+                    } else {
+                        offset = Math.min(alteredOffset, length);
+                    }
+                }
+                positionAtCursorOffset(offset, false);
+            }
         }
 
         public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
             mActionPopupWindow = actionPopupWindow;
         }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent event) {
+            boolean superResult = super.onTouchEvent(event);
+            if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+                // Reset the touch word offset when the user has lifted their finger.
+                mTouchWordOffset = 0;
+            }
+            return superResult;
+        }
     }
 
     /**
@@ -3871,6 +4081,11 @@
         private float mDownPositionX, mDownPositionY;
         private boolean mGestureStayedInTapRegion;
 
+        // 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;
+
         SelectionModifierCursorController() {
             resetTouchOffsets();
         }
@@ -3920,6 +4135,22 @@
             if (mEndHandle != null) mEndHandle.hide();
         }
 
+        public void enterDrag() {
+            // Just need to init the handles / hide insertion cursor.
+            show();
+            mDragAcceleratorActive = true;
+            // Start location of selection.
+            mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
+                    mLastDownPositionY);
+            // Don't show the handles until user has lifted finger.
+            hide();
+
+            // This stops scrolling parents from intercepting the touch event, allowing
+            // the user to continue dragging across the screen to select text; TextView will
+            // scroll as necessary.
+            mTextView.getParent().requestDisallowInterceptTouchEvent(true);
+        }
+
         public void onTouchEvent(MotionEvent event) {
             // This is done even when the View does not have focus, so that long presses can start
             // selection and tap can move cursor from this tap position.
@@ -3928,7 +4159,7 @@
                     final float x = event.getX();
                     final float y = event.getY();
 
-                    // Remember finger down position, to be able to start selection from there
+                    // Remember finger down position, to be able to start selection from there.
                     mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y);
 
                     // Double tap detection
@@ -3967,23 +4198,112 @@
                     break;
 
                 case MotionEvent.ACTION_MOVE:
+                    final ViewConfiguration viewConfiguration = ViewConfiguration.get(
+                            mTextView.getContext());
+
                     if (mGestureStayedInTapRegion) {
                         final float deltaX = event.getX() - mDownPositionX;
                         final float deltaY = event.getY() - mDownPositionY;
                         final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
 
-                        final ViewConfiguration viewConfiguration = ViewConfiguration.get(
-                                mTextView.getContext());
                         int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop();
 
                         if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) {
                             mGestureStayedInTapRegion = false;
                         }
                     }
+
+                    if (mStartHandle != null && mStartHandle.isShowing()) {
+                        // Don't do the drag if the handles are showing already.
+                        break;
+                    }
+
+                    if (mStartOffset != -1) {
+                        final int rawOffset = mTextView.getOffsetForPosition(event.getX(),
+                                event.getY());
+                        int offset = rawOffset;
+
+                        // We don't start "dragging" until the user is past the initial word that
+                        // gets selected on long press.
+                        int firstWordStart = getWordStart(mStartOffset);
+                        int firstWordEnd = getWordEnd(mStartOffset, false);
+                        if (offset > firstWordEnd || offset < firstWordStart) {
+
+                            // Basically the goal in the below code is to have the highlight be
+                            // offset so that your finger isn't covering the end point.
+                            int fingerOffset = viewConfiguration.getScaledTouchSlop();
+                            float mx = event.getX();
+                            float my = event.getY();
+                            if (mx > fingerOffset) mx -= fingerOffset;
+                            if (my > fingerOffset) my -= fingerOffset;
+                            offset = mTextView.getOffsetForPosition(mx, my);
+
+                            // Perform the check for closeness at edge of view, if we're very close
+                            // don't adjust the offset to be in front of the finger - otherwise the
+                            // user can't select words at the edge.
+                            if (mTextView.getWidth() - fingerOffset > mx) {
+                                // We're going by word, so we need to make sure that the offset
+                                // that we get is within this, so we'll get the previous boundary.
+                                final WordIterator wordIterator = getWordIteratorWithText();
+
+                                final int precedingOffset = wordIterator.preceding(offset);
+                                if (mStartOffset < offset) {
+                                    // Expanding with bottom handle, in this case the selection end
+                                    // is before the finger.
+                                    offset = Math.max(precedingOffset - 1, 0);
+                                } else {
+                                    // Expand with the start handle, in this case the selection
+                                    // start is before the finger.
+                                    if (precedingOffset == WordIterator.DONE) {
+                                        offset = 0;
+                                    } else {
+                                        offset = wordIterator.preceding(precedingOffset);
+                                    }
+                                }
+                            }
+                            if (offset == WordIterator.DONE)
+                                offset = rawOffset;
+
+                            // Need to adjust start offset based on direction of movement.
+                            int newStart = mStartOffset < offset ? getWordStart(mStartOffset)
+                                    : getWordEnd(mStartOffset, true);
+                            Selection.setSelection((Spannable) mTextView.getText(), newStart,
+                                    offset);
+                        }
+                    }
                     break;
 
                 case MotionEvent.ACTION_UP:
                     mPreviousTapUpTime = SystemClock.uptimeMillis();
+                    if (mDragAcceleratorActive) {
+                        // No longer dragging to select text, let the parent intercept events.
+                        mTextView.getParent().requestDisallowInterceptTouchEvent(false);
+
+                        show();
+                        int startOffset = mTextView.getSelectionStart();
+                        int endOffset = mTextView.getSelectionEnd();
+
+                        // 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;
+
+                            // Also update the selection with the right offsets in this case.
+                            Selection.setSelection((Spannable) mTextView.getText(),
+                                    startOffset, endOffset);
+                        }
+
+                        // Need to do this to display the handles.
+                        mStartHandle.showAtLocation(startOffset);
+                        mEndHandle.showAtLocation(endOffset);
+
+                        // No longer the first dragging motion, reset.
+                        mDragAcceleratorActive = false;
+                        mStartOffset = -1;
+                    }
                     break;
             }
         }
@@ -4010,6 +4330,8 @@
 
         public void resetTouchOffsets() {
             mMinTouchOffset = mMaxTouchOffset = -1;
+            mStartOffset = -1;
+            mDragAcceleratorActive = false;
         }
 
         /**
@@ -4019,6 +4341,13 @@
             return mStartHandle != null && mStartHandle.isDragging();
         }
 
+        /**
+         * @return true if the user is selecting text using the drag accelerator.
+         */
+        public boolean isDragAcceleratorActive() {
+            return mDragAcceleratorActive;
+        }
+
         public void onTouchModeChanged(boolean isInTouchMode) {
             if (!isInTouchMode) {
                 hide();
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 632f5c7..9bef1fe 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -8098,7 +8098,14 @@
     public boolean onTouchEvent(MotionEvent event) {
         final int action = event.getActionMasked();
 
-        if (mEditor != null) mEditor.onTouchEvent(event);
+        if (mEditor != null) {
+            mEditor.onTouchEvent(event);
+
+            if (mEditor.mSelectionModifierCursorController != null &&
+                    mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
+                return true;
+            }
+        }
 
         final boolean superResult = super.onTouchEvent(event);
 
@@ -9104,7 +9111,7 @@
         return getLayout().getLineForVertical((int) y);
     }
 
-    private int getOffsetAtCoordinate(int line, float x) {
+    int getOffsetAtCoordinate(int line, float x) {
         x = convertToLocalHorizontalCoordinate(x);
         return getLayout().getOffsetForHorizontal(line, x);
     }
@@ -9726,4 +9733,4 @@
             TextView.this.spanChange(buf, what, s, -1, e, -1);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/core/res/res/drawable-hdpi/text_select_handle_left_mtrl_alpha.png b/core/res/res/drawable-hdpi/text_select_handle_left_mtrl_alpha.png
index 9cdc25b..1550b44 100644
--- a/core/res/res/drawable-hdpi/text_select_handle_left_mtrl_alpha.png
+++ b/core/res/res/drawable-hdpi/text_select_handle_left_mtrl_alpha.png
Binary files differ
diff --git a/core/res/res/drawable-hdpi/text_select_handle_right_mtrl_alpha.png b/core/res/res/drawable-hdpi/text_select_handle_right_mtrl_alpha.png
index 276d480..b309dfd 100644
--- a/core/res/res/drawable-hdpi/text_select_handle_right_mtrl_alpha.png
+++ b/core/res/res/drawable-hdpi/text_select_handle_right_mtrl_alpha.png
Binary files differ
diff --git a/core/res/res/drawable-mdpi/text_select_handle_left_mtrl_alpha.png b/core/res/res/drawable-mdpi/text_select_handle_left_mtrl_alpha.png
index 95c0168..b36a413 100644
--- a/core/res/res/drawable-mdpi/text_select_handle_left_mtrl_alpha.png
+++ b/core/res/res/drawable-mdpi/text_select_handle_left_mtrl_alpha.png
Binary files differ
diff --git a/core/res/res/drawable-mdpi/text_select_handle_right_mtrl_alpha.png b/core/res/res/drawable-mdpi/text_select_handle_right_mtrl_alpha.png
index 569332a..afd0bd2 100644
--- a/core/res/res/drawable-mdpi/text_select_handle_right_mtrl_alpha.png
+++ b/core/res/res/drawable-mdpi/text_select_handle_right_mtrl_alpha.png
Binary files differ
diff --git a/core/res/res/drawable-xhdpi/text_select_handle_left_mtrl_alpha.png b/core/res/res/drawable-xhdpi/text_select_handle_left_mtrl_alpha.png
index a01ac10..58f8c43 100644
--- a/core/res/res/drawable-xhdpi/text_select_handle_left_mtrl_alpha.png
+++ b/core/res/res/drawable-xhdpi/text_select_handle_left_mtrl_alpha.png
Binary files differ
diff --git a/core/res/res/drawable-xhdpi/text_select_handle_right_mtrl_alpha.png b/core/res/res/drawable-xhdpi/text_select_handle_right_mtrl_alpha.png
index d3602d9..42a893d 100644
--- a/core/res/res/drawable-xhdpi/text_select_handle_right_mtrl_alpha.png
+++ b/core/res/res/drawable-xhdpi/text_select_handle_right_mtrl_alpha.png
Binary files differ
diff --git a/core/res/res/drawable-xxhdpi/text_select_handle_left_mtrl_alpha.png b/core/res/res/drawable-xxhdpi/text_select_handle_left_mtrl_alpha.png
index 75085ce..d0f274a 100644
--- a/core/res/res/drawable-xxhdpi/text_select_handle_left_mtrl_alpha.png
+++ b/core/res/res/drawable-xxhdpi/text_select_handle_left_mtrl_alpha.png
Binary files differ
diff --git a/core/res/res/drawable-xxhdpi/text_select_handle_right_mtrl_alpha.png b/core/res/res/drawable-xxhdpi/text_select_handle_right_mtrl_alpha.png
index e2eb5be..f1f637a 100644
--- a/core/res/res/drawable-xxhdpi/text_select_handle_right_mtrl_alpha.png
+++ b/core/res/res/drawable-xxhdpi/text_select_handle_right_mtrl_alpha.png
Binary files differ
diff --git a/core/res/res/drawable-xxxhdpi/text_select_handle_left_mtrl_alpha.png b/core/res/res/drawable-xxxhdpi/text_select_handle_left_mtrl_alpha.png
new file mode 100644
index 0000000..643168f
--- /dev/null
+++ b/core/res/res/drawable-xxxhdpi/text_select_handle_left_mtrl_alpha.png
Binary files differ
diff --git a/core/res/res/drawable-xxxhdpi/text_select_handle_right_mtrl_alpha.png b/core/res/res/drawable-xxxhdpi/text_select_handle_right_mtrl_alpha.png
new file mode 100644
index 0000000..e8f3aad
--- /dev/null
+++ b/core/res/res/drawable-xxxhdpi/text_select_handle_right_mtrl_alpha.png
Binary files differ