/*
* Copyright (C) 2008 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.view;
import android.annotation.AnyThread;
import android.annotation.BinderThread;
import android.annotation.NonNull;
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;
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;
import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
import android.view.inputmethod.InputContentInfo;
import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicBoolean;
public class InputConnectionWrapper implements InputConnection {
private static final int MAX_WAIT_TIME_MILLIS = 2000;
private final IInputContext mIInputContext;
@NonNull
private final WeakReference<AbstractInputMethodService> mInputMethodService;
@MissingMethodFlags
private final int mMissingMethods;
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 onUnbindInput.onUnbindInput()
is not yet called back on the UI thread. /**
* {@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.
*/
@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.
/**
* 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.
*/
@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.
/**
* Makes the given InputContextCallback available for use in the future.
*/
@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 InputConnectionWrapper.MAX_WAIT_TIME_MILLIS
milliseconds. The caller must be synchronized on this callback object.
/**
* 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(
@NonNull WeakReference<AbstractInputMethodService> inputMethodService,
IInputContext inputContext, @MissingMethodFlags final int missingMethods,
@NonNull AtomicBoolean isUnbindIssued) {
mInputMethodService = inputMethodService;
mIInputContext = inputContext;
mMissingMethods = missingMethods;
mIsUnbindIssued = isUnbindIssued;
}
@AnyThread
public CharSequence getTextAfterCursor(int length, int flags) {
if (mIsUnbindIssued.get()) {
return null;
}
CharSequence value = null;
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.getTextAfterCursor(length, flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mTextAfterCursor;
}
}
callback.dispose();
} catch (RemoteException e) {
return null;
}
return value;
}
@AnyThread
public CharSequence getTextBeforeCursor(int length, int flags) {
if (mIsUnbindIssued.get()) {
return null;
}
CharSequence value = null;
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.getTextBeforeCursor(length, flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mTextBeforeCursor;
}
}
callback.dispose();
} catch (RemoteException e) {
return null;
}
return value;
}
@AnyThread
public CharSequence getSelectedText(int flags) {
if (mIsUnbindIssued.get()) {
return null;
}
if (isMethodMissing(MissingMethodFlags.GET_SELECTED_TEXT)) {
// This method is not implemented.
return null;
}
CharSequence value = null;
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.getSelectedText(flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mSelectedText;
}
}
callback.dispose();
} catch (RemoteException e) {
return null;
}
return value;
}
@AnyThread
public int getCursorCapsMode(int reqModes) {
if (mIsUnbindIssued.get()) {
return 0;
}
int value = 0;
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.getCursorCapsMode(reqModes, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mCursorCapsMode;
}
}
callback.dispose();
} catch (RemoteException e) {
return 0;
}
return value;
}
@AnyThread
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
if (mIsUnbindIssued.get()) {
return null;
}
ExtractedText value = null;
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.getExtractedText(request, flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mExtractedText;
}
}
callback.dispose();
} catch (RemoteException e) {
return null;
}
return value;
}
@AnyThread
public boolean commitText(CharSequence text, int newCursorPosition) {
try {
mIInputContext.commitText(text, newCursorPosition);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean commitCompletion(CompletionInfo text) {
if (isMethodMissing(MissingMethodFlags.COMMIT_CORRECTION)) {
// This method is not implemented.
return false;
}
try {
mIInputContext.commitCompletion(text);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean commitCorrection(CorrectionInfo correctionInfo) {
try {
mIInputContext.commitCorrection(correctionInfo);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean setSelection(int start, int end) {
try {
mIInputContext.setSelection(start, end);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean performEditorAction(int actionCode) {
try {
mIInputContext.performEditorAction(actionCode);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean performContextMenuAction(int id) {
try {
mIInputContext.performContextMenuAction(id);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean setComposingRegion(int start, int end) {
if (isMethodMissing(MissingMethodFlags.SET_COMPOSING_REGION)) {
// This method is not implemented.
return false;
}
try {
mIInputContext.setComposingRegion(start, end);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean setComposingText(CharSequence text, int newCursorPosition) {
try {
mIInputContext.setComposingText(text, newCursorPosition);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean finishComposingText() {
try {
mIInputContext.finishComposingText();
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean beginBatchEdit() {
try {
mIInputContext.beginBatchEdit();
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean endBatchEdit() {
try {
mIInputContext.endBatchEdit();
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean sendKeyEvent(KeyEvent event) {
try {
mIInputContext.sendKeyEvent(event);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean clearMetaKeyStates(int states) {
try {
mIInputContext.clearMetaKeyStates(states);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
try {
mIInputContext.deleteSurroundingText(beforeLength, afterLength);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
if (isMethodMissing(MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS)) {
// This method is not implemented.
return false;
}
try {
mIInputContext.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean reportFullscreenMode(boolean enabled) {
// Nothing should happen when called from input method.
return false;
}
@AnyThread
public boolean performPrivateCommand(String action, Bundle data) {
try {
mIInputContext.performPrivateCommand(action, data);
return true;
} catch (RemoteException e) {
return false;
}
}
@AnyThread
public boolean requestCursorUpdates(int cursorUpdateMode) {
if (mIsUnbindIssued.get()) {
return false;
}
boolean result = false;
if (isMethodMissing(MissingMethodFlags.REQUEST_CURSOR_UPDATES)) {
// This method is not implemented.
return false;
}
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.requestUpdateCursorAnchorInfo(cursorUpdateMode, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
result = callback.mRequestUpdateCursorAnchorInfoResult;
}
}
callback.dispose();
} catch (RemoteException e) {
return false;
}
return result;
}
@AnyThread
public Handler getHandler() {
// Nothing should happen when called from input method.
return null;
}
@AnyThread
public void closeConnection() {
// Nothing should happen when called from input method.
}
@AnyThread
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
if (mIsUnbindIssued.get()) {
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;
}
}
callback.dispose();
} catch (RemoteException e) {
return false;
}
return result;
}
@AnyThread
private boolean isMethodMissing(@MissingMethodFlags final int methodFlag) {
return (mMissingMethods & methodFlag) == methodFlag;
}
@AnyThread
@Override
public String toString() {
return "InputConnectionWrapper{idHash=#"
+ Integer.toHexString(System.identityHashCode(this))
+ " mMissingMethods="
+ InputConnectionInspector.getMissingMethodFlagsAsString(mMissingMethods) + "}";
}
}