OSDN Git Service

Do not use hint text for Editor cursor positioning
authorSiyamed Sinir <siyamed@google.com>
Thu, 18 Feb 2016 03:44:41 +0000 (19:44 -0800)
committerSiyamed Sinir <siyamed@google.com>
Thu, 17 Mar 2016 01:00:23 +0000 (18:00 -0700)
When hint text is used for cursor positioning, the cursor may end up
in opposite direction when the locale and the hint text directions are
different. This CL uses the main text layout and discards the hint
layout while deciding on the position of those three views. The change
also includes fix for cursor and floating popup not displaying for
some cases after the change.

Bug: 22358087
Change-Id: I083d17b06273bc1d9b67656c5e12d4ff1231219c

core/java/android/widget/Editor.java
core/java/android/widget/TextView.java
core/tests/coretests/AndroidManifest.xml
core/tests/coretests/src/android/widget/EditorCursorTest.java
core/tests/coretests/src/android/widget/TextViewActivityTest.java
core/tests/coretests/src/android/widget/espresso/FloatingToolbarEspressoUtils.java
core/tests/coretests/src/android/widget/espresso/TextViewAssertions.java

index 4bcb406..942cbcb 100644 (file)
@@ -1827,7 +1827,7 @@ public class Editor {
             return;
         }
 
-        Layout layout = getActiveLayout();
+        Layout layout = mTextView.getLayout();
         final int offset = mTextView.getSelectionStart();
         final int line = layout.getLineForOffset(offset);
         final int top = layout.getLineTop(line);
