From 38be9e32f161828b3a8d3f4e0046af3213350622 Mon Sep 17 00:00:00 2001 From: Shan Huang Date: Mon, 21 Mar 2022 21:44:05 +0000 Subject: [PATCH] Migrate InputMethodService to use 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 CL migrates the client side IME code to register and unregister callbacks, and also updates the WM side logic to make sure IME callback priority works correctly with other application callbacks. Test: In BackTestApp home activity, Show IME -> try swipe back. Open WidgetTestActivity, show IME by clicking on EditText -> try swipe back. Test: Install Micrsoft SwiftKey. Set as primary IME. Repeat the above steps in BackTestApp and make sure IME behaves correctly. Bug: 228358882 Change-Id: Idb246cf1858a7515f6fc34a5ccde33f25f56d404 --- .../InputMethodService.java | 60 ++++++++++++++++ .../server/wm/BackNavigationController.java | 19 ++++- .../wm/BackNavigationControllerTests.java | 70 +++++++++++++++---- 3 files changed, 135 insertions(+), 14 deletions(-) diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 4fdd534253283..6ece5efae537e 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,7 +135,10 @@ import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; import android.window.WindowMetricsHelper; +import android.window.WindowOnBackInvokedDispatcher; import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.IInputContentUriToken; @@ -345,6 +349,9 @@ public class InputMethodService extends AbstractInputMethodService { **/ private RingBuffer mPendingEvents; + /** Callback to handle back invocation when IME window is shown. */ + private OnBackInvokedCallback mBackCallback; + /** * Returns whether {@link InputMethodService} is responsible for rendering the back button and * the IME switcher button or not when the gestural navigation is enabled. @@ -1605,6 +1612,7 @@ public class InputMethodService extends AbstractInputMethodService { @Override public void onDestroy() { mDestroyed = true; super.onDestroy(); + unregisterOnBackInvokedCallback(); mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener( mInsetsComputer); doFinishInput(); @@ -2579,6 +2587,7 @@ public class InputMethodService extends AbstractInputMethodService { cancelImeSurfaceRemoval(); mInShowWindow = false; Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + registerOnBackInvokedCallback(); } @@ -2624,6 +2633,56 @@ public class InputMethodService extends AbstractInputMethodService { if (doShowInput) startExtractingText(false); } + /** + * Registers an {@link OnBackInvokedCallback} to handle back invocation when ahead-of-time + * back dispatching is enabled. We keep the KEYCODE_BACK based legacy code around to handle + * back on older devices. + */ + private void registerOnBackInvokedCallback() { + if (mBackCallback != null) { + // A back callback has already been registered. + return; + } + final ViewRootImpl viewRootImpl = mRootView == null ? null : mRootView.getViewRootImpl(); + if (viewRootImpl != null && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled( + viewRootImpl.mContext)) { + final OnBackInvokedCallback callback = () -> { + KeyEvent downEvent = createKeyEvent( + KeyEvent.ACTION_DOWN, false /* isTracking */); + onKeyDown(KeyEvent.KEYCODE_BACK, downEvent); + boolean hasStartedTracking = + (downEvent.getFlags() & KeyEvent.FLAG_START_TRACKING) != 0; + KeyEvent upEvent = createKeyEvent(KeyEvent.ACTION_UP, hasStartedTracking); + onKeyUp(KeyEvent.KEYCODE_BACK, upEvent); + }; + viewRootImpl.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, callback); + mBackCallback = callback; + } + } + + private KeyEvent createKeyEvent(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 void unregisterOnBackInvokedCallback() { + final ViewRootImpl viewRootImpl = mRootView == null ? null : mRootView.getViewRootImpl(); + if (viewRootImpl != null + && mBackCallback != null + && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled( + viewRootImpl.mContext)) { + viewRootImpl.getOnBackInvokedDispatcher() + .unregisterOnBackInvokedCallback(mBackCallback); + } + mBackCallback = null; + } + /** * Applies the IME visibility in {@link android.view.ImeInsetsSourceConsumer}. * @@ -2669,6 +2728,7 @@ public class InputMethodService extends AbstractInputMethodService { } mLastWasInFullscreenMode = mIsFullscreen; updateFullscreenMode(); + unregisterOnBackInvokedCallback(); } /** diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index b37f980ce9a01..0ed671804fe49 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -167,6 +167,23 @@ class BackNavigationController { currentActivity = window.mActivityRecord; currentTask = window.getTask(); callbackInfo = window.getOnBackInvokedCallbackInfo(); + final DisplayContent displayContent = window.getDisplayContent(); + + // When IME is shown, return the more prioritized callback between IME and app. + // Priority ordering follows: OVERLAY, IME, DEFAULT. + if (displayContent != null && displayContent.getImeContainer().isVisible()) { + WindowState imeWindow = displayContent.getImeContainer().getWindow( + windowState -> windowState.getOnBackInvokedCallbackInfo() != null); + if (imeWindow != null) { + OnBackInvokedCallbackInfo imeCallbackInfo = + imeWindow.getOnBackInvokedCallbackInfo(); + if (imeCallbackInfo != null && (callbackInfo == null + || callbackInfo.getPriority() <= imeCallbackInfo.getPriority())) { + callbackInfo = imeCallbackInfo; + } + } + } + if (callbackInfo == null) { Slog.e(TAG, "No callback registered, returning null."); return null; @@ -189,12 +206,10 @@ class BackNavigationController { // If we don't need to set up the animation, we return early. This is the case when // - We have an application callback. // - We don't have any ActivityRecord or Task to animate. - // - The IME is opened, and we just need to close it. // - The home activity is the focused activity. if (backType == BackNavigationInfo.TYPE_CALLBACK || currentActivity == null || currentTask == null - || currentTask.getDisplayContent().getImeContainer().isVisible() || currentActivity.isActivityTypeHome()) { return infoBuilder .setType(backType) diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index 49cd343ef4af8..82e54110b9718 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -20,6 +20,8 @@ import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_EXT_ENABLE_ON_BACK import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; import static android.window.BackNavigationInfo.typeToString; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -81,8 +83,8 @@ public class BackNavigationControllerTests extends WindowTestsBase { @Test public void backNavInfo_HomeWhenBackToLauncher() { - Task task = createTopTaskWithActivity(); - IOnBackInvokedCallback callback = withSystemCallback(task); + IOnBackInvokedCallback callback = + withCallback(createTopTaskWithActivity(), OnBackInvokedDispatcher.PRIORITY_SYSTEM); SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class); BackNavigationInfo backNavigationInfo = mBackNavigationController.startBackNavigation(mWm, @@ -103,7 +105,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { public void backTypeCrossTaskWhenBackToPreviousTask() { Task taskA = createTask(mDefaultDisplay); createActivityRecord(taskA); - withSystemCallback(createTopTaskWithActivity()); + withCallback(createTopTaskWithActivity(), OnBackInvokedDispatcher.PRIORITY_SYSTEM); BackNavigationInfo backNavigationInfo = startBackNavigation(); assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNotNull(); assertThat(typeToString(backNavigationInfo.getType())) @@ -155,7 +157,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { @Test public void preparesForBackToHome() { Task task = createTopTaskWithActivity(); - withSystemCallback(task); + withCallback(task, OnBackInvokedDispatcher.PRIORITY_SYSTEM); BackNavigationInfo backNavigationInfo = startBackNavigation(); assertThat(typeToString(backNavigationInfo.getType())) @@ -165,7 +167,8 @@ public class BackNavigationControllerTests extends WindowTestsBase { @Test public void backTypeCallback() { Task task = createTopTaskWithActivity(); - IOnBackInvokedCallback appCallback = withAppCallback(task); + IOnBackInvokedCallback appCallback = + withCallback(task, OnBackInvokedDispatcher.PRIORITY_DEFAULT); BackNavigationInfo backNavigationInfo = startBackNavigation(); assertThat(typeToString(backNavigationInfo.getType())) @@ -226,18 +229,61 @@ public class BackNavigationControllerTests extends WindowTestsBase { 1, appLatch.getCount()); } - private IOnBackInvokedCallback withSystemCallback(Task task) { + @Test + public void returnsImeCallback_imeVisible() { + // Set up a top activity with a default priority callback. + IOnBackInvokedCallback appCallback = + withCallback(createTopTaskWithActivity(), OnBackInvokedDispatcher.PRIORITY_DEFAULT); + IOnBackInvokedCallback imeCallback = createOnBackInvokedCallback(); + + // Set up an IME window with also a default priority callback. + final DisplayArea.Tokens imeContainer = mDisplayContent.getImeContainer(); + final WindowState imeWindow = createImeWindow(); + imeWindow.setOnBackInvokedCallbackInfo( + new OnBackInvokedCallbackInfo( + imeCallback, OnBackInvokedDispatcher.PRIORITY_DEFAULT)); + spyOn(imeContainer); + // Simulate IME becoming visible. + doReturn(true).when(imeContainer).isVisible(); + doReturn(imeWindow).when(imeContainer).getWindow(any()); + BackNavigationInfo backNavigationInfo = startBackNavigation(); + + // Expect the IME callback to be selected. + assertThat(backNavigationInfo.getOnBackInvokedCallback()).isEqualTo(imeCallback); + } + + @Test + public void returnsAppOverlayCallback_imeVisible() { + // Set up a top activity with an overlay priority callback. + IOnBackInvokedCallback appCallback = + withCallback(createTopTaskWithActivity(), OnBackInvokedDispatcher.PRIORITY_OVERLAY); + IOnBackInvokedCallback imeCallback = createOnBackInvokedCallback(); + + // Set up an IME window with a default priority callback. + final DisplayArea.Tokens imeContainer = mDisplayContent.getImeContainer(); + final WindowState imeWindow = createImeWindow(); + imeWindow.setOnBackInvokedCallbackInfo( + new OnBackInvokedCallbackInfo( + imeCallback, OnBackInvokedDispatcher.PRIORITY_DEFAULT)); + spyOn(imeContainer); + // Simulate IME becoming visible. + doReturn(true).when(imeContainer).isVisible(); + doReturn(imeWindow).when(imeContainer).getWindow(any()); + BackNavigationInfo backNavigationInfo = startBackNavigation(); + + // Expect the app callback to be selected. + assertThat(backNavigationInfo.getOnBackInvokedCallback()).isEqualTo(appCallback); + } + + private IOnBackInvokedCallback withCallback(Task task, int priority) { IOnBackInvokedCallback callback = createOnBackInvokedCallback(); task.getTopMostActivity().getTopChild().setOnBackInvokedCallbackInfo( - new OnBackInvokedCallbackInfo(callback, OnBackInvokedDispatcher.PRIORITY_SYSTEM)); + new OnBackInvokedCallbackInfo(callback, priority)); return callback; } - private IOnBackInvokedCallback withAppCallback(Task task) { - IOnBackInvokedCallback callback = createOnBackInvokedCallback(); - task.getTopMostActivity().getTopChild().setOnBackInvokedCallbackInfo( - new OnBackInvokedCallbackInfo(callback, OnBackInvokedDispatcher.PRIORITY_DEFAULT)); - return callback; + private WindowState createImeWindow() { + return createWindow(null, W_INPUT_METHOD, "mImeWindow", 12345 /* fake ime uide */); } @Nullable