Set cursor position in handwriting initiation

When handwriting is initiated for an unfocused EditText, if the stylus
down point was inside the EditText's bounds, then the EditText will
automatically set its cursor position nearest to the stylus down point
when it gains focus. If the stylus down point was outside the EditText's
bounds (within the extended handwriting bounds), then we must calculate
and set the cursor position manually.

Bug: 288461219
Test: atest HandwritingInitiatorTest
Change-Id: I62593f1ff01a7ec39c2eb6e1352d63a7f8b79f88
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index 297754f..dfade01 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -24,6 +24,7 @@
 import android.graphics.RectF;
 import android.graphics.Region;
 import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
 import android.widget.TextView;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -81,6 +82,8 @@
     private int mConnectionCount = 0;
     private final InputMethodManager mImm;
 
+    private final int[] mTempLocation = new int[2];
+
     private final Rect mTempRect = new Rect();
 
     private final RectF mTempRectF = new RectF();
@@ -429,7 +432,19 @@
         return null;
     }
 
-    private static void requestFocusWithoutReveal(View view) {
+    private void requestFocusWithoutReveal(View view) {
+        if (view instanceof EditText editText && !mState.mStylusDownWithinEditorBounds) {
+            // If the stylus down point was inside the EditText's bounds, then the EditText will
+            // automatically set its cursor position nearest to the stylus down point when it
+            // gains focus. If the stylus down point was outside the EditText's bounds (within
+            // the extended handwriting bounds), then we must calculate and set the cursor
+            // position manually.
+            view.getLocationInWindow(mTempLocation);
+            int offset = editText.getOffsetForPosition(
+                    mState.mStylusDownX - mTempLocation[0],
+                    mState.mStylusDownY - mTempLocation[1]);
+            editText.setSelection(offset);
+        }
         if (view.getRevealOnFocusHint()) {
             view.setRevealOnFocusHint(false);
             view.requestFocus();
@@ -457,6 +472,10 @@
             if (getViewHandwritingArea(connectedView, handwritingArea)
                     && isInHandwritingArea(handwritingArea, x, y, connectedView, isHover)
                     && shouldTriggerStylusHandwritingForView(connectedView)) {
+                if (!isHover && mState != null) {
+                    mState.mStylusDownWithinEditorBounds =
+                            contains(handwritingArea, x, y, 0f, 0f, 0f, 0f);
+                }
                 return connectedView;
             }
         }
@@ -475,7 +494,12 @@
             }
 
             final float distance = distance(handwritingArea, x, y);
-            if (distance == 0f) return view;
+            if (distance == 0f) {
+                if (!isHover && mState != null) {
+                    mState.mStylusDownWithinEditorBounds = true;
+                }
+                return view;
+            }
             if (distance < minDistance) {
                 minDistance = distance;
                 bestCandidate = view;
@@ -658,6 +682,12 @@
         private boolean mExceedHandwritingSlop;
 
         /**
+         * Whether the stylus down point of the MotionEvent sequence was within the editor's bounds
+         * (not including the extended handwriting bounds).
+         */
+        private boolean mStylusDownWithinEditorBounds;
+
+        /**
          * A view which has requested focus and is pending input connection creation. When an input
          * connection is created for the view, a handwriting session should be started for the view.
          */
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
index c46118d..f39bddd 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
@@ -88,8 +88,8 @@
     }
 
     private HandwritingInitiator mHandwritingInitiator;
-    private View mTestView1;
-    private View mTestView2;
+    private EditText mTestView1;
+    private EditText mTestView2;
     private Context mContext;
 
     @Before
@@ -123,6 +123,9 @@
 
     @Test
     public void onTouchEvent_startHandwriting_when_stylusMoveOnce_withinHWArea() {
+        mTestView1.setText("hello");
+        when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
+
         mHandwritingInitiator.onInputConnectionCreated(mTestView1);
         final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
         final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
@@ -141,6 +144,9 @@
         // After IMM.startHandwriting is triggered, onTouchEvent should return true for ACTION_MOVE
         // events so that the events are not dispatched to the view tree.
         assertThat(onTouchEventResult2).isTrue();
+        // Since the stylus down point was inside the TextView's bounds, the handwriting initiator
+        // does not need to set the cursor position.
+        verify(mTestView1, never()).setSelection(anyInt());
     }
 
     @Test
@@ -185,6 +191,9 @@
 
     @Test
     public void onTouchEvent_startHandwriting_when_stylusMove_withinExtendedHWArea() {
+        mTestView1.setText("hello");
+        when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
+
         mHandwritingInitiator.onInputConnectionCreated(mTestView1);
         final int x1 = sHwArea1.left - HW_BOUNDS_OFFSETS_LEFT_PX / 2;
         final int y1 = sHwArea1.top - HW_BOUNDS_OFFSETS_TOP_PX / 2;
@@ -199,6 +208,9 @@
 
         // Stylus movement within extended HandwritingArea should trigger IMM.startHandwriting once.
         verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1);
+        // Since the stylus down point was outside the TextView's bounds, the handwriting initiator
+        // sets the cursor position.
+        verify(mTestView1).setSelection(4);
     }
 
     @Test
