blob: 7a668fb74cb76dedf38b267e12b3acc435a952c7 [file] [log] [blame]
/*
* 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.inputmethod;
import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetCursorCapsModeProto;
import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetExtractedTextProto;
import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetSelectedTextProto;
import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetSurroundingTextProto;
import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetTextAfterCursorProto;
import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetTextBeforeCursorProto;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Trace;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.DumpableInputConnection;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.InputMethodManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.view.IInputContext;
import java.lang.ref.WeakReference;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Takes care of remote method invocations of {@link InputConnection} in the IME client side.
*
* <p>{@link android.inputmethodservice.RemoteInputConnection} code is executed in the IME process.
* It makes IInputContext binder calls under the hood. {@link RemoteInputConnectionImpl} receives
* {@link IInputContext} binder calls in the IME client (editor app) process, and forwards them to
* {@link InputConnection} that the IME client provided, on the {@link Looper} associated to the
* {@link InputConnection}.</p>
*/
public final class RemoteInputConnectionImpl extends IInputContext.Stub {
private static final String TAG = "RemoteInputConnectionImpl";
private static final boolean DEBUG = false;
@GuardedBy("mLock")
@Nullable
private InputConnection mInputConnection;
@NonNull
private final Looper mLooper;
private final Handler mH;
private final Object mLock = new Object();
@GuardedBy("mLock")
private boolean mFinished = false;
private final InputMethodManager mParentInputMethodManager;
private final WeakReference<View> mServedView;
public RemoteInputConnectionImpl(@NonNull Looper looper,
@NonNull InputConnection inputConnection,
@NonNull InputMethodManager inputMethodManager, @Nullable View servedView) {
mInputConnection = inputConnection;
mLooper = looper;
mH = new Handler(mLooper);
mParentInputMethodManager = inputMethodManager;
mServedView = new WeakReference<>(servedView);
}
/**
* @return {@link InputConnection} to which incoming IPCs will be dispatched.
*/
@Nullable
private InputConnection getInputConnection() {
synchronized (mLock) {
return mInputConnection;
}
}
/**
* @return {@code true} until the target {@link InputConnection} receives
* {@link InputConnection#closeConnection()} as a result of {@link #deactivate()}.
*/
public boolean isFinished() {
synchronized (mLock) {
return mFinished;
}
}
public boolean isActive() {
return mParentInputMethodManager.isActive() && !isFinished();
}
public View getServedView() {
return mServedView.get();
}
/**
* Called when this object needs to be permanently deactivated.
*
* <p>Multiple invocations will be simply ignored.</p>
*/
public void deactivate() {
if (isFinished()) {
// This is a small performance optimization. Still only the 1st call of
// reportFinish() will take effect.
return;
}
dispatch(() -> {
// Note that we do not need to worry about race condition here, because 1) mFinished is
// updated only inside this block, and 2) the code here is running on a Handler hence we
// assume multiple closeConnection() tasks will not be handled at the same time.
if (isFinished()) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_INPUT, "InputConnection#closeConnection");
try {
InputConnection ic = getInputConnection();
// Note we do NOT check isActive() here, because this is safe
// for an IME to call at any time, and we need to allow it
// through to clean up our state after the IME has switched to
// another client.
if (ic == null) {
return;
}
try {
ic.closeConnection();
} catch (AbstractMethodError ignored) {
// TODO(b/199934664): See if we can remove this by providing a default impl.
}
} finally {
synchronized (mLock) {
mInputConnection = null;
mFinished = true;
}
Trace.traceEnd(Trace.TRACE_TAG_INPUT);
}
// Notify the app that the InputConnection was closed.
final View servedView = mServedView.get();
if (servedView != null) {
final Handler handler = servedView.getHandler();
// The handler is null if the view is already detached. When that's the case, for
// now, we simply don't dispatch this callback.
if (handler != null) {
if (DEBUG) {
Log.v(TAG, "Calling View.onInputConnectionClosed: view=" + servedView);
}
if (handler.getLooper().isCurrentThread()) {
servedView.onInputConnectionClosedInternal();
} else {
handler.post(servedView::onInputConnectionClosedInternal);
}
}
}
});
}
@Override
public String toString() {
return "RemoteInputConnectionImpl{"
+ "connection=" + getInputConnection()
+ " finished=" + isFinished()
+ " mParentInputMethodManager.isActive()=" + mParentInputMethodManager.isActive()
+ " mServedView=" + mServedView.get()
+ "}";
}
/**
* Called by {@link InputMethodManager} to dump the editor state.
*
* @param proto {@link ProtoOutputStream} to which the editor state should be dumped.
* @param fieldId the ID to be passed to
* {@link DumpableInputConnection#dumpDebug(ProtoOutputStream, long)}.
*/
public void dumpDebug(ProtoOutputStream proto, long fieldId) {
synchronized (mLock) {
// Check that the call is initiated in the target thread of the current InputConnection
// {@link InputConnection#getHandler} since the messages to IInputConnectionWrapper are
// executed on this thread. Otherwise the messages are dispatched to the correct thread
// in IInputConnectionWrapper, but this is not wanted while dumpng, for performance
// reasons.
if ((mInputConnection instanceof DumpableInputConnection)
&& mLooper.isCurrentThread()) {
((DumpableInputConnection) mInputConnection).dumpDebug(proto, fieldId);
}
}
}
/**
* Invoke {@link InputConnection#reportFullscreenMode(boolean)} or schedule it on the target
* thread associated with {@link InputConnection#getHandler()}.
*
* @param enabled the parameter to be passed to
* {@link InputConnection#reportFullscreenMode(boolean)}.
*/
public void dispatchReportFullscreenMode(boolean enabled) {
dispatch(() -> {
final InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
return;
}
ic.reportFullscreenMode(enabled);
});
}
@Override
public void getTextAfterCursor(int length, int flags,
AndroidFuture future /* T=CharSequence */) {
dispatchWithTracing("getTextAfterCursor", future, () -> {
final InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "getTextAfterCursor on inactive InputConnection");
return null;
}
if (length < 0) {
Log.i(TAG, "Returning null to getTextAfterCursor due to an invalid length="
+ length);
return null;
}
return ic.getTextAfterCursor(length, flags);
}, useImeTracing() ? result -> buildGetTextAfterCursorProto(length, flags, result) : null);
}
@Override
public void getTextBeforeCursor(int length, int flags,
AndroidFuture future /* T=CharSequence */) {
dispatchWithTracing("getTextBeforeCursor", future, () -> {
final InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "getTextBeforeCursor on inactive InputConnection");
return null;
}
if (length < 0) {
Log.i(TAG, "Returning null to getTextBeforeCursor due to an invalid length="
+ length);
return null;
}
return ic.getTextBeforeCursor(length, flags);
}, useImeTracing() ? result -> buildGetTextBeforeCursorProto(length, flags, result) : null);
}
@Override
public void getSelectedText(int flags, AndroidFuture future /* T=CharSequence */) {
dispatchWithTracing("getSelectedText", future, () -> {
final InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "getSelectedText on inactive InputConnection");
return null;
}
try {
return ic.getSelectedText(flags);
} catch (AbstractMethodError ignored) {
// TODO(b/199934664): See if we can remove this by providing a default impl.
return null;
}
}, useImeTracing() ? result -> buildGetSelectedTextProto(flags, result) : null);
}
@Override
public void getSurroundingText(int beforeLength, int afterLength, int flags,
AndroidFuture future /* T=SurroundingText */) {
dispatchWithTracing("getSurroundingText", future, () -> {
final InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "getSurroundingText on inactive InputConnection");
return null;
}
if (beforeLength < 0) {
Log.i(TAG, "Returning null to getSurroundingText due to an invalid"
+ " beforeLength=" + beforeLength);
return null;
}
if (afterLength < 0) {
Log.i(TAG, "Returning null to getSurroundingText due to an invalid"
+ " afterLength=" + afterLength);
return null;
}
return ic.getSurroundingText(beforeLength, afterLength, flags);
}, useImeTracing() ? result -> buildGetSurroundingTextProto(
beforeLength, afterLength, flags, result) : null);
}
@Override
public void getCursorCapsMode(int reqModes, AndroidFuture future /* T=Integer */) {
dispatchWithTracing("getCursorCapsMode", future, () -> {
final InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "getCursorCapsMode on inactive InputConnection");
return 0;
}
return ic.getCursorCapsMode(reqModes);
}, useImeTracing() ? result -> buildGetCursorCapsModeProto(reqModes, result) : null);
}
@Override
public void getExtractedText(ExtractedTextRequest request, int flags,
AndroidFuture future /* T=ExtractedText */) {
dispatchWithTracing("getExtractedText", future, () -> {
final InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "getExtractedText on inactive InputConnection");
return null;
}
return ic.getExtractedText(request, flags);
}, useImeTracing() ? result -> buildGetExtractedTextProto(request, flags, result) : null);
}
@Override
public void commitText(CharSequence text, int newCursorPosition) {
dispatchWithTracing("commitText", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "commitText on inactive InputConnection");
return;
}
ic.commitText(text, newCursorPosition);
});
}
@Override
public void commitCompletion(CompletionInfo text) {
dispatchWithTracing("commitCompletion", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "commitCompletion on inactive InputConnection");
return;
}
ic.commitCompletion(text);
});
}
@Override
public void commitCorrection(CorrectionInfo info) {
dispatchWithTracing("commitCorrection", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "commitCorrection on inactive InputConnection");
return;
}
try {
ic.commitCorrection(info);
} catch (AbstractMethodError ignored) {
// TODO(b/199934664): See if we can remove this by providing a default impl.
}
});
}
@Override
public void setSelection(int start, int end) {
dispatchWithTracing("setSelection", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "setSelection on inactive InputConnection");
return;
}
ic.setSelection(start, end);
});
}
@Override
public void performEditorAction(int id) {
dispatchWithTracing("performEditorAction", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "performEditorAction on inactive InputConnection");
return;
}
ic.performEditorAction(id);
});
}
@Override
public void performContextMenuAction(int id) {
dispatchWithTracing("performContextMenuAction", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "performContextMenuAction on inactive InputConnection");
return;
}
ic.performContextMenuAction(id);
});
}
@Override
public void setComposingRegion(int start, int end) {
dispatchWithTracing("setComposingRegion", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "setComposingRegion on inactive InputConnection");
return;
}
try {
ic.setComposingRegion(start, end);
} catch (AbstractMethodError ignored) {
// TODO(b/199934664): See if we can remove this by providing a default impl.
}
});
}
@Override
public void setComposingText(CharSequence text, int newCursorPosition) {
dispatchWithTracing("setComposingText", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "setComposingText on inactive InputConnection");
return;
}
ic.setComposingText(text, newCursorPosition);
});
}
@Override
public void finishComposingText() {
dispatchWithTracing("finishComposingText", () -> {
if (isFinished()) {
// In this case, #finishComposingText() is guaranteed to be called already.
// There should be no negative impact if we ignore this call silently.
if (DEBUG) {
Log.w(TAG, "Bug 35301295: Redundant finishComposingText.");
}
return;
}
InputConnection ic = getInputConnection();
// Note we do NOT check isActive() here, because this is safe
// for an IME to call at any time, and we need to allow it
// through to clean up our state after the IME has switched to
// another client.
if (ic == null) {
Log.w(TAG, "finishComposingText on inactive InputConnection");
return;
}
ic.finishComposingText();
});
}
@Override
public void sendKeyEvent(KeyEvent event) {
dispatchWithTracing("sendKeyEvent", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "sendKeyEvent on inactive InputConnection");
return;
}
ic.sendKeyEvent(event);
});
}
@Override
public void clearMetaKeyStates(int states) {
dispatchWithTracing("clearMetaKeyStates", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "clearMetaKeyStates on inactive InputConnection");
return;
}
ic.clearMetaKeyStates(states);
});
}
@Override
public void deleteSurroundingText(int beforeLength, int afterLength) {
dispatchWithTracing("deleteSurroundingText", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "deleteSurroundingText on inactive InputConnection");
return;
}
ic.deleteSurroundingText(beforeLength, afterLength);
});
}
@Override
public void deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
dispatchWithTracing("deleteSurroundingTextInCodePoints", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "deleteSurroundingTextInCodePoints on inactive InputConnection");
return;
}
try {
ic.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
} catch (AbstractMethodError ignored) {
// TODO(b/199934664): See if we can remove this by providing a default impl.
}
});
}
@Override
public void beginBatchEdit() {
dispatchWithTracing("beginBatchEdit", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "beginBatchEdit on inactive InputConnection");
return;
}
ic.beginBatchEdit();
});
}
@Override
public void endBatchEdit() {
dispatchWithTracing("endBatchEdit", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "endBatchEdit on inactive InputConnection");
return;
}
ic.endBatchEdit();
});
}
@Override
public void performSpellCheck() {
dispatchWithTracing("performSpellCheck", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "performSpellCheck on inactive InputConnection");
return;
}
ic.performSpellCheck();
});
}
@Override
public void performPrivateCommand(String action, Bundle data) {
dispatchWithTracing("performPrivateCommand", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "performPrivateCommand on inactive InputConnection");
return;
}
ic.performPrivateCommand(action, data);
});
}
@Override
public void requestCursorUpdates(int cursorUpdateMode, int imeDisplayId,
AndroidFuture future /* T=Boolean */) {
dispatchWithTracing("requestCursorUpdates", future, () -> {
final InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "requestCursorAnchorInfo on inactive InputConnection");
return false;
}
if (mParentInputMethodManager.getDisplayId() != imeDisplayId) {
// requestCursorUpdates() is not currently supported across displays.
return false;
}
try {
return ic.requestCursorUpdates(cursorUpdateMode);
} catch (AbstractMethodError ignored) {
// TODO(b/199934664): See if we can remove this by providing a default impl.
return false;
}
});
}
@Override
public void commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts,
AndroidFuture future /* T=Boolean */) {
dispatchWithTracing("commitContent", future, () -> {
final InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "commitContent on inactive InputConnection");
return false;
}
if (inputContentInfo == null || !inputContentInfo.validate()) {
Log.w(TAG, "commitContent with invalid inputContentInfo=" + inputContentInfo);
return false;
}
try {
return ic.commitContent(inputContentInfo, flags, opts);
} catch (AbstractMethodError ignored) {
// TODO(b/199934664): See if we can remove this by providing a default impl.
return false;
}
});
}
@Override
public void setImeConsumesInput(boolean imeConsumesInput) {
dispatchWithTracing("setImeConsumesInput", () -> {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "setImeConsumesInput on inactive InputConnection");
return;
}
ic.setImeConsumesInput(imeConsumesInput);
});
}
private void dispatch(@NonNull Runnable runnable) {
// If we are calling this from the target thread, then we can call right through.
// Otherwise, we need to send the message to the target thread.
if (mLooper.isCurrentThread()) {
runnable.run();
return;
}
mH.post(runnable);
}
private void dispatchWithTracing(@NonNull String methodName, @NonNull Runnable runnable) {
final Runnable actualRunnable;
if (Trace.isTagEnabled(Trace.TRACE_TAG_INPUT)) {
actualRunnable = () -> {
Trace.traceBegin(Trace.TRACE_TAG_INPUT, "InputConnection#" + methodName);
try {
runnable.run();
} finally {
Trace.traceEnd(Trace.TRACE_TAG_INPUT);
}
};
} else {
actualRunnable = runnable;
}
dispatch(actualRunnable);
}
private <T> void dispatchWithTracing(@NonNull String methodName,
@NonNull AndroidFuture untypedFuture, @NonNull Supplier<T> supplier) {
dispatchWithTracing(methodName, untypedFuture, supplier, null /* dumpProtoProvider */);
}
private <T> void dispatchWithTracing(@NonNull String methodName,
@NonNull AndroidFuture untypedFuture, @NonNull Supplier<T> supplier,
@Nullable Function<T, byte[]> dumpProtoProvider) {
@SuppressWarnings("unchecked")
final AndroidFuture<T> future = untypedFuture;
dispatchWithTracing(methodName, () -> {
final T result;
try {
result = supplier.get();
} catch (Throwable throwable) {
future.completeExceptionally(throwable);
throw throwable;
}
future.complete(result);
if (dumpProtoProvider != null) {
final byte[] icProto = dumpProtoProvider.apply(result);
ImeTracing.getInstance().triggerClientDump(
TAG + "#" + methodName, mParentInputMethodManager, icProto);
}
});
}
private static boolean useImeTracing() {
return ImeTracing.getInstance().isEnabled();
}
}