/* * 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.CursorAnchorInfo; 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.IConnectionlessHandwritingCallback; 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; private static final int DO_COMMIT_HANDWRITING_DELEGATION_TEXT_IF_AVAILABLE = 170; private static final int DO_DISCARD_HANDWRITING_DELEGATION_TEXT = 180; final WeakReference mTarget; final Context mContext; @UnsupportedAppUsage final HandlerCaller mCaller; final WeakReference 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. * *

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.

*/ @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: { final SomeArgs args = (SomeArgs) msg.obj; if (isValid(inputMethod, target, "DO_CAN_START_STYLUS_HANDWRITING")) { inputMethod.canStartStylusHandwriting(msg.arg1, (IConnectionlessHandwritingCallback) args.arg1, (CursorAnchorInfo) args.arg2, msg.arg2 != 0); } args.recycle(); 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) 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; } case DO_COMMIT_HANDWRITING_DELEGATION_TEXT_IF_AVAILABLE: { if (isValid(inputMethod, target, "DO_COMMIT_HANDWRITING_DELEGATION_TEXT_IF_AVAILABLE")) { inputMethod.commitHandwritingDelegationTextIfAvailable(); } return; } case DO_DISCARD_HANDWRITING_DELEGATION_TEXT: { if (isValid(inputMethod, target, "DO_DISCARD_HANDWRITING_DELEGATION_TEXT")) { inputMethod.discardHandwritingDelegationText(); } 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, @NonNull 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, @NonNull 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, IConnectionlessHandwritingCallback connectionlessCallback, CursorAnchorInfo cursorAnchorInfo, boolean isConnectionlessForDelegation) throws RemoteException { mCaller.executeOrSendMessage(mCaller.obtainMessageIIOO(DO_CAN_START_STYLUS_HANDWRITING, requestId, isConnectionlessForDelegation ? 1 : 0, connectionlessCallback, cursorAnchorInfo)); } @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 stylusEvents) throws RemoteException { mCaller.executeOrSendMessage( mCaller.obtainMessageIOO(DO_START_STYLUS_HANDWRITING, requestId, channel, stylusEvents)); } @BinderThread @Override public void commitHandwritingDelegationTextIfAvailable() { mCaller.executeOrSendMessage( mCaller.obtainMessage(DO_COMMIT_HANDWRITING_DELEGATION_TEXT_IF_AVAILABLE)); } @BinderThread @Override public void discardHandwritingDelegationText() { mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_DISCARD_HANDWRITING_DELEGATION_TEXT)); } @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; } } }