From f0404bd4c3cc8e2ffbe9b1faf63ed475c906a9a3 Mon Sep 17 00:00:00 2001 From: Haoyu Zhang Date: Mon, 7 Feb 2022 15:22:59 -0800 Subject: [PATCH] Scribe for View: handwriting initiation for unfocused Bug: 211763729 Test: atest FrameworksCoreTests:HandwritingInitiatorTest Test: atest FrameworksCoreTests:HandwritableViewInfoTest Test: atest FrameworksCoreTests:HandwritingAreaTrackerTest Test: manually test Change-Id: Ica9ee82cf1dc41feb5e4ff0f52ee13c1e4de8ad4 --- .../android/view/HandwritingInitiator.java | 242 ++++++++++++++++-- core/java/android/view/View.java | 55 +++- core/java/android/widget/EditText.java | 7 + .../view/stylus/HandwritableViewInfoTest.java | 87 +++++++ .../stylus/HandwritingAreaTrackerTest.java | 149 +++++++++++ .../view/stylus/HandwritingInitiatorTest.java | 45 ++-- .../view/stylus/HandwritingTestUtil.java | 59 +++++ 7 files changed, 601 insertions(+), 43 deletions(-) create mode 100644 core/tests/coretests/src/android/view/stylus/HandwritableViewInfoTest.java create mode 100644 core/tests/coretests/src/android/view/stylus/HandwritingAreaTrackerTest.java create mode 100644 core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index c87c13d861d53..c1413beb01438 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -24,6 +24,9 @@ import android.view.inputmethod.InputMethodManager; import com.android.internal.annotations.VisibleForTesting; import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; /** * Initiates handwriting mode once it detects stylus movement in handwritable areas. @@ -58,6 +61,7 @@ public class HandwritingInitiator { private final long mTapTimeoutInMillis; private State mState = new State(); + private final HandwritingAreaTracker mHandwritingAreasTracker = new HandwritingAreaTracker(); /** * Helper method to reset the internal state of this class. @@ -83,8 +87,8 @@ public class HandwritingInitiator { private final InputMethodManager mImm; @VisibleForTesting - public HandwritingInitiator(ViewConfiguration viewConfiguration, - InputMethodManager inputMethodManager) { + public HandwritingInitiator(@NonNull ViewConfiguration viewConfiguration, + @NonNull InputMethodManager inputMethodManager) { mTouchSlop = viewConfiguration.getScaledTouchSlop(); mTapTimeoutInMillis = ViewConfiguration.getTapTimeout(); mImm = inputMethodManager; @@ -98,7 +102,7 @@ public class HandwritingInitiator { * @param motionEvent the stylus MotionEvent. */ @VisibleForTesting - public void onTouchEvent(MotionEvent motionEvent) { + public void onTouchEvent(@NonNull MotionEvent motionEvent) { final int maskedAction = motionEvent.getActionMasked(); switch (maskedAction) { case MotionEvent.ACTION_DOWN: @@ -151,11 +155,20 @@ public class HandwritingInitiator { final float y = motionEvent.getY(pointerIndex); if (largerThanTouchSlop(x, y, mState.mStylusDownX, mState.mStylusDownY)) { mState.mExceedTouchSlop = true; - tryStartHandwriting(); + View candidateView = + findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY); + if (candidateView != null) { + if (candidateView == getConnectedView()) { + startHandwriting(candidateView); + } else { + candidateView.requestFocus(); + } + } } } } + @Nullable private View getConnectedView() { if (mConnectedView == null) return null; return mConnectedView.get(); @@ -178,13 +191,16 @@ public class HandwritingInitiator { clearConnectedView(); return; } + final View connectedView = getConnectedView(); if (connectedView == view) { ++mConnectionCount; } else { mConnectedView = new WeakReference<>(view); mConnectionCount = 1; - tryStartHandwriting(); + if (mState.mShouldInitHandwriting) { + tryStartHandwriting(); + } } } @@ -233,17 +249,10 @@ public class HandwritingInitiator { return; } - final ViewParent viewParent = connectedView.getParent(); - // Do a final check before startHandwriting. - if (viewParent != null && connectedView.isAttachedToWindow()) { - final Rect editorBounds = - new Rect(0, 0, connectedView.getWidth(), connectedView.getHeight()); - if (viewParent.getChildVisibleRect(connectedView, editorBounds, null)) { - final int roundedInitX = Math.round(mState.mStylusDownX); - final int roundedInitY = Math.round(mState.mStylusDownY); - if (editorBounds.contains(roundedInitX, roundedInitY)) { - startHandwriting(connectedView); - } + Rect handwritingArea = getViewHandwritingArea(connectedView); + if (handwritingArea != null) { + if (contains(handwritingArea, mState.mStylusDownX, mState.mStylusDownY)) { + startHandwriting(connectedView); } } reset(); @@ -251,10 +260,79 @@ public class HandwritingInitiator { /** For test only. */ @VisibleForTesting - public void startHandwriting(View view) { + public void startHandwriting(@NonNull View view) { mImm.startStylusHandwriting(view); } + /** + * Notify that the handwriting area for the given view might be updated. + * @param view the view whose handwriting area might be updated. + */ + public void updateHandwritingAreasForView(@NonNull View view) { + mHandwritingAreasTracker.updateHandwritingAreaForView(view); + } + + /** + * Given the location of the stylus event, return the best candidate view to initialize + * handwriting mode. + * + * @param x the x coordinates of the stylus event, in the coordinates of the window. + * @param y the y coordinates of the stylus event, in the coordinates of the window. + */ + @Nullable + private View findBestCandidateView(float x, float y) { + // If the connectedView is not null and do not set any handwriting area, it will check + // whether the connectedView's boundary contains the initial stylus position. If true, + // directly return the connectedView. + final View connectedView = getConnectedView(); + if (connectedView != null && connectedView.isAutoHandwritingEnabled()) { + final Rect handwritingArea = getViewHandwritingArea(connectedView); + if (handwritingArea != null && contains(handwritingArea, x, y)) { + return connectedView; + } + } + + // Check the registered handwriting areas. + final List handwritableViewInfos = + mHandwritingAreasTracker.computeViewInfos(); + for (HandwritableViewInfo viewInfo : handwritableViewInfos) { + final View view = viewInfo.getView(); + if (!view.isAutoHandwritingEnabled()) continue; + final Rect rect = viewInfo.getHandwritingArea(); + if (rect != null && contains(rect, x, y)) { + return viewInfo.getView(); + } + } + return null; + } + + /** + * Return the handwriting area of the given view, represented in the window's coordinate. + * If the view didn't set any handwriting area, it will return the view's boundary. + * It will return null if the view or its handwriting area is not visible. + */ + @Nullable + private static Rect getViewHandwritingArea(@NonNull View view) { + final ViewParent viewParent = view.getParent(); + if (viewParent != null && view.isAttachedToWindow() && view.isAggregatedVisible()) { + Rect handwritingArea = view.getHandwritingArea(); + if (handwritingArea == null) { + handwritingArea = new Rect(0, 0, view.getWidth(), view.getHeight()); + } + if (viewParent.getChildVisibleRect(view, handwritingArea, null)) { + return handwritingArea; + } + } + return null; + } + + /** + * Return true if the (x, y) is inside by the given {@link Rect}. + */ + private boolean contains(@NonNull Rect rect, float x, float y) { + return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom; + } + private boolean largerThanTouchSlop(float x1, float y1, float x2, float y2) { float dx = x1 - x2; float dy = y1 - y2; @@ -291,4 +369,134 @@ public class HandwritingInitiator { private float mStylusDownX = Float.NaN; private float mStylusDownY = Float.NaN; } + + /** The helper method to check if the given view is still active for handwriting. */ + private static boolean isViewActive(@Nullable View view) { + return view != null && view.isAttachedToWindow() && view.isAggregatedVisible() + && view.isAutoHandwritingEnabled(); + } + + /** + * A class used to track the handwriting areas set by the Views. + * + * @hide + */ + @VisibleForTesting + public static class HandwritingAreaTracker { + private final List mHandwritableViewInfos; + + public HandwritingAreaTracker() { + mHandwritableViewInfos = new ArrayList<>(); + } + + /** + * Notify this tracker that the handwriting area of the given view has been updated. + * This method does three things: + * a) iterate over the all the tracked ViewInfos and remove those already invalid ones. + * b) mark the given view's ViewInfo to be dirty. So that next time when + * {@link #computeViewInfos} is called, this view's handwriting area will be recomputed. + * c) If no the given view is not in the tracked ViewInfo list, a new ViewInfo object will + * be created and added to the list. + * + * @param view the view whose handwriting area is updated. + */ + public void updateHandwritingAreaForView(@NonNull View view) { + Iterator iterator = mHandwritableViewInfos.iterator(); + boolean found = false; + while (iterator.hasNext()) { + final HandwritableViewInfo handwritableViewInfo = iterator.next(); + final View curView = handwritableViewInfo.getView(); + if (!isViewActive(curView)) { + iterator.remove(); + } + if (curView == view) { + found = true; + handwritableViewInfo.mIsDirty = true; + } + } + if (!found && isViewActive(view)) { + // The given view is not tracked. Create a new HandwritableViewInfo for it and add + // to the list. + mHandwritableViewInfos.add(new HandwritableViewInfo(view)); + } + } + + /** + * Update the handwriting areas and return a list of ViewInfos containing the view + * reference and its handwriting area. + */ + @NonNull + public List computeViewInfos() { + mHandwritableViewInfos.removeIf(viewInfo -> !viewInfo.update()); + return mHandwritableViewInfos; + } + } + + /** + * A class that reference to a View and its handwriting area(in the ViewRoot's coordinate.) + * + * @hide + */ + @VisibleForTesting + public static class HandwritableViewInfo { + final WeakReference mViewRef; + Rect mHandwritingArea = null; + @VisibleForTesting + public boolean mIsDirty = true; + + @VisibleForTesting + public HandwritableViewInfo(@NonNull View view) { + mViewRef = new WeakReference<>(view); + } + + /** Return the tracked view. */ + @Nullable + public View getView() { + return mViewRef.get(); + } + + /** + * Return the tracked handwriting area, represented in the ViewRoot's coordinates. + * Notice, the caller should not modify the returned Rect. + */ + @Nullable + public Rect getHandwritingArea() { + return mHandwritingArea; + } + + /** + * Update the handwriting area in this ViewInfo. + * + * @return true if this ViewInfo is still valid. Or false if this ViewInfo has become + * invalid due to either view is no longer visible, or the handwriting area set by the + * view is removed. {@link HandwritingAreaTracker} no longer need to keep track of this + * HandwritableViewInfo this method returns false. + */ + public boolean update() { + final View view = getView(); + if (!isViewActive(view)) { + return false; + } + + if (!mIsDirty) { + return true; + } + final Rect localRect = view.getHandwritingArea(); + if (localRect == null) { + return false; + } + + ViewParent parent = view.getParent(); + if (parent != null) { + final Rect newRect = new Rect(localRect); + if (parent.getChildVisibleRect(view, newRect, null /* offset */)) { + mHandwritingArea = newRect; + } else { + mHandwritingArea = null; + } + } + mIsDirty = false; + return true; + } + } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 4ff7e2297ea02..da474d0672569 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -4745,9 +4745,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private List mSystemGestureExclusionRects = null; private List mKeepClearRects = null; private boolean mPreferKeepClear = false; + private Rect mHandwritingArea = null; /** - * Used to track {@link #mSystemGestureExclusionRects} and {@link #mKeepClearRects} + * Used to track {@link #mSystemGestureExclusionRects}, {@link #mKeepClearRects} and + * {@link #mHandwritingArea}. */ public RenderNode.PositionUpdateListener mPositionUpdateListener; private Runnable mPositionChangedUpdate; @@ -11692,7 +11694,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private void updatePositionUpdateListener() { final ListenerInfo info = getListenerInfo(); if (getSystemGestureExclusionRects().isEmpty() - && collectPreferKeepClearRects().isEmpty()) { + && collectPreferKeepClearRects().isEmpty() + && (info.mHandwritingArea == null || !isAutoHandwritingEnabled())) { if (info.mPositionUpdateListener != null) { mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener); info.mPositionChangedUpdate = null; @@ -11702,6 +11705,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, info.mPositionChangedUpdate = () -> { updateSystemGestureExclusionRects(); updateKeepClearRects(); + updateHandwritingArea(); }; info.mPositionUpdateListener = new RenderNode.PositionUpdateListener() { @Override @@ -11857,6 +11861,51 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return Collections.emptyList(); } + /** + * Set a list of handwriting areas in this view. If there is any stylus {@link MotionEvent} + * occurs within those areas, it will trigger stylus handwriting mode. This can be disabled by + * disabling the auto handwriting initiation by calling + * {@link #setAutoHandwritingEnabled(boolean)} with false. + * + * @attr rects a list of handwriting area in the view's local coordiniates. + * + * @see android.view.inputmethod.InputMethodManager#startStylusHandwriting(View) + * @see #setAutoHandwritingEnabled(boolean) + * + * @hide + */ + public void setHandwritingArea(@Nullable Rect rect) { + final ListenerInfo info = getListenerInfo(); + info.mHandwritingArea = rect; + updatePositionUpdateListener(); + postUpdate(this::updateHandwritingArea); + } + + /** + * Return the handwriting areas set on this view, in its local coordinates. + * Notice: the caller of this method should not modify the Rect returned. + * @see #setHandwritingArea(Rect) + * + * @hide + */ + @Nullable + public Rect getHandwritingArea() { + final ListenerInfo info = mListenerInfo; + if (info != null) { + return info.mHandwritingArea; + } + return null; + } + + void updateHandwritingArea() { + // If autoHandwritingArea is not enabled, do nothing. + if (!isAutoHandwritingEnabled()) return; + final AttachInfo ai = mAttachInfo; + if (ai != null) { + ai.mViewRootImpl.getHandwritingInitiator().updateHandwritingAreasForView(this); + } + } + /** * Compute the view's coordinate within the surface. * @@ -31154,6 +31203,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } else { mPrivateFlags4 &= ~PFLAG4_AUTO_HANDWRITING_ENABLED; } + updatePositionUpdateListener(); + postUpdate(this::updateHandwritingArea); } /** diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java index 2c612804d7183..9c0900b35de65 100644 --- a/core/java/android/widget/EditText.java +++ b/core/java/android/widget/EditText.java @@ -17,6 +17,7 @@ package android.widget; import android.content.Context; +import android.graphics.Rect; import android.text.Editable; import android.text.Selection; import android.text.Spannable; @@ -173,6 +174,12 @@ public class EditText extends TextView { return EditText.class.getName(); } + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + setHandwritingArea(new Rect(0, 0, w, h)); + } + /** @hide */ @Override protected boolean supportsAutoSizeText() { diff --git a/core/tests/coretests/src/android/view/stylus/HandwritableViewInfoTest.java b/core/tests/coretests/src/android/view/stylus/HandwritableViewInfoTest.java new file mode 100644 index 0000000000000..4bea54ba83032 --- /dev/null +++ b/core/tests/coretests/src/android/view/stylus/HandwritableViewInfoTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2022 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.view.stylus; + +import static android.view.stylus.HandwritingTestUtil.createView; + +import static com.google.common.truth.Truth.assertThat; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; +import android.view.HandwritingInitiator; +import android.view.View; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class HandwritableViewInfoTest { + + @Test + public void constructorTest() { + final Rect rect = new Rect(1, 2, 3, 4); + final View view = createView(rect); + final HandwritingInitiator.HandwritableViewInfo handwritableViewInfo = + new HandwritingInitiator.HandwritableViewInfo(view); + + assertThat(handwritableViewInfo.getView()).isEqualTo(view); + // It's labeled dirty by default. + assertTrue(handwritableViewInfo.mIsDirty); + } + + @Test + public void update() { + final Rect rect = new Rect(1, 2, 3, 4); + final View view = createView(rect); + final HandwritingInitiator.HandwritableViewInfo handwritableViewInfo = + new HandwritingInitiator.HandwritableViewInfo(view); + + assertThat(handwritableViewInfo.getView()).isEqualTo(view); + + final boolean isViewInfoValid = handwritableViewInfo.update(); + + assertTrue(isViewInfoValid); + assertThat(handwritableViewInfo.getHandwritingArea()).isEqualTo(rect); + assertFalse(handwritableViewInfo.mIsDirty); + } + + @Test + public void update_viewDisableAutoHandwriting() { + final Rect rect = new Rect(1, 2, 3, 4); + final View view = HandwritingTestUtil.createView(rect, false /* autoHandwritingEnabled */); + final HandwritingInitiator.HandwritableViewInfo handwritableViewInfo = + new HandwritingInitiator.HandwritableViewInfo(view); + + assertThat(handwritableViewInfo.getView()).isEqualTo(view); + + final boolean isViewInfoValid = handwritableViewInfo.update(); + + // Return false because the view disabled autoHandwriting. + assertFalse(isViewInfoValid); + // The view disabled the autoHandwriting, and it won't update the handwriting area. + assertThat(handwritableViewInfo.getHandwritingArea()).isNull(); + } + +} diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingAreaTrackerTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingAreaTrackerTest.java new file mode 100644 index 0000000000000..db4707a07435f --- /dev/null +++ b/core/tests/coretests/src/android/view/stylus/HandwritingAreaTrackerTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2022 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.view.stylus; + +import static android.view.stylus.HandwritingTestUtil.createView; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Instrumentation; +import android.content.Context; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; +import android.view.HandwritingInitiator; +import android.view.View; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + + +/** + * Tests for {@link HandwritingInitiator.HandwritingAreaTracker} + * + * Build/Install/Run: + * atest FrameworksCoreTests:android.view.stylus.HandwritingAreaTrackerTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class HandwritingAreaTrackerTest { + HandwritingInitiator.HandwritingAreaTracker mHandwritingAreaTracker; + Context mContext; + + @Before + public void setup() { + final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation(); + mContext = mInstrumentation.getTargetContext(); + mHandwritingAreaTracker = new HandwritingInitiator.HandwritingAreaTracker(); + } + + @Test + public void updateHandwritingAreaForView_singleView() { + Rect rect = new Rect(0, 0, 100, 100); + View view = createView(rect); + mHandwritingAreaTracker.updateHandwritingAreaForView(view); + + List viewInfos = + mHandwritingAreaTracker.computeViewInfos(); + + assertThat(viewInfos.size()).isEqualTo(1); + assertThat(viewInfos.get(0).getHandwritingArea()).isEqualTo(rect); + assertThat(viewInfos.get(0).getView()).isEqualTo(view); + } + + @Test + public void updateHandwritingAreaForView_multipleViews() { + Rect rect1 = new Rect(0, 0, 100, 100); + Rect rect2 = new Rect(100, 100, 200, 200); + + View view1 = createView(rect1); + View view2 = createView(rect2); + mHandwritingAreaTracker.updateHandwritingAreaForView(view1); + mHandwritingAreaTracker.updateHandwritingAreaForView(view2); + + List viewInfos = + mHandwritingAreaTracker.computeViewInfos(); + + assertThat(viewInfos.size()).isEqualTo(2); + assertThat(viewInfos.get(0).getView()).isEqualTo(view1); + assertThat(viewInfos.get(0).getHandwritingArea()).isEqualTo(rect1); + + assertThat(viewInfos.get(1).getView()).isEqualTo(view2); + assertThat(viewInfos.get(1).getHandwritingArea()).isEqualTo(rect2); + } + + @Test + public void updateHandwritingAreaForView_afterDisableAutoHandwriting() { + Rect rect1 = new Rect(0, 0, 100, 100); + Rect rect2 = new Rect(100, 100, 200, 200); + + View view1 = createView(rect1); + View view2 = createView(rect2); + mHandwritingAreaTracker.updateHandwritingAreaForView(view1); + mHandwritingAreaTracker.updateHandwritingAreaForView(view2); + + // There should be 2 views tracked. + assertThat(mHandwritingAreaTracker.computeViewInfos().size()).isEqualTo(2); + + // Disable autoHandwriting for view1 and update handwriting area. + view1.setAutoHandwritingEnabled(false); + mHandwritingAreaTracker.updateHandwritingAreaForView(view1); + + List viewInfos = + mHandwritingAreaTracker.computeViewInfos(); + // The view1 has disabled the autoHandwriting, it's not tracked anymore. + assertThat(viewInfos.size()).isEqualTo(1); + + // view2 is still tracked. + assertThat(viewInfos.get(0).getView()).isEqualTo(view2); + assertThat(viewInfos.get(0).getHandwritingArea()).isEqualTo(rect2); + } + + @Test + public void updateHandwritingAreaForView_removesInactiveView() { + Rect rect1 = new Rect(0, 0, 100, 100); + Rect rect2 = new Rect(100, 100, 200, 200); + + View view1 = createView(rect1); + View view2 = createView(rect2); + mHandwritingAreaTracker.updateHandwritingAreaForView(view1); + mHandwritingAreaTracker.updateHandwritingAreaForView(view2); + + // There should be 2 viewInfos tracked. + assertThat(mHandwritingAreaTracker.computeViewInfos().size()).isEqualTo(2); + + // Disable autoHandwriting for view1, but update handwriting area for view2. + view1.setAutoHandwritingEnabled(false); + mHandwritingAreaTracker.updateHandwritingAreaForView(view2); + + List viewInfos = + mHandwritingAreaTracker.computeViewInfos(); + // The view1 has disabled the autoHandwriting, it's not tracked anymore. + assertThat(viewInfos.size()).isEqualTo(1); + + // view2 is still tracked. + assertThat(viewInfos.get(0).getView()).isEqualTo(view2); + assertThat(viewInfos.get(0).getHandwritingArea()).isEqualTo(rect2); + } +} diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index e11fe17bac9f4..1ae9649dfe063 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -19,6 +19,7 @@ package android.view.stylus; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; +import static android.view.stylus.HandwritingTestUtil.createView; import static com.google.common.truth.Truth.assertThat; @@ -38,7 +39,6 @@ import android.view.InputDevice; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; -import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -78,7 +78,8 @@ public class HandwritingInitiatorTest { mHandwritingInitiator = spy(new HandwritingInitiator(viewConfiguration, inputMethodManager)); - mTestView = createMockView(sHwArea, true); + mTestView = createView(sHwArea, true); + mHandwritingInitiator.updateHandwritingAreasForView(mTestView); } @Test @@ -194,9 +195,26 @@ public class HandwritingInitiatorTest { verify(mHandwritingInitiator, never()).startHandwriting(mTestView); } + @Test + public void onTouchEvent_focusView_stylusMoveOnce_withinHWArea() { + final int x1 = (sHwArea.left + sHwArea.right) / 2; + final int y1 = (sHwArea.top + sHwArea.bottom) / 2; + MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); + mHandwritingInitiator.onTouchEvent(stylusEvent1); + + final int x2 = x1 + TOUCH_SLOP * 2; + final int y2 = y1; + + MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0); + mHandwritingInitiator.onTouchEvent(stylusEvent2); + + // HandwritingInitiator will request focus for the registered view. + verify(mTestView, times(1)).requestFocus(); + } + @Test public void autoHandwriting_whenDisabled_wontStartHW() { - View mockView = createMockView(sHwArea, false); + View mockView = createView(sHwArea, false); mHandwritingInitiator.onInputConnectionCreated(mockView); final int x1 = (sHwArea.left + sHwArea.right) / 2; final int y1 = (sHwArea.top + sHwArea.bottom) / 2; @@ -273,25 +291,4 @@ public class HandwritingInitiatorTest { 1 /* yPrecision */, 0 /* deviceId */, 0 /* edgeFlags */, InputDevice.SOURCE_TOUCHSCREEN, 0 /* flags */); } - - private View createMockView(Rect viewBound, boolean autoHandwritingEnabled) { - // mock a parent so that HandwritingInitiator can get - ViewGroup parent = new ViewGroup(mContext) { - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - // We don't layout this view. - } - @Override - public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) { - r.set(viewBound); - return true; - } - }; - - View mockView = mock(View.class); - when(mockView.isAttachedToWindow()).thenReturn(true); - when(mockView.isAutoHandwritingEnabled()).thenReturn(autoHandwritingEnabled); - parent.addView(mockView); - return mockView; - } } diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java new file mode 100644 index 0000000000000..6daf880fa2ab5 --- /dev/null +++ b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 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.view.stylus; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.app.Instrumentation; +import android.content.Context; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; + +import androidx.test.platform.app.InstrumentationRegistry; + +public class HandwritingTestUtil { + public static View createView(Rect handwritingArea) { + return createView(handwritingArea, true); + } + + public static View createView(Rect handwritingArea, boolean autoHandwritingEnabled) { + final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + final Context context = instrumentation.getTargetContext(); + // mock a parent so that HandwritingInitiator can get + final ViewGroup parent = new ViewGroup(context) { + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // We don't layout this view. + } + @Override + public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) { + r.set(handwritingArea); + return true; + } + }; + + View view = spy(new View(context)); + when(view.isAttachedToWindow()).thenReturn(true); + when(view.isAggregatedVisible()).thenReturn(true); + when(view.getHandwritingArea()).thenReturn(handwritingArea); + view.setAutoHandwritingEnabled(autoHandwritingEnabled); + parent.addView(view); + return view; + } +}