Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 38be9e32 authored by Shan Huang's avatar Shan Huang
Browse files

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
parent 6585242e
Loading
Loading
Loading
Loading
+60 −0
Original line number Diff line number Diff line
@@ -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<MotionEvent> 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();
    }

    /**
+17 −2
Original line number Diff line number Diff line
@@ -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)
+58 −12
Original line number Diff line number Diff line
@@ -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,20 +229,63 @@ public class BackNavigationControllerTests extends WindowTestsBase {
                1, appLatch.getCount());
    }

    private IOnBackInvokedCallback withSystemCallback(Task task) {
        IOnBackInvokedCallback callback = createOnBackInvokedCallback();
        task.getTopMostActivity().getTopChild().setOnBackInvokedCallbackInfo(
                new OnBackInvokedCallbackInfo(callback, OnBackInvokedDispatcher.PRIORITY_SYSTEM));
        return callback;
    @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);
    }

    private IOnBackInvokedCallback withAppCallback(Task task) {
    @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_DEFAULT));
                new OnBackInvokedCallbackInfo(callback, priority));
        return callback;
    }

    private WindowState createImeWindow() {
        return createWindow(null, W_INPUT_METHOD, "mImeWindow", 12345 /* fake ime uide */);
    }

    @Nullable
    private BackNavigationInfo startBackNavigation() {
        return mBackNavigationController.startBackNavigation(mWm, new StubTransaction());