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

Commit 2553e488 authored by Yohei Yukawa's avatar Yohei Yukawa
Browse files

Make IMM more robust for window focus stealing

This CL is a generalized version of my previous CL [1], which addresed
Bug 31056744 where InputMethodManager (IMM) fails to recover from
failure mode when IMMS#startInputOrWindowGainedFocus() failes because
the app's window is no longer eligible to be the IME target.

This CL finally addressed one TODO in that CL. InputBindResult now has
the error code, which allows us to force restart input upon the next
window-focus-in event.  This should make IMM much more robust for
that kind of failure modes.  For instance, Bug 70629102 is fixed as
demonstrated in a newly added CTS test case [2].  Hopefully this may
also fix Bug 31056744, which we still do not know how to reproduce.

 [1]: I60adb38013b063918b074c7b947649eada77b2c8
      8e9214b4
 [2]: I4ea24c87cbbd05e4e68ad7dfafb774c8520188e2

Bug: 31056744
Fixes: 70629102
Test: Added a test case for Bug 70629102
      atest CtsInputMethodTestCases
Test: Manually made sure that Bug 28281870 is still fixed:
      1. Open app that has EditText.
      2. Start Input.
      3. Long press the task switch button to start multi-window mode.
      4. Tap the EditText that is used in step 2.
      5. Make sure that the IME still works as expected
