blob: b99996ff83c8ccedc05ac8dcc9cf2be91aec4c23 [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 android.inputmethodservice;
import android.annotation.BinderThread;
import android.annotation.DurationMillisLong;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.util.Log;
import android.view.InputChannel;
import android.view.MotionEvent;
import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethod;
import android.view.inputmethod.InputMethodSession;
import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.inputmethod.CancellationGroup;
import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
import com.android.internal.inputmethod.IInputMethod;
import com.android.internal.inputmethod.IInputMethodSession;
import com.android.internal.inputmethod.IInputMethodSessionCallback;
import com.android.internal.inputmethod.IRemoteInputConnection;
import com.android.internal.inputmethod.InlineSuggestionsRequestInfo;
import com.android.internal.inputmethod.InputMethodNavButtonFlags;
import com.android.internal.os.HandlerCaller;
import com.android.internal.os.SomeArgs;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Implements the internal IInputMethod interface to convert incoming calls
* on to it back to calls on the public InputMethod interface, scheduling
* them on the main thread of the process.
*/
class IInputMethodWrapper extends IInputMethod.Stub
implements HandlerCaller.Callback {
private static final String TAG = "InputMethodWrapper";
private static final int DO_DUMP = 1;
private static final int DO_INITIALIZE_INTERNAL = 10;
private static final int DO_SET_INPUT_CONTEXT = 20;
private static final int DO_UNSET_INPUT_CONTEXT = 30;
private static final int DO_START_INPUT = 32;
private static final int DO_ON_NAV_BUTTON_FLAGS_CHANGED = 35;
private static final int DO_CREATE_SESSION = 40;
private static final int DO_SET_SESSION_ENABLED = 45;
private static final int DO_SHOW_SOFT_INPUT = 60;
private static final int DO_HIDE_SOFT_INPUT = 70;
private static final int DO_CHANGE_INPUTMETHOD_SUBTYPE = 80;
private static final int DO_CREATE_INLINE_SUGGESTIONS_REQUEST = 90;
private static final int DO_CAN_START_STYLUS_HANDWRITING = 100;
private static final int DO_START_STYLUS_HANDWRITING = 110;
private static final int DO_INIT_INK_WINDOW = 120;
private static final int DO_FINISH_STYLUS_HANDWRITING = 130;
private static final int DO_UPDATE_TOOL_TYPE = 140;
private static final int DO_REMOVE_STYLUS_HANDWRITING_WINDOW = 150;
private static final int DO_SET_STYLUS_WINDOW_IDLE_TIMEOUT = 160;
final WeakReference<InputMethodServiceInternal> mTarget;
final Context mContext;
@UnsupportedAppUsage
final HandlerCaller mCaller;
final WeakReference<InputMethod> mInputMethod;
final int mTargetSdkVersion;
/**
* This is not {@code null} only between {@link #bindInput(InputBinding)} and
* {@link #unbindInput()} so that {@link RemoteInputConnection} can query if
* {@link #unbindInput()} has already been called or not, mainly to avoid unnecessary
* blocking operations.
*
* <p>This field must be set and cleared only from the binder thread(s), where the system
* guarantees that {@link #bindInput(InputBinding)},
* {@link #startInput(IInputMethod.StartInputParams)}, 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>
*/
@Nullable
CancellationGroup mCancellationGroup = null;
// NOTE: we should have a cache of these.
static final class InputMethodSessionCallbackWrapper implements InputMethod.SessionCallback {
final Context mContext;
final InputChannel mChannel;
final IInputMethodSessionCallback mCb;
InputMethodSessionCallbackWrapper(Context context, InputChannel channel,
IInputMethodSessionCallback cb) {
mContext = context;
mChannel = channel;
mCb = cb;
}
@Override
public void sessionCreated(InputMethodSession session) {
try {
if (session != null) {
IInputMethodSessionWrapper wrap =
new IInputMethodSessionWrapper(mContext, session, mChannel);
mCb.sessionCreated(wrap);
} else {
if (mChannel != null) {
mChannel.dispose();
}
mCb.sessionCreated(null);
}
} catch (RemoteException e) {
}
}
}
IInputMethodWrapper(InputMethodServiceInternal imsInternal, InputMethod inputMethod) {
mTarget = new WeakReference<>(imsInternal);
mContext = imsInternal.getContext().getApplicationContext();
mCaller = new HandlerCaller(mContext, null, this, true /*asyncHandler*/);
mInputMethod = new WeakReference<>(inputMethod);
mTargetSdkVersion = imsInternal.getContext().getApplicationInfo().targetSdkVersion;
}
@MainThread
@Override
public void executeMessage(Message msg) {
final InputMethod inputMethod = mInputMethod.get();
final InputMethodServiceInternal target = mTarget.get();
switch (msg.what) {
case DO_DUMP: {
SomeArgs args = (SomeArgs) msg.obj;
if (isValid(inputMethod, target, "DO_DUMP")) {
final FileDescriptor fd = (FileDescriptor) args.arg1;
final PrintWriter fout = (PrintWriter) args.arg2;
final String[] dumpArgs = (String[]) args.arg3;
final CountDownLatch latch = (CountDownLatch) args.arg4;
try {
target.dump(fd, fout, dumpArgs);
} catch (RuntimeException e) {
fout.println("Exception: " + e);
} finally {
latch.countDown();
}
}
args.recycle();
return;
}
case DO_INITIALIZE_INTERNAL:
if (isValid(inputMethod, target, "DO_INITIALIZE_INTERNAL")) {
inputMethod.initializeInternal((IInputMethod.InitParams) msg.obj);
}
return;
case DO_SET_INPUT_CONTEXT: {
if (isValid(inputMethod, target, "DO_SET_INPUT_CONTEXT")) {
inputMethod.bindInput((InputBinding) msg.obj);
}
return;
}
case DO_UNSET_INPUT_CONTEXT:
if (isValid(inputMethod, target, "DO_UNSET_INPUT_CONTEXT")) {
inputMethod.unbindInput();
}
return;
case DO_START_INPUT: {
final SomeArgs args = (SomeArgs) msg.obj;
if (isValid(inputMethod, target, "DO_START_INPUT")) {
final InputConnection inputConnection = (InputConnection) args.arg1;
final IInputMethod.StartInputParams params =
(IInputMethod.StartInputParams) args.arg2;
inputMethod.dispatchStartInput(inputConnection, params);
}
args.recycle();
return;
}
case DO_ON_NAV_BUTTON_FLAGS_CHANGED:
if (isValid(inputMethod, target, "DO_ON_NAV_BUTTON_FLAGS_CHANGED")) {
inputMethod.onNavButtonFlagsChanged(msg.arg1);
}
return;
case DO_CREATE_SESSION: {
SomeArgs args = (SomeArgs) msg.obj;
if (isValid(inputMethod, target, "DO_CREATE_SESSION")) {
inputMethod.createSession(new InputMethodSessionCallbackWrapper(
mContext, (InputChannel) args.arg1,
(IInputMethodSessionCallback) args.arg2));
}
args.recycle();
return;
}
case DO_SET_SESSION_ENABLED:
if (isValid(inputMethod, target, "DO_SET_SESSION_ENABLED")) {
inputMethod.setSessionEnabled((InputMethodSession) msg.obj, msg.arg1 != 0);
}
return;
case DO_SHOW_SOFT_INPUT: {
final SomeArgs args = (SomeArgs) msg.obj;
final ImeTracker.Token statsToken = (ImeTracker.Token) args.arg3;
if (isValid(inputMethod, target, "DO_SHOW_SOFT_INPUT")) {
ImeTracker.forLogging().onProgress(
statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
inputMethod.showSoftInputWithToken(
msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1, statsToken);
} else {
ImeTracker.forLogging().onFailed(
statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
}
args.recycle();
return;
}
case DO_HIDE_SOFT_INPUT: {
final SomeArgs args = (SomeArgs) msg.obj;
final ImeTracker.Token statsToken = (ImeTracker.Token) args.arg3;
if (isValid(inputMethod, target, "DO_HIDE_SOFT_INPUT")) {
ImeTracker.forLogging().onProgress(
statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
inputMethod.hideSoftInputWithToken(msg.arg1, (ResultReceiver) args.arg2,
(IBinder) args.arg1, statsToken);
} else {
ImeTracker.forLogging().onFailed(
statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
}
args.recycle();
return;
}
case DO_CHANGE_INPUTMETHOD_SUBTYPE:
if (isValid(inputMethod, target, "DO_CHANGE_INPUTMETHOD_SUBTYPE")) {
inputMethod.changeInputMethodSubtype((InputMethodSubtype) msg.obj);
}
return;
case DO_CREATE_INLINE_SUGGESTIONS_REQUEST: {
final SomeArgs args = (SomeArgs) msg.obj;
if (isValid(inputMethod, target, "DO_CREATE_INLINE_SUGGESTIONS_REQUEST")) {
inputMethod.onCreateInlineSuggestionsRequest(
(InlineSuggestionsRequestInfo) args.arg1,
(IInlineSuggestionsRequestCallback) args.arg2);
}
args.recycle();
return;
}
case DO_CAN_START_STYLUS_HANDWRITING: {
if (isValid(inputMethod, target, "DO_CAN_START_STYLUS_HANDWRITING")) {
inputMethod.canStartStylusHandwriting(msg.arg1);
}
return;
}
case DO_UPDATE_TOOL_TYPE: {
if (isValid(inputMethod, target, "DO_UPDATE_TOOL_TYPE")) {
inputMethod.updateEditorToolType(msg.arg1);
}
return;
}
case DO_START_STYLUS_HANDWRITING: {
final SomeArgs args = (SomeArgs) msg.obj;
if (isValid(inputMethod, target, "DO_START_STYLUS_HANDWRITING")) {
inputMethod.startStylusHandwriting(msg.arg1, (InputChannel) args.arg1,
(List<MotionEvent>) args.arg2);
}
args.recycle();
return;
}
case DO_INIT_INK_WINDOW: {
if (isValid(inputMethod, target, "DO_INIT_INK_WINDOW")) {
inputMethod.initInkWindow();
}
return;
}
case DO_FINISH_STYLUS_HANDWRITING: {
if (isValid(inputMethod, target, "DO_FINISH_STYLUS_HANDWRITING")) {
inputMethod.finishStylusHandwriting();
}
return;
}
case DO_REMOVE_STYLUS_HANDWRITING_WINDOW: {
if (isValid(inputMethod, target, "DO_REMOVE_STYLUS_HANDWRITING_WINDOW")) {
inputMethod.removeStylusHandwritingWindow();
}
return;
}
case DO_SET_STYLUS_WINDOW_IDLE_TIMEOUT: {
if (isValid(inputMethod, target, "DO_SET_STYLUS_WINDOW_IDLE_TIMEOUT")) {
inputMethod.setStylusWindowIdleTimeoutForTest((long) msg.obj);
}
return;
}
}
Log.w(TAG, "Unhandled message code: " + msg.what);
}
@BinderThread
@Override
protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
InputMethodServiceInternal target = mTarget.get();
if (target == null) {
return;
}
if (target.getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
!= PackageManager.PERMISSION_GRANTED) {
fout.println("Permission Denial: can't dump InputMethodManager from from pid="
+ Binder.getCallingPid()
+ ", uid=" + Binder.getCallingUid());
return;
}
CountDownLatch latch = new CountDownLatch(1);
mCaller.getHandler().sendMessageAtFrontOfQueue(mCaller.obtainMessageOOOO(DO_DUMP,
fd, fout, args, latch));
try {
if (!latch.await(5, TimeUnit.SECONDS)) {
fout.println("Timeout waiting for dump");
}
} catch (InterruptedException e) {
fout.println("Interrupted waiting for dump");
}
}
@BinderThread
@Override
public void initializeInternal(@NonNull IInputMethod.InitParams params) {
mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_INITIALIZE_INTERNAL, params));
}
@BinderThread
@Override
public void onCreateInlineSuggestionsRequest(InlineSuggestionsRequestInfo requestInfo,
IInlineSuggestionsRequestCallback cb) {
mCaller.executeOrSendMessage(
mCaller.obtainMessageOO(DO_CREATE_INLINE_SUGGESTIONS_REQUEST, requestInfo, cb));
}
@BinderThread
@Override
public void bindInput(InputBinding binding) {
if (mCancellationGroup != null) {
Log.e(TAG, "bindInput must be paired with unbindInput.");
}
mCancellationGroup = new CancellationGroup();
InputConnection ic = new RemoteInputConnection(mTarget,
IRemoteInputConnection.Stub.asInterface(binding.getConnectionToken()),
mCancellationGroup);
InputBinding nu = new InputBinding(ic, binding);
mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_INPUT_CONTEXT, nu));
}
@BinderThread
@Override
public void unbindInput() {
if (mCancellationGroup != null) {
// Signal the flag then forget it.
mCancellationGroup.cancelAll();
mCancellationGroup = null;
} else {
Log.e(TAG, "unbindInput must be paired with bindInput.");
}
mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_UNSET_INPUT_CONTEXT));
}
@BinderThread
@Override
public void startInput(@NonNull IInputMethod.StartInputParams params) {
if (mCancellationGroup == null) {
Log.e(TAG, "startInput must be called after bindInput.");
mCancellationGroup = new CancellationGroup();
}
params.editorInfo.makeCompatible(mTargetSdkVersion);
final InputConnection ic = params.remoteInputConnection == null ? null
: new RemoteInputConnection(mTarget, params.remoteInputConnection,
mCancellationGroup);
mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_START_INPUT, ic, params));
}
@BinderThread
@Override
public void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) {
mCaller.executeOrSendMessage(
mCaller.obtainMessageI(DO_ON_NAV_BUTTON_FLAGS_CHANGED, navButtonFlags));
}
@BinderThread
@Override
public void createSession(InputChannel channel, IInputMethodSessionCallback callback) {
mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_CREATE_SESSION,
channel, callback));
}
@BinderThread
@Override
public void setSessionEnabled(IInputMethodSession session, boolean enabled) {
try {
InputMethodSession ls = ((IInputMethodSessionWrapper)
session).getInternalInputMethodSession();
if (ls == null) {
Log.w(TAG, "Session is already finished: " + session);
return;
}
mCaller.executeOrSendMessage(mCaller.obtainMessageIO(
DO_SET_SESSION_ENABLED, enabled ? 1 : 0, ls));
} catch (ClassCastException e) {
Log.w(TAG, "Incoming session not of correct type: " + session, e);
}
}
@BinderThread
@Override
public void showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
@InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) {
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_SHOW_SOFT_INPUT,
flags, showInputToken, resultReceiver, statsToken));
}
@BinderThread
@Override
public void hideSoftInput(IBinder hideInputToken, @Nullable ImeTracker.Token statsToken,
int flags, ResultReceiver resultReceiver) {
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_HIDE_SOFT_INPUT,
flags, hideInputToken, resultReceiver, statsToken));
}
@BinderThread
@Override
public void changeInputMethodSubtype(InputMethodSubtype subtype) {
mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_CHANGE_INPUTMETHOD_SUBTYPE,
subtype));
}
@BinderThread
@Override
public void canStartStylusHandwriting(int requestId)
throws RemoteException {
mCaller.executeOrSendMessage(
mCaller.obtainMessageI(DO_CAN_START_STYLUS_HANDWRITING, requestId));
}
@BinderThread
@Override
public void updateEditorToolType(int toolType)
throws RemoteException {
mCaller.executeOrSendMessage(
mCaller.obtainMessageI(DO_UPDATE_TOOL_TYPE, toolType));
}
@BinderThread
@Override
public void startStylusHandwriting(int requestId, @NonNull InputChannel channel,
@Nullable List<MotionEvent> stylusEvents)
throws RemoteException {
mCaller.executeOrSendMessage(
mCaller.obtainMessageIOO(DO_START_STYLUS_HANDWRITING, requestId, channel,
stylusEvents));
}
@BinderThread
@Override
public void initInkWindow() {
mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_INIT_INK_WINDOW));
}
@BinderThread
@Override
public void finishStylusHandwriting() {
mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_FINISH_STYLUS_HANDWRITING));
}
@BinderThread
@Override
public void removeStylusHandwritingWindow() {
mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_STYLUS_HANDWRITING_WINDOW));
}
@BinderThread
@Override
public void setStylusWindowIdleTimeoutForTest(@DurationMillisLong long timeout) {
mCaller.executeOrSendMessage(
mCaller.obtainMessageO(DO_SET_STYLUS_WINDOW_IDLE_TIMEOUT, timeout));
}
private static boolean isValid(InputMethod inputMethod, InputMethodServiceInternal target,
String msg) {
if (inputMethod != null && target != null && !target.isServiceDestroyed()) {
return true;
} else {
Log.w(TAG, "Ignoring " + msg + ", InputMethod:" + inputMethod
+ ", InputMethodServiceInternal:" + target);
return false;
}
}
}