diff --git a/core/api/current.txt b/core/api/current.txt index 554a9e1393e39..03c292f389491 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -50382,12 +50382,6 @@ package android.view { field public static final int VERTICAL_GRAVITY_MASK = 112; // 0x70 } - public class HandwritingDelegateConfiguration { - ctor public HandwritingDelegateConfiguration(@IdRes int, @NonNull Runnable); - method public int getDelegatorViewId(); - method @NonNull public Runnable getInitiationCallback(); - } - public class HapticFeedbackConstants { field public static final int CLOCK_TICK = 4; // 0x4 field public static final int CONFIRM = 16; // 0x10 @@ -51985,6 +51979,8 @@ package android.view { method @Nullable public CharSequence getAccessibilityPaneTitle(); method @IdRes public int getAccessibilityTraversalAfter(); method @IdRes public int getAccessibilityTraversalBefore(); + method @NonNull public String getAllowedHandwritingDelegatePackageName(); + method @NonNull public String getAllowedHandwritingDelegatorPackageName(); method public float getAlpha(); method public android.view.animation.Animation getAnimation(); method @Nullable public android.graphics.Matrix getAnimationMatrix(); @@ -52040,7 +52036,7 @@ package android.view { method public float getHandwritingBoundsOffsetLeft(); method public float getHandwritingBoundsOffsetRight(); method public float getHandwritingBoundsOffsetTop(); - method @Nullable public android.view.HandwritingDelegateConfiguration getHandwritingDelegateConfiguration(); + method @Nullable public Runnable getHandwritingDelegatorCallback(); method public final boolean getHasOverlappingRendering(); method public final int getHeight(); method public void getHitRect(android.graphics.Rect); @@ -52196,6 +52192,7 @@ package android.view { method public boolean isFocused(); method public final boolean isFocusedByDefault(); method public boolean isForceDarkAllowed(); + method public boolean isHandwritingDelegate(); method public boolean isHapticFeedbackEnabled(); method public boolean isHardwareAccelerated(); method public boolean isHorizontalFadingEdgeEnabled(); @@ -52366,6 +52363,8 @@ package android.view { method public void setAccessibilityTraversalBefore(@IdRes int); method public void setActivated(boolean); method public void setAllowClickWhenDisabled(boolean); + method public void setAllowedHandwritingDelegatePackage(@NonNull String); + method public void setAllowedHandwritingDelegatorPackage(@NonNull String); method public void setAlpha(@FloatRange(from=0.0, to=1.0) float); method public void setAnimation(android.view.animation.Animation); method public void setAnimationMatrix(@Nullable android.graphics.Matrix); @@ -52408,7 +52407,7 @@ package android.view { method public void setForegroundTintList(@Nullable android.content.res.ColorStateList); method public void setForegroundTintMode(@Nullable android.graphics.PorterDuff.Mode); method public void setHandwritingBoundsOffsets(float, float, float, float); - method public void setHandwritingDelegateConfiguration(@Nullable android.view.HandwritingDelegateConfiguration); + method public void setHandwritingDelegatorCallback(@Nullable Runnable); method public void setHapticFeedbackEnabled(boolean); method public void setHasTransientState(boolean); method public void setHorizontalFadingEdgeEnabled(boolean); @@ -52421,6 +52420,7 @@ package android.view { method public void setImportantForAutofill(int); method public void setImportantForContentCapture(int); method public void setIsCredential(boolean); + method public void setIsHandwritingDelegate(boolean); method public void setKeepScreenOn(boolean); method public void setKeyboardNavigationCluster(boolean); method public void setLabelFor(@IdRes int); @@ -55619,6 +55619,8 @@ package android.view.inputmethod { } public final class InputMethodManager { + method public boolean acceptStylusHandwritingDelegation(@NonNull android.view.View); + method public boolean acceptStylusHandwritingDelegation(@NonNull android.view.View, @NonNull String); method public void dispatchKeyEventFromInputMethod(@Nullable android.view.View, @NonNull android.view.KeyEvent); method public void displayCompletions(android.view.View, android.view.inputmethod.CompletionInfo[]); method @Nullable public android.view.inputmethod.InputMethodInfo getCurrentInputMethodInfo(); @@ -55640,6 +55642,8 @@ package android.view.inputmethod { method public boolean isInputMethodSuppressingSpellChecker(); method public boolean isStylusHandwritingAvailable(); method @Deprecated public boolean isWatchingCursor(android.view.View); + method public void prepareStylusHandwritingDelegation(@NonNull android.view.View); + method public void prepareStylusHandwritingDelegation(@NonNull android.view.View, @NonNull String); method public void restartInput(android.view.View); method public void sendAppPrivateCommand(android.view.View, String, android.os.Bundle); method @Deprecated public void setAdditionalInputMethodSubtypes(@NonNull String, @NonNull android.view.inputmethod.InputMethodSubtype[]); diff --git a/core/java/android/view/HandwritingDelegateConfiguration.java b/core/java/android/view/HandwritingDelegateConfiguration.java deleted file mode 100644 index 719c614f42f0e..0000000000000 --- a/core/java/android/view/HandwritingDelegateConfiguration.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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; - -import android.annotation.IdRes; -import android.annotation.NonNull; - -/** - * Configuration for a view to act as a handwriting initiation delegate. This allows handwriting - * mode for a delegator editor view to be initiated by stylus movement on the delegate view. - * - *
If a stylus {@link MotionEvent} occurs within the delegate view's bounds, the callback - * returned by {@link #getInitiationCallback()} will be called. The callback implementation is - * expected to show and focus the delegator editor view. If a view with identifier matching {@link - * #getDelegatorViewId()} creates an input connection while the same stylus {@link MotionEvent} - * sequence is ongoing, handwriting mode will be initiated for that view. - * - *
A common use case is a custom view which looks like a text editor but does not actually - * support text editing itself, and clicking on the custom view causes an EditText to be shown. To - * support handwriting initiation in this case, {@link View#setHandwritingDelegateConfiguration} can - * be called on the custom view to configure it as a delegate, and set the EditText as the delegator - * by passing the EditText's identifier as the {@code delegatorViewId}. The {@code - * initiationCallback} implementation is typically the same as the click listener implementation - * which shows the EditText. - */ -public class HandwritingDelegateConfiguration { - @IdRes private final int mDelegatorViewId; - @NonNull private final Runnable mInitiationCallback; - - /** - * Constructs a HandwritingDelegateConfiguration instance. - * - * @param delegatorViewId identifier of the delegator editor view for which handwriting mode - * should be initiated - * @param initiationCallback callback called when a stylus {@link MotionEvent} occurs within - * this view's bounds. This will be called from the UI thread. - */ - public HandwritingDelegateConfiguration( - @IdRes int delegatorViewId, @NonNull Runnable initiationCallback) { - mDelegatorViewId = delegatorViewId; - mInitiationCallback = initiationCallback; - } - - /** - * Returns the identifier of the delegator editor view for which handwriting mode should be - * initiated. - */ - public int getDelegatorViewId() { - return mDelegatorViewId; - } - - /** - * Returns the callback which should be called when a stylus {@link MotionEvent} occurs within - * the delegate view's bounds. The callback should only be called from the UI thread. - */ - @NonNull - public Runnable getInitiationCallback() { - return mInitiationCallback; - } -} diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index 8d221ab0b70a9..98f61a3663810 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -16,7 +16,6 @@ package android.view; -import android.annotation.IdRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -172,15 +171,11 @@ public class HandwritingInitiator { if (candidateView != null) { if (candidateView == getConnectedView()) { startHandwriting(candidateView); - } else if (candidateView.getHandwritingDelegateConfiguration() != null) { - mState.mDelegatorViewId = - candidateView - .getHandwritingDelegateConfiguration() - .getDelegatorViewId(); - candidateView - .getHandwritingDelegateConfiguration() - .getInitiationCallback() - .run(); + } else if (candidateView.getHandwritingDelegatorCallback() != null) { + mImm.prepareStylusHandwritingDelegation( + candidateView, + candidateView.getAllowedHandwritingDelegatePackageName()); + candidateView.getHandwritingDelegatorCallback().run(); } else { if (candidateView.getRevealOnFocusHint()) { candidateView.setRevealOnFocusHint(false); @@ -227,6 +222,9 @@ public class HandwritingInitiator { } else { mConnectedView = new WeakReference<>(view); mConnectionCount = 1; + if (view.isHandwritingDelegate() && tryAcceptStylusHandwritingDelegation(view)) { + return; + } if (mState != null && mState.mShouldInitHandwriting) { tryStartHandwriting(); } @@ -279,17 +277,15 @@ public class HandwritingInitiator { } final Rect handwritingArea = getViewHandwritingArea(connectedView); - if ((mState.mDelegatorViewId != View.NO_ID - && mState.mDelegatorViewId == connectedView.getId()) - || isInHandwritingArea( - handwritingArea, mState.mStylusDownX, mState.mStylusDownY, connectedView)) { + if (isInHandwritingArea( + handwritingArea, mState.mStylusDownX, mState.mStylusDownY, connectedView)) { startHandwriting(connectedView); } else { mState.mShouldInitHandwriting = false; } } - /** For test only. */ + /** Starts a stylus handwriting session for the view. */ @VisibleForTesting public void startHandwriting(@NonNull View view) { mImm.startStylusHandwriting(view); @@ -297,6 +293,23 @@ public class HandwritingInitiator { mState.mShouldInitHandwriting = false; } + /** + * Starts a stylus handwriting session for the delegate view, if {@link + * InputMethodManager#prepareStylusHandwritingDelegation} was previously called. + */ + @VisibleForTesting + public boolean tryAcceptStylusHandwritingDelegation(@NonNull View view) { + if (mImm.acceptStylusHandwritingDelegation( + view, view.getAllowedHandwritingDelegatorPackageName())) { + if (mState != null) { + mState.mHasInitiatedHandwriting = true; + mState.mShouldInitHandwriting = false; + } + return true; + } + return false; + } + /** * Notify that the handwriting area for the given view might be updated. * @param view the view whose handwriting area might be updated. @@ -542,13 +555,6 @@ public class HandwritingInitiator { * built InputConnection. */ private boolean mExceedHandwritingSlop; - /** - * If the current ongoing stylus MotionEvent sequence started over a handwriting initiation - * delegate view, then this is the view identifier of the corresponding delegator view. If - * the delegator view creates an input connection while the MotionEvent sequence is still - * ongoing, then handwriting mode will be initiated for the delegator view. - */ - @IdRes private int mDelegatorViewId = View.NO_ID; /** The pointer id of the stylus pointer that is being tracked. */ private final int mStylusPointerId; diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 7d18bf082bcbe..fee20517a499d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -5101,12 +5101,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private boolean mHoveringTouchDelegate = false; - /** - * Configuration for this view to act as a handwriting initiation delegate. This allows - * handwriting mode for a delegator editor view to be initiated by stylus movement on this - * delegate view. - */ - private HandwritingDelegateConfiguration mHandwritingDelegateConfiguration; + // These two fields are set if the view is a handwriting delegator. + private Runnable mHandwritingDelegatorCallback; + private String mAllowedHandwritingDelegatePackageName; + + // These two fields are set if the view is a handwriting delegate. + private boolean mIsHandwritingDelegate; + private String mAllowedHandwritingDelegatorPackageName; /** * Solid color to use as a background when creating the drawing cache. Enables @@ -12410,27 +12411,168 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Configures this view to act as a handwriting initiation delegate. This allows handwriting - * mode for a delegator editor view to be initiated by stylus movement on this delegate view. + * Sets a callback which should be called when a stylus {@link MotionEvent} occurs within this + * view's bounds. The callback will be called from the UI thread. + * + *
Setting a callback allows this view to act as a handwriting delegator, so that handwriting + * mode for a delegate editor view can be initiated by stylus movement on this delegator view. + * The callback implementation is expected to show and focus the delegate editor view. If a view + * which returns {@code true} for {@link #isHandwritingDelegate()} creates an input connection + * while the same stylus {@link MotionEvent} sequence is ongoing, handwriting mode will be + * initiated for that view. + * + *
A common use case is a custom view which looks like a text editor but does not actually + * support text editing itself, and clicking on the custom view causes an EditText to be shown. + * To support handwriting initiation in this case, this method can be called on the custom view + * to configure it as a delegator. The EditText should call {@link #setIsHandwritingDelegate} to + * set it as a delegate. The {@code callback} implementation is typically the same as the click + * listener implementation which shows the EditText. * *
If {@code null} is passed, this view will no longer act as a handwriting initiation - * delegate. + * delegator. + * + * @param callback a callback which should be called when a stylus {@link MotionEvent} occurs + * within this view's bounds */ - public void setHandwritingDelegateConfiguration( - @Nullable HandwritingDelegateConfiguration configuration) { - mHandwritingDelegateConfiguration = configuration; - if (configuration != null) { + public void setHandwritingDelegatorCallback(@Nullable Runnable callback) { + mHandwritingDelegatorCallback = callback; + if (callback != null) { + // By default, the delegate must be from the same package as the delegator view. + mAllowedHandwritingDelegatePackageName = mContext.getOpPackageName(); setHandwritingArea(new Rect(0, 0, getWidth(), getHeight())); + } else { + mAllowedHandwritingDelegatePackageName = null; } } /** - * If this view has been configured as a handwriting initiation delegate, returns the delegate - * configuration. + * Returns the callback set by {@link #setHandwritingDelegatorCallback} which should be called + * when a stylus {@link MotionEvent} occurs within this view's bounds. The callback should only + * be called from the UI thread. */ @Nullable - public HandwritingDelegateConfiguration getHandwritingDelegateConfiguration() { - return mHandwritingDelegateConfiguration; + public Runnable getHandwritingDelegatorCallback() { + return mHandwritingDelegatorCallback; + } + + /** + * Specifies that this view may act as a handwriting initiation delegator for a delegate editor + * view from the specified package. If this method is not called, delegators may only be used to + * initiate handwriting mode for a delegate editor view from the same package as the delegator + * view. This method allows specifying a different trusted package which may contain a delegate + * editor view linked to this delegator view. This should be called after {@link + * #setHandwritingDelegatorCallback}. + * + *
If this method is called on the delegator view, then {@link + * #setAllowedHandwritingDelegatorPackage} should also be called on the delegate editor view. + * + *
For example, to configure a delegator view in package 1: + * + *
+ * delegatorView.setHandwritingDelegatorCallback(callback); + * delegatorView.setAllowedHandwritingDelegatePackage(package2);+ * + * Then to configure the corresponding delegate editor view in package 2: + * + *
+ * delegateEditorView.setIsHandwritingDelegate(true); + * delegateEditorView.setAllowedHandwritingDelegatorPackage(package1);+ * + * @param allowedPackageName the package name of a delegate editor view linked to this delegator + * view + * @throws IllegalStateException If the view has not been configured as a handwriting delegator + * using {@link #setHandwritingDelegatorCallback}. + */ + public void setAllowedHandwritingDelegatePackage(@NonNull String allowedPackageName) { + if (mHandwritingDelegatorCallback == null) { + throw new IllegalStateException("This view is not a handwriting delegator."); + } + mAllowedHandwritingDelegatePackageName = allowedPackageName; + } + + /** + * Returns the allowed package for delegate editor views for which this view may act as a + * handwriting delegator. If {@link #setAllowedHandwritingDelegatePackage} has not been called, + * this will return this view's package name, since by default delegators may only be used to + * initiate handwriting mode for a delegate editor view from the same package as the delegator + * view. This will return a different allowed package if set by {@link + * #setAllowedHandwritingDelegatePackage}. + * + * @throws IllegalStateException If the view has not been configured as a handwriting delegator + * using {@link #setHandwritingDelegatorCallback}. + */ + @NonNull + public String getAllowedHandwritingDelegatePackageName() { + if (mHandwritingDelegatorCallback == null) { + throw new IllegalStateException("This view is not a handwriting delegator."); + } + return mAllowedHandwritingDelegatePackageName; + } + + /** + * Sets this view to be a handwriting delegate. If a delegate view creates an input connection + * while a stylus {@link MotionEvent} sequence from a delegator view is ongoing, handwriting + * mode will be initiated for the delegate view. + * + * @param isHandwritingDelegate whether this view is a handwriting initiation delegate + * @see #setHandwritingDelegatorCallback(Runnable) + */ + public void setIsHandwritingDelegate(boolean isHandwritingDelegate) { + mIsHandwritingDelegate = isHandwritingDelegate; + if (mIsHandwritingDelegate) { + // By default, the delegator must be from the same package as the delegate view. + mAllowedHandwritingDelegatorPackageName = mContext.getOpPackageName(); + } else { + mAllowedHandwritingDelegatePackageName = null; + } + } + + /** + * Returns whether this view has been set as a handwriting delegate by {@link + * #setIsHandwritingDelegate}. + */ + public boolean isHandwritingDelegate() { + return mIsHandwritingDelegate; + } + + /** + * Specifies that a view from the specified package may act as a handwriting delegator for this + * delegate editor view. If this method is not called, only views from the same package as the + * delegate editor view may act as a handwriting delegator. This method allows specifying a + * different trusted package which may contain a delegator view linked to this delegate editor + * view. This should be called after {@link #setIsHandwritingDelegate}. + * + *
If this method is called on the delegate editor view, then {@link + * #setAllowedHandwritingDelegatePackage} should also be called on the delegator view. + * + * @param allowedPackageName the package name of a delegator view linked to this delegate editor + * view + * @throws IllegalStateException If the view has not been configured as a handwriting delegate + * using {@link #setIsHandwritingDelegate}. + */ + public void setAllowedHandwritingDelegatorPackage(@NonNull String allowedPackageName) { + if (!mIsHandwritingDelegate) { + throw new IllegalStateException("This view is not a handwriting delegate."); + } + mAllowedHandwritingDelegatorPackageName = allowedPackageName; + } + + /** + * Returns the allowed package for views which may act as a handwriting delegator for this + * delegate editor view. If {@link #setAllowedHandwritingDelegatorPackage} has not been called, + * this will return this view's package name, since by default only views from the same package + * as the delegator editor view may act as a handwriting delegator. This will return a different + * allowed package if set by {@link #setAllowedHandwritingDelegatorPackage}. + * + * @throws IllegalStateException If the view has not been configured as a handwriting delegate + * using {@link #setIsHandwritingDelegate}. + */ + @NonNull + public String getAllowedHandwritingDelegatorPackageName() { + if (!mIsHandwritingDelegate) { + throw new IllegalStateException("This view is not a handwriting delegate."); + } + return mAllowedHandwritingDelegatorPackageName; } /** @@ -24474,7 +24616,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } rebuildOutline(); - if (onCheckIsTextEditor() || mHandwritingDelegateConfiguration != null) { + if (onCheckIsTextEditor() || mHandwritingDelegatorCallback != null) { setHandwritingArea(new Rect(0, 0, newWidth, newHeight)); } } diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java index 966026198a211..db17a533c303e 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -504,6 +504,40 @@ final class IInputMethodManagerGlobalInvoker { } } + @AnyThread + static void prepareStylusHandwritingDelegation( + @NonNull IInputMethodClient client, + @NonNull String delegatePackageName, + @NonNull String delegatorPackageName) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.prepareStylusHandwritingDelegation( + client, delegatePackageName, delegatorPackageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static boolean acceptStylusHandwritingDelegation( + @NonNull IInputMethodClient client, + @NonNull String delegatePackageName, + @NonNull String delegatorPackageName) { + final IInputMethodManager service = getService(); + if (service == null) { + return false; + } + try { + return service.acceptStylusHandwritingDelegation( + client, delegatePackageName, delegatorPackageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + @AnyThread @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) static boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) { diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 642182b5ddfde..36d2b8a897793 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -99,6 +99,7 @@ import android.view.WindowManager; import android.view.WindowManager.LayoutParams.SoftInputModeFlags; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; +import android.widget.Editor; import android.window.ImeOnBackInvokedDispatcher; import android.window.WindowOnBackInvokedDispatcher; @@ -1552,11 +1553,7 @@ public final class InputMethodManager { if (fallbackContext == null) { return false; } - if (Settings.Global.getInt(fallbackContext.getContentResolver(), - Settings.Global.STYLUS_HANDWRITING_ENABLED, 0) == 0) { - if (DEBUG) { - Log.d(TAG, "Stylus handwriting is not enabled in settings."); - } + if (!isStylusHandwritingEnabled(fallbackContext)) { return false; } return IInputMethodManagerGlobalInvoker.isStylusHandwritingAvailableAsUser(userId); @@ -2233,37 +2230,175 @@ public final class InputMethodManager { * @see #isStylusHandwritingAvailable() */ public void startStylusHandwriting(@NonNull View view) { + startStylusHandwritingInternal(view, null /* delegatorPackageName */); + } + + private boolean startStylusHandwritingInternal( + @NonNull View view, @Nullable String delegatorPackageName) { + Objects.requireNonNull(view); + // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { - fallbackImm.startStylusHandwriting(view); + fallbackImm.startStylusHandwritingInternal(view, delegatorPackageName); } - Objects.requireNonNull(view); - if (Settings.Global.getInt(view.getContext().getContentResolver(), - Settings.Global.STYLUS_HANDWRITING_ENABLED, 0) == 0) { - Log.d(TAG, "Ignoring startStylusHandwriting(view) as stylus handwriting is disabled"); - return; + boolean useDelegation = !TextUtils.isEmpty(delegatorPackageName); + if (!isStylusHandwritingEnabled(view.getContext())) { + Log.w(TAG, "Stylus handwriting pref is disabled. " + + "Ignoring calls to start stylus handwriting."); + return false; } checkFocus(); synchronized (mH) { if (!hasServedByInputMethodLocked(view)) { Log.w(TAG, - "Ignoring startStylusHandwriting() as view=" + view + " is not served."); - return; + "Ignoring startStylusHandwriting as view=" + view + " is not served."); + return false; } if (view.getViewRootImpl() != mCurRootView) { - Log.w(TAG, "Ignoring startStylusHandwriting: View's window does not have focus."); - return; + Log.w(TAG, + "Ignoring startStylusHandwriting: View's window does not have focus."); + return false; } - - IInputMethodManagerGlobalInvoker.startStylusHandwriting(mClient); - // TODO(b/210039666): do we need any extra work for supporting non-native - // UI toolkits? + if (useDelegation) { + return IInputMethodManagerGlobalInvoker.acceptStylusHandwritingDelegation( + mClient, view.getContext().getOpPackageName(), delegatorPackageName); + } else { + IInputMethodManagerGlobalInvoker.startStylusHandwriting(mClient); + } + return false; } } + private boolean isStylusHandwritingEnabled(@NonNull Context context) { + if (Settings.Global.getInt(context.getContentResolver(), + Settings.Global.STYLUS_HANDWRITING_ENABLED, 0) == 0) { + Log.d(TAG, "Stylus handwriting pref is disabled."); + return false; + } + return true; + } + + /** + * Prepares delegation of starting stylus handwriting session to a different editor in same + * or different window than the view on which initial handwriting stroke was detected. + * + * Delegation can be used to start stylus handwriting session before the {@link Editor} view or + * its {@link InputConnection} is started. Calling this method starts buffering of stylus + * motion events until {@link #acceptStylusHandwritingDelegation(View)} is called, at which + * point the handwriting session can be started and the buffered stylus motion events will be + * delivered to the IME. + * e.g. Delegation can be used when initial handwriting stroke is + * on a pseudo {@link Editor} like widget (with no {@link InputConnection}) but actual + * {@link Editor} is on a different window. + * + *
Note: If an actual {@link Editor} capable of {@link InputConnection} is being scribbled + * upon using stylus, use {@link #startStylusHandwriting(View)} instead.
+ * + * @param delegatorView the view that receives initial stylus stroke and delegates it to the + * actual editor. Its window must {@link View#hasWindowFocus have focus}. + * @see #prepareStylusHandwritingDelegation(View, String) + * @see #acceptStylusHandwritingDelegation(View) + * @see #startStylusHandwriting(View) + */ + public void prepareStylusHandwritingDelegation(@NonNull View delegatorView) { + prepareStylusHandwritingDelegation( + delegatorView, delegatorView.getContext().getOpPackageName()); + } + + /** + * Prepares delegation of starting stylus handwriting session to a different editor in same or a + * different window in a different package than the view on which initial handwriting stroke + * was detected. + * + * Delegation can be used to start stylus handwriting session before the {@link Editor} view or + * its {@link InputConnection} is started. Calling this method starts buffering of stylus + * motion events until {@link #acceptStylusHandwritingDelegation(View, String)} is called, at + * which point the handwriting session can be started and the buffered stylus motion events will + * be delivered to the IME. + * e.g. Delegation can be used when initial handwriting stroke is + * on a pseudo {@link Editor} like widget (with no {@link InputConnection}) but actual + * {@link Editor} is on a different window in the given package. + * + *Note: If delegator and delegate are in same package use + * {@link #prepareStylusHandwritingDelegation(View)} instead.
+ * + * @param delegatorView the view that receives initial stylus stroke and delegates it to the + * actual editor. Its window must {@link View#hasWindowFocus have focus}. + * @param delegatePackageName package name that contains actual {@link Editor} which should + * start stylus handwriting session by calling {@link #acceptStylusHandwritingDelegation}. + * @see #prepareStylusHandwritingDelegation(View) + * @see #acceptStylusHandwritingDelegation(View, String) + */ + public void prepareStylusHandwritingDelegation( + @NonNull View delegatorView, @NonNull String delegatePackageName) { + Objects.requireNonNull(delegatorView); + Objects.requireNonNull(delegatePackageName); + + // Re-dispatch if there is a context mismatch. + final InputMethodManager fallbackImm = + getFallbackInputMethodManagerIfNecessary(delegatorView); + if (fallbackImm != null) { + fallbackImm.prepareStylusHandwritingDelegation(delegatorView, delegatePackageName); + } + + if (!isStylusHandwritingEnabled(delegatorView.getContext())) { + Log.w(TAG, "Stylus handwriting pref is disabled. " + + "Ignoring prepareStylusHandwritingDelegation()."); + return; + } + IInputMethodManagerGlobalInvoker.prepareStylusHandwritingDelegation( + mClient, + delegatePackageName, + delegatorView.getContext().getOpPackageName()); + } + + /** + * Accepts and starts a stylus handwriting session on the delegate view, if handwriting + * initiation delegation was previously requested using + * {@link #prepareStylusHandwritingDelegation(View)} from the delegator. + * + *Note: If delegator and delegate are in different application packages, use + * {@link #acceptStylusHandwritingDelegation(View, String)} instead.
+ * + * @param delegateView delegate view capable of receiving input via {@link InputConnection} + * on which {@link #startStylusHandwriting(View)} will be called. + * @return {@code true} if view belongs to same application package as used in + * {@link #prepareStylusHandwritingDelegation(View)} and handwriting session can start. + * @see #acceptStylusHandwritingDelegation(View, String) + * @see #prepareStylusHandwritingDelegation(View) + */ + public boolean acceptStylusHandwritingDelegation(@NonNull View delegateView) { + return startStylusHandwritingInternal( + delegateView, delegateView.getContext().getOpPackageName()); + } + + /** + * Accepts and starts a stylus handwriting session on the delegate view, if handwriting + * initiation delegation was previously requested using + * {@link #prepareStylusHandwritingDelegation(View, String)} from te delegator and the view + * belongs to a specified delegate package. + * + *Note: If delegator and delegate are in same application package use + * {@link #acceptStylusHandwritingDelegation(View)} instead.
+ * + * @param delegateView delegate view capable of receiving input via {@link InputConnection} + * on which {@link #startStylusHandwriting(View)} will be called. + * @param delegatorPackageName package name of the delegator that handled initial stylus stroke. + * @return {@code true} if view belongs to allowed delegate package declared in + * {@link #prepareStylusHandwritingDelegation(View, String)} and handwriting session can start. + * @see #prepareStylusHandwritingDelegation(View, String) + * @see #acceptStylusHandwritingDelegation(View) + */ + public boolean acceptStylusHandwritingDelegation( + @NonNull View delegateView, @NonNull String delegatorPackageName) { + Objects.requireNonNull(delegatorPackageName); + + return startStylusHandwritingInternal(delegateView, delegatorPackageName); + } + /** * This method toggles the input method window display. * If the input window is already displayed, it gets hidden. diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 9116cb3ec9fa2..5805d0e050d16 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -148,6 +148,15 @@ interface IInputMethodManager { /** Start Stylus handwriting session **/ void startStylusHandwriting(in IInputMethodClient client); + /** Prepares delegation of starting stylus handwriting session to a different editor **/ + void prepareStylusHandwritingDelegation(in IInputMethodClient client, + in String delegatePackageName, + in String delegatorPackageName); + + /** Accepts and starts a stylus handwriting session for the delegate view **/ + boolean acceptStylusHandwritingDelegation(in IInputMethodClient client, + in String delegatePackageName, in String delegatorPackageName); + /** Returns {@code true} if currently selected IME supports Stylus handwriting. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)") diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index 95aa5d0de1194..76f5277cfe0b3 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -32,7 +32,6 @@ import android.app.Instrumentation; import android.content.Context; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; -import android.view.HandwritingDelegateConfiguration; import android.view.HandwritingInitiator; import android.view.InputDevice; import android.view.MotionEvent; @@ -210,14 +209,11 @@ public class HandwritingInitiatorTest { @Test public void onTouchEvent_startHandwriting_delegate() { - int delegatorViewId = 234; - View delegatorView = new View(mContext); - delegatorView.setId(delegatorViewId); + View delegateView = new View(mContext); + delegateView.setIsHandwritingDelegate(true); - mTestView.setHandwritingDelegateConfiguration( - new HandwritingDelegateConfiguration( - delegatorViewId, - () -> mHandwritingInitiator.onInputConnectionCreated(delegatorView))); + mTestView.setHandwritingDelegatorCallback( + () -> mHandwritingInitiator.onInputConnectionCreated(delegateView)); final int x1 = (sHwArea.left + sHwArea.right) / 2; final int y1 = (sHwArea.top + sHwArea.bottom) / 2; @@ -229,7 +225,7 @@ public class HandwritingInitiatorTest { MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0); mHandwritingInitiator.onTouchEvent(stylusEvent2); - verify(mHandwritingInitiator, times(1)).startHandwriting(delegatorView); + verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(delegateView); } @Test diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java index 1c7294f8a9d5c..eb4dba63dea9b 100644 --- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java +++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java @@ -20,12 +20,14 @@ import static android.view.InputDevice.SOURCE_STYLUS; import android.Manifest; import android.annotation.AnyThread; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.UiThread; import android.hardware.input.InputManager; import android.os.IBinder; import android.os.Looper; +import android.text.TextUtils; import android.util.Slog; import android.view.BatchedInputEventReceiver; import android.view.Choreographer; @@ -35,6 +37,8 @@ import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.MotionEvent; import android.view.SurfaceControl; +import android.view.View; +import android.view.inputmethod.InputMethodManager; import com.android.server.LocalServices; import com.android.server.input.InputManagerInternal; @@ -52,6 +56,9 @@ final class HandwritingModeController { // TODO(b/210039666): flip the flag. static final boolean DEBUG = true; private static final int EVENT_BUFFER_SIZE = 100; + // A longer event buffer used for handwriting delegation + // TODO(b/210039666): make this device touch sampling rate dependent. + private static final int LONG_EVENT_BUFFER = EVENT_BUFFER_SIZE * 20; // This must be the looper for the UiThread. private final Looper mLooper; @@ -63,6 +70,9 @@ final class HandwritingModeController { private Runnable mInkWindowInitRunnable; private boolean mRecordingGesture; private int mCurrentDisplayId; + // when set, package names are used for handwriting delegation. + private @Nullable String mDelegatePackageName; + private @Nullable String mDelegatorPackageName; private HandwritingEventReceiverSurface mHandwritingSurface; @@ -137,6 +147,41 @@ final class HandwritingModeController { return mRecordingGesture; } + boolean hasOngoingStylusHandwritingSession() { + return mHandwritingSurface != null && mHandwritingSurface.isIntercepting(); + } + + /** + * Prepare delegation of stylus handwriting to a different editor + * @see InputMethodManager#prepareStylusHandwritingDelegation(View, String) + */ + void prepareStylusHandwritingDelegation( + @NonNull String delegatePackageName, @NonNull String delegatorPackageName) { + mDelegatePackageName = delegatePackageName; + mDelegatorPackageName = delegatorPackageName; + ((ArrayList) mHandwritingBuffer).ensureCapacity(LONG_EVENT_BUFFER); + // TODO(b/210039666): cancel delegation after a timeout or next input method client binding. + } + + @Nullable String getDelegatePackageName() { + return mDelegatePackageName; + } + + @Nullable String getDelegatorPackageName() { + return mDelegatorPackageName; + } + + /** + * Clear any pending handwriting delegation info. + */ + void clearPendingHandwritingDelegation() { + if (DEBUG) { + Slog.d(TAG, "clearPendingHandwritingDelegation"); + } + mDelegatorPackageName = null; + mDelegatePackageName = null; + } + /** * Starts a {@link HandwritingSession} to transfer to the IME. * @@ -223,6 +268,7 @@ final class HandwritingModeController { } } + clearPendingHandwritingDelegation(); mRecordingGesture = false; } @@ -259,7 +305,10 @@ final class HandwritingModeController { mInkWindowInitRunnable = null; } - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + // If handwriting delegation is ongoing, don't clear the buffer so that multiple strokes + // can be buffered across windows. + if (TextUtils.isEmpty(mDelegatePackageName) + && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) { mRecordingGesture = false; mHandwritingBuffer.clear(); return; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index f5875abe17e65..8ef4e4afae9bc 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -236,6 +236,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private static final int MSG_FINISH_HANDWRITING = 1110; private static final int MSG_REMOVE_HANDWRITING_WINDOW = 1120; + private static final int MSG_PREPARE_HANDWRITING_DELEGATION = 1130; + private static final int MSG_SET_INTERACTIVE = 3030; private static final int MSG_HARD_KEYBOARD_SWITCH_CHANGED = 4000; @@ -2688,6 +2690,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mHandler.obtainMessage(MSG_RESET_HANDWRITING).sendToTarget(); } + @AnyThread + void schedulePrepareStylusHandwritingDelegation( + @NonNull String delegatePackageName, @NonNull String delegatorPackageName) { + mHandler.obtainMessage( + MSG_PREPARE_HANDWRITING_DELEGATION, + new Pair<>(delegatePackageName, delegatorPackageName)).sendToTarget(); + } + @AnyThread void scheduleRemoveStylusHandwritingWindow() { mHandler.obtainMessage(MSG_REMOVE_HANDWRITING_WINDOW).sendToTarget(); @@ -3304,6 +3314,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub "InputMethodManagerService#startStylusHandwriting"); int uid = Binder.getCallingUid(); synchronized (ImfLock.class) { + mHwController.clearPendingHandwritingDelegation(); if (!canInteractWithImeLocked(uid, client, "startStylusHandwriting", null /* statsToken */)) { return; @@ -3331,6 +3342,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub "There is no ongoing stylus gesture to start stylus handwriting."); return; } + if (mHwController.hasOngoingStylusHandwritingSession()) { + // prevent duplicate calls to startStylusHandwriting(). + Slog.e(TAG, + "Stylus handwriting session is already ongoing." + + " Ignoring startStylusHandwriting()."); + return; + } if (DEBUG) Slog.v(TAG, "Client requesting Stylus Handwriting to be started"); final IInputMethodInvoker curMethod = getCurMethodLocked(); if (curMethod != null) { @@ -3345,6 +3363,68 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } + @Override + public void prepareStylusHandwritingDelegation( + @NonNull IInputMethodClient client, + @NonNull String delegatePackageName, + @NonNull String delegatorPackageName) { + if (!verifyClientAndPackageMatch(client, delegatorPackageName)) { + Slog.w(TAG, "prepareStylusHandwritingDelegation() fail"); + throw new IllegalArgumentException("Delegator doesn't match Uid"); + } + schedulePrepareStylusHandwritingDelegation(delegatePackageName, delegatorPackageName); + } + + @Override + public boolean acceptStylusHandwritingDelegation( + @NonNull IInputMethodClient client, + @NonNull String delegatePackageName, + @NonNull String delegatorPackageName) { + if (!verifyDelegator(client, delegatePackageName, delegatorPackageName)) { + return false; + } + + startStylusHandwriting(client); + return true; + } + + private boolean verifyClientAndPackageMatch( + @NonNull IInputMethodClient client, @NonNull String packageName) { + ClientState cs; + synchronized (ImfLock.class) { + cs = mClients.get(client.asBinder()); + } + if (cs == null) { + throw new IllegalArgumentException("unknown client " + client.asBinder()); + } + return InputMethodUtils.checkIfPackageBelongsToUid( + mPackageManagerInternal, cs.mUid, packageName); + } + + private boolean verifyDelegator( + @NonNull IInputMethodClient client, + @NonNull String delegatePackageName, + @NonNull String delegatorPackageName) { + if (!verifyClientAndPackageMatch(client, delegatePackageName)) { + Slog.w(TAG, "Delegate package does not belong to the same user. Ignoring" + + " startStylusHandwriting"); + return false; + } + synchronized (ImfLock.class) { + if (!delegatorPackageName.equals(mHwController.getDelegatorPackageName())) { + Slog.w(TAG, + "Delegator package does not match. Ignoring startStylusHandwriting"); + return false; + } + if (!delegatePackageName.equals(mHwController.getDelegatePackageName())) { + Slog.w(TAG, + "Delegate package does not match. Ignoring startStylusHandwriting"); + return false; + } + } + return true; + } + @BinderThread @Override public void reportPerceptibleAsync(IBinder windowToken, boolean perceptible) { @@ -4852,6 +4932,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } return true; } + case MSG_PREPARE_HANDWRITING_DELEGATION: + synchronized (ImfLock.class) { + String delegate = (String) ((Pair) msg.obj).first; + String delegator = (String) ((Pair) msg.obj).second; + mHwController.prepareStylusHandwritingDelegation(delegate, delegator); + } + return true; case MSG_START_HANDWRITING: synchronized (ImfLock.class) { IInputMethodInvoker curMethod = getCurMethodLocked();