From a6666f2221af6972f3177296157d6b5a8b2fb4ad Mon Sep 17 00:00:00 2001 From: Shan Huang Date: Fri, 22 Apr 2022 20:54:19 +0000 Subject: [PATCH] Migrate IME to handle back with OnBackInvokedDispatcher. We currently close the IME by having the target application forward KEYCODE_BACK to the IME process through InputMethodManager#dispatchInputEvent and having the IME handle the keycode in InputMethodService#onKeyDown. When apps opt in to OnBackInvokedDispatcher API, we will not dispatch KEYCODE_BACK to apps anymore. Thus we need to migrate IME to the new API for it to close on back invocation. This implementation forwards OnBackInvokedCallbacks from the IME process to the app process. This is necessary because all callbacks need to exist in the app process for them to be considered by hardware back keys. While back gestures go through WM to resolve callbacks from the focused window, hw keys are directly sent to the focused window's ViewRootImpl, bypassing server side back nav logic. Bug: 228358882 Test: atest CtsInputMethodTestCases:KeyboardVisibilityControlTest Test: atest CtsInputMethodTestCases:InputMethodServiceTest Test: atest CtsInputMethodTestCases Change-Id: Ie207b63b11a56c9b2173f26b734a27b13ebccc60 --- core/api/test-current.txt | 1 + .../android/content/pm/ApplicationInfo.java | 17 ++ .../IInputMethodWrapper.java | 15 +- .../InputMethodService.java | 70 ++++++- core/java/android/view/ViewRootImpl.java | 40 ++-- .../android/view/inputmethod/InputMethod.java | 8 +- .../view/inputmethod/InputMethodManager.java | 26 ++- .../window/ImeOnBackInvokedDispatcher.aidl | 20 ++ .../window/ImeOnBackInvokedDispatcher.java | 187 ++++++++++++++++++ .../window/OnBackInvokedDispatcher.java | 15 ++ .../window/ProxyOnBackInvokedDispatcher.java | 16 ++ .../window/WindowOnBackInvokedDispatcher.java | 37 +++- .../android/internal/view/IInputMethod.aidl | 4 +- .../internal/view/IInputMethodManager.aidl | 3 +- .../inputmethod/IInputMethodInvoker.java | 6 +- .../InputMethodManagerService.java | 47 +++-- .../server/wm/BackNavigationController.java | 1 - 17 files changed, 461 insertions(+), 52 deletions(-) create mode 100644 core/java/android/window/ImeOnBackInvokedDispatcher.aidl create mode 100644 core/java/android/window/ImeOnBackInvokedDispatcher.java diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 4681d49432566..3aa99b682a91b 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -800,6 +800,7 @@ package android.content.pm { method public boolean hasRequestForegroundServiceExemption(); method public boolean isPrivilegedApp(); method public boolean isSystemApp(); + method public void setEnableOnBackInvokedCallback(boolean); field public static final int PRIVATE_FLAG_PRIVILEGED = 8; // 0x8 field public int privateFlags; } diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 2961b55057946..24c383692c09c 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -2752,4 +2752,21 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { mKnownActivityEmbeddingCerts.add(knownCert.toUpperCase(Locale.US)); } } + + /** + * Sets whether the application will use the {@link android.window.OnBackInvokedCallback} + * navigation system instead of the {@link android.view.KeyEvent#KEYCODE_BACK} and related + * callbacks. Intended to be used from tests only. + * + * @see #isOnBackInvokedCallbackEnabled() + * @hide + */ + @TestApi + public void setEnableOnBackInvokedCallback(boolean isEnable) { + if (isEnable) { + privateFlagsExt |= PRIVATE_FLAG_EXT_ENABLE_ON_BACK_INVOKED_CALLBACK; + } else { + privateFlagsExt &= ~PRIVATE_FLAG_EXT_ENABLE_ON_BACK_INVOKED_CALLBACK; + } + } } diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java index f9ed0e3db4994..3f49b73a78fcb 100644 --- a/core/java/android/inputmethodservice/IInputMethodWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -37,6 +37,7 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodSession; import android.view.inputmethod.InputMethodSubtype; +import android.window.ImeOnBackInvokedDispatcher; import com.android.internal.inputmethod.CancellationGroup; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; @@ -194,7 +195,9 @@ class IInputMethodWrapper extends IInputMethod.Stub case DO_START_INPUT: { final SomeArgs args = (SomeArgs) msg.obj; final IBinder startInputToken = (IBinder) args.arg1; - final IInputContext inputContext = (IInputContext) args.arg2; + final IInputContext inputContext = (IInputContext) ((SomeArgs) args.arg2).arg1; + final ImeOnBackInvokedDispatcher imeDispatcher = + (ImeOnBackInvokedDispatcher) ((SomeArgs) args.arg2).arg2; final EditorInfo info = (EditorInfo) args.arg3; final CancellationGroup cancellationGroup = (CancellationGroup) args.arg4; final boolean restarting = args.argi5 == 1; @@ -205,7 +208,7 @@ class IInputMethodWrapper extends IInputMethod.Stub : null; info.makeCompatible(mTargetSdkVersion); inputMethod.dispatchStartInputWithToken(ic, info, restarting, startInputToken, - navButtonFlags); + navButtonFlags, imeDispatcher); args.recycle(); return; } @@ -348,13 +351,17 @@ class IInputMethodWrapper extends IInputMethod.Stub @Override public void startInput(IBinder startInputToken, IInputContext inputContext, EditorInfo attribute, boolean restarting, - @InputMethodNavButtonFlags int navButtonFlags) { + @InputMethodNavButtonFlags int navButtonFlags, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { if (mCancellationGroup == null) { Log.e(TAG, "startInput must be called after bindInput."); mCancellationGroup = new CancellationGroup(); } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = inputContext; + args.arg2 = imeDispatcher; mCaller.executeOrSendMessage(mCaller.obtainMessageOOOOII(DO_START_INPUT, startInputToken, - inputContext, attribute, mCancellationGroup, restarting ? 1 : 0, navButtonFlags)); + args, attribute, mCancellationGroup, restarting ? 1 : 0, navButtonFlags)); } @BinderThread diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index efd4f06818380..25296bc0a8b91 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -101,6 +101,7 @@ import android.view.BatchedInputEventReceiver.SimpleBatchedInputEventReceiver; import android.view.Choreographer; import android.view.Gravity; import android.view.InputChannel; +import android.view.InputDevice; import android.view.InputEventReceiver; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -134,6 +135,9 @@ import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; +import android.window.ImeOnBackInvokedDispatcher; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; import android.window.WindowMetricsHelper; import com.android.internal.annotations.GuardedBy; @@ -344,6 +348,9 @@ public class InputMethodService extends AbstractInputMethodService { * A circular buffer of size MAX_EVENTS_BUFFER in case IME is taking too long to add ink view. **/ private RingBuffer mPendingEvents; + private ImeOnBackInvokedDispatcher mImeDispatcher; + private Boolean mBackCallbackRegistered = false; + private final OnBackInvokedCallback mCompatBackCallback = this::compatHandleBack; /** * Returns whether {@link InputMethodService} is responsible for rendering the back button and @@ -797,7 +804,13 @@ public class InputMethodService extends AbstractInputMethodService { @Override public final void dispatchStartInputWithToken(@Nullable InputConnection inputConnection, @NonNull EditorInfo editorInfo, boolean restarting, - @NonNull IBinder startInputToken, @InputMethodNavButtonFlags int navButtonFlags) { + @NonNull IBinder startInputToken, @InputMethodNavButtonFlags int navButtonFlags, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + mImeDispatcher = imeDispatcher; + if (mWindow != null) { + mWindow.getOnBackInvokedDispatcher().setImeOnBackInvokedDispatcher( + imeDispatcher); + } mPrivOps.reportStartInputAsync(startInputToken); mNavigationBarController.onNavButtonFlagsChanged(navButtonFlags); if (restarting) { @@ -1496,6 +1509,10 @@ public class InputMethodService extends AbstractInputMethodService { Context.LAYOUT_INFLATER_SERVICE); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMS.initSoftInputWindow"); mWindow = new SoftInputWindow(this, mTheme, mDispatcherState); + if (mImeDispatcher != null) { + mWindow.getOnBackInvokedDispatcher() + .setImeOnBackInvokedDispatcher(mImeDispatcher); + } mNavigationBarController.onSoftInputWindowCreated(mWindow); { final Window window = mWindow.getWindow(); @@ -1608,6 +1625,8 @@ public class InputMethodService extends AbstractInputMethodService { // when IME developers are doing something unsupported. InputMethodPrivilegedOperationsRegistry.remove(mToken); } + unregisterCompatOnBackInvokedCallback(); + mImeDispatcher = null; } /** @@ -2568,9 +2587,47 @@ public class InputMethodService extends AbstractInputMethodService { cancelImeSurfaceRemoval(); mInShowWindow = false; Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + registerCompatOnBackInvokedCallback(); } + /** + * Registers an {@link OnBackInvokedCallback} to handle back invocation when ahead-of-time + * back dispatching is enabled. We keep the {@link KeyEvent#KEYCODE_BACK} based legacy code + * around to handle back on older devices. + */ + private void registerCompatOnBackInvokedCallback() { + if (mBackCallbackRegistered) { + return; + } + if (mWindow != null) { + mWindow.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, mCompatBackCallback); + mBackCallbackRegistered = true; + } + } + + private void unregisterCompatOnBackInvokedCallback() { + if (!mBackCallbackRegistered) { + return; + } + if (mWindow != null) { + mWindow.getOnBackInvokedDispatcher() + .unregisterOnBackInvokedCallback(mCompatBackCallback); + mBackCallbackRegistered = false; + } + } + + private KeyEvent createBackKeyEvent(int action, boolean isTracking) { + final long when = SystemClock.uptimeMillis(); + return new KeyEvent(when, when, action, + KeyEvent.KEYCODE_BACK, 0 /* repeat */, 0 /* metaState */, + KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, + KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY + | (isTracking ? KeyEvent.FLAG_TRACKING : 0), + InputDevice.SOURCE_KEYBOARD); + } + private boolean prepareWindow(boolean showInput) { boolean doShowInput = false; mDecorViewVisible = true; @@ -2658,6 +2715,7 @@ public class InputMethodService extends AbstractInputMethodService { } mLastWasInFullscreenMode = mIsFullscreen; updateFullscreenMode(); + unregisterCompatOnBackInvokedCallback(); } /** @@ -3797,4 +3855,14 @@ public class InputMethodService extends AbstractInputMethodService { proto.end(token); } }; + + private void compatHandleBack() { + final KeyEvent downEvent = createBackKeyEvent( + KeyEvent.ACTION_DOWN, false /* isTracking */); + onKeyDown(KeyEvent.KEYCODE_BACK, downEvent); + final boolean hasStartedTracking = + (downEvent.getFlags() & KeyEvent.FLAG_START_TRACKING) != 0; + final KeyEvent upEvent = createBackKeyEvent(KeyEvent.ACTION_UP, hasStartedTracking); + onKeyUp(KeyEvent.KEYCODE_BACK, upEvent); + } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 335cd2799c011..b76c3c0a760ce 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -6107,6 +6107,28 @@ public final class ViewRootImpl implements ViewParent, @Override protected int onProcess(QueuedInputEvent q) { + if (q.mEvent instanceof KeyEvent) { + final KeyEvent event = (KeyEvent) q.mEvent; + + // If the new back dispatch is enabled, intercept KEYCODE_BACK before it reaches the + // view tree or IME, and invoke the appropriate {@link OnBackInvokedCallback}. + if (isBack(event) + && mContext != null + && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) { + OnBackInvokedCallback topCallback = + getOnBackInvokedDispatcher().getTopCallback(); + if (event.getAction() == KeyEvent.ACTION_UP) { + if (topCallback != null) { + topCallback.onBackInvoked(); + return FINISH_HANDLED; + } + } else { + // Drop other actions such as {@link KeyEvent.ACTION_DOWN}. + return FINISH_NOT_HANDLED; + } + } + } + if (mInputQueue != null && q.mEvent instanceof KeyEvent) { mInputQueue.sendInputEvent(q.mEvent, q, true, this); return DEFER; @@ -6458,24 +6480,6 @@ public final class ViewRootImpl implements ViewParent, return FINISH_HANDLED; } - // If the new back dispatch is enabled, intercept KEYCODE_BACK before it reaches the - // view tree and invoke the appropriate {@link OnBackInvokedCallback}. - if (isBack(event) - && mContext != null - && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) { - OnBackInvokedCallback topCallback = - getOnBackInvokedDispatcher().getTopCallback(); - if (event.getAction() == KeyEvent.ACTION_UP) { - if (topCallback != null) { - topCallback.onBackInvoked(); - return FINISH_HANDLED; - } - } else { - // Drop other actions such as {@link KeyEvent.ACTION_DOWN}. - return FINISH_NOT_HANDLED; - } - } - // Deliver the key to the view hierarchy. if (mView.dispatchKeyEvent(event)) { return FINISH_HANDLED; diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java index 6209b46997e86..dbdc0daff2c1b 100644 --- a/core/java/android/view/inputmethod/InputMethod.java +++ b/core/java/android/view/inputmethod/InputMethod.java @@ -29,6 +29,7 @@ import android.util.Log; import android.view.InputChannel; import android.view.MotionEvent; import android.view.View; +import android.window.ImeOnBackInvokedDispatcher; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; import com.android.internal.inputmethod.InputMethodNavButtonFlags; @@ -232,6 +233,10 @@ public interface InputMethod { * long as your implementation of {@link InputMethod} relies on such * IPCs * @param navButtonFlags {@link InputMethodNavButtonFlags} in the initial state of this session. + * @param imeDispatcher The {@link ImeOnBackInvokedDispatcher }} to be set on the + * IME's {@link android.window.WindowOnBackInvokedDispatcher}, so that IME + * {@link android.window.OnBackInvokedCallback}s can be forwarded to + * the client requesting to start input. * @see #startInput(InputConnection, EditorInfo) * @see #restartInput(InputConnection, EditorInfo) * @see EditorInfo @@ -240,7 +245,8 @@ public interface InputMethod { @MainThread default void dispatchStartInputWithToken(@Nullable InputConnection inputConnection, @NonNull EditorInfo editorInfo, boolean restarting, - @NonNull IBinder startInputToken, @InputMethodNavButtonFlags int navButtonFlags) { + @NonNull IBinder startInputToken, @InputMethodNavButtonFlags int navButtonFlags, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { if (restarting) { restartInput(inputConnection, editorInfo); } else { diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index d9bde5825fde7..e2e9a8557793c 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -91,6 +91,8 @@ import android.view.View; import android.view.ViewRootImpl; import android.view.WindowManager.LayoutParams.SoftInputModeFlags; import android.view.autofill.AutofillManager; +import android.window.ImeOnBackInvokedDispatcher; +import android.window.WindowOnBackInvokedDispatcher; import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.DirectBootAwareness; @@ -105,6 +107,7 @@ import com.android.internal.inputmethod.StartInputFlags; import com.android.internal.inputmethod.StartInputReason; import com.android.internal.inputmethod.UnbindReason; import com.android.internal.os.SomeArgs; +import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethodClient; import com.android.internal.view.IInputMethodManager; import com.android.internal.view.IInputMethodSession; @@ -278,6 +281,21 @@ public final class InputMethodManager { */ private static final String SUBTYPE_MODE_VOICE = "voice"; + /** + * Provide this to {@link IInputMethodManager#startInputOrWindowGainedFocus( + * int, IInputMethodClient, IBinder, int, int, int, EditorInfo, IInputContext, int)} to receive + * {@link android.window.OnBackInvokedCallback} registrations from IME. + */ + private final ImeOnBackInvokedDispatcher mImeDispatcher = + new ImeOnBackInvokedDispatcher(Handler.getMain()) { + @Override + public WindowOnBackInvokedDispatcher getReceivingDispatcher() { + synchronized (mH) { + return mCurRootView != null ? mCurRootView.getOnBackInvokedDispatcher() : null; + } + } + }; + /** * Ensures that {@link #sInstance} becomes non-{@code null} for application that have directly * or indirectly relied on {@link #sInstance} via reflection or something like that. @@ -740,7 +758,8 @@ public final class InputMethodManager { windowFlags, null, null, null, - mCurRootView.mContext.getApplicationInfo().targetSdkVersion); + mCurRootView.mContext.getApplicationInfo().targetSdkVersion, + mImeDispatcher); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1687,6 +1706,8 @@ public final class InputMethodManager { mServedConnecting = false; clearConnectionLocked(); } + // Clear the back callbacks held by the ime dispatcher to avoid memory leaks. + mImeDispatcher.clear(); } public void displayCompletions(View view, CompletionInfo[] completions) { @@ -2359,7 +2380,8 @@ public final class InputMethodManager { softInputMode, windowFlags, tba, servedInputConnection, servedInputConnection == null ? null : servedInputConnection.asIRemoteAccessibilityInputConnection(), - view.getContext().getApplicationInfo().targetSdkVersion); + view.getContext().getApplicationInfo().targetSdkVersion, + mImeDispatcher); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.aidl b/core/java/android/window/ImeOnBackInvokedDispatcher.aidl new file mode 100644 index 0000000000000..04e64203dd390 --- /dev/null +++ b/core/java/android/window/ImeOnBackInvokedDispatcher.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/os/ParcelFileDescriptor.aidl +** +** Copyright 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.window; + +parcelable ImeOnBackInvokedDispatcher; diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java new file mode 100644 index 0000000000000..d5763aa25884f --- /dev/null +++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java @@ -0,0 +1,187 @@ +/* + * 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.window; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.util.Log; + +import java.util.HashMap; + +/** + * A {@link OnBackInvokedDispatcher} for IME that forwards {@link OnBackInvokedCallback} + * registrations from the IME process to the app process to be registered on the app window. + *

