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

Commit dbe201d2 authored by Yohei Yukawa's avatar Yohei Yukawa Committed by Android (Google) Code Review
Browse files

Merge "Let blocked InputConnection APIs fail upon IInputMethod.unbindInput()" into rvc-dev

parents b23d4176 f87f7508
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