diff --git a/core/api/test-current.txt b/core/api/test-current.txt index d6a067d35f9b0..2c0b6e9ee89df 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 0bdbfbcf0dfc5..48c102bd48831 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -6095,6 +6095,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; @@ -6446,24 +6468,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 aaae82d07976e..247117e707c85 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -187,7 +187,6 @@ class BackNavigationController { if (backType == BackNavigationInfo.TYPE_CALLBACK || currentActivity == null || currentTask == null - || currentTask.getDisplayContent().getImeContainer().isVisible() || currentActivity.isActivityTypeHome()) { return infoBuilder .setType(backType)