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

This commit is contained in:
Yohei Yukawa
2020-04-03 02:11:04 +00:00
committed by Android (Google) Code Review
10 changed files with 792 additions and 498 deletions

View File

@@ -18,6 +18,7 @@ package android.inputmethodservice;
import android.annotation.BinderThread; import android.annotation.BinderThread;
import android.annotation.MainThread; import android.annotation.MainThread;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage; import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
@@ -37,6 +38,7 @@ import android.view.inputmethod.InputMethodSession;
import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations;
import com.android.internal.inputmethod.CancellationGroup;
import com.android.internal.os.HandlerCaller; import com.android.internal.os.HandlerCaller;
import com.android.internal.os.SomeArgs; import com.android.internal.os.SomeArgs;
import com.android.internal.view.IInlineSuggestionsRequestCallback; import com.android.internal.view.IInlineSuggestionsRequestCallback;
@@ -52,7 +54,6 @@ import java.io.PrintWriter;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* Implements the internal IInputMethod interface to convert incoming calls * 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 * <p>This field must be set and cleared only from the binder thread(s), where the system
* guarantees that {@link #bindInput(InputBinding)}, * 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 * {@link #unbindInput()} are called with the same order as the original calls
* in {@link com.android.server.inputmethod.InputMethodManagerService}. * in {@link com.android.server.inputmethod.InputMethodManagerService}.
* See {@link IBinder#FLAG_ONEWAY} for detailed semantics.</p> * See {@link IBinder#FLAG_ONEWAY} for detailed semantics.</p>
*/ */
AtomicBoolean mIsUnbindIssued = null; @Nullable
CancellationGroup mCancellationGroup = null;
// NOTE: we should have a cache of these. // NOTE: we should have a cache of these.
static final class InputMethodSessionCallbackWrapper implements InputMethod.SessionCallback { static final class InputMethodSessionCallbackWrapper implements InputMethod.SessionCallback {
@@ -187,11 +189,11 @@ class IInputMethodWrapper extends IInputMethod.Stub
final IBinder startInputToken = (IBinder) args.arg1; final IBinder startInputToken = (IBinder) args.arg1;
final IInputContext inputContext = (IInputContext) args.arg2; final IInputContext inputContext = (IInputContext) args.arg2;
final EditorInfo info = (EditorInfo) args.arg3; final EditorInfo info = (EditorInfo) args.arg3;
final AtomicBoolean isUnbindIssued = (AtomicBoolean) args.arg4; final CancellationGroup cancellationGroup = (CancellationGroup) args.arg4;
SomeArgs moreArgs = (SomeArgs) args.arg5; SomeArgs moreArgs = (SomeArgs) args.arg5;
final InputConnection ic = inputContext != null final InputConnection ic = inputContext != null
? new InputConnectionWrapper( ? new InputConnectionWrapper(
mTarget, inputContext, moreArgs.argi3, isUnbindIssued) mTarget, inputContext, moreArgs.argi3, cancellationGroup)
: null; : null;
info.makeCompatible(mTargetSdkVersion); info.makeCompatible(mTargetSdkVersion);
inputMethod.dispatchStartInputWithToken( inputMethod.dispatchStartInputWithToken(
@@ -295,15 +297,15 @@ class IInputMethodWrapper extends IInputMethod.Stub
@BinderThread @BinderThread
@Override @Override
public void bindInput(InputBinding binding) { public void bindInput(InputBinding binding) {
if (mIsUnbindIssued != null) { if (mCancellationGroup != null) {
Log.e(TAG, "bindInput must be paired with unbindInput."); Log.e(TAG, "bindInput must be paired with unbindInput.");
} }
mIsUnbindIssued = new AtomicBoolean(); mCancellationGroup = new CancellationGroup();
// This IInputContext is guaranteed to implement all the methods. // This IInputContext is guaranteed to implement all the methods.
final int missingMethodFlags = 0; final int missingMethodFlags = 0;
InputConnection ic = new InputConnectionWrapper(mTarget, InputConnection ic = new InputConnectionWrapper(mTarget,
IInputContext.Stub.asInterface(binding.getConnectionToken()), missingMethodFlags, IInputContext.Stub.asInterface(binding.getConnectionToken()), missingMethodFlags,
mIsUnbindIssued); mCancellationGroup);
InputBinding nu = new InputBinding(ic, binding); InputBinding nu = new InputBinding(ic, binding);
mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_INPUT_CONTEXT, nu)); mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_INPUT_CONTEXT, nu));
} }
@@ -311,10 +313,10 @@ class IInputMethodWrapper extends IInputMethod.Stub
@BinderThread @BinderThread
@Override @Override
public void unbindInput() { public void unbindInput() {
if (mIsUnbindIssued != null) { if (mCancellationGroup != null) {
// Signal the flag then forget it. // Signal the flag then forget it.
mIsUnbindIssued.set(true); mCancellationGroup.cancelAll();
mIsUnbindIssued = null; mCancellationGroup = null;
} else { } else {
Log.e(TAG, "unbindInput must be paired with bindInput."); 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, public void startInput(IBinder startInputToken, IInputContext inputContext,
@InputConnectionInspector.MissingMethodFlags final int missingMethods, @InputConnectionInspector.MissingMethodFlags final int missingMethods,
EditorInfo attribute, boolean restarting, boolean shouldPreRenderIme) { EditorInfo attribute, boolean restarting, boolean shouldPreRenderIme) {
if (mIsUnbindIssued == null) { if (mCancellationGroup == null) {
Log.e(TAG, "startInput must be called after bindInput."); Log.e(TAG, "startInput must be called after bindInput.");
mIsUnbindIssued = new AtomicBoolean(); mCancellationGroup = new CancellationGroup();
} }
SomeArgs args = SomeArgs.obtain(); SomeArgs args = SomeArgs.obtain();
args.argi1 = restarting ? 1 : 0; args.argi1 = restarting ? 1 : 0;
args.argi2 = shouldPreRenderIme ? 1 : 0; args.argi2 = shouldPreRenderIme ? 1 : 0;
args.argi3 = missingMethods; args.argi3 = missingMethods;
mCaller.executeOrSendMessage(mCaller.obtainMessageOOOOO( mCaller.executeOrSendMessage(mCaller.obtainMessageOOOOO(DO_START_INPUT, startInputToken,
DO_START_INPUT, startInputToken, inputContext, attribute, mIsUnbindIssued, args)); inputContext, attribute, mCancellationGroup, args));
} }
@BinderThread @BinderThread

View File

@@ -39,6 +39,7 @@ import android.view.inputmethod.ExtractedText;
import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.GuardedBy;
import com.android.internal.inputmethod.IMultiClientInputMethodSession; import com.android.internal.inputmethod.IMultiClientInputMethodSession;
import com.android.internal.inputmethod.CancellationGroup;
import com.android.internal.os.SomeArgs; import com.android.internal.os.SomeArgs;
import com.android.internal.util.function.pooled.PooledLambda; import com.android.internal.util.function.pooled.PooledLambda;
import com.android.internal.view.IInputContext; import com.android.internal.view.IInputContext;
@@ -46,7 +47,6 @@ import com.android.internal.view.IInputMethodSession;
import com.android.internal.view.InputConnectionWrapper; import com.android.internal.view.InputConnectionWrapper;
import java.lang.ref.WeakReference; 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. * Re-dispatches all the incoming per-client events to the specified {@link Looper} thread.
@@ -80,19 +80,19 @@ final class MultiClientInputMethodClientCallbackAdaptor {
@Nullable @Nullable
InputEventReceiver mInputEventReceiver; InputEventReceiver mInputEventReceiver;
private final AtomicBoolean mFinished = new AtomicBoolean(false); private final CancellationGroup mCancellationGroup = new CancellationGroup();
IInputMethodSession.Stub createIInputMethodSession() { IInputMethodSession.Stub createIInputMethodSession() {
synchronized (mSessionLock) { synchronized (mSessionLock) {
return new InputMethodSessionImpl( return new InputMethodSessionImpl(
mSessionLock, mCallbackImpl, mHandler, mFinished); mSessionLock, mCallbackImpl, mHandler, mCancellationGroup);
} }
} }
IMultiClientInputMethodSession.Stub createIMultiClientInputMethodSession() { IMultiClientInputMethodSession.Stub createIMultiClientInputMethodSession() {
synchronized (mSessionLock) { synchronized (mSessionLock) {
return new MultiClientInputMethodSessionImpl( return new MultiClientInputMethodSessionImpl(
mSessionLock, mCallbackImpl, mHandler, mFinished); mSessionLock, mCallbackImpl, mHandler, mCancellationGroup);
} }
} }
@@ -105,7 +105,7 @@ final class MultiClientInputMethodClientCallbackAdaptor {
mHandler = new Handler(looper, null, true); mHandler = new Handler(looper, null, true);
mReadChannel = readChannel; mReadChannel = readChannel;
mInputEventReceiver = new ImeInputEventReceiver(mReadChannel, mHandler.getLooper(), 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 static final class ImeInputEventReceiver extends InputEventReceiver {
private final AtomicBoolean mFinished; private final CancellationGroup mCancellationGroupOnFinishSession;
private final KeyEvent.DispatcherState mDispatcherState; private final KeyEvent.DispatcherState mDispatcherState;
private final MultiClientInputMethodServiceDelegate.ClientCallback mClientCallback; private final MultiClientInputMethodServiceDelegate.ClientCallback mClientCallback;
private final KeyEventCallbackAdaptor mKeyEventCallbackAdaptor; private final KeyEventCallbackAdaptor mKeyEventCallbackAdaptor;
ImeInputEventReceiver(InputChannel readChannel, Looper looper, AtomicBoolean finished, ImeInputEventReceiver(InputChannel readChannel, Looper looper,
CancellationGroup cancellationGroupOnFinishSession,
KeyEvent.DispatcherState dispatcherState, KeyEvent.DispatcherState dispatcherState,
MultiClientInputMethodServiceDelegate.ClientCallback callback) { MultiClientInputMethodServiceDelegate.ClientCallback callback) {
super(readChannel, looper); super(readChannel, looper);
mFinished = finished; mCancellationGroupOnFinishSession = cancellationGroupOnFinishSession;
mDispatcherState = dispatcherState; mDispatcherState = dispatcherState;
mClientCallback = callback; mClientCallback = callback;
mKeyEventCallbackAdaptor = new KeyEventCallbackAdaptor(callback); mKeyEventCallbackAdaptor = new KeyEventCallbackAdaptor(callback);
@@ -156,7 +157,7 @@ final class MultiClientInputMethodClientCallbackAdaptor {
@Override @Override
public void onInputEvent(InputEvent event) { public void onInputEvent(InputEvent event) {
if (mFinished.get()) { if (mCancellationGroupOnFinishSession.isCanceled()) {
// The session has been finished. // The session has been finished.
finishInputEvent(event, false); finishInputEvent(event, false);
return; return;
@@ -187,14 +188,14 @@ final class MultiClientInputMethodClientCallbackAdaptor {
private CallbackImpl mCallbackImpl; private CallbackImpl mCallbackImpl;
@GuardedBy("mSessionLock") @GuardedBy("mSessionLock")
private Handler mHandler; private Handler mHandler;
private final AtomicBoolean mSessionFinished; private final CancellationGroup mCancellationGroupOnFinishSession;
InputMethodSessionImpl(Object lock, CallbackImpl callback, Handler handler, InputMethodSessionImpl(Object lock, CallbackImpl callback, Handler handler,
AtomicBoolean sessionFinished) { CancellationGroup cancellationGroupOnFinishSession) {
mSessionLock = lock; mSessionLock = lock;
mCallbackImpl = callback; mCallbackImpl = callback;
mHandler = handler; mHandler = handler;
mSessionFinished = sessionFinished; mCancellationGroupOnFinishSession = cancellationGroupOnFinishSession;
} }
@Override @Override
@@ -272,7 +273,7 @@ final class MultiClientInputMethodClientCallbackAdaptor {
if (mCallbackImpl == null || mHandler == null) { if (mCallbackImpl == null || mHandler == null) {
return; return;
} }
mSessionFinished.set(true); mCancellationGroupOnFinishSession.cancelAll();
mHandler.sendMessage(PooledLambda.obtainMessage( mHandler.sendMessage(PooledLambda.obtainMessage(
CallbackImpl::finishSession, mCallbackImpl)); CallbackImpl::finishSession, mCallbackImpl));
mCallbackImpl = null; mCallbackImpl = null;
@@ -311,14 +312,14 @@ final class MultiClientInputMethodClientCallbackAdaptor {
private CallbackImpl mCallbackImpl; private CallbackImpl mCallbackImpl;
@GuardedBy("mSessionLock") @GuardedBy("mSessionLock")
private Handler mHandler; private Handler mHandler;
private final AtomicBoolean mSessionFinished; private final CancellationGroup mCancellationGroupOnFinishSession;
MultiClientInputMethodSessionImpl(Object lock, CallbackImpl callback, MultiClientInputMethodSessionImpl(Object lock, CallbackImpl callback,
Handler handler, AtomicBoolean sessionFinished) { Handler handler, CancellationGroup cancellationGroupOnFinishSession) {
mSessionLock = lock; mSessionLock = lock;
mCallbackImpl = callback; mCallbackImpl = callback;
mHandler = handler; mHandler = handler;
mSessionFinished = sessionFinished; mCancellationGroupOnFinishSession = cancellationGroupOnFinishSession;
} }
@Override @Override
@@ -335,7 +336,7 @@ final class MultiClientInputMethodClientCallbackAdaptor {
new WeakReference<>(null); new WeakReference<>(null);
args.arg1 = (inputContext == null) ? null args.arg1 = (inputContext == null) ? null
: new InputConnectionWrapper(fakeIMS, inputContext, missingMethods, : new InputConnectionWrapper(fakeIMS, inputContext, missingMethods,
mSessionFinished); mCancellationGroupOnFinishSession);
args.arg2 = editorInfo; args.arg2 = editorInfo;
args.argi1 = controlFlags; args.argi1 = controlFlags;
args.argi2 = softInputMode; args.argi2 = softInputMode;

View File

@@ -0,0 +1,348 @@
/*
* 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;
}
}
}

View File

@@ -0,0 +1,21 @@
/*
* 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);
}

View File

@@ -1,5 +1,5 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -14,19 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.internal.view; package com.android.internal.inputmethod;
import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedText;
/** oneway interface IExtractedTextResultCallback {
* {@hide} void onResult(in ExtractedText result);
*/
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);
} }

View File

@@ -0,0 +1,21 @@
/*
* 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 IIntResultCallback {
void onResult(int result);
}

View File

@@ -0,0 +1,132 @@
/*
* 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.BinderThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicReference;
/**
* Defines a set of factory methods to create {@link android.os.IBinder}-based callbacks that are
* associated with completable objects defined in {@link CancellationGroup.Completable}.
*/
public final class ResultCallbacks {
/**
* Not intended to be instantiated.
*/
private ResultCallbacks() {
}
@AnyThread
@Nullable
private static <T> T unwrap(@NonNull AtomicReference<WeakReference<T>> atomicRef) {
final WeakReference<T> ref = atomicRef.getAndSet(null);
if (ref == null) {
// Double-call is guaranteed to be ignored here.
return null;
}
final T value = ref.get();
ref.clear();
return value;
}
/**
* Creates {@link IIntResultCallback.Stub} that is to set
* {@link CancellationGroup.Completable.Int} when receiving the result.
*
* @param value {@link CancellationGroup.Completable.Int} to be set when receiving the result.
* @return {@link IIntResultCallback.Stub} that can be passed as a binder IPC parameter.
*/
@AnyThread
public static IIntResultCallback.Stub of(@NonNull CancellationGroup.Completable.Int value) {
final AtomicReference<WeakReference<CancellationGroup.Completable.Int>>
atomicRef = new AtomicReference<>(new WeakReference<>(value));
return new IIntResultCallback.Stub() {
@BinderThread
@Override
public void onResult(int result) {
final CancellationGroup.Completable.Int value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onComplete(result);
}
};
}
/**
* Creates {@link ICharSequenceResultCallback.Stub} that is to set
* {@link CancellationGroup.Completable.CharSequence} when receiving the result.
*
* @param value {@link CancellationGroup.Completable.CharSequence} to be set when receiving the
* result.
* @return {@link ICharSequenceResultCallback.Stub} that can be passed as a binder IPC
* parameter.
*/
@AnyThread
public static ICharSequenceResultCallback.Stub of(
@NonNull CancellationGroup.Completable.CharSequence value) {
final AtomicReference<WeakReference<CancellationGroup.Completable.CharSequence>> atomicRef =
new AtomicReference<>(new WeakReference<>(value));
return new ICharSequenceResultCallback.Stub() {
@BinderThread
@Override
public void onResult(CharSequence result) {
final CancellationGroup.Completable.CharSequence value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onComplete(result);
}
};
}
/**
* Creates {@link IExtractedTextResultCallback.Stub} that is to set
* {@link CancellationGroup.Completable.ExtractedText} when receiving the result.
*
* @param value {@link CancellationGroup.Completable.ExtractedText} to be set when receiving the
* result.
* @return {@link IExtractedTextResultCallback.Stub} that can be passed as a binder IPC
* parameter.
*/
@AnyThread
public static IExtractedTextResultCallback.Stub of(
@NonNull CancellationGroup.Completable.ExtractedText value) {
final AtomicReference<WeakReference<CancellationGroup.Completable.ExtractedText>>
atomicRef = new AtomicReference<>(new WeakReference<>(value));
return new IExtractedTextResultCallback.Stub() {
@BinderThread
@Override
public void onResult(android.view.inputmethod.ExtractedText result) {
final CancellationGroup.Completable.ExtractedText value = unwrap(atomicRef);
if (value == null) {
return;
}
value.onComplete(result);
}
};
}
}

View File

@@ -29,6 +29,7 @@ import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputConnectionInspector; import android.view.inputmethod.InputConnectionInspector;
@@ -36,6 +37,9 @@ import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputContentInfo;
import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.GuardedBy;
import com.android.internal.inputmethod.ICharSequenceResultCallback;
import com.android.internal.inputmethod.IExtractedTextResultCallback;
import com.android.internal.inputmethod.IIntResultCallback;
import com.android.internal.os.SomeArgs; import com.android.internal.os.SomeArgs;
public abstract class IInputConnectionWrapper extends IInputContext.Stub { public abstract class IInputConnectionWrapper extends IInputContext.Stub {
@@ -111,28 +115,31 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub {
abstract protected boolean isActive(); abstract protected boolean isActive();
public void getTextAfterCursor(int length, int flags, int seq, IInputContextCallback callback) { public void getTextAfterCursor(int length, int flags, ICharSequenceResultCallback callback) {
dispatchMessage(obtainMessageIISC(DO_GET_TEXT_AFTER_CURSOR, length, flags, seq, callback)); dispatchMessage(mH.obtainMessage(DO_GET_TEXT_AFTER_CURSOR, length, flags, callback));
}
public void getTextBeforeCursor(int length, int flags, int seq, IInputContextCallback callback) {
dispatchMessage(obtainMessageIISC(DO_GET_TEXT_BEFORE_CURSOR, length, flags, seq, callback));
} }
public void getSelectedText(int flags, int seq, IInputContextCallback callback) { public void getTextBeforeCursor(int length, int flags, ICharSequenceResultCallback callback) {
dispatchMessage(obtainMessageISC(DO_GET_SELECTED_TEXT, flags, seq, callback)); dispatchMessage(mH.obtainMessage(DO_GET_TEXT_BEFORE_CURSOR, length, flags, callback));
} }
public void getCursorCapsMode(int reqModes, int seq, IInputContextCallback callback) { public void getSelectedText(int flags, ICharSequenceResultCallback callback) {
dispatchMessage(obtainMessageISC(DO_GET_CURSOR_CAPS_MODE, reqModes, seq, callback)); dispatchMessage(mH.obtainMessage(DO_GET_SELECTED_TEXT, flags, 0 /* unused */, callback));
} }
public void getExtractedText(ExtractedTextRequest request, public void getCursorCapsMode(int reqModes, IIntResultCallback callback) {
int flags, int seq, IInputContextCallback callback) { dispatchMessage(
dispatchMessage(obtainMessageIOSC(DO_GET_EXTRACTED_TEXT, flags, mH.obtainMessage(DO_GET_CURSOR_CAPS_MODE, reqModes, 0 /* unused */, callback));
request, seq, callback));
} }
public void getExtractedText(ExtractedTextRequest request, int flags,
IExtractedTextResultCallback callback) {
final SomeArgs args = SomeArgs.obtain();
args.arg1 = request;
args.arg2 = callback;
dispatchMessage(mH.obtainMessage(DO_GET_EXTRACTED_TEXT, flags, 0 /* unused */, args));
}
public void commitText(CharSequence text, int newCursorPosition) { public void commitText(CharSequence text, int newCursorPosition) {
dispatchMessage(obtainMessageIO(DO_COMMIT_TEXT, newCursorPosition, text)); dispatchMessage(obtainMessageIO(DO_COMMIT_TEXT, newCursorPosition, text));
} }
@@ -199,10 +206,9 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub {
dispatchMessage(obtainMessageOO(DO_PERFORM_PRIVATE_COMMAND, action, data)); dispatchMessage(obtainMessageOO(DO_PERFORM_PRIVATE_COMMAND, action, data));
} }
public void requestUpdateCursorAnchorInfo(int cursorUpdateMode, int seq, public void requestUpdateCursorAnchorInfo(int cursorUpdateMode, IIntResultCallback callback) {
IInputContextCallback callback) { dispatchMessage(mH.obtainMessage(DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO, cursorUpdateMode,
dispatchMessage(obtainMessageISC(DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO, cursorUpdateMode, 0 /* unused */, callback));
seq, callback));
} }
public void closeConnection() { public void closeConnection() {
@@ -210,9 +216,12 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub {
} }
public void commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts, public void commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts,
int seq, IInputContextCallback callback) { IIntResultCallback callback) {
dispatchMessage(obtainMessageIOOSC(DO_COMMIT_CONTENT, flags, inputContentInfo, opts, seq, final SomeArgs args = SomeArgs.obtain();
callback)); args.arg1 = inputContentInfo;
args.arg2 = opts;
args.arg3 = callback;
dispatchMessage(mH.obtainMessage(DO_COMMIT_CONTENT, flags, 0 /* unused */, args));
} }
void dispatchMessage(Message msg) { void dispatchMessage(Message msg) {
@@ -231,100 +240,97 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub {
void executeMessage(Message msg) { void executeMessage(Message msg) {
switch (msg.what) { switch (msg.what) {
case DO_GET_TEXT_AFTER_CURSOR: { case DO_GET_TEXT_AFTER_CURSOR: {
SomeArgs args = (SomeArgs)msg.obj; final ICharSequenceResultCallback callback = (ICharSequenceResultCallback) msg.obj;
final InputConnection ic = getInputConnection();
final CharSequence result;
if (ic == null || !isActive()) {
Log.w(TAG, "getTextAfterCursor on inactive InputConnection");
result = null;
} else {
result = ic.getTextAfterCursor(msg.arg1, msg.arg2);
}
try { try {
final IInputContextCallback callback = (IInputContextCallback) args.arg6; callback.onResult(result);
final int callbackSeq = args.argi6;
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "getTextAfterCursor on inactive InputConnection");
callback.setTextAfterCursor(null, callbackSeq);
return;
}
callback.setTextAfterCursor(ic.getTextAfterCursor(
msg.arg1, msg.arg2), callbackSeq);
} catch (RemoteException e) { } catch (RemoteException e) {
Log.w(TAG, "Got RemoteException calling setTextAfterCursor", e); Log.w(TAG, "Failed to return the result to getTextAfterCursor()."
} finally { + " result=" + result, e);
args.recycle();
} }
return; return;
} }
case DO_GET_TEXT_BEFORE_CURSOR: { case DO_GET_TEXT_BEFORE_CURSOR: {
SomeArgs args = (SomeArgs)msg.obj; final ICharSequenceResultCallback callback = (ICharSequenceResultCallback) msg.obj;
final InputConnection ic = getInputConnection();
final CharSequence result;
if (ic == null || !isActive()) {
Log.w(TAG, "getTextBeforeCursor on inactive InputConnection");
result = null;
} else {
result = ic.getTextBeforeCursor(msg.arg1, msg.arg2);
}
try { try {
final IInputContextCallback callback = (IInputContextCallback) args.arg6; callback.onResult(result);
final int callbackSeq = args.argi6;
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "getTextBeforeCursor on inactive InputConnection");
callback.setTextBeforeCursor(null, callbackSeq);
return;
}
callback.setTextBeforeCursor(ic.getTextBeforeCursor(
msg.arg1, msg.arg2), callbackSeq);
} catch (RemoteException e) { } catch (RemoteException e) {
Log.w(TAG, "Got RemoteException calling setTextBeforeCursor", e); Log.w(TAG, "Failed to return the result to getTextBeforeCursor()."
} finally { + " result=" + result, e);
args.recycle();
} }
return; return;
} }
case DO_GET_SELECTED_TEXT: { case DO_GET_SELECTED_TEXT: {
SomeArgs args = (SomeArgs)msg.obj; final ICharSequenceResultCallback callback = (ICharSequenceResultCallback) msg.obj;
final InputConnection ic = getInputConnection();
final CharSequence result;
if (ic == null || !isActive()) {
Log.w(TAG, "getSelectedText on inactive InputConnection");
result = null;
} else {
result = ic.getSelectedText(msg.arg1);
}
try { try {
final IInputContextCallback callback = (IInputContextCallback) args.arg6; callback.onResult(result);
final int callbackSeq = args.argi6;
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "getSelectedText on inactive InputConnection");
callback.setSelectedText(null, callbackSeq);
return;
}
callback.setSelectedText(ic.getSelectedText(
msg.arg1), callbackSeq);
} catch (RemoteException e) { } catch (RemoteException e) {
Log.w(TAG, "Got RemoteException calling setSelectedText", e); Log.w(TAG, "Failed to return the result to getSelectedText()."
} finally { + " result=" + result, e);
args.recycle();
} }
return; return;
} }
case DO_GET_CURSOR_CAPS_MODE: { case DO_GET_CURSOR_CAPS_MODE: {
SomeArgs args = (SomeArgs)msg.obj; final IIntResultCallback callback = (IIntResultCallback) msg.obj;
final InputConnection ic = getInputConnection();
final int result;
if (ic == null || !isActive()) {
Log.w(TAG, "getCursorCapsMode on inactive InputConnection");
result = 0;
} else {
result = ic.getCursorCapsMode(msg.arg1);
}
try { try {
final IInputContextCallback callback = (IInputContextCallback) args.arg6; callback.onResult(result);
final int callbackSeq = args.argi6;
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "getCursorCapsMode on inactive InputConnection");
callback.setCursorCapsMode(0, callbackSeq);
return;
}
callback.setCursorCapsMode(ic.getCursorCapsMode(msg.arg1),
callbackSeq);
} catch (RemoteException e) { } catch (RemoteException e) {
Log.w(TAG, "Got RemoteException calling setCursorCapsMode", e); Log.w(TAG, "Failed to return the result to getCursorCapsMode()."
} finally { + " result=" + result, e);
args.recycle();
} }
return; return;
} }
case DO_GET_EXTRACTED_TEXT: { case DO_GET_EXTRACTED_TEXT: {
SomeArgs args = (SomeArgs)msg.obj; final SomeArgs args = (SomeArgs) msg.obj;
try { try {
final IInputContextCallback callback = (IInputContextCallback) args.arg6; final ExtractedTextRequest request = (ExtractedTextRequest) args.arg1;
final int callbackSeq = args.argi6; final IExtractedTextResultCallback callback =
InputConnection ic = getInputConnection(); (IExtractedTextResultCallback) args.arg2;
final InputConnection ic = getInputConnection();
final ExtractedText result;
if (ic == null || !isActive()) { if (ic == null || !isActive()) {
Log.w(TAG, "getExtractedText on inactive InputConnection"); Log.w(TAG, "getExtractedText on inactive InputConnection");
callback.setExtractedText(null, callbackSeq); result = null;
return; } else {
result = ic.getExtractedText(request, msg.arg1);
}
try {
callback.onResult(result);
} catch (RemoteException e) {
Log.w(TAG, "Failed to return the result to getExtractedText()."
+ " result=" + result, e);
} }
callback.setExtractedText(ic.getExtractedText(
(ExtractedTextRequest)args.arg1, msg.arg1), callbackSeq);
} catch (RemoteException e) {
Log.w(TAG, "Got RemoteException calling setExtractedText", e);
} finally { } finally {
args.recycle(); args.recycle();
} }
@@ -494,22 +500,20 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub {
return; return;
} }
case DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO: { case DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO: {
SomeArgs args = (SomeArgs)msg.obj; final IIntResultCallback callback = (IIntResultCallback) msg.obj;
final InputConnection ic = getInputConnection();
final boolean result;
if (ic == null || !isActive()) {
Log.w(TAG, "requestCursorAnchorInfo on inactive InputConnection");
result = false;
} else {
result = ic.requestCursorUpdates(msg.arg1);
}
try { try {
final IInputContextCallback callback = (IInputContextCallback) args.arg6; callback.onResult(result ? 1 : 0);
final int callbackSeq = args.argi6;
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "requestCursorAnchorInfo on inactive InputConnection");
callback.setRequestUpdateCursorAnchorInfoResult(false, callbackSeq);
return;
}
callback.setRequestUpdateCursorAnchorInfoResult(
ic.requestCursorUpdates(msg.arg1), callbackSeq);
} catch (RemoteException e) { } catch (RemoteException e) {
Log.w(TAG, "Got RemoteException calling requestCursorAnchorInfo", e); Log.w(TAG, "Failed to return the result to requestCursorUpdates()."
} finally { + " result=" + result, e);
args.recycle();
} }
return; return;
} }
@@ -547,26 +551,28 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub {
final int flags = msg.arg1; final int flags = msg.arg1;
SomeArgs args = (SomeArgs) msg.obj; SomeArgs args = (SomeArgs) msg.obj;
try { try {
final IInputContextCallback callback = (IInputContextCallback) args.arg6; final IIntResultCallback callback = (IIntResultCallback) args.arg3;
final int callbackSeq = args.argi6; final InputConnection ic = getInputConnection();
InputConnection ic = getInputConnection(); final boolean result;
if (ic == null || !isActive()) { if (ic == null || !isActive()) {
Log.w(TAG, "commitContent on inactive InputConnection"); Log.w(TAG, "commitContent on inactive InputConnection");
callback.setCommitContentResult(false, callbackSeq); result = false;
return; } else {
final InputContentInfo inputContentInfo = (InputContentInfo) args.arg1;
if (inputContentInfo == null || !inputContentInfo.validate()) {
Log.w(TAG, "commitContent with invalid inputContentInfo="
+ inputContentInfo);
result = false;
} else {
result = ic.commitContent(inputContentInfo, flags, (Bundle) args.arg2);
}
} }
final InputContentInfo inputContentInfo = (InputContentInfo) args.arg1; try {
if (inputContentInfo == null || !inputContentInfo.validate()) { callback.onResult(result ? 1 : 0);
Log.w(TAG, "commitContent with invalid inputContentInfo=" } catch (RemoteException e) {
+ inputContentInfo); Log.w(TAG, "Failed to return the result to commitContent()."
callback.setCommitContentResult(false, callbackSeq); + " result=" + result, e);
return;
} }
final boolean result =
ic.commitContent(inputContentInfo, flags, (Bundle) args.arg2);
callback.setCommitContentResult(result, callbackSeq);
} catch (RemoteException e) {
Log.w(TAG, "Got RemoteException calling commitContent", e);
} finally { } finally {
args.recycle(); args.recycle();
} }
@@ -588,40 +594,6 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub {
return mH.obtainMessage(what, 0, 0, arg1); return mH.obtainMessage(what, 0, 0, arg1);
} }
Message obtainMessageISC(int what, int arg1, int callbackSeq, IInputContextCallback callback) {
final SomeArgs args = SomeArgs.obtain();
args.arg6 = callback;
args.argi6 = callbackSeq;
return mH.obtainMessage(what, arg1, 0, args);
}
Message obtainMessageIISC(int what, int arg1, int arg2, int callbackSeq,
IInputContextCallback callback) {
final SomeArgs args = SomeArgs.obtain();
args.arg6 = callback;
args.argi6 = callbackSeq;
return mH.obtainMessage(what, arg1, arg2, args);
}
Message obtainMessageIOOSC(int what, int arg1, Object objArg1, Object objArg2, int callbackSeq,
IInputContextCallback callback) {
final SomeArgs args = SomeArgs.obtain();
args.arg1 = objArg1;
args.arg2 = objArg2;
args.arg6 = callback;
args.argi6 = callbackSeq;
return mH.obtainMessage(what, arg1, 0, args);
}
Message obtainMessageIOSC(int what, int arg1, Object arg2, int callbackSeq,
IInputContextCallback callback) {
final SomeArgs args = SomeArgs.obtain();
args.arg1 = arg2;
args.arg6 = callback;
args.argi6 = callbackSeq;
return mH.obtainMessage(what, arg1, 0, args);
}
Message obtainMessageIO(int what, int arg1, Object arg2) { Message obtainMessageIO(int what, int arg1, Object arg2) {
return mH.obtainMessage(what, arg1, 0, arg2); return mH.obtainMessage(what, arg1, 0, arg2);
} }

View File

@@ -23,7 +23,9 @@ import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputContentInfo;
import com.android.internal.view.IInputContextCallback; import com.android.internal.inputmethod.ICharSequenceResultCallback;
import com.android.internal.inputmethod.IExtractedTextResultCallback;
import com.android.internal.inputmethod.IIntResultCallback;
/** /**
* Interface from an input method to the application, allowing it to perform * Interface from an input method to the application, allowing it to perform
@@ -31,14 +33,14 @@ import com.android.internal.view.IInputContextCallback;
* {@hide} * {@hide}
*/ */
oneway interface IInputContext { oneway interface IInputContext {
void getTextBeforeCursor(int length, int flags, int seq, IInputContextCallback callback); void getTextBeforeCursor(int length, int flags, ICharSequenceResultCallback callback);
void getTextAfterCursor(int length, int flags, int seq, IInputContextCallback callback); void getTextAfterCursor(int length, int flags, ICharSequenceResultCallback callback);
void getCursorCapsMode(int reqModes, int seq, IInputContextCallback callback); void getCursorCapsMode(int reqModes, IIntResultCallback callback);
void getExtractedText(in ExtractedTextRequest request, int flags, int seq, void getExtractedText(in ExtractedTextRequest request, int flags,
IInputContextCallback callback); IExtractedTextResultCallback callback);
void deleteSurroundingText(int beforeLength, int afterLength); void deleteSurroundingText(int beforeLength, int afterLength);
void deleteSurroundingTextInCodePoints(int beforeLength, int afterLength); void deleteSurroundingTextInCodePoints(int beforeLength, int afterLength);
@@ -71,11 +73,10 @@ import com.android.internal.view.IInputContextCallback;
void setComposingRegion(int start, int end); void setComposingRegion(int start, int end);
void getSelectedText(int flags, int seq, IInputContextCallback callback); void getSelectedText(int flags, ICharSequenceResultCallback callback);
void requestUpdateCursorAnchorInfo(int cursorUpdateMode, int seq, void requestUpdateCursorAnchorInfo(int cursorUpdateMode, IIntResultCallback callback);
IInputContextCallback callback);
void commitContent(in InputContentInfo inputContentInfo, int flags, in Bundle opts, int sec, void commitContent(in InputContentInfo inputContentInfo, int flags, in Bundle opts,
IInputContextCallback callback); IIntResultCallback callback);
} }

View File

@@ -17,14 +17,12 @@
package com.android.internal.view; package com.android.internal.view;
import android.annotation.AnyThread; import android.annotation.AnyThread;
import android.annotation.BinderThread;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage; import android.annotation.Nullable;
import android.inputmethodservice.AbstractInputMethodService; import android.inputmethodservice.AbstractInputMethodService;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CompletionInfo;
@@ -36,10 +34,15 @@ import android.view.inputmethod.InputConnectionInspector;
import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags; import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputContentInfo;
import com.android.internal.inputmethod.CancellationGroup;
import com.android.internal.inputmethod.ResultCallbacks;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.TimeUnit;
public class InputConnectionWrapper implements InputConnection { public class InputConnectionWrapper implements InputConnection {
private static final String TAG = "InputConnectionWrapper";
private static final int MAX_WAIT_TIME_MILLIS = 2000; private static final int MAX_WAIT_TIME_MILLIS = 2000;
private final IInputContext mIInputContext; private final IInputContext mIInputContext;
@NonNull @NonNull
@@ -49,257 +52,94 @@ public class InputConnectionWrapper implements InputConnection {
private final int mMissingMethods; private final int mMissingMethods;
/** /**
* {@code true} if the system already decided to take away IME focus from the target app. This * Signaled when the system decided to take away IME focus from the target app.
* can be signaled even when the corresponding signal is in the task queue and *
* {@link InputMethodService#onUnbindInput()} is not yet called back on the UI thread. * <p>This is expected to be signaled immediately when the IME process receives
* {@link IInputMethod#unbindInput()}.</p>
*/ */
@NonNull @NonNull
private final AtomicBoolean mIsUnbindIssued; private final CancellationGroup mCancellationGroup;
static class InputContextCallback extends IInputContextCallback.Stub {
private static final String TAG = "InputConnectionWrapper.ICC";
public int mSeq;
public boolean mHaveValue;
public CharSequence mTextBeforeCursor;
public CharSequence mTextAfterCursor;
public CharSequence mSelectedText;
public ExtractedText mExtractedText;
public int mCursorCapsMode;
public boolean mRequestUpdateCursorAnchorInfoResult;
public boolean mCommitContentResult;
// A 'pool' of one InputContextCallback. Each ICW request will attempt to gain
// exclusive access to this object.
private static InputContextCallback sInstance = new InputContextCallback();
private static int sSequenceNumber = 1;
/**
* Returns an InputContextCallback object that is guaranteed not to be in use by
* any other thread. The returned object's 'have value' flag is cleared and its expected
* sequence number is set to a new integer. We use a sequence number so that replies that
* occur after a timeout has expired are not interpreted as replies to a later request.
*/
@UnsupportedAppUsage
@AnyThread
private static InputContextCallback getInstance() {
synchronized (InputContextCallback.class) {
// Return sInstance if it's non-null, otherwise construct a new callback
InputContextCallback callback;
if (sInstance != null) {
callback = sInstance;
sInstance = null;
// Reset the callback
callback.mHaveValue = false;
} else {
callback = new InputContextCallback();
}
// Set the sequence number
callback.mSeq = sSequenceNumber++;
return callback;
}
}
/**
* Makes the given InputContextCallback available for use in the future.
*/
@UnsupportedAppUsage
@AnyThread
private void dispose() {
synchronized (InputContextCallback.class) {
// If sInstance is non-null, just let this object be garbage-collected
if (sInstance == null) {
// Allow any objects being held to be gc'ed
mTextAfterCursor = null;
mTextBeforeCursor = null;
mExtractedText = null;
sInstance = this;
}
}
}
@BinderThread
public void setTextBeforeCursor(CharSequence textBeforeCursor, int seq) {
synchronized (this) {
if (seq == mSeq) {
mTextBeforeCursor = textBeforeCursor;
mHaveValue = true;
notifyAll();
} else {
Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+ ") in setTextBeforeCursor, ignoring.");
}
}
}
@BinderThread
public void setTextAfterCursor(CharSequence textAfterCursor, int seq) {
synchronized (this) {
if (seq == mSeq) {
mTextAfterCursor = textAfterCursor;
mHaveValue = true;
notifyAll();
} else {
Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+ ") in setTextAfterCursor, ignoring.");
}
}
}
@BinderThread
public void setSelectedText(CharSequence selectedText, int seq) {
synchronized (this) {
if (seq == mSeq) {
mSelectedText = selectedText;
mHaveValue = true;
notifyAll();
} else {
Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+ ") in setSelectedText, ignoring.");
}
}
}
@BinderThread
public void setCursorCapsMode(int capsMode, int seq) {
synchronized (this) {
if (seq == mSeq) {
mCursorCapsMode = capsMode;
mHaveValue = true;
notifyAll();
} else {
Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+ ") in setCursorCapsMode, ignoring.");
}
}
}
@BinderThread
public void setExtractedText(ExtractedText extractedText, int seq) {
synchronized (this) {
if (seq == mSeq) {
mExtractedText = extractedText;
mHaveValue = true;
notifyAll();
} else {
Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+ ") in setExtractedText, ignoring.");
}
}
}
@BinderThread
public void setRequestUpdateCursorAnchorInfoResult(boolean result, int seq) {
synchronized (this) {
if (seq == mSeq) {
mRequestUpdateCursorAnchorInfoResult = result;
mHaveValue = true;
notifyAll();
} else {
Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+ ") in setCursorAnchorInfoRequestResult, ignoring.");
}
}
}
@BinderThread
public void setCommitContentResult(boolean result, int seq) {
synchronized (this) {
if (seq == mSeq) {
mCommitContentResult = result;
mHaveValue = true;
notifyAll();
} else {
Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+ ") in setCommitContentResult, ignoring.");
}
}
}
/**
* Waits for a result for up to {@link #MAX_WAIT_TIME_MILLIS} milliseconds.
*
* <p>The caller must be synchronized on this callback object.
*/
@AnyThread
void waitForResultLocked() {
long startTime = SystemClock.uptimeMillis();
long endTime = startTime + MAX_WAIT_TIME_MILLIS;
while (!mHaveValue) {
long remainingTime = endTime - SystemClock.uptimeMillis();
if (remainingTime <= 0) {
Log.w(TAG, "Timed out waiting on IInputContextCallback");
return;
}
try {
wait(remainingTime);
} catch (InterruptedException e) {
}
}
}
}
public InputConnectionWrapper( public InputConnectionWrapper(
@NonNull WeakReference<AbstractInputMethodService> inputMethodService, @NonNull WeakReference<AbstractInputMethodService> inputMethodService,
IInputContext inputContext, @MissingMethodFlags final int missingMethods, IInputContext inputContext, @MissingMethodFlags int missingMethods,
@NonNull AtomicBoolean isUnbindIssued) { @NonNull CancellationGroup cancellationGroup) {
mInputMethodService = inputMethodService; mInputMethodService = inputMethodService;
mIInputContext = inputContext; mIInputContext = inputContext;
mMissingMethods = missingMethods; mMissingMethods = missingMethods;
mIsUnbindIssued = isUnbindIssued; mCancellationGroup = cancellationGroup;
}
@AnyThread
private static void logInternal(@Nullable String methodName, boolean timedOut,
@Nullable Object defaultValue) {
if (timedOut) {
Log.w(TAG, methodName + " didn't respond in " + MAX_WAIT_TIME_MILLIS + " msec."
+ " Returning default: " + defaultValue);
} else {
Log.w(TAG, methodName + " was canceled before complete. Returning default: "
+ defaultValue);
}
}
@AnyThread
private static int getResultOrZero(@NonNull CancellationGroup.Completable.Int value,
@NonNull String methodName) {
final boolean timedOut = value.await(MAX_WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS);
if (value.hasValue()) {
return value.getValue();
}
logInternal(methodName, timedOut, 0);
return 0;
}
@AnyThread
@Nullable
private static <T> T getResultOrNull(@NonNull CancellationGroup.Completable.Values<T> value,
@NonNull String methodName) {
final boolean timedOut = value.await(MAX_WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS);
if (value.hasValue()) {
return value.getValue();
}
logInternal(methodName, timedOut, null);
return null;
} }
@AnyThread @AnyThread
public CharSequence getTextAfterCursor(int length, int flags) { public CharSequence getTextAfterCursor(int length, int flags) {
if (mIsUnbindIssued.get()) { if (mCancellationGroup.isCanceled()) {
return null; return null;
} }
CharSequence value = null; final CancellationGroup.Completable.CharSequence value =
mCancellationGroup.createCompletableCharSequence();
try { try {
InputContextCallback callback = InputContextCallback.getInstance(); mIInputContext.getTextAfterCursor(length, flags, ResultCallbacks.of(value));
mIInputContext.getTextAfterCursor(length, flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mTextAfterCursor;
}
}
callback.dispose();
} catch (RemoteException e) { } catch (RemoteException e) {
return null; return null;
} }
return value; return getResultOrNull(value, "getTextAfterCursor()");
} }
@AnyThread @AnyThread
public CharSequence getTextBeforeCursor(int length, int flags) { public CharSequence getTextBeforeCursor(int length, int flags) {
if (mIsUnbindIssued.get()) { if (mCancellationGroup.isCanceled()) {
return null; return null;
} }
CharSequence value = null; final CancellationGroup.Completable.CharSequence value =
mCancellationGroup.createCompletableCharSequence();
try { try {
InputContextCallback callback = InputContextCallback.getInstance(); mIInputContext.getTextBeforeCursor(length, flags, ResultCallbacks.of(value));
mIInputContext.getTextBeforeCursor(length, flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mTextBeforeCursor;
}
}
callback.dispose();
} catch (RemoteException e) { } catch (RemoteException e) {
return null; return null;
} }
return value; return getResultOrNull(value, "getTextBeforeCursor()");
} }
@AnyThread @AnyThread
public CharSequence getSelectedText(int flags) { public CharSequence getSelectedText(int flags) {
if (mIsUnbindIssued.get()) { if (mCancellationGroup.isCanceled()) {
return null; return null;
} }
@@ -307,67 +147,46 @@ public class InputConnectionWrapper implements InputConnection {
// This method is not implemented. // This method is not implemented.
return null; return null;
} }
CharSequence value = null; final CancellationGroup.Completable.CharSequence value =
mCancellationGroup.createCompletableCharSequence();
try { try {
InputContextCallback callback = InputContextCallback.getInstance(); mIInputContext.getSelectedText(flags, ResultCallbacks.of(value));
mIInputContext.getSelectedText(flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mSelectedText;
}
}
callback.dispose();
} catch (RemoteException e) { } catch (RemoteException e) {
return null; return null;
} }
return value; return getResultOrNull(value, "getSelectedText()");
} }
@AnyThread @AnyThread
public int getCursorCapsMode(int reqModes) { public int getCursorCapsMode(int reqModes) {
if (mIsUnbindIssued.get()) { if (mCancellationGroup.isCanceled()) {
return 0; return 0;
} }
int value = 0; final CancellationGroup.Completable.Int value =
mCancellationGroup.createCompletableInt();
try { try {
InputContextCallback callback = InputContextCallback.getInstance(); mIInputContext.getCursorCapsMode(reqModes, ResultCallbacks.of(value));
mIInputContext.getCursorCapsMode(reqModes, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mCursorCapsMode;
}
}
callback.dispose();
} catch (RemoteException e) { } catch (RemoteException e) {
return 0; return 0;
} }
return value; return getResultOrZero(value, "getCursorCapsMode()");
} }
@AnyThread @AnyThread
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
if (mIsUnbindIssued.get()) { if (mCancellationGroup.isCanceled()) {
return null; return null;
} }
ExtractedText value = null; final CancellationGroup.Completable.ExtractedText value =
mCancellationGroup.createCompletableExtractedText();
try { try {
InputContextCallback callback = InputContextCallback.getInstance(); mIInputContext.getExtractedText(request, flags, ResultCallbacks.of(value));
mIInputContext.getExtractedText(request, flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mExtractedText;
}
}
callback.dispose();
} catch (RemoteException e) { } catch (RemoteException e) {
return null; return null;
} }
return value; return getResultOrNull(value, "getExtractedText()");
} }
@AnyThread @AnyThread
@@ -563,29 +382,22 @@ public class InputConnectionWrapper implements InputConnection {
@AnyThread @AnyThread
public boolean requestCursorUpdates(int cursorUpdateMode) { public boolean requestCursorUpdates(int cursorUpdateMode) {
if (mIsUnbindIssued.get()) { if (mCancellationGroup.isCanceled()) {
return false; return false;
} }
boolean result = false;
if (isMethodMissing(MissingMethodFlags.REQUEST_CURSOR_UPDATES)) { if (isMethodMissing(MissingMethodFlags.REQUEST_CURSOR_UPDATES)) {
// This method is not implemented. // This method is not implemented.
return false; return false;
} }
final CancellationGroup.Completable.Int value = mCancellationGroup.createCompletableInt();
try { try {
InputContextCallback callback = InputContextCallback.getInstance(); mIInputContext.requestUpdateCursorAnchorInfo(cursorUpdateMode,
mIInputContext.requestUpdateCursorAnchorInfo(cursorUpdateMode, callback.mSeq, callback); ResultCallbacks.of(value));
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
result = callback.mRequestUpdateCursorAnchorInfoResult;
}
}
callback.dispose();
} catch (RemoteException e) { } catch (RemoteException e) {
return false; return false;
} }
return result; return getResultOrZero(value, "requestUpdateCursorAnchorInfo()") != 0;
} }
@AnyThread @AnyThread
@@ -601,38 +413,31 @@ public class InputConnectionWrapper implements InputConnection {
@AnyThread @AnyThread
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) { public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
if (mIsUnbindIssued.get()) { if (mCancellationGroup.isCanceled()) {
return false; return false;
} }
boolean result = false;
if (isMethodMissing(MissingMethodFlags.COMMIT_CONTENT)) { if (isMethodMissing(MissingMethodFlags.COMMIT_CONTENT)) {
// This method is not implemented. // This method is not implemented.
return false; return false;
} }
try {
if ((flags & InputConnection.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
final AbstractInputMethodService inputMethodService = mInputMethodService.get();
if (inputMethodService == null) {
// This basically should not happen, because it's the the caller of this method.
return false;
}
inputMethodService.exposeContent(inputContentInfo, this);
}
InputContextCallback callback = InputContextCallback.getInstance(); if ((flags & InputConnection.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
mIInputContext.commitContent(inputContentInfo, flags, opts, callback.mSeq, callback); final AbstractInputMethodService inputMethodService = mInputMethodService.get();
synchronized (callback) { if (inputMethodService == null) {
callback.waitForResultLocked(); // This basically should not happen, because it's the caller of this method.
if (callback.mHaveValue) { return false;
result = callback.mCommitContentResult;
}
} }
callback.dispose(); inputMethodService.exposeContent(inputContentInfo, this);
}
final CancellationGroup.Completable.Int value = mCancellationGroup.createCompletableInt();
try {
mIInputContext.commitContent(inputContentInfo, flags, opts, ResultCallbacks.of(value));
} catch (RemoteException e) { } catch (RemoteException e) {
return false; return false;
} }
return result; return getResultOrZero(value, "commitContent()") != 0;
} }
@AnyThread @AnyThread