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.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

View File

@@ -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;

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");
* 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);
}

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

View File

@@ -17,14 +17,12 @@
package com.android.internal.view;
import android.annotation.AnyThread;
import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.annotation.Nullable;
import android.inputmethodservice.AbstractInputMethodService;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
import android.view.KeyEvent;
import android.view.inputmethod.CompletionInfo;
@@ -36,10 +34,15 @@ import android.view.inputmethod.InputConnectionInspector;
import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
import android.view.inputmethod.InputContentInfo;
import com.android.internal.inputmethod.CancellationGroup;
import com.android.internal.inputmethod.ResultCallbacks;
import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.TimeUnit;
public class InputConnectionWrapper implements InputConnection {
private static final String TAG = "InputConnectionWrapper";
private static final int MAX_WAIT_TIME_MILLIS = 2000;
private final IInputContext mIInputContext;
@NonNull
@@ -49,257 +52,94 @@ public class InputConnectionWrapper implements InputConnection {
private final int mMissingMethods;
/**
* {@code true} if the system already decided to take away IME focus from the target app. This
* 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.
* Signaled when the system decided to take away IME focus from the target app.
*
* <p>This is expected to be signaled immediately when the IME process receives
* {@link IInputMethod#unbindInput()}.</p>
*/
@NonNull
private final AtomicBoolean mIsUnbindIssued;
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) {
}
}
}
}
private final CancellationGroup mCancellationGroup;
public InputConnectionWrapper(
@NonNull WeakReference<AbstractInputMethodService> inputMethodService,
IInputContext inputContext, @MissingMethodFlags final int missingMethods,
@NonNull AtomicBoolean isUnbindIssued) {
IInputContext inputContext, @MissingMethodFlags int missingMethods,
@NonNull CancellationGroup cancellationGroup) {
mInputMethodService = inputMethodService;
mIInputContext = inputContext;
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
public CharSequence getTextAfterCursor(int length, int flags) {
if (mIsUnbindIssued.get()) {
if (mCancellationGroup.isCanceled()) {
return null;
}
CharSequence value = null;
final CancellationGroup.Completable.CharSequence value =
mCancellationGroup.createCompletableCharSequence();
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.getTextAfterCursor(length, flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mTextAfterCursor;
}
}
callback.dispose();
mIInputContext.getTextAfterCursor(length, flags, ResultCallbacks.of(value));
} catch (RemoteException e) {
return null;
}
return value;
return getResultOrNull(value, "getTextAfterCursor()");
}
@AnyThread
public CharSequence getTextBeforeCursor(int length, int flags) {
if (mIsUnbindIssued.get()) {
if (mCancellationGroup.isCanceled()) {
return null;
}
CharSequence value = null;
final CancellationGroup.Completable.CharSequence value =
mCancellationGroup.createCompletableCharSequence();
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.getTextBeforeCursor(length, flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mTextBeforeCursor;
}
}
callback.dispose();
mIInputContext.getTextBeforeCursor(length, flags, ResultCallbacks.of(value));
} catch (RemoteException e) {
return null;
}
return value;
return getResultOrNull(value, "getTextBeforeCursor()");
}
@AnyThread
public CharSequence getSelectedText(int flags) {
if (mIsUnbindIssued.get()) {
if (mCancellationGroup.isCanceled()) {
return null;
}
@@ -307,67 +147,46 @@ public class InputConnectionWrapper implements InputConnection {
// This method is not implemented.
return null;
}
CharSequence value = null;
final CancellationGroup.Completable.CharSequence value =
mCancellationGroup.createCompletableCharSequence();
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.getSelectedText(flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mSelectedText;
}
}
callback.dispose();
mIInputContext.getSelectedText(flags, ResultCallbacks.of(value));
} catch (RemoteException e) {
return null;
}
return value;
return getResultOrNull(value, "getSelectedText()");
}
@AnyThread
public int getCursorCapsMode(int reqModes) {
if (mIsUnbindIssued.get()) {
if (mCancellationGroup.isCanceled()) {
return 0;
}
int value = 0;
final CancellationGroup.Completable.Int value =
mCancellationGroup.createCompletableInt();
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.getCursorCapsMode(reqModes, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mCursorCapsMode;
}
}
callback.dispose();
mIInputContext.getCursorCapsMode(reqModes, ResultCallbacks.of(value));
} catch (RemoteException e) {
return 0;
}
return value;
return getResultOrZero(value, "getCursorCapsMode()");
}
@AnyThread
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
if (mIsUnbindIssued.get()) {
if (mCancellationGroup.isCanceled()) {
return null;
}
ExtractedText value = null;
final CancellationGroup.Completable.ExtractedText value =
mCancellationGroup.createCompletableExtractedText();
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.getExtractedText(request, flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mExtractedText;
}
}
callback.dispose();
mIInputContext.getExtractedText(request, flags, ResultCallbacks.of(value));
} catch (RemoteException e) {
return null;
}
return value;
return getResultOrNull(value, "getExtractedText()");
}
@AnyThread
@@ -563,29 +382,22 @@ public class InputConnectionWrapper implements InputConnection {
@AnyThread
public boolean requestCursorUpdates(int cursorUpdateMode) {
if (mIsUnbindIssued.get()) {
if (mCancellationGroup.isCanceled()) {
return false;
}
boolean result = false;
if (isMethodMissing(MissingMethodFlags.REQUEST_CURSOR_UPDATES)) {
// This method is not implemented.
return false;
}
final CancellationGroup.Completable.Int value = mCancellationGroup.createCompletableInt();
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.requestUpdateCursorAnchorInfo(cursorUpdateMode, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
result = callback.mRequestUpdateCursorAnchorInfoResult;
}
}
callback.dispose();
mIInputContext.requestUpdateCursorAnchorInfo(cursorUpdateMode,
ResultCallbacks.of(value));
} catch (RemoteException e) {
return false;
}
return result;
return getResultOrZero(value, "requestUpdateCursorAnchorInfo()") != 0;
}
@AnyThread
@@ -601,38 +413,31 @@ public class InputConnectionWrapper implements InputConnection {
@AnyThread
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
if (mIsUnbindIssued.get()) {
if (mCancellationGroup.isCanceled()) {
return false;
}
boolean result = false;
if (isMethodMissing(MissingMethodFlags.COMMIT_CONTENT)) {
// This method is not implemented.
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();
mIInputContext.commitContent(inputContentInfo, flags, opts, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
result = callback.mCommitContentResult;
}
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 caller of this method.
return false;
}
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) {
return false;
}
return result;
return getResultOrZero(value, "commitContent()") != 0;
}
@AnyThread