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