Clamp EditText cursor in the drawable boundaries.

EditText tried to draw outside of the padding boundaries because of a
cursor positioning issue in RTL. This CL removes that fix and instead
clamps the cursor position if cursor is outside of the clipped view
boundary.

Bug: 23397961
Change-Id: Id5f1fbe2a0f571100c89b21758fbb81b14d5da57
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 3a61fcd..1826dd8 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -1851,8 +1851,7 @@
         updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset, clamped));
 
         if (mCursorCount == 2) {
-            updateCursorPosition(1, middle, bottom,
-                    layout.getSecondaryHorizontal(offset, clamped));
+            updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset, clamped));
         }
     }
 
@@ -2151,21 +2150,60 @@
         return mSelectionModifierCursorController;
     }
 
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public Drawable[] getCursorDrawable() {
+        return mCursorDrawable;
+    }
+
     private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
         if (mCursorDrawable[cursorIndex] == null)
             mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable(
                     mTextView.mCursorDrawableRes);
-
-        if (mTempRect == null) mTempRect = new Rect();
-        mCursorDrawable[cursorIndex].getPadding(mTempRect);
-        final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
-        horizontal = Math.max(0.5f, horizontal - 0.5f);
-        final int left = (int) (horizontal) - mTempRect.left;
-        mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
+        final Drawable drawable = mCursorDrawable[cursorIndex];
+        final int left = clampCursorHorizontalPosition(drawable, horizontal);
+        final int width = drawable.getIntrinsicWidth();
+        drawable.setBounds(left, top - mTempRect.top, left + width,
                 bottom + mTempRect.bottom);
     }
 
     /**
+     * Return clamped position for the cursor. If the cursor is within the boundaries of the view,
+     * then it is offset with the left padding of the cursor drawable. If the cursor is at
+     * the beginning or the end of the text then its drawable edge is aligned with left or right of
+     * the view boundary.
+     *
+     * @param drawable   Cursor drawable.
+     * @param horizontal Horizontal position for the cursor.
+     * @return The clamped horizontal position for the cursor.
+     */
+    private final int clampCursorHorizontalPosition(final Drawable drawable, float
+            horizontal) {
+        horizontal = Math.max(0.5f, horizontal - 0.5f);
+        if (mTempRect == null) mTempRect = new Rect();
+        drawable.getPadding(mTempRect);
+        int scrollX = mTextView.getScrollX();
+        float horizontalDiff = horizontal - scrollX;
+        int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
+                - mTextView.getCompoundPaddingRight();
+
+        final int left;
+        if (horizontalDiff >= (viewClippedWidth - 1f)) {
+            // at the rightmost position
+            final int cursorWidth = drawable.getIntrinsicWidth();
+            left = viewClippedWidth + scrollX - (cursorWidth - mTempRect.right);
+        } else if (Math.abs(horizontalDiff) <= 1f) {
+            // at the leftmost position
+            left = scrollX - mTempRect.left;
+        } else {
+            left = (int) horizontal - mTempRect.left;
+        }
+        return left;
+    }
+
+    /**
      * Called by the framework in response to a text auto-correction (such as fixing a typo using a
      * a dictionary) from the current input method, provided by it calling
      * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
@@ -3919,8 +3957,8 @@
             final Layout layout = mTextView.getLayout();
             if (layout != null && oldDrawable != mDrawable && isShowing()) {
                 // Update popup window position.
-                mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX -
-                        getHorizontalOffset() + getCursorOffset());
+                mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX -
+                        getHorizontalOffset() + getCursorOffset();
                 mPositionX += mTextView.viewportToContentHorizontalOffset();
                 mPositionHasChanged = true;
                 updatePosition(mLastParentX, mLastParentY, false, false);
@@ -4049,8 +4087,8 @@
                 final int line = layout.getLineForOffset(offset);
                 mPrevLine = line;
 
-                mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX -
-                        getHorizontalOffset() + getCursorOffset());
+                mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX -
+                        getHorizontalOffset() + getCursorOffset();
                 mPositionY = layout.getLineBottom(line);
 
                 // Take TextView's padding and scroll into account.
@@ -4062,6 +4100,17 @@
             }
         }
 
+        /**
+         * Return the clamped horizontal position for the first cursor.
+         *
+         * @param layout Text layout.
+         * @param offset Character offset for the cursor.
+         * @return The clamped horizontal position for the cursor.
+         */
+        int getCursorHorizontalPosition(Layout layout, int offset) {
+            return (int) (layout.getPrimaryHorizontal(offset) - 0.5f);
+        }
+
         public void updatePosition(int parentPositionX, int parentPositionY,
                 boolean parentPositionChanged, boolean parentScrolled) {
             positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
@@ -4300,6 +4349,16 @@
         }
 
         @Override
+        int getCursorHorizontalPosition(Layout layout, int offset) {
+            final Drawable drawable = mCursorCount > 0 ? mCursorDrawable[0] : null;
+            if (drawable != null) {
+                final float horizontal = layout.getPrimaryHorizontal(offset);
+                return clampCursorHorizontalPosition(drawable, horizontal) + mTempRect.left;
+            }
+            return super.getCursorHorizontalPosition(layout, offset);
+        }
+
+        @Override
         public boolean onTouchEvent(MotionEvent ev) {
             final boolean result = super.onTouchEvent(ev);
 
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 712a04b..ec9cb63 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -5452,15 +5452,9 @@
         return (int) Math.max(0, mShadowDy + mShadowRadius);
     }
 
