| /* |
| * 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<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: { |
| 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<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; |
| } |
| 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<MotionEvent> 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; |
| } |
| } |
| } |