+ * The app process creates and propagates an instance of {@link ImeOnBackInvokedDispatcher} + * to the IME to be set on the IME window's {@link WindowOnBackInvokedDispatcher}. + *

+ * @see WindowOnBackInvokedDispatcher#setImeOnBackInvokedDispatcher + * + * @hide + */ +public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parcelable { + + private static final String TAG = "ImeBackDispatcher"; + static final String RESULT_KEY_ID = "id"; + static final String RESULT_KEY_CALLBACK = "callback"; + static final String RESULT_KEY_PRIORITY = "priority"; + static final int RESULT_CODE_REGISTER = 0; + static final int RESULT_CODE_UNREGISTER = 1; + @NonNull + private final ResultReceiver mResultReceiver; + + public ImeOnBackInvokedDispatcher(Handler handler) { + mResultReceiver = new ResultReceiver(handler) { + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + WindowOnBackInvokedDispatcher dispatcher = getReceivingDispatcher(); + if (dispatcher != null) { + receive(resultCode, resultData, dispatcher); + } + } + }; + } + + /** + * Override this method to return the {@link WindowOnBackInvokedDispatcher} of the window + * that should receive the forwarded callback. + */ + @Nullable + protected WindowOnBackInvokedDispatcher getReceivingDispatcher() { + return null; + } + + ImeOnBackInvokedDispatcher(Parcel in) { + mResultReceiver = in.readTypedObject(ResultReceiver.CREATOR); + } + + @Override + public void registerOnBackInvokedCallback( + @OnBackInvokedDispatcher.Priority int priority, + @NonNull OnBackInvokedCallback callback) { + final Bundle bundle = new Bundle(); + final IOnBackInvokedCallback iCallback = + new WindowOnBackInvokedDispatcher.OnBackInvokedCallbackWrapper(callback); + bundle.putBinder(RESULT_KEY_CALLBACK, iCallback.asBinder()); + bundle.putInt(RESULT_KEY_PRIORITY, priority); + bundle.putInt(RESULT_KEY_ID, callback.hashCode()); + mResultReceiver.send(RESULT_CODE_REGISTER, bundle); + } + + @Override + public void unregisterOnBackInvokedCallback( + @NonNull OnBackInvokedCallback callback) { + Bundle bundle = new Bundle(); + bundle.putInt(RESULT_KEY_ID, callback.hashCode()); + mResultReceiver.send(RESULT_CODE_UNREGISTER, bundle); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeTypedObject(mResultReceiver, flags); + } + + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public ImeOnBackInvokedDispatcher createFromParcel(Parcel in) { + return new ImeOnBackInvokedDispatcher(in); + } + public ImeOnBackInvokedDispatcher[] newArray(int size) { + return new ImeOnBackInvokedDispatcher[size]; + } + }; + + private final HashMap mImeCallbackMap = new HashMap<>(); + + private void receive( + int resultCode, Bundle resultData, + @NonNull OnBackInvokedDispatcher receivingDispatcher) { + final int callbackId = resultData.getInt(RESULT_KEY_ID); + if (resultCode == RESULT_CODE_REGISTER) { + int priority = resultData.getInt(RESULT_KEY_PRIORITY); + final IOnBackInvokedCallback callback = IOnBackInvokedCallback.Stub.asInterface( + resultData.getBinder(RESULT_KEY_CALLBACK)); + registerReceivedCallback( + callback, priority, callbackId, receivingDispatcher); + } else if (resultCode == RESULT_CODE_UNREGISTER) { + unregisterReceivedCallback(callbackId, receivingDispatcher); + } + } + + private void registerReceivedCallback( + @NonNull IOnBackInvokedCallback iCallback, + @OnBackInvokedDispatcher.Priority int priority, + int callbackId, + @NonNull OnBackInvokedDispatcher receivingDispatcher) { + final ImeOnBackInvokedCallback imeCallback = + new ImeOnBackInvokedCallback(iCallback); + mImeCallbackMap.put(callbackId, imeCallback); + receivingDispatcher.registerOnBackInvokedCallback(priority, imeCallback); + } + + private void unregisterReceivedCallback( + int callbackId, OnBackInvokedDispatcher receivingDispatcher) { + final OnBackInvokedCallback callback = mImeCallbackMap.get(callbackId); + if (callback == null) { + Log.e(TAG, "Ime callback not found. Ignoring unregisterReceivedCallback. " + + "callbackId: " + callbackId); + return; + } + receivingDispatcher.unregisterOnBackInvokedCallback(callback); + } + + /** Clears all registered callbacks on the instance. */ + public void clear() { + mImeCallbackMap.clear(); + } + + static class ImeOnBackInvokedCallback implements OnBackInvokedCallback { + @NonNull + private final IOnBackInvokedCallback mIOnBackInvokedCallback; + + ImeOnBackInvokedCallback(@NonNull IOnBackInvokedCallback iCallback) { + mIOnBackInvokedCallback = iCallback; + } + + @Override + public void onBackInvoked() { + try { + if (mIOnBackInvokedCallback != null) { + mIOnBackInvokedCallback.onBackInvoked(); + } + } catch (RemoteException e) { + Log.e(TAG, "Exception when invoking forwarded callback. e: ", e); + } + } + + IOnBackInvokedCallback getIOnBackInvokedCallback() { + return mIOnBackInvokedCallback; + } + } +} diff --git a/core/java/android/window/OnBackInvokedDispatcher.java b/core/java/android/window/OnBackInvokedDispatcher.java index 6bc2b5043e793..3539049af2197 100644 --- a/core/java/android/window/OnBackInvokedDispatcher.java +++ b/core/java/android/window/OnBackInvokedDispatcher.java @@ -96,4 +96,19 @@ public interface OnBackInvokedDispatcher { * @hide */ default void registerSystemOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { } + + + /** + * Sets an {@link ImeOnBackInvokedDispatcher} to forward {@link OnBackInvokedCallback}s + * from IME to the app process to be registered on the app window. + * + * Only call this on the IME window. Create the {@link ImeOnBackInvokedDispatcher} from + * the application process and override + * {@link ImeOnBackInvokedDispatcher#getReceivingDispatcher()} to point to the app + * window's {@link WindowOnBackInvokedDispatcher}. + * + * @hide + */ + default void setImeOnBackInvokedDispatcher( + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { } } diff --git a/core/java/android/window/ProxyOnBackInvokedDispatcher.java b/core/java/android/window/ProxyOnBackInvokedDispatcher.java index 44093971a23eb..bedf5038f6695 100644 --- a/core/java/android/window/ProxyOnBackInvokedDispatcher.java +++ b/core/java/android/window/ProxyOnBackInvokedDispatcher.java @@ -49,6 +49,7 @@ public class ProxyOnBackInvokedDispatcher implements OnBackInvokedDispatcher { private final List> mCallbacks = new ArrayList<>(); private final Object mLock = new Object(); private OnBackInvokedDispatcher mActualDispatcher = null; + private ImeOnBackInvokedDispatcher mImeDispatcher; @Override public void registerOnBackInvokedCallback( @@ -108,6 +109,9 @@ public class ProxyOnBackInvokedDispatcher implements OnBackInvokedDispatcher { Log.v(TAG, String.format("Proxy transferring %d callbacks to %s", mCallbacks.size(), mActualDispatcher)); } + if (mImeDispatcher != null) { + mActualDispatcher.setImeOnBackInvokedDispatcher(mImeDispatcher); + } for (Pair callbackPair : mCallbacks) { int priority = callbackPair.second; if (priority >= 0) { @@ -117,6 +121,7 @@ public class ProxyOnBackInvokedDispatcher implements OnBackInvokedDispatcher { } } mCallbacks.clear(); + mImeDispatcher = null; } private void clearCallbacksOnDispatcher() { @@ -142,6 +147,7 @@ public class ProxyOnBackInvokedDispatcher implements OnBackInvokedDispatcher { } synchronized (mLock) { mCallbacks.clear(); + mImeDispatcher = null; } } @@ -169,4 +175,14 @@ public class ProxyOnBackInvokedDispatcher implements OnBackInvokedDispatcher { transferCallbacksToDispatcher(); } } + + @Override + public void setImeOnBackInvokedDispatcher( + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + if (mActualDispatcher != null) { + mActualDispatcher.setImeOnBackInvokedDispatcher(imeDispatcher); + } else { + mImeDispatcher = imeDispatcher; + } + } } diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 781859cecb2cb..edfdbcc1f4f8c 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -55,6 +55,8 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { .getInt("persist.wm.debug.predictive_back", 1) != 0; private static final boolean ALWAYS_ENFORCE_PREDICTIVE_BACK = SystemProperties .getInt("persist.wm.debug.predictive_back_always_enforce", 0) != 0; + @Nullable + private ImeOnBackInvokedDispatcher mImeDispatcher; /** Convenience hashmap to quickly decide if a callback has been added. */ private final HashMap mAllCallbacks = new HashMap<>(); @@ -94,6 +96,10 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { private void registerOnBackInvokedCallbackUnchecked( @NonNull OnBackInvokedCallback callback, @Priority int priority) { + if (mImeDispatcher != null) { + mImeDispatcher.registerOnBackInvokedCallback(priority, callback); + return; + } if (!mOnBackInvokedCallbacks.containsKey(priority)) { mOnBackInvokedCallbacks.put(priority, new ArrayList<>()); } @@ -120,6 +126,10 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @Override public void unregisterOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { + if (mImeDispatcher != null) { + mImeDispatcher.unregisterOnBackInvokedCallback(callback); + return; + } if (!mAllCallbacks.containsKey(callback)) { if (DEBUG) { Log.i(TAG, "Callback not found. returning..."); @@ -153,6 +163,9 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { } mAllCallbacks.clear(); mOnBackInvokedCallbacks.clear(); + if (mImeDispatcher != null) { + mImeDispatcher = null; + } } private void setTopOnBackInvokedCallback(@Nullable OnBackInvokedCallback callback) { @@ -160,14 +173,18 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { return; } try { - if (callback == null) { - mWindowSession.setOnBackInvokedCallbackInfo(mWindow, null); - } else { + OnBackInvokedCallbackInfo callbackInfo = null; + if (callback != null) { int priority = mAllCallbacks.get(callback); - mWindowSession.setOnBackInvokedCallbackInfo( - mWindow, new OnBackInvokedCallbackInfo( - new OnBackInvokedCallbackWrapper(callback), priority)); + final IOnBackInvokedCallback iCallback = + callback instanceof ImeOnBackInvokedDispatcher + .ImeOnBackInvokedCallback + ? ((ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback) + callback).getIOnBackInvokedCallback() + : new OnBackInvokedCallbackWrapper(callback); + callbackInfo = new OnBackInvokedCallbackInfo(iCallback, priority); } + mWindowSession.setOnBackInvokedCallbackInfo(mWindow, callbackInfo); if (DEBUG && callback == null) { Log.d(TAG, TextUtils.formatSimple("setTopOnBackInvokedCallback(null) Callers:%s", Debug.getCallers(5, " "))); @@ -190,7 +207,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { return null; } - private static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub { + static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub { private final WeakReference mCallback; OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) { @@ -270,4 +287,10 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { return featureFlagEnabled && (appRequestsPredictiveBack || ALWAYS_ENFORCE_PREDICTIVE_BACK); } + + @Override + public void setImeOnBackInvokedDispatcher( + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + mImeDispatcher = imeDispatcher; + } } diff --git a/core/java/com/android/internal/view/IInputMethod.aidl b/core/java/com/android/internal/view/IInputMethod.aidl index 40d89db6165ce..4e2526a281b37 100644 --- a/core/java/com/android/internal/view/IInputMethod.aidl +++ b/core/java/com/android/internal/view/IInputMethod.aidl @@ -23,6 +23,7 @@ import android.view.MotionEvent; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputBinding; import android.view.inputmethod.InputMethodSubtype; +import android.window.ImeOnBackInvokedDispatcher; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; import com.android.internal.view.IInlineSuggestionsRequestCallback; import com.android.internal.view.IInputContext; @@ -47,7 +48,8 @@ oneway interface IInputMethod { void unbindInput(); void startInput(in IBinder startInputToken, in IInputContext inputContext, - in EditorInfo attribute, boolean restarting, int navigationBarFlags); + in EditorInfo attribute, boolean restarting, int navigationBarFlags, + in ImeOnBackInvokedDispatcher imeDispatcher); void onNavButtonFlagsChanged(int navButtonFlags); diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index d7bb2cb10b8c4..315776045afea 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -20,6 +20,7 @@ import android.os.ResultReceiver; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.EditorInfo; +import android.window.ImeOnBackInvokedDispatcher; import com.android.internal.inputmethod.InputBindResult; import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; @@ -57,7 +58,7 @@ interface IInputMethodManager { /* @android.view.WindowManager.LayoutParams.SoftInputModeFlags */ int softInputMode, int windowFlags, in EditorInfo attribute, in IInputContext inputContext, in IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, - int unverifiedTargetSdkVersion); + int unverifiedTargetSdkVersion, in ImeOnBackInvokedDispatcher imeDispatcher); void showInputMethodPickerFromClient(in IInputMethodClient client, int auxiliarySubtypeMode); diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java index e62c5c13f04d3..1703310320869 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java @@ -30,6 +30,7 @@ import android.view.MotionEvent; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputBinding; import android.view.inputmethod.InputMethodSubtype; +import android.window.ImeOnBackInvokedDispatcher; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; import com.android.internal.inputmethod.InputMethodNavButtonFlags; @@ -148,10 +149,11 @@ final class IInputMethodInvoker { @AnyThread void startInput(IBinder startInputToken, IInputContext inputContext, EditorInfo attribute, - boolean restarting, @InputMethodNavButtonFlags int navButtonFlags) { + boolean restarting, @InputMethodNavButtonFlags int navButtonFlags, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { try { mTarget.startInput(startInputToken, inputContext, attribute, restarting, - navButtonFlags); + navButtonFlags, imeDispatcher); } catch (RemoteException e) { logRemoteException(e); } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 6af00b3fbeea2..c759c645a3181 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -150,6 +150,7 @@ import android.view.inputmethod.InputMethodEditorTraceProto.InputMethodServiceTr import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; +import android.window.ImeOnBackInvokedDispatcher; import com.android.internal.annotations.GuardedBy; import com.android.internal.content.PackageMonitor; @@ -615,6 +616,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ IInputContext mCurInputContext; + /** + * The {@link ImeOnBackInvokedDispatcher} last provided by the current client to + * receive {@link android.window.OnBackInvokedCallback}s forwarded from IME. + */ + ImeOnBackInvokedDispatcher mCurImeDispatcher; + /** * The {@link IRemoteAccessibilityInputConnection} last provided by the current client. */ @@ -2623,7 +2630,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final SessionState session = mCurClient.curSession; setEnabledSessionLocked(session); session.method.startInput(startInputToken, mCurInputContext, mCurAttribute, restarting, - navButtonFlags); + navButtonFlags, mCurImeDispatcher); if (mShowRequested) { if (DEBUG) Slog.v(TAG, "Attach new input asks to show input"); showCurrentInputLocked(mCurFocusedWindow, getAppShowFlagsLocked(), null, @@ -2733,7 +2740,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, @NonNull EditorInfo attribute, @StartInputFlags int startInputFlags, @StartInputReason int startInputReason, - int unverifiedTargetSdkVersion) { + int unverifiedTargetSdkVersion, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { // If no method is currently selected, do nothing. final String selectedMethodId = getSelectedMethodIdLocked(); if (selectedMethodId == null) { @@ -2777,6 +2785,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mCurClient = cs; mCurInputContext = inputContext; mCurRemoteAccessibilityInputConnection = remoteAccessibilityInputConnection; + mCurImeDispatcher = imeDispatcher; mCurVirtualDisplayToScreenMatrix = getVirtualDisplayToScreenMatrixLocked(cs.selfReportedDisplayId, mDisplayIdToShowIme); @@ -3780,10 +3789,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode, int windowFlags, @Nullable EditorInfo attribute, IInputContext inputContext, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, - int unverifiedTargetSdkVersion) { + int unverifiedTargetSdkVersion, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { return startInputOrWindowGainedFocusInternal(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, attribute, inputContext, - remoteAccessibilityInputConnection, unverifiedTargetSdkVersion); + remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, + imeDispatcher); } @NonNull @@ -3792,7 +3803,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode, int windowFlags, @Nullable EditorInfo attribute, @Nullable IInputContext inputContext, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, - int unverifiedTargetSdkVersion) { + int unverifiedTargetSdkVersion, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { if (windowToken == null) { Slog.e(TAG, "windowToken cannot be null."); return InputBindResult.NULL; @@ -3829,7 +3841,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub result = startInputOrWindowGainedFocusInternalLocked(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, attribute, inputContext, remoteAccessibilityInputConnection, - unverifiedTargetSdkVersion, userId); + unverifiedTargetSdkVersion, userId, imeDispatcher); } finally { Binder.restoreCallingIdentity(ident); } @@ -3857,7 +3869,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @SoftInputModeFlags int softInputMode, int windowFlags, EditorInfo attribute, IInputContext inputContext, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, - int unverifiedTargetSdkVersion, @UserIdInt int userId) { + int unverifiedTargetSdkVersion, @UserIdInt int userId, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { if (DEBUG) { Slog.v(TAG, "startInputOrWindowGainedFocusInternalLocked: reason=" + InputMethodDebug.startInputReasonToString(startInputReason) @@ -3868,7 +3881,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + InputMethodDebug.startInputFlagsToString(startInputFlags) + " softInputMode=" + InputMethodDebug.softInputModeToString(softInputMode) + " windowFlags=#" + Integer.toHexString(windowFlags) - + " unverifiedTargetSdkVersion=" + unverifiedTargetSdkVersion); + + " unverifiedTargetSdkVersion=" + unverifiedTargetSdkVersion + + " imeDispatcher=" + imeDispatcher); } final ClientState cs = mClients.get(client.asBinder()); @@ -3952,7 +3966,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (attribute != null) { return startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, attribute, startInputFlags, - startInputReason, unverifiedTargetSdkVersion); + startInputReason, unverifiedTargetSdkVersion, imeDispatcher); } return new InputBindResult( InputBindResult.ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY, @@ -3993,7 +4007,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (isTextEditor && attribute != null && shouldRestoreImeVisibility(windowToken, softInputMode)) { res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, - attribute, startInputFlags, startInputReason, unverifiedTargetSdkVersion); + attribute, startInputFlags, startInputReason, unverifiedTargetSdkVersion, + imeDispatcher); showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null, SoftInputShowHideReason.SHOW_RESTORE_IME_VISIBILITY); return res; @@ -4033,7 +4048,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (attribute != null) { res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, attribute, startInputFlags, - startInputReason, unverifiedTargetSdkVersion); + startInputReason, unverifiedTargetSdkVersion, + imeDispatcher); didStart = true; } showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null, @@ -4065,7 +4081,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (attribute != null) { res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, attribute, startInputFlags, - startInputReason, unverifiedTargetSdkVersion); + startInputReason, unverifiedTargetSdkVersion, + imeDispatcher); didStart = true; } showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null, @@ -4085,7 +4102,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (attribute != null) { res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, attribute, startInputFlags, - startInputReason, unverifiedTargetSdkVersion); + startInputReason, unverifiedTargetSdkVersion, + imeDispatcher); didStart = true; } showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null, @@ -4115,7 +4133,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, attribute, startInputFlags, - startInputReason, unverifiedTargetSdkVersion); + startInputReason, unverifiedTargetSdkVersion, + imeDispatcher); } else { res = InputBindResult.NULL_EDITOR_INFO; } diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index b37f980ce9a01..068a0ffc9c876 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -194,7 +194,6 @@ class BackNavigationController { if (backType == BackNavigationInfo.TYPE_CALLBACK || currentActivity == null || currentTask == null - || currentTask.getDisplayContent().getImeContainer().isVisible() || currentActivity.isActivityTypeHome()) { return infoBuilder .setType(backType)