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