@@ -2192,27 +2192,35 @@ public class Editor {
             mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable(
                     mTextView.mCursorDrawableRes);
         final Drawable drawable = mCursorDrawable[cursorIndex];
-        final int left = clampCursorHorizontalPosition(drawable, horizontal);
+        final int left = clampHorizontalPosition(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
+     * Return clamped position for the drawable. If the drawable is within the boundaries of the
+     * view, then it is offset with the left padding of the cursor drawable. If the drawable is at
      * the beginning or the end of the text then its drawable edge is aligned with left or right of
-     * the view boundary.
+     * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
+     * of the view.
      *
-     * @param drawable   Cursor drawable.
-     * @param horizontal Horizontal position for the cursor.
-     * @return The clamped horizontal position for the cursor.
+     * @param drawable Drawable. Can be null.
+     * @param horizontal Horizontal position for the drawable.
+     * @return The clamped horizontal position for the drawable.
      */
-    private final int clampCursorHorizontalPosition(final Drawable drawable, float
-            horizontal) {
+    private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
         horizontal = Math.max(0.5f, horizontal - 0.5f);
         if (mTempRect == null) mTempRect = new Rect();
-        drawable.getPadding(mTempRect);
+
+        int drawableWidth = 0;
+        if (drawable != null) {
+            drawable.getPadding(mTempRect);
+            drawableWidth = drawable.getIntrinsicWidth();
+        } else {
+            mTempRect.setEmpty();
+        }
+
         int scrollX = mTextView.getScrollX();
         float horizontalDiff = horizontal - scrollX;
         int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
@@ -2221,9 +2229,11 @@ public class Editor {
         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) {
+            left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
+        } else if (Math.abs(horizontalDiff) <= 1f ||
+                (TextUtils.isEmpty(mTextView.getText())
+                        && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
+                        && horizontal <= 1f)) {
             // at the leftmost position
             left = scrollX - mTempRect.left;
         } else {
@@ -3772,10 +3782,10 @@ public class Editor {
                                 + mHandleHeight);
             } else {
                 // We have a single cursor.
-                Layout layout = getActiveLayout();
+                Layout layout = mTextView.getLayout();
                 int line = layout.getLineForOffset(mTextView.getSelectionStart());
-                float primaryHorizontal =
-                        layout.getPrimaryHorizontal(mTextView.getSelectionStart());
+                float primaryHorizontal = clampHorizontalPosition(null,
+                        layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
                 mSelectionBounds.set(
                         primaryHorizontal,
                         layout.getLineTop(line),
@@ -4152,7 +4162,7 @@ public class Editor {
                 prepareCursorControllers();
                 return;
             }
-            layout = getActiveLayout();
+            layout = mTextView.getLayout();
 
             boolean offsetChanged = offset != mPreviousOffset;
             if (offsetChanged || parentScrolled) {
@@ -4322,19 +4332,6 @@ public class Editor {
         public void onDetached() {}
     }
 
-    /**
-     * Returns the active layout (hint or text layout). Note that the text layout can be null.
-     */
-    private Layout getActiveLayout() {
-        Layout layout = mTextView.getLayout();
-        Layout hintLayout = mTextView.getHintLayout();
-        if (TextUtils.isEmpty(layout.getText()) && hintLayout != null &&
-                !TextUtils.isEmpty(hintLayout.getText())) {
-            layout = hintLayout;
-        }
-        return layout;
-    }
-
     private class InsertionHandleView extends HandleView {
         private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
         private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
@@ -4431,7 +4428,7 @@ public class Editor {
             final Drawable drawable = mCursorCount > 0 ? mCursorDrawable[0] : null;
             if (drawable != null) {
                 final float horizontal = layout.getPrimaryHorizontal(offset);
-                return clampCursorHorizontalPosition(drawable, horizontal) + mTempRect.left;
+                return clampHorizontalPosition(drawable, horizontal) + mTempRect.left;
             }
             return super.getCursorHorizontalPosition(layout, offset);
         }
index fbedbda..97d4f4e 100644 (file)
@@ -285,8 +285,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
 
     private static final RectF TEMP_RECTF = new RectF();
 
-    // XXX should be much larger
-    private static final int VERY_WIDE = 1024*1024;
+    /** @hide */
+    static final int VERY_WIDE = 1024 * 1024; // XXX should be much larger
     private static final int ANIMATED_SCROLL_GAP = 250;
 
     private static final InputFilter[] NO_FILTERS = new InputFilter[0];
index bfa2b10..b780778 100644 (file)
     <!-- accessibility test permissions -->
     <uses-permission android:name="android.permission.RETRIEVE_WINDOW_CONTENT" />
 
-    <application android:theme="@style/Theme">
+    <application android:theme="@style/Theme" android:supportsRtl="true">
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="org.apache.http.legacy" android:required="false" />
         <meta-data
index 04c8b8c..6d650ff 100644 (file)
 
 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.Choreographer;
 import android.view.ViewGroup;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.widget.espresso.TextViewAssertions.hasInsertionPointerOnLeft;
+import static android.widget.espresso.TextViewAssertions.hasInsertionPointerOnRight;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.isEmptyString;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.sameInstance;
+
 public class EditorCursorTest extends ActivityInstrumentationTestCase2<TextViewActivity> {
 
+
+    private final static String LTR_STRING = "aaaaaaaaaaaaaaaaaaaaaa";
+    private final static String LTR_HINT = "hint";
+    private final static String RTL_STRING = "مرحبا الروبوت مرحبا الروبوت مرحبا الروبوت";
+    private final static String RTL_HINT = "الروبوت";
+    private final static int CURSOR_BLINK_MS = 500;
+
     private EditText mEditText;
-    private final String RTL_STRING = "مرحبا الروبوت مرحبا الروبوت مرحبا الروبوت";
 
     public EditorCursorTest() {
         super(TextViewActivity.class);
@@ -55,110 +73,160 @@ public class EditorCursorTest extends ActivityInstrumentationTestCase2<TextViewA
             @Override
             public void run() {
                 getActivity().setContentView(layout);
-                mEditText.requestFocus();
             }
         });
         getInstrumentation().waitForIdleSync();
+        onView(sameInstance(mEditText)).perform(click());
     }
 
     @SmallTest
-    public void testCursorIsInViewBoundariesWhenOnRightForLtr() throws Exception {
+    public void testCursorIsInViewBoundariesWhenOnRightForLtr() {
         // 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();
+        setEditTextText(LTR_STRING, LTR_STRING.length());
 
-        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);
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
     }
 
     @SmallTest
-    public void testCursorIsInViewBoundariesWhenOnLeftForLtr() throws Exception {
+    public void testCursorIsInViewBoundariesWhenOnLeftForLtr() {
         // Asserts that when an EditText has LTR text, and cursor is at the beginning,
         // cursor is drawn to the left edge of the view
+        setEditTextText(LTR_STRING, 0);
 
-        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);
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
     }
 
     @SmallTest
-    public void testCursorIsInViewBoundariesWhenOnRightForRtl() throws Exception {
+    public void testCursorIsInViewBoundariesWhenOnRightForRtl() {
         // Asserts that when an EditText has RTL text, and cursor is at the end,
         // cursor is drawn to the left edge of the view
+        setEditTextText(RTL_STRING, 0);
 
-        getActivity().runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                mEditText.setText(RTL_STRING);
-                mEditText.setSelection(0, 0);
-            }
-        });
-        getInstrumentation().waitForIdleSync();
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+    }
+
+    @SmallTest
+    public void testCursorIsInViewBoundariesWhenOnLeftForRtl() {
+        // Asserts that when an EditText has RTL text, and cursor is at the beginning,
+        // cursor is drawn to the right edge of the view
+        setEditTextText(RTL_STRING, RTL_STRING.length());
+
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
+    }
+
+    /* Tests for cursor positioning with hint */
+    @SmallTest
+    public void testCursorIsOnLeft_withFirstStrongLtrAlgorithm() {
+        setEditTextHint(null, TextView.TEXT_DIRECTION_FIRST_STRONG_LTR, 0);
+        assertThat(mEditText.getText().toString(), isEmptyString());
+        assertThat(mEditText.getHint(), nullValue());
+
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
 
-        Drawable drawable = mEditText.getEditorForTesting().getCursorDrawable()[0];
-        Rect drawableBounds = drawable.getBounds();
-        Rect drawablePadding = new Rect();
-        drawable.getPadding(drawablePadding);
+        setEditTextHint(RTL_HINT, TextView.TEXT_DIRECTION_FIRST_STRONG_LTR, 0);
+        assertThat(mEditText.getText().toString(), isEmptyString());
 
-        int maxRight = mEditText.getWidth() - mEditText.getCompoundPaddingRight()
-                - mEditText.getCompoundPaddingLeft() + mEditText.getScrollX();
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
 
-        int diff = drawableBounds.right - drawablePadding.right - maxRight;
-        assertTrue(diff >= 0 && diff <= 1);
+        setEditTextHint(LTR_HINT, TextView.TEXT_DIRECTION_FIRST_STRONG_LTR, 0);
+        assertThat(mEditText.getText().toString(), isEmptyString());
+
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
     }
 
     @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
+    public void testCursorIsOnRight_withFirstStrongRtlAlgorithm() {
+        setEditTextHint(null, TextView.TEXT_DIRECTION_FIRST_STRONG_RTL, 0);
+        assertThat(mEditText.getText().toString(), isEmptyString());
+        assertThat(mEditText.getHint(), nullValue());
+
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+
+        setEditTextHint(LTR_HINT, TextView.TEXT_DIRECTION_FIRST_STRONG_RTL, 0);
+        assertThat(mEditText.getText().toString(), isEmptyString());
+
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+
+        setEditTextHint(RTL_HINT, TextView.TEXT_DIRECTION_FIRST_STRONG_RTL, 0);
+        assertThat(mEditText.getText().toString(), isEmptyString());
+
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+    }
+
+    @SmallTest
+    public void testCursorIsOnLeft_withLtrAlgorithm() {
+        setEditTextHint(null, TextView.TEXT_DIRECTION_LTR, 0);
+        assertThat(mEditText.getText().toString(), isEmptyString());
+        assertThat(mEditText.getHint(), nullValue());
+
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
+
+        setEditTextHint(RTL_HINT, TextView.TEXT_DIRECTION_LTR, 0);
+        assertThat(mEditText.getText().toString(), isEmptyString());
+
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
 
+        setEditTextHint(LTR_HINT, TextView.TEXT_DIRECTION_LTR, 0);
+        assertThat(mEditText.getText().toString(), isEmptyString());
+
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
+    }
+
+    @SmallTest
+    public void testCursorIsOnRight_withRtlAlgorithm() {
+        setEditTextHint(null, TextView.TEXT_DIRECTION_RTL, 0);
+        assertThat(mEditText.getText().toString(), isEmptyString());
+        assertThat(mEditText.getHint(), nullValue());
+
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+
+        setEditTextHint(LTR_HINT, TextView.TEXT_DIRECTION_RTL, 0);
+        assertThat(mEditText.getText().toString(), isEmptyString());
+
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+
+        setEditTextHint(RTL_HINT, TextView.TEXT_DIRECTION_RTL, 0);
+        assertThat(mEditText.getText().toString(), isEmptyString());
+
+        onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+    }
+
+    private void setEditTextProperties(final String text, final String hint,
+            final Integer textDirection, final Integer selection) {
         getActivity().runOnUiThread(new Runnable() {
             @Override
             public void run() {
-                mEditText.setText(RTL_STRING);
-                int length = mEditText.getText().length();
-                mEditText.setSelection(length, length);
+                if (textDirection != null) mEditText.setTextDirection(textDirection);
+                if (text != null) mEditText.setText(text);
+                if (hint != null) mEditText.setHint(hint);
+                if (selection != null) mEditText.setSelection(selection);
             }
         });
         getInstrumentation().waitForIdleSync();
 
-        Drawable drawable = mEditText.getEditorForTesting().getCursorDrawable()[0];
-        Rect drawableBounds = drawable.getBounds();
-        Rect drawablePadding = new Rect();
-        drawable.getPadding(drawablePadding);
+        // wait for cursor to be drawn. updateCursorPositions function is called during draw() and
+        // only when cursor is visible during blink.
+        final CountDownLatch latch = new CountDownLatch(1);
+        mEditText.postOnAnimationDelayed(new Runnable() {
+            @Override
+            public void run() {
+                latch.countDown();
+            }
+        }, CURSOR_BLINK_MS);
+        try {
+            assertThat("Problem while waiting for the cursor to blink",
+                    latch.await(10, TimeUnit.SECONDS), equalTo(true));
+        } catch (Exception e) {
+            fail("Problem while waiting for the cursor to blink");
+        }
+    }
 
-        int diff = drawableBounds.left - mEditText.getScrollX() + drawablePadding.left;
-        assertTrue(diff >= 0 && diff <= 1);
+    private void setEditTextHint(final String hint, final int textDirection, final int selection) {
+        setEditTextProperties(null, hint, textDirection, selection);
     }
 
+    private void setEditTextText(final String text, final Integer selection) {
+        setEditTextProperties(text, null, null, selection);
+    }
 }
index 4a4727f..91d57e7 100644 (file)
 
 package android.widget;
 
+import static android.support.test.espresso.action.ViewActions.longClick;
 import static android.widget.espresso.DragHandleUtils.assertNoSelectionHandles;
 import static android.widget.espresso.DragHandleUtils.onHandleView;
+import static android.widget.espresso.FloatingToolbarEspressoUtils.onFloatingToolBarItem;
 import static android.widget.espresso.TextViewActions.clickOnTextAtIndex;
 import static android.widget.espresso.TextViewActions.doubleTapAndDragOnText;
 import static android.widget.espresso.TextViewActions.doubleClickOnTextAtIndex;
@@ -49,6 +51,7 @@ import android.test.ActivityInstrumentationTestCase2;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.text.Selection;
 import android.text.Spannable;
+import android.text.InputType;
 import android.view.KeyEvent;
 
 import static org.hamcrest.Matchers.anyOf;
@@ -234,6 +237,33 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2<TextV
     }
 
     @SmallTest
+    public void testToolbarAppearsAfterSelection_withFirstStringLtrAlgorithmAndRtlHint()
+            throws Exception {
+        // after the hint layout change, the floating toolbar was not visible in the case below
+        // this test tests that the floating toolbar is displayed on the screen and is visible to
+        // user.
+        final TextView textView = (TextView) getActivity().findViewById(R.id.textview);
+        textView.post(new Runnable() {
+            @Override
+            public void run() {
+                textView.setTextDirection(TextView.TEXT_DIRECTION_FIRST_STRONG_LTR);
+                textView.setInputType(InputType.TYPE_CLASS_TEXT);
+                textView.setSingleLine(true);
+                textView.setHint("الروبوت");
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+
+        onView(withId(R.id.textview)).perform(typeTextIntoFocusedView("test"));
+        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(1));
+        onFloatingToolBarItem(withText(com.android.internal.R.string.cut)).perform(click());
+        onView(withId(R.id.textview)).perform(longClick());
+        sleepForFloatingToolbarPopup();
+
+        assertFloatingToolbarIsDisplayed();
+    }
+
+    @SmallTest
     public void testToolbarAndInsertionHandle() throws Exception {
         final String text = "text";
         onView(withId(R.id.textview)).perform(click());
index f02fe00..0f7f359 100644 (file)
@@ -54,6 +54,16 @@ public class FloatingToolbarEspressoUtils {
     }
 
     /**
+     * Creates a {@link ViewInteraction} for the floating bar menu item with the given matcher.
+     *
+     * @param matcher The matcher for the menu item.
+     */
+    public static ViewInteraction onFloatingToolBarItem(Matcher<View> matcher) {
+        return onView(matcher)
+                .inRoot(withDecorView(hasDescendant(withTagValue(is(TAG)))));
+    }
+
+    /**
      * Asserts that the floating toolbar is displayed on screen.
      *
      * @throws AssertionError if the assertion fails
index 37c7425..6e44cd8 100644 (file)
@@ -19,15 +19,23 @@ package android.widget.espresso;
 import static android.support.test.espresso.matcher.ViewMatchers.assertThat;
 import static com.android.internal.util.Preconditions.checkNotNull;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.number.IsCloseTo.closeTo;
 
+import android.annotation.IntDef;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.support.test.espresso.NoMatchingViewException;
 import android.support.test.espresso.ViewAssertion;
 import android.view.View;
+import android.widget.EditText;
 import android.widget.TextView;
 
 import junit.framework.AssertionFailedError;
 import org.hamcrest.Matcher;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * A collection of assertions on a {@link android.widget.TextView}.
  */
@@ -113,6 +121,22 @@ public final class TextViewAssertions {
     }
 
     /**
+     * Returns a {@link ViewAssertion} that asserts that the EditText insertion pointer is on
+     * the left edge.
+     */
+    public static ViewAssertion hasInsertionPointerOnLeft() {
+        return new CursorPositionAssertion(CursorPositionAssertion.LEFT);
+    }
+
+    /**
+     * Returns a {@link ViewAssertion} that asserts that the EditText insertion pointer is on
+     * the right edge.
+     */
+    public static ViewAssertion hasInsertionPointerOnRight() {
+        return new CursorPositionAssertion(CursorPositionAssertion.RIGHT);
+    }
+
+    /**
      * A {@link ViewAssertion} to check the selected text in a {@link TextView}.
      */
     private static final class TextSelectionAssertion implements ViewAssertion {
@@ -142,4 +166,54 @@ public final class TextViewAssertions {
             }
         }
     }
+
+    /**
+     * {@link ViewAssertion} to check that EditText cursor is on a given position.
+     */
+    static class CursorPositionAssertion implements ViewAssertion {
+
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({LEFT, RIGHT})
+        public @interface CursorEdgePositionType {}
+        public static final int LEFT = 0;
+        public static final int RIGHT = 1;
+
+        private final int mPosition;
+
+        private CursorPositionAssertion(@CursorEdgePositionType int position) {
+            this.mPosition = position;
+        }
+
+        @Override
+        public void check(View view, NoMatchingViewException exception) {
+            if (!(view instanceof EditText)) {
+                throw new AssertionFailedError("View should be an instance of EditText");
+            }
+            EditText editText = (EditText) view;
+            Drawable drawable = editText.getEditorForTesting().getCursorDrawable()[0];
+            Rect drawableBounds = drawable.getBounds();
+            Rect drawablePadding = new Rect();
+            drawable.getPadding(drawablePadding);
+
+            final int diff;
+            final String positionStr;
+            switch (mPosition) {
+                case LEFT:
+                    positionStr = "left";
+                    diff = drawableBounds.left - editText.getScrollX() + drawablePadding.left;
+                    break;
+                case RIGHT:
+                    positionStr = "right";
+                    int maxRight = editText.getWidth() - editText.getCompoundPaddingRight()
+                            - editText.getCompoundPaddingLeft() + editText.getScrollX();
+                    diff = drawableBounds.right - drawablePadding.right - maxRight;
+                    break;
+                default:
+                    throw new AssertionFailedError("Unknown position for cursor assertion");
+            }
+
+            assertThat("Cursor should be on the " + positionStr, Double.valueOf(diff),
+                    closeTo(0f, 1f));
+        }
+    }
 }