-    private int getFudgedPaddingRight() {
-        // Add sufficient space for cursor and tone marks
-        int cursorWidth = 2 + (int)mTextPaint.density; // adequate for Material cursors
-        return Math.max(0, getCompoundPaddingRight() - (cursorWidth - 1));
-    }
-
     @Override
     protected int getRightPaddingOffset() {
-        return -(getFudgedPaddingRight() - mPaddingRight) +
+        return -(getCompoundPaddingRight() - mPaddingRight) +
                 (int) Math.max(0, mShadowDx + mShadowRadius);
     }
 
@@ -5805,7 +5799,7 @@
 
         float clipLeft = compoundPaddingLeft + scrollX;
         float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
-        float clipRight = right - left - getFudgedPaddingRight() + scrollX;
+        float clipRight = right - left - getCompoundPaddingRight() + scrollX;
         float clipBottom = bottom - top + scrollY -
                 ((scrollY == maxScrollY) ? 0 : extendedPaddingBottom);
 
diff --git a/core/tests/coretests/src/android/widget/EditorCursorTest.java b/core/tests/coretests/src/android/widget/EditorCursorTest.java
new file mode 100644
index 0000000..04c8b8c
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/EditorCursorTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.ViewGroup;
+
+public class EditorCursorTest extends ActivityInstrumentationTestCase2<TextViewActivity> {
+
+    private EditText mEditText;
+    private final String RTL_STRING = "مرحبا الروبوت مرحبا الروبوت مرحبا الروبوت";
+
+    public EditorCursorTest() {
+        super(TextViewActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mEditText = new EditText(getActivity());
+        mEditText.setTextSize(30);
+        mEditText.setSingleLine(true);
+        mEditText.setLines(1);
+        mEditText.setPadding(15, 15, 15, 15);
+        ViewGroup.LayoutParams editTextLayoutParams = new ViewGroup.LayoutParams(200,
+                ViewGroup.LayoutParams.WRAP_CONTENT);
+
+        mEditText.setLayoutParams(editTextLayoutParams);
+
+        final FrameLayout layout = new FrameLayout(getActivity());
+        ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT);
+        layout.setLayoutParams(layoutParams);
+        layout.addView(mEditText);
+
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                getActivity().setContentView(layout);
+                mEditText.requestFocus();
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+    }
+
+    @SmallTest
+    public void testCursorIsInViewBoundariesWhenOnRightForLtr() throws Exception {
+        // Asserts that when an EditText has LTR text, and cursor is at the end (right),
+        // cursor is drawn to the right edge of the view
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mEditText.setText("aaaaaaaaaaaaaaaaaaaaaa");
+                int length = mEditText.getText().length();
+                mEditText.setSelection(length, length);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+
+        Editor editor = mEditText.getEditorForTesting();
+        Drawable drawable = editor.getCursorDrawable()[0];
+        Rect drawableBounds = drawable.getBounds();
+        Rect drawablePadding = new Rect();
+        drawable.getPadding(drawablePadding);
+
+        // right edge of the view including the scroll
+        int maxRight = mEditText.getWidth() - mEditText.getCompoundPaddingRight()
+                - mEditText.getCompoundPaddingLeft() + +mEditText.getScrollX();
+        int diff = drawableBounds.right - drawablePadding.right - maxRight;
+        assertTrue(diff >= 0 && diff <= 1);
+    }
+
+    @SmallTest
+    public void testCursorIsInViewBoundariesWhenOnLeftForLtr() throws Exception {
+        // Asserts that when an EditText has LTR text, and cursor is at the beginning,
+        // cursor is drawn to the left edge of the view
+
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mEditText.setText("aaaaaaaaaaaaaaaaaaaaaa");
+                mEditText.setSelection(0, 0);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+
+        Drawable drawable = mEditText.getEditorForTesting().getCursorDrawable()[0];
+        Rect drawableBounds = drawable.getBounds();
+        Rect drawablePadding = new Rect();
+        drawable.getPadding(drawablePadding);
+
+        int diff = drawableBounds.left + drawablePadding.left;
+        assertTrue(diff >= 0 && diff <= 1);
+    }
+
+    @SmallTest
+    public void testCursorIsInViewBoundariesWhenOnRightForRtl() throws Exception {
+        // Asserts that when an EditText has RTL text, and cursor is at the end,
+        // cursor is drawn to the left edge of the view
+
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mEditText.setText(RTL_STRING);
+                mEditText.setSelection(0, 0);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+
+        Drawable drawable = mEditText.getEditorForTesting().getCursorDrawable()[0];
+        Rect drawableBounds = drawable.getBounds();
+        Rect drawablePadding = new Rect();
+        drawable.getPadding(drawablePadding);
+
+        int maxRight = mEditText.getWidth() - mEditText.getCompoundPaddingRight()
+                - mEditText.getCompoundPaddingLeft() + mEditText.getScrollX();
+
+        int diff = drawableBounds.right - drawablePadding.right - maxRight;
+        assertTrue(diff >= 0 && diff <= 1);
+    }
+
+    @SmallTest
+    public void testCursorIsInViewBoundariesWhenOnLeftForRtl() throws Exception {
+        // Asserts that when an EditText has RTL text, and cursor is at the beginning,
+        // cursor is drawn to the right edge of the view
+
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mEditText.setText(RTL_STRING);
+                int length = mEditText.getText().length();
+                mEditText.setSelection(length, length);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+
+        Drawable drawable = mEditText.getEditorForTesting().getCursorDrawable()[0];
+        Rect drawableBounds = drawable.getBounds();
+        Rect drawablePadding = new Rect();
+        drawable.getPadding(drawablePadding);
+
+        int diff = drawableBounds.left - mEditText.getScrollX() + drawablePadding.left;
+        assertTrue(diff >= 0 && diff <= 1);
+    }
+
+}