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

Commit f87f7508 authored by Yohei Yukawa's avatar Yohei Yukawa
Browse files

Let blocked InputConnection APIs fail upon IInputMethod.unbindInput()

This is a follow up CL to our previous CL [1], which implemented
fail-fast mode for blocking InputConnection APIs based on
IInputMethod.unbindInput() async signal from IMMS to IMS.

What was not implemented in the previous CL was a mechanism to
immediately unblock a sync InputConnection API call that is already
requested to the IME client process and waiting for its response.

With this CL, any blocking InputConnection API fails immediately when
IInputMethod#onUnbindInput() is delivered to the IME process, without
waiting for the full time-out period (MAX_WAIT_TIME_MILLIS == 2 sec)
to pass.

Implementation Note:

The key idea is to use CountDownLatch to compose multiple wait
conditions.composed wait condition. The CountDownLatch is initialized
with 1 then will be decremented when:

 A. received a result from the IME client
 B. received IInputMethod.unbindInput()

Hence InputConnectionWrapper can simply wait for the CountDownLatch to
become 0 with an existing timeout (MAX_WAIT_TIME_MILLIS) then returns
failure unless the CountDownLatch became 0 because of A.

 [1]: I0f816c6ca4c5c0664962432b913f074605fedd27
      1d113d04

Fix: 36897707
Test: atest InputConnectionBlockingMethodTest
Test: InputConnectionBlockingMethodTest#*FailFastAfterUnbindInput()
      take shorter time to complete.
Test: Monitor logcat with `adb logcat -s InputConnectionWrapper:*`
      while running `atest InputConnectionBlockingMethodTest`
Change-Id: Ic65a95eb5d0fd56f505a02fd9083bcf6694b6734
parent 75463684
Loading
Loading
Loading
Loading
+17 −15
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.inputmethodservice;

import android.annotation.BinderThread;
import android.annotation.MainThread;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -37,6 +38,7 @@ import android.view.inputmethod.InputMethodSession;
import android.view.inputmethod.InputMethodSubtype;