Test: atest CtsViewTestCates
Change-Id: I7572d4b9d678f3669ca54d55718877b145015777
parent 14b1f3ed
Loading
Loading
Loading
Loading
+31 −49
Original line number Diff line number Diff line
@@ -281,10 +281,10 @@ public final class InputMethodManager {
    boolean mActive = false;

    /**
     * Set whenever this client becomes inactive, to know we need to reset
     * state with the IME the next time we receive focus.
     * {@code true} if next {@link #onPostWindowFocus(View, View, int, boolean, int)} needs to
     * restart input.
     */
    boolean mHasBeenInactive = true;
    boolean mRestartOnNextWindowFocus = true;

    /**
     * As reported by IME through InputConnection.
@@ -489,7 +489,7 @@ public final class InputMethodManager {
                            // Some other client has starting using the IME, so note
                            // that this happened and make sure our own editor's
                            // state is reset.
                            mHasBeenInactive = true;
                            mRestartOnNextWindowFocus = true;
                            try {
                                // Note that finishComposingText() is allowed to run
                                // even when we are not active.
@@ -500,7 +500,7 @@ public final class InputMethodManager {
                        // Check focus again in case that "onWindowFocus" is called before
                        // handling this message.
                        if (mServedView != null && mServedView.hasWindowFocus()) {
                            if (checkFocusNoStartInput(mHasBeenInactive)) {
                            if (checkFocusNoStartInput(mRestartOnNextWindowFocus)) {
                                final int reason = active ?
                                        InputMethodClient.START_INPUT_REASON_ACTIVATED_BY_IMMS :
                                        InputMethodClient.START_INPUT_REASON_DEACTIVATED_BY_IMMS;
@@ -1336,7 +1336,14 @@ public final class InputMethodManager {
                        windowFlags, tba, servedContext, missingMethodFlags,
                        view.getContext().getApplicationInfo().targetSdkVersion);
                if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
                if (res != null) {
                if (res == null) {
                    Log.wtf(TAG, "startInputOrWindowGainedFocus must not return"
                            + " null. startInputReason="
                            + InputMethodClient.getStartInputReason(startInputReason)
                            + " editorInfo=" + tba
                            + " controlFlags=#" + Integer.toHexString(controlFlags));
                    return false;
                }
                if (res.id != null) {
                    setInputChannelLocked(res.channel);
                    mBindSequence = res.sequence;
@@ -1344,38 +1351,13 @@ public final class InputMethodManager {
                    mCurId = res.id;
                    mNextUserActionNotificationSequenceNumber =
                            res.userActionNotificationSequenceNumber;
                    } else {
                        if (res.channel != null && res.channel != mCurChannel) {
                } else if (res.channel != null && res.channel != mCurChannel) {
                    res.channel.dispose();
                }
                        if (mCurMethod == null) {
                            // This means there is no input method available.
                            if (DEBUG) Log.v(TAG, "ABORT input: no input method!");
                            return true;
                        }
                    }
                } else {
                    if (startInputReason
                            == InputMethodClient.START_INPUT_REASON_WINDOW_FOCUS_GAIN) {
                        // We are here probably because of an obsolete window-focus-in message sent
                        // to windowGainingFocus.  Since IMMS determines whether a Window can have
                        // IME focus or not by using the latest window focus state maintained in the
                        // WMS, this kind of race condition cannot be avoided.  One obvious example
                        // would be that we have already received a window-focus-out message but the
                        // UI thread is still handling previous window-focus-in message here.
                        // TODO: InputBindResult should have the error code.
                        if (DEBUG) Log.w(TAG, "startInputOrWindowGainedFocus failed. "
                                + "Window focus may have already been lost. "
                                + "win=" + windowGainingFocus + " view=" + dumpViewInfo(view));
                        if (!mActive) {
                            // mHasBeenInactive is a latch switch to forcefully refresh IME focus
                            // state when an inactive (mActive == false) client is gaining window
                            // focus. In case we have unnecessary disable the latch due to this
                            // spurious wakeup, we re-enable the latch here.
                            // TODO: Come up with more robust solution.
                            mHasBeenInactive = true;
                        }
                    }
                switch (res.result) {
                    case InputBindResult.ResultCode.ERROR_NOT_IME_TARGET_WINDOW:
                        mRestartOnNextWindowFocus = true;
                        break;
                }
                if (mCurMethod != null && mCompletions != null) {
                    try {
@@ -1551,9 +1533,9 @@ public final class InputMethodManager {
                    + " softInputMode=" + InputMethodClient.softInputModeToString(softInputMode)
                    + " first=" + first + " flags=#"
                    + Integer.toHexString(windowFlags));
            if (mHasBeenInactive) {
                if (DEBUG) Log.v(TAG, "Has been inactive!  Starting fresh");
                mHasBeenInactive = false;
            if (mRestartOnNextWindowFocus) {
                if (DEBUG) Log.v(TAG, "Restarting due to mRestartOnNextWindowFocus");
                mRestartOnNextWindowFocus = false;
                forceNewFocus = true;
            }
            focusInLocked(focusedView != null ? focusedView : rootView);
@@ -2485,7 +2467,7 @@ public final class InputMethodManager {
        p.println("  mMainLooper=" + mMainLooper);
        p.println("  mIInputContext=" + mIInputContext);
        p.println("  mActive=" + mActive
                + " mHasBeenInactive=" + mHasBeenInactive
                + " mRestartOnNextWindowFocus=" + mRestartOnNextWindowFocus
                + " mBindSequence=" + mBindSequence
                + " mCurId=" + mCurId);
        p.println("  mFullscreenMode=" + mFullscreenMode);
+1 −0
Original line number Diff line number Diff line
@@ -56,6 +56,7 @@ interface IInputMethodManager {
            in ResultReceiver resultReceiver);
    // If windowToken is null, this just does startInput().  Otherwise this reports that a window
    // has gained focus, and if 'attribute' is non-null then also does startInput.
    // @NonNull
    InputBindResult startInputOrWindowGainedFocus(
            /* @InputMethodClient.StartInputReason */ int startInputReason,
            in IInputMethodClient client, in IBinder windowToken, int controlFlags,
+196 −7
Original line number Diff line number Diff line
@@ -16,16 +16,133 @@

package com.android.internal.view;

import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.annotation.IntDef;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
import android.view.InputChannel;

import java.lang.annotation.Retention;

/**
 * Bundle of information returned by input method manager about a successful
 * binding to an input method.
 */
public final class InputBindResult implements Parcelable {
    static final String TAG = "InputBindResult";

    @Retention(SOURCE)
    @IntDef({
            ResultCode.SUCCESS_WITH_IME_SESSION,
            ResultCode.SUCCESS_WAITING_IME_SESSION,
            ResultCode.SUCCESS_WAITING_IME_BINDING,
            ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY,
            ResultCode.ERROR_NULL,
            ResultCode.ERROR_NO_IME,
            ResultCode.ERROR_INVALID_PACKAGE_NAME,
            ResultCode.ERROR_SYSTEM_NOT_READY,
            ResultCode.ERROR_IME_NOT_CONNECTED,
            ResultCode.ERROR_INVALID_USER,
            ResultCode.ERROR_NULL_EDITOR_INFO,
            ResultCode.ERROR_NOT_IME_TARGET_WINDOW,
    })
    public @interface ResultCode {
        /**
         * Indicates that everything in this result object including {@link #method} is valid.
         */
        int SUCCESS_WITH_IME_SESSION = 0;
        /**
         * Indicates that this is a temporary binding until the
         * {@link android.inputmethodservice.InputMethodService} (IMS) establishes a valid session
         * to {@link com.android.server.InputMethodManagerService} (IMMS).
         *
         * <p>Note that in this state the IMS is already bound to IMMS but the logical session
         * is not yet established on top of the IPC channel.</p>
         *
         * <p>Some of fields such as {@link #channel} is not yet available.</p>
         *
         * @see android.inputmethodservice.InputMethodService##onCreateInputMethodSessionInterface()
         **/
        int SUCCESS_WAITING_IME_SESSION = 1;
        /**
         * Indicates that this is a temporary binding until the
         * {@link android.inputmethodservice.InputMethodService} (IMS) establishes a valid session
         * to {@link com.android.server.InputMethodManagerService} (IMMS).
         *
         * <p>Note that in this state the IMMS has already initiated a connection to the IMS but
         * the binding process is not completed yet.</p>
         *
         * <p>Some of fields such as {@link #channel} is not yet available.</p>
         * @see android.content.ServiceConnection#onServiceConnected(ComponentName, IBinder)
         */
        int SUCCESS_WAITING_IME_BINDING = 2;
        /**
         * Indicates that this is not intended for starting input but just for reporting window
         * focus change from the application process.
         *
         * <p>All other fields do not have meaningful value.</p>
         */
        int SUCCESS_REPORT_WINDOW_FOCUS_ONLY = 3;
        /**
         * Indicates somehow
         * {@link com.android.server.InputMethodManagerService#startInputOrWindowGainedFocus} is
         * trying to return null {@link InputBindResult}, which must never happen.
         */
        int ERROR_NULL = 4;
        /**
         * Indicates that {@link com.android.server.InputMethodManagerService} recognizes no IME.
         */
        int ERROR_NO_IME = 5;
        /**
         * Indicates that {@link android.view.inputmethod.EditorInfo#packageName} does not match
         * the caller UID.
         *
         * @see android.view.inputmethod.EditorInfo#packageName
         */
        int ERROR_INVALID_PACKAGE_NAME = 6;
        /**
         * Indicates that the system is still in an early stage of the boot process and any 3rd
         * party application is not allowed to run.
         *
         * @see com.android.server.SystemService#PHASE_THIRD_PARTY_APPS_CAN_START
         */
        int ERROR_SYSTEM_NOT_READY = 7;
        /**
         * Indicates that {@link com.android.server.InputMethodManagerService} tried to connect to
         * an {@link android.inputmethodservice.InputMethodService} but failed.
         *
         * @see android.content.Context#bindServiceAsUser(Intent, ServiceConnection, int, UserHandle)
         */
        int ERROR_IME_NOT_CONNECTED = 8;
        /**
         * Indicates that the caller is not the foreground user (or does not have
         * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission).
         */
        int ERROR_INVALID_USER = 9;
        /**
         * Indicates that the caller should have specified non-null
         * {@link android.view.inputmethod.EditorInfo}.
         */
        int ERROR_NULL_EDITOR_INFO = 10;
        /**
         * Indicates that the target window the client specified cannot be the IME target right now.
         *
         * <p>Due to the asynchronous nature of Android OS, we cannot completely avoid this error.
         * The client should try to restart input when its {@link android.view.Window} is focused
         * again.</p>
         *
         * @see com.android.server.wm.WindowManagerService#inputMethodClientHasFocus(IInputMethodClient)
         */
        int ERROR_NOT_IME_TARGET_WINDOW = 11;
    }

    @ResultCode
    public final int result;

    /**
     * The input method service.
@@ -53,8 +170,10 @@ public final class InputBindResult implements Parcelable {
     */
    public final int userActionNotificationSequenceNumber;

    public InputBindResult(IInputMethodSession _method, InputChannel _channel,
    public InputBindResult(@ResultCode int _result,
            IInputMethodSession _method, InputChannel _channel,
            String _id, int _sequence, int _userActionNotificationSequenceNumber) {
        result = _result;
        method = _method;
        channel = _channel;
        id = _id;
@@ -63,6 +182,7 @@ public final class InputBindResult implements Parcelable {
    }

    InputBindResult(Parcel source) {
        result = source.readInt();
        method = IInputMethodSession.Stub.asInterface(source.readStrongBinder());
        if (source.readInt() != 0) {
            channel = InputChannel.CREATOR.createFromParcel(source);
@@ -76,9 +196,9 @@ public final class InputBindResult implements Parcelable {

    @Override
    public String toString() {
        return "InputBindResult{" + method + " " + id
                + " sequence:" + sequence
                + " userActionNotificationSequenceNumber:" + userActionNotificationSequenceNumber
        return "InputBindResult{result=" + getResultString() + " method="+ method + " id=" + id
                + " sequence=" + sequence
                + " userActionNotificationSequenceNumber=" + userActionNotificationSequenceNumber
                + "}";
    }

@@ -90,6 +210,7 @@ public final class InputBindResult implements Parcelable {
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(result);
        dest.writeStrongInterface(method);
        if (channel != null) {
            dest.writeInt(1);
@@ -122,4 +243,72 @@ public final class InputBindResult implements Parcelable {
    public int describeContents() {
        return channel != null ? channel.describeContents() : 0;
    }

    public String getResultString() {
        switch (result) {
            case ResultCode.SUCCESS_WITH_IME_SESSION:
                return "SUCCESS_WITH_IME_SESSION";
            case ResultCode.SUCCESS_WAITING_IME_SESSION:
                return "SUCCESS_WAITING_IME_SESSION";
            case ResultCode.SUCCESS_WAITING_IME_BINDING:
                return "SUCCESS_WAITING_IME_BINDING";
            case ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY:
                return "SUCCESS_REPORT_WINDOW_FOCUS_ONLY";
            case ResultCode.ERROR_NULL:
                return "ERROR_NULL";
            case ResultCode.ERROR_NO_IME:
                return "ERROR_NO_IME";
            case ResultCode.ERROR_INVALID_PACKAGE_NAME:
                return "ERROR_INVALID_PACKAGE_NAME";
            case ResultCode.ERROR_SYSTEM_NOT_READY:
                return "ERROR_SYSTEM_NOT_READY";
            case ResultCode.ERROR_IME_NOT_CONNECTED:
                return "ERROR_IME_NOT_CONNECTED";
            case ResultCode.ERROR_INVALID_USER:
                return "ERROR_INVALID_USER";
            case ResultCode.ERROR_NULL_EDITOR_INFO:
                return "ERROR_NULL_EDITOR_INFO";
            case ResultCode.ERROR_NOT_IME_TARGET_WINDOW:
                return "ERROR_NOT_IME_TARGET_WINDOW";
            default:
                return "Unknown(" + result + ")";
        }
    }

    private static InputBindResult error(@ResultCode int result) {
        return new InputBindResult(result, null, null, null, -1, -1);
    }

    /**
     * Predefined error object for {@link ResultCode#ERROR_NULL}.
     */
    public static final InputBindResult NULL = error(ResultCode.ERROR_NULL);
    /**
     * Predefined error object for {@link ResultCode#NO_IME}.
     */
    public static final InputBindResult NO_IME = error(ResultCode.ERROR_NO_IME);
    /**
     * Predefined error object for {@link ResultCode#ERROR_INVALID_PACKAGE_NAME}.
     */
    public static final InputBindResult INVALID_PACKAGE_NAME =
            error(ResultCode.ERROR_INVALID_PACKAGE_NAME);
    /**
     * Predefined error object for {@link ResultCode#ERROR_NULL_EDITOR_INFO}.
     */
    public static final InputBindResult NULL_EDITOR_INFO = error(ResultCode.ERROR_NULL_EDITOR_INFO);
    /**
     * Predefined error object for {@link ResultCode#ERROR_NOT_IME_TARGET_WINDOW}.
     */
    public static final InputBindResult NOT_IME_TARGET_WINDOW =
            error(ResultCode.ERROR_NOT_IME_TARGET_WINDOW);
    /**
     * Predefined error object for {@link ResultCode#ERROR_IME_NOT_CONNECTED}.
     */
    public static final InputBindResult IME_NOT_CONNECTED =
            error(ResultCode.ERROR_IME_NOT_CONNECTED);
    /**
     * Predefined error object for {@link ResultCode#ERROR_INVALID_USER}.
     */
    public static final InputBindResult INVALID_USER = error(ResultCode.ERROR_INVALID_USER);

}
+47 −26

File changed.

Preview size limit exceeded, changes collapsed.