1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.graphics.Matrix;
23 import android.graphics.Rect;
24 import android.graphics.RectF;
25 import android.graphics.Region;
26 import android.view.inputmethod.InputMethodManager;
27 import android.widget.EditText;
28 import android.widget.TextView;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 
32 import java.lang.ref.WeakReference;
33 import java.util.ArrayList;
34 import java.util.Iterator;
35 import java.util.List;
36 
37 /**
38  * Initiates handwriting mode once it detects stylus movement in handwritable areas.
39  *
40  * It is designed to be used by  {@link ViewRootImpl}. For every stylus related MotionEvent that is
41  * dispatched to view tree, ViewRootImpl should call {@link #onTouchEvent} method of this class.
42  * And it will automatically request to enter the handwriting mode when the conditions meet.
43  *
44  * Notice that ViewRootImpl should still dispatch MotionEvents to view tree as usual.
45  * And if it successfully enters the handwriting mode, the ongoing MotionEvent stream will be
46  * routed to the input method. Input system will fabricate an ACTION_CANCEL and send to
47  * ViewRootImpl.
48  *
49  * This class does nothing if:
50  * a) MotionEvents are not from stylus.
51  * b) The user taps or long-clicks with a stylus etc.
52  * c) Stylus pointer down position is not within a handwritable area.
53  *
54  * Used by InputMethodManager.
55  * @hide
56  */
57 public class HandwritingInitiator {
58     /**
59      * The maximum amount of distance a stylus touch can wander before it is considered
60      * handwriting.
61      */
62     private final int mHandwritingSlop;
63     /**
64      * The timeout used to distinguish tap or long click from handwriting. If the stylus doesn't
65      * move before this timeout, it's not considered as handwriting.
66      */
67     private final long mHandwritingTimeoutInMillis;
68 
69     private State mState;
70     private final HandwritingAreaTracker mHandwritingAreasTracker = new HandwritingAreaTracker();
71 
72     /** The reference to the View that currently has the input connection. */
73     @Nullable
74     @VisibleForTesting
75     public WeakReference<View> mConnectedView = null;
76 
77     /**
78      * When InputConnection restarts for a View, View#onInputConnectionCreatedInternal
79      * might be called before View#onInputConnectionClosedInternal, so we need to count the input
80      * connections and only set mConnectedView to null when mConnectionCount is zero.
81      */
82     private int mConnectionCount = 0;
83     private final InputMethodManager mImm;
84 
85     private final int[] mTempLocation = new int[2];
86 
87     private final Rect mTempRect = new Rect();
88 
89     private final RectF mTempRectF = new RectF();
90 
91     private final Region mTempRegion = new Region();
92 
93     private final Matrix mTempMatrix = new Matrix();
94 
95     /**
96      * The handwrite-able View that is currently the target of a hovering stylus pointer. This is
97      * used to help determine whether the handwriting PointerIcon should be shown in
98      * {@link #onResolvePointerIcon(Context, MotionEvent)} so that we can reduce the number of calls
99      * to {@link #findBestCandidateView(float, float, boolean)}.
100      */
101     @Nullable
102     private WeakReference<View> mCachedHoverTarget = null;
103 
104     /**
105      * Whether to show the hover icon for the current connected view.
106      * Hover icon should be hidden for the current connected view after handwriting is initiated
107      * for it until one of the following events happens:
108      * a) user performs a click or long click. In other words, if it receives a series of motion
109      * events that don't trigger handwriting, show hover icon again.
110      * b) the stylus hovers on another editor that supports handwriting (or a handwriting delegate).
111      * c) the current connected editor lost focus.
112      *
113      * If the stylus is hovering on an unconnected editor that supports handwriting, we always show
114      * the hover icon.
115      */
116     private boolean mShowHoverIconForConnectedView = true;
117 
118     @VisibleForTesting
HandwritingInitiator(@onNull ViewConfiguration viewConfiguration, @NonNull InputMethodManager inputMethodManager)119     public HandwritingInitiator(@NonNull ViewConfiguration viewConfiguration,
120             @NonNull InputMethodManager inputMethodManager) {
121         mHandwritingSlop = viewConfiguration.getScaledHandwritingSlop();
122         mHandwritingTimeoutInMillis = ViewConfiguration.getLongPressTimeout();
123         mImm = inputMethodManager;
124     }
125 
126     /**
127      * Notify the HandwritingInitiator that a new MotionEvent has arrived.
128      *
129      * <p>The return value indicates whether the event has been fully handled by the
130      * HandwritingInitiator and should not be dispatched to the view tree. This will be true for
131      * ACTION_MOVE events from a stylus gesture after handwriting mode has been initiated, in order
132      * to suppress other actions such as scrolling.
133      *
134      * <p>If HandwritingInitiator triggers the handwriting mode, a fabricated ACTION_CANCEL event
135      * will be sent to the ViewRootImpl.
136      *
137      * @param motionEvent the stylus {@link MotionEvent}
138      * @return true if the event has been fully handled by the {@link HandwritingInitiator} and
139      * should not be dispatched to the {@link View} tree, or false if the event should be dispatched
140      * to the {@link View} tree as usual
141      */
142     @VisibleForTesting
onTouchEvent(@onNull MotionEvent motionEvent)143     public boolean onTouchEvent(@NonNull MotionEvent motionEvent) {
144         final int maskedAction = motionEvent.getActionMasked();
145         switch (maskedAction) {
146             case MotionEvent.ACTION_DOWN:
147             case MotionEvent.ACTION_POINTER_DOWN:
148                 mState = null;
149                 final int actionIndex = motionEvent.getActionIndex();
150                 final int toolType = motionEvent.getToolType(actionIndex);
151                 // TOOL_TYPE_ERASER is also from stylus. This indicates that the user is holding
152                 // the eraser button during handwriting.
153                 if (toolType != MotionEvent.TOOL_TYPE_STYLUS
154                         && toolType != MotionEvent.TOOL_TYPE_ERASER) {
155                     // The motion event is not from a stylus event, ignore it.
156                     return false;
157                 }
158                 mState = new State(motionEvent);
159                 break;
160             case MotionEvent.ACTION_POINTER_UP:
161                 final int pointerId = motionEvent.getPointerId(motionEvent.getActionIndex());
162                 if (mState == null || pointerId != mState.mStylusPointerId) {
163                     // ACTION_POINTER_UP is from another stylus pointer, ignore the event.
164                     return false;
165                 }
166                 // Deliberately fall through.
167             case MotionEvent.ACTION_CANCEL:
168             case MotionEvent.ACTION_UP:
169                 // If it's ACTION_CANCEL or ACTION_UP, all the pointers go up. There is no need to
170                 // check whether the stylus we are tracking goes up.
171                 if (mState != null) {
172                     mState.mShouldInitHandwriting = false;
173                     if (!mState.mHasInitiatedHandwriting
174                             && !mState.mHasPreparedHandwritingDelegation) {
175                         // The user just did a click, long click or another stylus gesture,
176                         // show hover icon again for the connected view.
177                         mShowHoverIconForConnectedView = true;
178                     }
179                 }
180                 return false;
181             case MotionEvent.ACTION_MOVE:
182                 if (mState == null) {
183                     return false;
184                 }
185 
186                 // Either we've already tried to initiate handwriting, or the ongoing MotionEvent
187                 // sequence is considered to be tap, long-click or other gestures.
188                 if (!mState.mShouldInitHandwriting || mState.mExceedHandwritingSlop) {
189                     return mState.mHasInitiatedHandwriting
190                             || mState.mHasPreparedHandwritingDelegation;
191                 }
192 
193                 final long timeElapsed =
194                         motionEvent.getEventTime() - mState.mStylusDownTimeInMillis;
195                 if (timeElapsed > mHandwritingTimeoutInMillis) {
196                     mState.mShouldInitHandwriting = false;
197                     return mState.mHasInitiatedHandwriting
198                             || mState.mHasPreparedHandwritingDelegation;
199                 }
200 
201                 final int pointerIndex = motionEvent.findPointerIndex(mState.mStylusPointerId);
202                 final float x = motionEvent.getX(pointerIndex);
203                 final float y = motionEvent.getY(pointerIndex);
204                 if (largerThanTouchSlop(x, y, mState.mStylusDownX, mState.mStylusDownY)) {
205                     mState.mExceedHandwritingSlop = true;
206                     View candidateView = findBestCandidateView(mState.mStylusDownX,
207                             mState.mStylusDownY, /* isHover */ false);
208                     if (candidateView != null) {
209                         if (candidateView == getConnectedView()) {
210                             if (!candidateView.hasFocus()) {
211                                 requestFocusWithoutReveal(candidateView);
212                             }
213                             startHandwriting(candidateView);
214                         } else if (candidateView.getHandwritingDelegatorCallback() != null) {
215                             String delegatePackageName =
216                                     candidateView.getAllowedHandwritingDelegatePackageName();
217                             if (delegatePackageName == null) {
218                                 delegatePackageName = candidateView.getContext().getOpPackageName();
219                             }
220                             mImm.prepareStylusHandwritingDelegation(
221                                     candidateView, delegatePackageName);
222                             candidateView.getHandwritingDelegatorCallback().run();
223                             mState.mHasPreparedHandwritingDelegation = true;
224                         } else {
225                             mState.mPendingConnectedView = new WeakReference<>(candidateView);
226                             requestFocusWithoutReveal(candidateView);
227                         }
228                     }
229                 }
230                 return mState.mHasInitiatedHandwriting || mState.mHasPreparedHandwritingDelegation;
231         }
232         return false;
233     }
234 
235     @Nullable
getConnectedView()236     private View getConnectedView() {
237         if (mConnectedView == null) return null;
238         return mConnectedView.get();
239     }
240 
clearConnectedView()241     private void clearConnectedView() {
242         mConnectedView = null;
243         mConnectionCount = 0;
244     }
245 
246     /**
247      * Notify HandwritingInitiator that a delegate view (see {@link View#isHandwritingDelegate})
248      * gained focus.
249      */
onDelegateViewFocused(@onNull View view)250     public void onDelegateViewFocused(@NonNull View view) {
251         if (view == getConnectedView()) {
252             if (tryAcceptStylusHandwritingDelegation(view)) {
253                 // A handwriting delegate view is accepted and handwriting starts; hide the
254                 // hover icon.
255                 mShowHoverIconForConnectedView = false;
256             }
257         }
258     }
259 
260     /**
261      * Notify HandwritingInitiator that a new InputConnection is created.
262      * The caller of this method should guarantee that each onInputConnectionCreated call
263      * is paired with a onInputConnectionClosed call.
264      * @param view the view that created the current InputConnection.
265      * @see  #onInputConnectionClosed(View)
266      */
onInputConnectionCreated(@onNull View view)267     public void onInputConnectionCreated(@NonNull View view) {
268         if (!view.isAutoHandwritingEnabled()) {
269             clearConnectedView();
270             return;
271         }
272 
273         final View connectedView = getConnectedView();
274         if (connectedView == view) {
275             ++mConnectionCount;
276         } else {
277             mConnectedView = new WeakReference<>(view);
278             mConnectionCount = 1;
279             // A new view just gain focus. By default, we should show hover icon for it.
280             mShowHoverIconForConnectedView = true;
281             if (view.isHandwritingDelegate() && tryAcceptStylusHandwritingDelegation(view)) {
282                 // A handwriting delegate view is accepted and handwriting starts; hide the
283                 // hover icon.
284                 mShowHoverIconForConnectedView = false;
285                 return;
286             }
287             if (mState != null && mState.mPendingConnectedView != null
288                     && mState.mPendingConnectedView.get() == view) {
289                 startHandwriting(view);
290             }
291         }
292     }
293 
294     /**
295      * Notify HandwritingInitiator that the InputConnection has closed for the given view.
296      * The caller of this method should guarantee that each onInputConnectionClosed call
297      * is paired with a onInputConnectionCreated call.
298      * @param view the view that closed the InputConnection.
299      */
onInputConnectionClosed(@onNull View view)300     public void onInputConnectionClosed(@NonNull View view) {
301         final View connectedView = getConnectedView();
302         if (connectedView == null) return;
303         if (connectedView == view) {
304             --mConnectionCount;
305             if (mConnectionCount == 0) {
306                 clearConnectedView();
307             }
308         } else {
309             // Unexpected branch, set mConnectedView to null to avoid further problem.
310             clearConnectedView();
311         }
312     }
313 
314     /** Starts a stylus handwriting session for the view. */
315     @VisibleForTesting
startHandwriting(@onNull View view)316     public void startHandwriting(@NonNull View view) {
317         mImm.startStylusHandwriting(view);
318         mState.mHasInitiatedHandwriting = true;
319         mState.mShouldInitHandwriting = false;
320         mShowHoverIconForConnectedView = false;
321         if (view instanceof TextView) {
322             ((TextView) view).hideHint();
323         }
324     }
325 
326     /**
327      * Starts a stylus handwriting session for the delegate view, if {@link
328      * InputMethodManager#prepareStylusHandwritingDelegation} was previously called.
329      */
330     @VisibleForTesting
tryAcceptStylusHandwritingDelegation(@onNull View view)331     public boolean tryAcceptStylusHandwritingDelegation(@NonNull View view) {
332         String delegatorPackageName =
333                 view.getAllowedHandwritingDelegatorPackageName();
334         if (delegatorPackageName == null) {
335             delegatorPackageName = view.getContext().getOpPackageName();
336         }
337         if (mImm.acceptStylusHandwritingDelegation(view, delegatorPackageName)) {
338             if (mState != null) {
339                 mState.mHasInitiatedHandwriting = true;
340                 mState.mShouldInitHandwriting = false;
341             }
342             if (view instanceof TextView) {
343                 ((TextView) view).hideHint();
344             }
345             return true;
346         }
347         return false;
348     }
349 
350     /**
351      * Notify that the handwriting area for the given view might be updated.
352      * @param view the view whose handwriting area might be updated.
353      */
updateHandwritingAreasForView(@onNull View view)354     public void updateHandwritingAreasForView(@NonNull View view) {
355         mHandwritingAreasTracker.updateHandwritingAreaForView(view);
356     }
357 
shouldTriggerStylusHandwritingForView(@onNull View view)358     private static boolean shouldTriggerStylusHandwritingForView(@NonNull View view) {
359         if (!view.shouldInitiateHandwriting()) {
360             return false;
361         }
362         // The view may be a handwriting initiation delegator, in which case it is not the editor
363         // view for which handwriting would be started. However, in almost all cases, the return
364         // values of View#isStylusHandwritingAvailable will be the same for the delegator view and
365         // the delegate editor view. So the delegator view can be used to decide whether handwriting
366         // should be triggered.
367         return view.isStylusHandwritingAvailable();
368     }
369 
370     /**
371      * Returns the pointer icon for the motion event, or null if it doesn't specify the icon.
372      * This gives HandwritingInitiator a chance to show the stylus handwriting icon over a
373      * handwrite-able area.
374      */
onResolvePointerIcon(Context context, MotionEvent event)375     public PointerIcon onResolvePointerIcon(Context context, MotionEvent event) {
376         final View hoverView = findHoverView(event);
377         if (hoverView == null) {
378             return null;
379         }
380 
381         if (mShowHoverIconForConnectedView) {
382             return PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING);
383         }
384 
385         if (hoverView != getConnectedView()) {
386             // The stylus is hovering on another view that supports handwriting. We should show
387             // hover icon. Also reset the mShowHoverIconForConnectedView so that hover
388             // icon is displayed again next time when the stylus hovers on connected view.
389             mShowHoverIconForConnectedView = true;
390             return PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING);
391         }
392         return null;
393     }
394 
getCachedHoverTarget()395     private View getCachedHoverTarget() {
396         if (mCachedHoverTarget == null) {
397             return null;
398         }
399         return mCachedHoverTarget.get();
400     }
401 
findHoverView(MotionEvent event)402     private View findHoverView(MotionEvent event) {
403         if (!event.isStylusPointer() || !event.isHoverEvent()) {
404             return null;
405         }
406 
407         if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER
408                 || event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE) {
409             final float hoverX = event.getX(event.getActionIndex());
410             final float hoverY = event.getY(event.getActionIndex());
411 
412             final View cachedHoverTarget = getCachedHoverTarget();
413             if (cachedHoverTarget != null) {
414                 final Rect handwritingArea = mTempRect;
415                 if (getViewHandwritingArea(cachedHoverTarget, handwritingArea)
416                         && isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget,
417                         /* isHover */ true)
418                         && shouldTriggerStylusHandwritingForView(cachedHoverTarget)) {
419                     return cachedHoverTarget;
420                 }
421             }
422 
423             final View candidateView = findBestCandidateView(hoverX, hoverY, /* isHover */ true);
424 
425             if (candidateView != null) {
426                 mCachedHoverTarget = new WeakReference<>(candidateView);
427                 return candidateView;
428             }
429         }
430 
431         mCachedHoverTarget = null;
432         return null;
433     }
434 
requestFocusWithoutReveal(View view)435     private void requestFocusWithoutReveal(View view) {
436         if (view instanceof EditText editText && !mState.mStylusDownWithinEditorBounds) {
437             // If the stylus down point was inside the EditText's bounds, then the EditText will
438             // automatically set its cursor position nearest to the stylus down point when it
439             // gains focus. If the stylus down point was outside the EditText's bounds (within
440             // the extended handwriting bounds), then we must calculate and set the cursor
441             // position manually.
442             view.getLocationInWindow(mTempLocation);
443             int offset = editText.getOffsetForPosition(
444                     mState.mStylusDownX - mTempLocation[0],
445                     mState.mStylusDownY - mTempLocation[1]);
446             editText.setSelection(offset);
447         }
448         if (view.getRevealOnFocusHint()) {
449             view.setRevealOnFocusHint(false);
450             view.requestFocus();
451             view.setRevealOnFocusHint(true);
452         } else {
453             view.requestFocus();
454         }
455     }
456 
457     /**
458      * Given the location of the stylus event, return the best candidate view to initialize
459      * handwriting mode.
460      *
461      * @param x the x coordinates of the stylus event, in the coordinates of the window.
462      * @param y the y coordinates of the stylus event, in the coordinates of the window.
463      */
464     @Nullable
findBestCandidateView(float x, float y, boolean isHover)465     private View findBestCandidateView(float x, float y, boolean isHover) {
466         // If the connectedView is not null and do not set any handwriting area, it will check
467         // whether the connectedView's boundary contains the initial stylus position. If true,
468         // directly return the connectedView.
469         final View connectedView = getConnectedView();
470         if (connectedView != null) {
471             Rect handwritingArea = mTempRect;
472             if (getViewHandwritingArea(connectedView, handwritingArea)
473                     && isInHandwritingArea(handwritingArea, x, y, connectedView, isHover)
474                     && shouldTriggerStylusHandwritingForView(connectedView)) {
475                 if (!isHover && mState != null) {
476                     mState.mStylusDownWithinEditorBounds =
477                             contains(handwritingArea, x, y, 0f, 0f, 0f, 0f);
478                 }
479                 return connectedView;
480             }
481         }
482 
483         float minDistance = Float.MAX_VALUE;
484         View bestCandidate = null;
485         // Check the registered handwriting areas.
486         final List<HandwritableViewInfo> handwritableViewInfos =
487                 mHandwritingAreasTracker.computeViewInfos();
488         for (HandwritableViewInfo viewInfo : handwritableViewInfos) {
489             final View view = viewInfo.getView();
490             final Rect handwritingArea = viewInfo.getHandwritingArea();
491             if (!isInHandwritingArea(handwritingArea, x, y, view, isHover)
492                     || !shouldTriggerStylusHandwritingForView(view)) {
493                 continue;
494             }
495 
496             final float distance = distance(handwritingArea, x, y);
497             if (distance == 0f) {
498                 if (!isHover && mState != null) {
499                     mState.mStylusDownWithinEditorBounds = true;
500                 }
501                 return view;
502             }
503             if (distance < minDistance) {
504                 minDistance = distance;
505                 bestCandidate = view;
506             }
507         }
508         return bestCandidate;
509     }
510 
511     /**
512      *  Return the square of the distance from point (x, y) to the given rect, which is mainly used
513      *  for comparison. The distance is defined to be: the shortest distance between (x, y) to any
514      *  point on rect. When (x, y) is contained by the rect, return 0f.
515      */
distance(@onNull Rect rect, float x, float y)516     private static float distance(@NonNull Rect rect, float x, float y) {
517         if (contains(rect, x, y, 0f, 0f, 0f, 0f)) {
518             return 0f;
519         }
520 
521         /* The distance between point (x, y) and rect, there are 2 basic cases:
522          * a) The distance is the distance from (x, y) to the closest corner on rect.
523          *                    o |     |
524          *         ---+-----+---
525          *            |     |
526          *         ---+-----+---
527          *            |     |
528          * b) The distance is the distance from (x, y) to the closest edge on rect.
529          *                      |  o  |
530          *         ---+-----+---
531          *            |     |
532          *         ---+-----+---
533          *            |     |
534          * We define xDistance as following(similar for yDistance):
535          *   If x is in [left, right) 0, else min(abs(x - left), abs(x - y))
536          * For case a, sqrt(xDistance^2 + yDistance^2) is the final distance.
537          * For case b, distance should be yDistance, which is also equal to
538          * sqrt(xDistance^2 + yDistance^2) because xDistance is 0.
539          */
540         final float xDistance;
541         if (x >= rect.left && x < rect.right) {
542             xDistance = 0f;
543         } else if (x < rect.left) {
544             xDistance = rect.left - x;
545         } else {
546             xDistance = x - rect.right;
547         }
548 
549         final float yDistance;
550         if (y >= rect.top && y < rect.bottom) {
551             yDistance = 0f;
552         } else if (y < rect.top) {
553             yDistance = rect.top - y;
554         } else {
555             yDistance = y - rect.bottom;
556         }
557         // We can omit sqrt here because we only need the distance for comparison.
558         return xDistance * xDistance + yDistance * yDistance;
559     }
560 
561     /**
562      * Return the handwriting area of the given view, represented in the window's coordinate.
563      * If the view didn't set any handwriting area, it will return the view's boundary.
564      *
565      * <p> The handwriting area is clipped to its visible part.
566      * Notice that the returned rectangle is the view's original handwriting area without the
567      * view's handwriting area extends. </p>
568      *
569      * @param view the {@link View} whose handwriting area we want to compute.
570      * @param rect the {@link Rect} to receive the result.
571      *
572      * @return true if the view's handwriting area is still visible, or false if it's clipped and
573      * fully invisible. This method only consider the clip by given view's parents, but not the case
574      * where a view is covered by its sibling view.
575      */
getViewHandwritingArea(@onNull View view, @NonNull Rect rect)576     private static boolean getViewHandwritingArea(@NonNull View view, @NonNull Rect rect) {
577         final ViewParent viewParent = view.getParent();
578         if (viewParent != null && view.isAttachedToWindow() && view.isAggregatedVisible()) {
579             final Rect localHandwritingArea = view.getHandwritingArea();
580             if (localHandwritingArea != null) {
581                 rect.set(localHandwritingArea);
582             } else {
583                 rect.set(0, 0, view.getWidth(), view.getHeight());
584             }
585             return viewParent.getChildVisibleRect(view, rect, null);
586         }
587         return false;
588     }
589 
590     /**
591      * Return true if the (x, y) is inside by the given {@link Rect} with the View's
592      * handwriting bounds with offsets applied.
593      */
isInHandwritingArea(@ullable Rect handwritingArea, float x, float y, View view, boolean isHover)594     private boolean isInHandwritingArea(@Nullable Rect handwritingArea,
595             float x, float y, View view, boolean isHover) {
596         if (handwritingArea == null) return false;
597 
598         if (!contains(handwritingArea, x, y,
599                 view.getHandwritingBoundsOffsetLeft(),
600                 view.getHandwritingBoundsOffsetTop(),
601                 view.getHandwritingBoundsOffsetRight(),
602                 view.getHandwritingBoundsOffsetBottom())) {
603             return false;
604         }
605 
606         // The returned handwritingArea computed by ViewParent#getChildVisibleRect didn't consider
607         // the case where a view is stacking on top of the editor. (e.g. DrawerLayout, popup)
608         // We must check the hit region of the editor again, and avoid the case where another
609         // view on top of the editor is handling MotionEvents.
610         ViewParent parent = view.getParent();
611         if (parent == null) {
612             return true;
613         }
614 
615         Region region = mTempRegion;
616         mTempRegion.set(0, 0, view.getWidth(), view.getHeight());
617         Matrix matrix = mTempMatrix;
618         matrix.reset();
619         if (!parent.getChildLocalHitRegion(view, region, matrix, isHover)) {
620             return false;
621         }
622 
623         // It's not easy to extend the region by the given handwritingBoundsOffset. Instead, we
624         // create a rectangle surrounding the motion event location and check if this rectangle
625         // overlaps with the hit region of the editor.
626         float left = x - view.getHandwritingBoundsOffsetRight();
627         float top = y - view.getHandwritingBoundsOffsetBottom();
628         float right = Math.max(x + view.getHandwritingBoundsOffsetLeft(), left + 1);
629         float bottom =  Math.max(y + view.getHandwritingBoundsOffsetTop(), top + 1);
630         RectF rectF = mTempRectF;
631         rectF.set(left, top, right, bottom);
632         matrix.mapRect(rectF);
633 
634         return region.op(Math.round(rectF.left), Math.round(rectF.top),
635                 Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT);
636     }
637 
638     /**
639      * Return true if the (x, y) is inside by the given {@link Rect} offset by the given
640      * offsetLeft, offsetTop, offsetRight and offsetBottom.
641      */
contains(@onNull Rect rect, float x, float y, float offsetLeft, float offsetTop, float offsetRight, float offsetBottom)642     private static boolean contains(@NonNull Rect rect, float x, float y,
643             float offsetLeft, float offsetTop, float offsetRight, float offsetBottom) {
644         return x >= rect.left - offsetLeft && x < rect.right  + offsetRight
645                 && y >= rect.top - offsetTop && y < rect.bottom + offsetBottom;
646     }
647 
largerThanTouchSlop(float x1, float y1, float x2, float y2)648     private boolean largerThanTouchSlop(float x1, float y1, float x2, float y2) {
649         float dx = x1 - x2;
650         float dy = y1 - y2;
651         return dx * dx + dy * dy > mHandwritingSlop * mHandwritingSlop;
652     }
653 
654     /** Object that keeps the MotionEvent related states for HandwritingInitiator. */
655     private static class State {
656         /**
657          * Whether it should initiate handwriting mode for the current MotionEvent sequence.
658          * (A series of MotionEvents from ACTION_DOWN to ACTION_UP)
659          *
660          * The purpose of this boolean value is:
661          * a) We should only request to start handwriting mode ONCE for each MotionEvent sequence.
662          * If we've already requested to enter handwriting mode for the ongoing MotionEvent
663          * sequence, this boolean is set to false. And it won't request to start handwriting again.
664          *
665          * b) If the MotionEvent sequence is considered to be tap, long-click or other gestures.
666          * This boolean will be set to false, and it won't request to start handwriting.
667          */
668         private boolean mShouldInitHandwriting;
669         /**
670          * Whether handwriting mode has already been initiated for the current MotionEvent sequence.
671          */
672         private boolean mHasInitiatedHandwriting;
673 
674         private boolean mHasPreparedHandwritingDelegation;
675 
676         /**
677          * Whether the current ongoing stylus MotionEvent sequence already exceeds the
678          * handwriting slop.
679          * It's used for the case where the stylus exceeds handwriting slop before the target View
680          * built InputConnection.
681          */
682         private boolean mExceedHandwritingSlop;
683 
684         /**
685          * Whether the stylus down point of the MotionEvent sequence was within the editor's bounds
686          * (not including the extended handwriting bounds).
687          */
688         private boolean mStylusDownWithinEditorBounds;
689 
690         /**
691          * A view which has requested focus and is pending input connection creation. When an input
692          * connection is created for the view, a handwriting session should be started for the view.
693          */
694         private WeakReference<View> mPendingConnectedView = null;
695 
696         /** The pointer id of the stylus pointer that is being tracked. */
697         private final int mStylusPointerId;
698         /** The time stamp when the stylus pointer goes down. */
699         private final long mStylusDownTimeInMillis;
700         /** The initial location where the stylus pointer goes down. */
701         private final float mStylusDownX;
702         private final float mStylusDownY;
703 
State(MotionEvent motionEvent)704         private State(MotionEvent motionEvent) {
705             final int actionIndex = motionEvent.getActionIndex();
706             mStylusPointerId = motionEvent.getPointerId(actionIndex);
707             mStylusDownTimeInMillis = motionEvent.getEventTime();
708             mStylusDownX = motionEvent.getX(actionIndex);
709             mStylusDownY = motionEvent.getY(actionIndex);
710 
711             mShouldInitHandwriting = true;
712             mHasInitiatedHandwriting = false;
713             mHasPreparedHandwritingDelegation = false;
714             mExceedHandwritingSlop = false;
715         }
716     }
717 
718     /** The helper method to check if the given view is still active for handwriting. */
isViewActive(@ullable View view)719     private static boolean isViewActive(@Nullable View view) {
720         return view != null && view.isAttachedToWindow() && view.isAggregatedVisible()
721                 && view.shouldInitiateHandwriting();
722     }
723 
724     /**
725      * A class used to track the handwriting areas set by the Views.
726      *
727      * @hide
728      */
729     @VisibleForTesting
730     public static class HandwritingAreaTracker {
731         private final List<HandwritableViewInfo> mHandwritableViewInfos;
732 
HandwritingAreaTracker()733         public HandwritingAreaTracker() {
734             mHandwritableViewInfos = new ArrayList<>();
735         }
736 
737         /**
738          * Notify this tracker that the handwriting area of the given view has been updated.
739          * This method does three things:
740          * a) iterate over the all the tracked ViewInfos and remove those already invalid ones.
741          * b) mark the given view's ViewInfo to be dirty. So that next time when
742          * {@link #computeViewInfos} is called, this view's handwriting area will be recomputed.
743          * c) If no the given view is not in the tracked ViewInfo list, a new ViewInfo object will
744          * be created and added to the list.
745          *
746          * @param view the view whose handwriting area is updated.
747          */
updateHandwritingAreaForView(@onNull View view)748         public void updateHandwritingAreaForView(@NonNull View view) {
749             Iterator<HandwritableViewInfo> iterator = mHandwritableViewInfos.iterator();
750             boolean found = false;
751             while (iterator.hasNext()) {
752                 final HandwritableViewInfo handwritableViewInfo = iterator.next();
753                 final View curView = handwritableViewInfo.getView();
754                 if (!isViewActive(curView)) {
755                     iterator.remove();
756                 }
757                 if (curView == view) {
758                     found = true;
759                     handwritableViewInfo.mIsDirty = true;
760                 }
761             }
762             if (!found && isViewActive(view)) {
763                 // The given view is not tracked. Create a new HandwritableViewInfo for it and add
764                 // to the list.
765                 mHandwritableViewInfos.add(new HandwritableViewInfo(view));
766             }
767         }
768 
769         /**
770          * Update the handwriting areas and return a list of ViewInfos containing the view
771          * reference and its handwriting area.
772          */
773         @NonNull
computeViewInfos()774         public List<HandwritableViewInfo> computeViewInfos() {
775             mHandwritableViewInfos.removeIf(viewInfo -> !viewInfo.update());
776             return mHandwritableViewInfos;
777         }
778     }
779 
780     /**
781      * A class that reference to a View and its handwriting area(in the ViewRoot's coordinate.)
782      *
783      * @hide
784      */
785     @VisibleForTesting
786     public static class HandwritableViewInfo {
787         final WeakReference<View> mViewRef;
788         Rect mHandwritingArea = null;
789         @VisibleForTesting
790         public boolean mIsDirty = true;
791 
792         @VisibleForTesting
HandwritableViewInfo(@onNull View view)793         public HandwritableViewInfo(@NonNull View view) {
794             mViewRef = new WeakReference<>(view);
795         }
796 
797         /** Return the tracked view. */
798         @Nullable
getView()799         public View getView() {
800             return mViewRef.get();
801         }
802 
803         /**
804          * Return the tracked handwriting area, represented in the ViewRoot's coordinates.
805          * Notice, the caller should not modify the returned Rect.
806          */
807         @Nullable
getHandwritingArea()808         public Rect getHandwritingArea() {
809             return mHandwritingArea;
810         }
811 
812         /**
813          * Update the handwriting area in this ViewInfo.
814          *
815          * @return true if this ViewInfo is still valid. Or false if this ViewInfo has become
816          * invalid due to either view is no longer visible, or the handwriting area set by the
817          * view is removed. {@link HandwritingAreaTracker} no longer need to keep track of this
818          * HandwritableViewInfo this method returns false.
819          */
update()820         public boolean update() {
821             final View view = getView();
822             if (!isViewActive(view)) {
823                 return false;
824             }
825 
826             if (!mIsDirty) {
827                 return true;
828             }
829             final Rect handwritingArea = view.getHandwritingArea();
830             if (handwritingArea == null) {
831                 return false;
832             }
833 
834             ViewParent parent = view.getParent();
835             if (parent != null) {
836                 if (mHandwritingArea == null) {
837                     mHandwritingArea = new Rect();
838                 }
839                 mHandwritingArea.set(handwritingArea);
840                 if (!parent.getChildVisibleRect(view, mHandwritingArea, null /* offset */)) {
841                     mHandwritingArea = null;
842                 }
843             }
844             mIsDirty = false;
845             return true;
846         }
847     }
848 }
849