import com.android.internal.inputmethod.IInputMethodPrivilegedOperations;
import com.android.internal.inputmethod.CancellationGroup;
import com.android.internal.os.HandlerCaller;
import com.android.internal.os.SomeArgs;
import com.android.internal.view.IInlineSuggestionsRequestCallback;
@@ -52,7 +54,6 @@ import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Implements the internal IInputMethod interface to convert incoming calls
@@ -90,12 +91,13 @@ class IInputMethodWrapper extends IInputMethod.Stub
     *
     * <p>This field must be set and cleared only from the binder thread(s), where the system
     * guarantees that {@link #bindInput(InputBinding)},
     * {@link #startInput(IBinder, IInputContext, int, EditorInfo, boolean)}, and
     * {@link #startInput(IBinder, IInputContext, int, EditorInfo, boolean, boolean)}, and
     * {@link #unbindInput()} are called with the same order as the original calls
     * in {@link com.android.server.inputmethod.InputMethodManagerService}.
     * See {@link IBinder#FLAG_ONEWAY} for detailed semantics.</p>
     */
    AtomicBoolean mIsUnbindIssued = null;
    @Nullable
    CancellationGroup mCancellationGroup = null;

    // NOTE: we should have a cache of these.
    static final class InputMethodSessionCallbackWrapper implements InputMethod.SessionCallback {
@@ -187,11 +189,11 @@ class IInputMethodWrapper extends IInputMethod.Stub
                final IBinder startInputToken = (IBinder) args.arg1;
                final IInputContext inputContext = (IInputContext) args.arg2;
                final EditorInfo info = (EditorInfo) args.arg3;
                final AtomicBoolean isUnbindIssued = (AtomicBoolean) args.arg4;
                final CancellationGroup cancellationGroup = (CancellationGroup) args.arg4;
                SomeArgs moreArgs = (SomeArgs) args.arg5;
                final InputConnection ic = inputContext != null
                        ? new InputConnectionWrapper(
                                mTarget, inputContext, moreArgs.argi3, isUnbindIssued)
                                mTarget, inputContext, moreArgs.argi3, cancellationGroup)
                        : null;
                info.makeCompatible(mTargetSdkVersion);
                inputMethod.dispatchStartInputWithToken(
@@ -295,15 +297,15 @@ class IInputMethodWrapper extends IInputMethod.Stub
    @BinderThread
    @Override
    public void bindInput(InputBinding binding) {
        if (mIsUnbindIssued != null) {
        if (mCancellationGroup != null) {
            Log.e(TAG, "bindInput must be paired with unbindInput.");
        }
        mIsUnbindIssued = new AtomicBoolean();
        mCancellationGroup = new CancellationGroup();
        // This IInputContext is guaranteed to implement all the methods.
        final int missingMethodFlags = 0;
        InputConnection ic = new InputConnectionWrapper(mTarget,
                IInputContext.Stub.asInterface(binding.getConnectionToken()), missingMethodFlags,
                mIsUnbindIssued);
                mCancellationGroup);
        InputBinding nu = new InputBinding(ic, binding);
        mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_INPUT_CONTEXT, nu));
    }
@@ -311,10 +313,10 @@ class IInputMethodWrapper extends IInputMethod.Stub
    @BinderThread
    @Override
    public void unbindInput() {
        if (mIsUnbindIssued != null) {
        if (mCancellationGroup != null) {
            // Signal the flag then forget it.
            mIsUnbindIssued.set(true);
            mIsUnbindIssued = null;
            mCancellationGroup.cancelAll();
            mCancellationGroup = null;
        } else {
            Log.e(TAG, "unbindInput must be paired with bindInput.");
        }
@@ -326,16 +328,16 @@ class IInputMethodWrapper extends IInputMethod.Stub
    public void startInput(IBinder startInputToken, IInputContext inputContext,
            @InputConnectionInspector.MissingMethodFlags final int missingMethods,
            EditorInfo attribute, boolean restarting, boolean shouldPreRenderIme) {
        if (mIsUnbindIssued == null) {
        if (mCancellationGroup == null) {
            Log.e(TAG, "startInput must be called after bindInput.");
            mIsUnbindIssued = new AtomicBoolean();
            mCancellationGroup = new CancellationGroup();
        }
        SomeArgs args = SomeArgs.obtain();
        args.argi1 = restarting ? 1 : 0;
        args.argi2 = shouldPreRenderIme ? 1 : 0;
        args.argi3 = missingMethods;
        mCaller.executeOrSendMessage(mCaller.obtainMessageOOOOO(
                DO_START_INPUT, startInputToken, inputContext, attribute, mIsUnbindIssued, args));
        mCaller.executeOrSendMessage(mCaller.obtainMessageOOOOO(DO_START_INPUT, startInputToken,
                inputContext, attribute, mCancellationGroup, args));
    }

    @BinderThread
+18 −17
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import android.view.inputmethod.ExtractedText;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.inputmethod.IMultiClientInputMethodSession;
import com.android.internal.inputmethod.CancellationGroup;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.internal.view.IInputContext;
@@ -46,7 +47,6 @@ import com.android.internal.view.IInputMethodSession;
import com.android.internal.view.InputConnectionWrapper;

import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Re-dispatches all the incoming per-client events to the specified {@link Looper} thread.
@@ -80,19 +80,19 @@ final class MultiClientInputMethodClientCallbackAdaptor {
    @Nullable
    InputEventReceiver mInputEventReceiver;

    private final AtomicBoolean mFinished = new AtomicBoolean(false);
    private final CancellationGroup mCancellationGroup = new CancellationGroup();

    IInputMethodSession.Stub createIInputMethodSession() {
        synchronized (mSessionLock) {
            return new InputMethodSessionImpl(
                    mSessionLock, mCallbackImpl, mHandler, mFinished);
                    mSessionLock, mCallbackImpl, mHandler, mCancellationGroup);
        }
    }

    IMultiClientInputMethodSession.Stub createIMultiClientInputMethodSession() {
        synchronized (mSessionLock) {
            return new MultiClientInputMethodSessionImpl(
                    mSessionLock, mCallbackImpl, mHandler, mFinished);
                    mSessionLock, mCallbackImpl, mHandler, mCancellationGroup);
        }
    }

@@ -105,7 +105,7 @@ final class MultiClientInputMethodClientCallbackAdaptor {
            mHandler = new Handler(looper, null, true);
            mReadChannel = readChannel;
            mInputEventReceiver = new ImeInputEventReceiver(mReadChannel, mHandler.getLooper(),
                    mFinished, mDispatcherState, mCallbackImpl.mOriginalCallback);
                    mCancellationGroup, mDispatcherState, mCallbackImpl.mOriginalCallback);
        }
    }

@@ -139,16 +139,17 @@ final class MultiClientInputMethodClientCallbackAdaptor {
    }

    private static final class ImeInputEventReceiver extends InputEventReceiver {
        private final AtomicBoolean mFinished;
        private final CancellationGroup mCancellationGroupOnFinishSession;
        private final KeyEvent.DispatcherState mDispatcherState;
        private final MultiClientInputMethodServiceDelegate.ClientCallback mClientCallback;
        private final KeyEventCallbackAdaptor mKeyEventCallbackAdaptor;

        ImeInputEventReceiver(InputChannel readChannel, Looper looper, AtomicBoolean finished,
        ImeInputEventReceiver(InputChannel readChannel, Looper looper,
                CancellationGroup cancellationGroupOnFinishSession,
                KeyEvent.DispatcherState dispatcherState,
                MultiClientInputMethodServiceDelegate.ClientCallback callback) {
            super(readChannel, looper);
            mFinished = finished;
            mCancellationGroupOnFinishSession = cancellationGroupOnFinishSession;
            mDispatcherState = dispatcherState;
            mClientCallback = callback;
            mKeyEventCallbackAdaptor = new KeyEventCallbackAdaptor(callback);
@@ -156,7 +157,7 @@ final class MultiClientInputMethodClientCallbackAdaptor {

        @Override
        public void onInputEvent(InputEvent event) {
            if (mFinished.get()) {
            if (mCancellationGroupOnFinishSession.isCanceled()) {
                // The session has been finished.
                finishInputEvent(event, false);
                return;
@@ -187,14 +188,14 @@ final class MultiClientInputMethodClientCallbackAdaptor {
        private CallbackImpl mCallbackImpl;
        @GuardedBy("mSessionLock")
        private Handler mHandler;
        private final AtomicBoolean mSessionFinished;
        private final CancellationGroup mCancellationGroupOnFinishSession;

        InputMethodSessionImpl(Object lock, CallbackImpl callback, Handler handler,
                AtomicBoolean sessionFinished) {
                CancellationGroup cancellationGroupOnFinishSession) {
            mSessionLock = lock;
            mCallbackImpl = callback;
            mHandler = handler;
            mSessionFinished = sessionFinished;
            mCancellationGroupOnFinishSession = cancellationGroupOnFinishSession;
        }

        @Override
@@ -272,7 +273,7 @@ final class MultiClientInputMethodClientCallbackAdaptor {
                if (mCallbackImpl == null || mHandler == null) {
                    return;
                }
                mSessionFinished.set(true);
                mCancellationGroupOnFinishSession.cancelAll();
                mHandler.sendMessage(PooledLambda.obtainMessage(
                        CallbackImpl::finishSession, mCallbackImpl));
                mCallbackImpl = null;
@@ -311,14 +312,14 @@ final class MultiClientInputMethodClientCallbackAdaptor {
        private CallbackImpl mCallbackImpl;
        @GuardedBy("mSessionLock")
        private Handler mHandler;
        private final AtomicBoolean mSessionFinished;
        private final CancellationGroup mCancellationGroupOnFinishSession;

        MultiClientInputMethodSessionImpl(Object lock, CallbackImpl callback,
                Handler handler, AtomicBoolean sessionFinished) {
                Handler handler, CancellationGroup cancellationGroupOnFinishSession) {
            mSessionLock = lock;
            mCallbackImpl = callback;
            mHandler = handler;
            mSessionFinished = sessionFinished;
            mCancellationGroupOnFinishSession = cancellationGroupOnFinishSession;
        }

        @Override
@@ -335,7 +336,7 @@ final class MultiClientInputMethodClientCallbackAdaptor {
                        new WeakReference<>(null);
                args.arg1 = (inputContext == null) ? null
                        : new InputConnectionWrapper(fakeIMS, inputContext, missingMethods,
                                mSessionFinished);
                                mCancellationGroupOnFinishSession);
                args.arg2 = editorInfo;
                args.argi1 = controlFlags;
                args.argi2 = softInputMode;
+348 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 com.android.internal.inputmethod;

import android.annotation.AnyThread;
import android.annotation.NonNull;
import android.annotation.Nullable;

import com.android.internal.annotations.GuardedBy;

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * A utility class, which works as both a factory class of completable objects and a cancellation
 * signal to cancel all the completable objects created by this object.
 */
public final class CancellationGroup {
    private final Object mLock = new Object();

    /**
     * List of {@link CountDownLatch}, which can be used to propagate {@link #cancelAll()} to
     * completable objects.
     *
     * <p>This will be lazily instantiated to avoid unnecessary object allocations.</p>
     */
    @Nullable
    @GuardedBy("mLock")
    private ArrayList<CountDownLatch> mLatchList = null;

    @GuardedBy("mLock")
    private boolean mCanceled = false;

    /**
     * An inner class to consolidate completable object types supported by
     * {@link CancellationGroup}.
     */
    public static final class Completable {

        /**
         * Not intended to be instantiated.
         */
        private Completable() {
        }

        /**
         * Base class of all the completable types supported by {@link CancellationGroup}.
         */
        protected static class ValueBase {
            /**
             * {@link CountDownLatch} to be signaled to unblock {@link #await(int, TimeUnit)}.
             */
            private final CountDownLatch mLatch = new CountDownLatch(1);

            /**
             * {@link CancellationGroup} to which this completable object belongs.
             */
            @NonNull
            private final CancellationGroup mParentGroup;

            /**
             * Lock {@link Object} to guard complete operations within this class.
             */
            protected final Object mValueLock = new Object();

            /**
             * {@code true} after {@link #onComplete()} gets called.
             */
            @GuardedBy("mValueLock")
            protected boolean mHasValue = false;

            /**
             * Base constructor.
             *
             * @param parentGroup {@link CancellationGroup} to which this completable object
             *                    belongs.
             */
            protected ValueBase(@NonNull CancellationGroup parentGroup) {
                mParentGroup = parentGroup;
            }

            /**
             * @return {@link true} if {@link #onComplete()} gets called already.
             */
            @AnyThread
            public boolean hasValue() {
                synchronized (mValueLock) {
                    return mHasValue;
                }
            }

            /**
             * Called by subclasses to signale {@link #mLatch}.
             */
            @AnyThread
            protected void onComplete() {
                mLatch.countDown();
            }

            /**
             * Blocks the calling thread until at least one of the following conditions is met.
             *
             * <p>
             *     <ol>
             *         <li>This object becomes ready to return the value.</li>
             *         <li>{@link CancellationGroup#cancelAll()} gets called.</li>
             *         <li>The given timeout period has passed.</li>
             *     </ol>
             * </p>
             *
             * <p>The caller can distinguish the case 1 and case 2 by calling {@link #hasValue()}.
             * Note that the return value of {@link #hasValue()} can change from {@code false} to
             * {@code true} at any time, even after this methods finishes with returning
             * {@code true}.</p>
             *
             * @param timeout length of the timeout.
             * @param timeUnit unit of {@code timeout}.
             * @return {@code false} if and only if the given timeout period has passed. Otherwise
             *         {@code true}.
             */
            @AnyThread
            public boolean await(int timeout, @NonNull TimeUnit timeUnit) {
                if (!mParentGroup.registerLatch(mLatch)) {
                    // Already canceled when this method gets called.
                    return false;
                }
                try {
                    return mLatch.await(timeout, timeUnit);
                } catch (InterruptedException e) {
                    return true;
                } finally {
                    mParentGroup.unregisterLatch(mLatch);
                }
            }
        }

        /**
         * Completable object of integer primitive.
         */
        public static final class Int extends ValueBase {
            @GuardedBy("mValueLock")
            private int mValue = 0;

            private Int(@NonNull CancellationGroup factory) {
                super(factory);
            }

            /**
             * Notify when a value is set to this completable object.
             *
             * @param value value to be set.
             */
            @AnyThread
            void onComplete(int value) {
                synchronized (mValueLock) {
                    if (mHasValue) {
                        throw new UnsupportedOperationException(
                                "onComplete() cannot be called multiple times");
                    }
                    mValue = value;
                    mHasValue = true;
                }
                onComplete();
            }

            /**
             * @return value associated with this object.
             * @throws UnsupportedOperationException when called while {@link #hasValue()} returns
             *                                       {@code false}.
             */
            @AnyThread
            public int getValue() {
                synchronized (mValueLock) {
                    if (!mHasValue) {
                        throw new UnsupportedOperationException(
                                "getValue() is allowed only if hasValue() returns true");
                    }
                    return mValue;
                }
            }
        }

        /**
         * Base class of completable object types.
         *
         * @param <T> type associated with this completable object.
         */
        public static class Values<T> extends ValueBase {
            @GuardedBy("mValueLock")
            @Nullable
            private T mValue = null;

            protected Values(@NonNull CancellationGroup factory) {
                super(factory);
            }

            /**
             * Notify when a value is set to this completable value object.
             *
             * @param value value to be set.
             */
            @AnyThread
            void onComplete(@Nullable T value) {
                synchronized (mValueLock) {
                    if (mHasValue) {
                        throw new UnsupportedOperationException(
                                "onComplete() cannot be called multiple times");
                    }
                    mValue = value;
                    mHasValue = true;
                }
                onComplete();
            }

            /**
             * @return value associated with this object.
             * @throws UnsupportedOperationException when called while {@link #hasValue()} returns
             *                                       {@code false}.
             */
            @AnyThread
            @Nullable
            public T getValue() {
                synchronized (mValueLock) {
                    if (!mHasValue) {
                        throw new UnsupportedOperationException(
                                "getValue() is allowed only if hasValue() returns true");
                    }
                    return mValue;
                }
            }
        }

        /**
         * Completable object of {@link java.lang.CharSequence}.
         */
        public static final class CharSequence extends Values<java.lang.CharSequence> {
            private CharSequence(@NonNull CancellationGroup factory) {
                super(factory);
            }
        }

        /**
         * Completable object of {@link android.view.inputmethod.ExtractedText}.
         */
        public static final class ExtractedText
                extends Values<android.view.inputmethod.ExtractedText> {
            private ExtractedText(@NonNull CancellationGroup factory) {
                super(factory);
            }
        }
    }

    /**
     * @return an instance of {@link Completable.Int} that is associated with this
     *         {@link CancellationGroup}.
     */
    @AnyThread
    public Completable.Int createCompletableInt() {
        return new Completable.Int(this);
    }

    /**
     * @return an instance of {@link Completable.CharSequence} that is associated with this
     *         {@link CancellationGroup}.
     */
    @AnyThread
    public Completable.CharSequence createCompletableCharSequence() {
        return new Completable.CharSequence(this);
    }

    /**
     * @return an instance of {@link Completable.ExtractedText} that is associated with this
     *         {@link CancellationGroup}.
     */
    @AnyThread
    public Completable.ExtractedText createCompletableExtractedText() {
        return new Completable.ExtractedText(this);
    }

    @AnyThread
    private boolean registerLatch(@NonNull CountDownLatch latch) {
        synchronized (mLock) {
            if (mCanceled) {
                return false;
            }
            if (mLatchList == null) {
                // Set the initial capacity to 1 with an assumption that usually there is up to 1
                // on-going operation.
                mLatchList = new ArrayList<>(1);
            }
            mLatchList.add(latch);
            return true;
        }
    }

    @AnyThread
    private void unregisterLatch(@NonNull CountDownLatch latch) {
        synchronized (mLock) {
            if (mLatchList != null) {
                mLatchList.remove(latch);
            }
        }
    }

    /**
     * Cancel all the completable objects created from this {@link CancellationGroup}.
     *
     * <p>Secondary calls will be silently ignored.</p>
     */
    @AnyThread
    public void cancelAll() {
        synchronized (mLock) {
            if (!mCanceled) {
                mCanceled = true;
                if (mLatchList != null) {
                    mLatchList.forEach(CountDownLatch::countDown);
                    mLatchList.clear();
                    mLatchList = null;
                }
            }
        }
    }

    /**
     * @return {@code true} if {@link #cancelAll()} is already called. {@code false} otherwise.
     */
    @AnyThread
    public boolean isCanceled() {
        synchronized (mLock) {
            return mCanceled;
        }
    }
}
+21 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 com.android.internal.inputmethod;

oneway interface ICharSequenceResultCallback {
    void onResult(in CharSequence result);
}
+4 −13
Original line number Diff line number Diff line
/*
 * Copyright (C) 2008 The Android Open Source Project
 * Copyright (C) 2020 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.
@@ -14,19 +14,10 @@
 * limitations under the License.
 */

package com.android.internal.view;
package com.android.internal.inputmethod;

import android.view.inputmethod.ExtractedText;

/**
 * {@hide}
 */
oneway interface IInputContextCallback {
    void setTextBeforeCursor(CharSequence textBeforeCursor, int seq);
    void setTextAfterCursor(CharSequence textAfterCursor, int seq);
    void setCursorCapsMode(int capsMode, int seq);
    void setExtractedText(in ExtractedText extractedText, int seq);
    void setSelectedText(CharSequence selectedText, int seq);
    void setRequestUpdateCursorAnchorInfoResult(boolean result, int seq);
    void setCommitContentResult(boolean result, int seq);
oneway interface IExtractedTextResultCallback {
    void onResult(in ExtractedText result);
}
Loading