@@ -221,6 +233,8 @@
 
     @Test
     public void onTouchEvent_startHandwriting_inputConnectionBuilt_stylusMoveInExtendedHWArea() {
+        mTestView1.setText("hello");
+        when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
         // The stylus down point is between mTestView1 and  mTestView2, but it is within the
         // extended handwriting area of both views. It is closer to mTestView1.
         final int x1 = sHwArea1.right + HW_BOUNDS_OFFSETS_RIGHT_PX / 2;
@@ -241,6 +255,9 @@
         // the stylus down point is closest to this view.
         mHandwritingInitiator.onInputConnectionCreated(mTestView1);
         verify(mHandwritingInitiator).startHandwriting(mTestView1);
+        // Since the stylus down point was outside the TextView's bounds, the handwriting initiator
+        // sets the cursor position.
+        verify(mTestView1).setSelection(4);
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java
index b4c72ca..3b2ab4c 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java
@@ -16,6 +16,9 @@
 
 package android.view.stylus;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
@@ -26,22 +29,23 @@
 import android.graphics.Region;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.EditText;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
 public class HandwritingTestUtil {
-    public static View createView(Rect handwritingArea) {
+    public static EditText createView(Rect handwritingArea) {
         return createView(handwritingArea, true /* autoHandwritingEnabled */,
                 true /* isStylusHandwritingAvailable */);
     }
 
-    public static View createView(Rect handwritingArea, boolean autoHandwritingEnabled,
+    public static EditText createView(Rect handwritingArea, boolean autoHandwritingEnabled,
             boolean isStylusHandwritingAvailable) {
         return createView(handwritingArea, autoHandwritingEnabled, isStylusHandwritingAvailable,
                 0, 0, 0, 0);
     }
 
-    public static View createView(Rect handwritingArea, boolean autoHandwritingEnabled,
+    public static EditText createView(Rect handwritingArea, boolean autoHandwritingEnabled,
             boolean isStylusHandwritingAvailable,
             float handwritingBoundsOffsetLeft, float handwritingBoundsOffsetTop,
             float handwritingBoundsOffsetRight, float handwritingBoundsOffsetBottom) {
@@ -68,7 +72,7 @@
             }
         };
 
-        View view = spy(new View(context));
+        EditText view = spy(new EditText(context));
         when(view.isAttachedToWindow()).thenReturn(true);
         when(view.isAggregatedVisible()).thenReturn(true);
         when(view.isStylusHandwritingAvailable()).thenReturn(isStylusHandwritingAvailable);
@@ -77,6 +81,13 @@
         when(view.getHandwritingBoundsOffsetTop()).thenReturn(handwritingBoundsOffsetTop);
         when(view.getHandwritingBoundsOffsetRight()).thenReturn(handwritingBoundsOffsetRight);
         when(view.getHandwritingBoundsOffsetBottom()).thenReturn(handwritingBoundsOffsetBottom);
+        doAnswer(invocation -> {
+            int[] outLocation = invocation.getArgument(0);
+            outLocation[0] = handwritingArea.left;
+            outLocation[1] = handwritingArea.top;
+            return null;
+        }).when(view).getLocationInWindow(any());
+        when(view.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(0);
         view.setAutoHandwritingEnabled(autoHandwritingEnabled);
         parent.addView(view);
         return view;