Loading core/java/android/text/flags/flags.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -120,6 +120,13 @@ flag { bug: "324676775" } flag { name: "handwriting_end_of_line_tap" namespace: "text" description: "Initiate handwriting when stylus taps at the end of a line in a focused non-empty TextView with the cursor at the end of that line" bug: "323376217" } flag { name: "handwriting_cursor_position" namespace: "text" Loading core/java/android/widget/TextView.java +50 −0 Original line number Diff line number Diff line Loading @@ -13118,6 +13118,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return superResult; } // At this point, the event is not a long press, otherwise it would be handled above. if (Flags.handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP && shouldStartHandwritingForEndOfLineTap(event)) { InputMethodManager imm = getInputMethodManager(); if (imm != null) { imm.startStylusHandwriting(this); return true; } } final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused(); Loading Loading @@ -13166,6 +13176,46 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return superResult; } /** * If handwriting is supported, the TextView is already focused and not empty, and the cursor is * at the end of a line, a stylus tap after the end of the line will trigger handwriting. */ private boolean shouldStartHandwritingForEndOfLineTap(MotionEvent actionUpEvent) { if (!onCheckIsTextEditor() || !isEnabled() || !isAutoHandwritingEnabled() || TextUtils.isEmpty(mText) || didTouchFocusSelect() || mLayout == null || !actionUpEvent.isStylusPointer()) { return false; } int cursorOffset = getSelectionStart(); if (cursorOffset < 0 || getSelectionEnd() != cursorOffset) { return false; } int cursorLine = mLayout.getLineForOffset(cursorOffset); int cursorLineEnd = mLayout.getLineEnd(cursorLine); if (cursorLine != mLayout.getLineCount() - 1) { cursorLineEnd--; } if (cursorLineEnd != cursorOffset) { return false; } // Check that the stylus down point is within the same line as the cursor. if (getLineAtCoordinate(actionUpEvent.getY()) != cursorLine) { return false; } // Check that the stylus down point is after the end of the line. float localX = convertToLocalHorizontalCoordinate(actionUpEvent.getX()); if (mLayout.getParagraphDirection(cursorLine) == Layout.DIR_RIGHT_TO_LEFT ? localX >= mLayout.getLineLeft(cursorLine) : localX <= mLayout.getLineRight(cursorLine)) { return false; } return isStylusHandwritingAvailable(); } /** * Returns true when need to show UIs, e.g. floating toolbar, etc, for finger based interaction. * services/core/java/com/android/server/inputmethod/HandwritingModeController.java +33 −4 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.server.inputmethod; import static com.android.text.flags.Flags.handwritingEndOfLineTap; import android.Manifest; import android.annotation.AnyThread; import android.annotation.NonNull; Loading @@ -30,6 +32,7 @@ import android.hardware.input.InputManagerGlobal; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.SystemClock; import android.text.TextUtils; import android.util.Slog; import android.view.BatchedInputEventReceiver; Loading Loading @@ -66,6 +69,7 @@ final class HandwritingModeController { // Use getHandwritingBufferSize() and not this value directly. private static final int LONG_EVENT_BUFFER_SIZE = EVENT_BUFFER_SIZE * 20; private static final long HANDWRITING_DELEGATION_IDLE_TIMEOUT_MS = 3000; private static final long AFTER_STYLUS_UP_ALLOW_PERIOD_MS = 200L; private final Context mContext; // This must be the looper for the UiThread. Loading @@ -78,6 +82,7 @@ final class HandwritingModeController { private InputEventReceiver mHandwritingEventReceiver; private Runnable mInkWindowInitRunnable; private boolean mRecordingGesture; private boolean mRecordingGestureAfterStylusUp; private int mCurrentDisplayId; // when set, package names are used for handwriting delegation. private @Nullable String mDelegatePackageName; Loading Loading @@ -155,6 +160,15 @@ final class HandwritingModeController { } boolean isStylusGestureOngoing() { if (mRecordingGestureAfterStylusUp && !mHandwritingBuffer.isEmpty()) { // If it is less than AFTER_STYLUS_UP_ALLOW_PERIOD_MS after the stylus up event, return // true so that handwriting can start. MotionEvent lastEvent = mHandwritingBuffer.get(mHandwritingBuffer.size() - 1); if (lastEvent.getActionMasked() == MotionEvent.ACTION_UP) { return SystemClock.uptimeMillis() - lastEvent.getEventTime() < AFTER_STYLUS_UP_ALLOW_PERIOD_MS; } } return mRecordingGesture; } Loading Loading @@ -277,7 +291,7 @@ final class HandwritingModeController { Slog.e(TAG, "Cannot start handwriting session: Invalid request id: " + requestId); return null; } if (!mRecordingGesture || mHandwritingBuffer.isEmpty()) { if (!isStylusGestureOngoing()) { Slog.e(TAG, "Cannot start handwriting session: No stylus gesture is being recorded."); return null; } Loading @@ -300,6 +314,7 @@ final class HandwritingModeController { mHandwritingEventReceiver.dispose(); mHandwritingEventReceiver = null; mRecordingGesture = false; mRecordingGestureAfterStylusUp = false; if (mHandwritingSurface.isIntercepting()) { throw new IllegalStateException( Loading Loading @@ -362,6 +377,7 @@ final class HandwritingModeController { clearPendingHandwritingDelegation(); } mRecordingGesture = false; mRecordingGestureAfterStylusUp = false; } private boolean onInputEvent(InputEvent ev) { Loading Loading @@ -412,15 +428,20 @@ final class HandwritingModeController { if ((TextUtils.isEmpty(mDelegatePackageName) || mDelegationConnectionlessFlow) && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) { mRecordingGesture = false; if (handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP) { mRecordingGestureAfterStylusUp = true; } else { mHandwritingBuffer.clear(); return; } } if (action == MotionEvent.ACTION_DOWN) { clearBufferIfRecordingAfterStylusUp(); mRecordingGesture = true; } if (!mRecordingGesture) { if (!mRecordingGesture && !mRecordingGestureAfterStylusUp) { return; } Loading @@ -430,12 +451,20 @@ final class HandwritingModeController { + " The rest of the gesture will not be recorded."); } mRecordingGesture = false; clearBufferIfRecordingAfterStylusUp(); return; } mHandwritingBuffer.add(MotionEvent.obtain(event)); } private void clearBufferIfRecordingAfterStylusUp() { if (mRecordingGestureAfterStylusUp) { mHandwritingBuffer.clear(); mRecordingGestureAfterStylusUp = false; } } static final class HandwritingSession { private final int mRequestId; private final InputChannel mHandwritingChannel; Loading Loading
core/java/android/text/flags/flags.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -120,6 +120,13 @@ flag { bug: "324676775" } flag { name: "handwriting_end_of_line_tap" namespace: "text" description: "Initiate handwriting when stylus taps at the end of a line in a focused non-empty TextView with the cursor at the end of that line" bug: "323376217" } flag { name: "handwriting_cursor_position" namespace: "text" Loading
core/java/android/widget/TextView.java +50 −0 Original line number Diff line number Diff line Loading @@ -13118,6 +13118,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return superResult; } // At this point, the event is not a long press, otherwise it would be handled above. if (Flags.handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP && shouldStartHandwritingForEndOfLineTap(event)) { InputMethodManager imm = getInputMethodManager(); if (imm != null) { imm.startStylusHandwriting(this); return true; } } final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused(); Loading Loading @@ -13166,6 +13176,46 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return superResult; } /** * If handwriting is supported, the TextView is already focused and not empty, and the cursor is * at the end of a line, a stylus tap after the end of the line will trigger handwriting. */ private boolean shouldStartHandwritingForEndOfLineTap(MotionEvent actionUpEvent) { if (!onCheckIsTextEditor() || !isEnabled() || !isAutoHandwritingEnabled() || TextUtils.isEmpty(mText) || didTouchFocusSelect() || mLayout == null || !actionUpEvent.isStylusPointer()) { return false; } int cursorOffset = getSelectionStart(); if (cursorOffset < 0 || getSelectionEnd() != cursorOffset) { return false; } int cursorLine = mLayout.getLineForOffset(cursorOffset); int cursorLineEnd = mLayout.getLineEnd(cursorLine); if (cursorLine != mLayout.getLineCount() - 1) { cursorLineEnd--; } if (cursorLineEnd != cursorOffset) { return false; } // Check that the stylus down point is within the same line as the cursor. if (getLineAtCoordinate(actionUpEvent.getY()) != cursorLine) { return false; } // Check that the stylus down point is after the end of the line. float localX = convertToLocalHorizontalCoordinate(actionUpEvent.getX()); if (mLayout.getParagraphDirection(cursorLine) == Layout.DIR_RIGHT_TO_LEFT ? localX >= mLayout.getLineLeft(cursorLine) : localX <= mLayout.getLineRight(cursorLine)) { return false; } return isStylusHandwritingAvailable(); } /** * Returns true when need to show UIs, e.g. floating toolbar, etc, for finger based interaction. *
services/core/java/com/android/server/inputmethod/HandwritingModeController.java +33 −4 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.server.inputmethod; import static com.android.text.flags.Flags.handwritingEndOfLineTap; import android.Manifest; import android.annotation.AnyThread; import android.annotation.NonNull; Loading @@ -30,6 +32,7 @@ import android.hardware.input.InputManagerGlobal; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.SystemClock; import android.text.TextUtils; import android.util.Slog; import android.view.BatchedInputEventReceiver; Loading Loading @@ -66,6 +69,7 @@ final class HandwritingModeController { // Use getHandwritingBufferSize() and not this value directly. private static final int LONG_EVENT_BUFFER_SIZE = EVENT_BUFFER_SIZE * 20; private static final long HANDWRITING_DELEGATION_IDLE_TIMEOUT_MS = 3000; private static final long AFTER_STYLUS_UP_ALLOW_PERIOD_MS = 200L; private final Context mContext; // This must be the looper for the UiThread. Loading @@ -78,6 +82,7 @@ final class HandwritingModeController { private InputEventReceiver mHandwritingEventReceiver; private Runnable mInkWindowInitRunnable; private boolean mRecordingGesture; private boolean mRecordingGestureAfterStylusUp; private int mCurrentDisplayId; // when set, package names are used for handwriting delegation. private @Nullable String mDelegatePackageName; Loading Loading @@ -155,6 +160,15 @@ final class HandwritingModeController { } boolean isStylusGestureOngoing() { if (mRecordingGestureAfterStylusUp && !mHandwritingBuffer.isEmpty()) { // If it is less than AFTER_STYLUS_UP_ALLOW_PERIOD_MS after the stylus up event, return // true so that handwriting can start. MotionEvent lastEvent = mHandwritingBuffer.get(mHandwritingBuffer.size() - 1); if (lastEvent.getActionMasked() == MotionEvent.ACTION_UP) { return SystemClock.uptimeMillis() - lastEvent.getEventTime() < AFTER_STYLUS_UP_ALLOW_PERIOD_MS; } } return mRecordingGesture; } Loading Loading @@ -277,7 +291,7 @@ final class HandwritingModeController { Slog.e(TAG, "Cannot start handwriting session: Invalid request id: " + requestId); return null; } if (!mRecordingGesture || mHandwritingBuffer.isEmpty()) { if (!isStylusGestureOngoing()) { Slog.e(TAG, "Cannot start handwriting session: No stylus gesture is being recorded."); return null; } Loading @@ -300,6 +314,7 @@ final class HandwritingModeController { mHandwritingEventReceiver.dispose(); mHandwritingEventReceiver = null; mRecordingGesture = false; mRecordingGestureAfterStylusUp = false; if (mHandwritingSurface.isIntercepting()) { throw new IllegalStateException( Loading Loading @@ -362,6 +377,7 @@ final class HandwritingModeController { clearPendingHandwritingDelegation(); } mRecordingGesture = false; mRecordingGestureAfterStylusUp = false; } private boolean onInputEvent(InputEvent ev) { Loading Loading @@ -412,15 +428,20 @@ final class HandwritingModeController { if ((TextUtils.isEmpty(mDelegatePackageName) || mDelegationConnectionlessFlow) && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) { mRecordingGesture = false; if (handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP) { mRecordingGestureAfterStylusUp = true; } else { mHandwritingBuffer.clear(); return; } } if (action == MotionEvent.ACTION_DOWN) { clearBufferIfRecordingAfterStylusUp(); mRecordingGesture = true; } if (!mRecordingGesture) { if (!mRecordingGesture && !mRecordingGestureAfterStylusUp) { return; } Loading @@ -430,12 +451,20 @@ final class HandwritingModeController { + " The rest of the gesture will not be recorded."); } mRecordingGesture = false; clearBufferIfRecordingAfterStylusUp(); return; } mHandwritingBuffer.add(MotionEvent.obtain(event)); } private void clearBufferIfRecordingAfterStylusUp() { if (mRecordingGestureAfterStylusUp) { mHandwritingBuffer.clear(); mRecordingGestureAfterStylusUp = false; } } static final class HandwritingSession { private final int mRequestId; private final InputChannel mHandwritingChannel; Loading