/* * Copyright (C) 2007-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.view.inputmethod; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR; import static android.view.inputmethod.Flags.initiationWithoutInputConnection; import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_IMMEDIATE; import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_MONITOR; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.DISPLAY_ID; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.EDITOR_INFO; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_INSETS_SOURCE_CONSUMER; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INPUT_CONNECTION; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INPUT_CONNECTION_CALL; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INPUT_METHOD_MANAGER; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.VIEW_ROOT_IMPL; import static android.view.inputmethod.InputMethodManagerProto.ACTIVE; import static android.view.inputmethod.InputMethodManagerProto.CUR_ID; import static android.view.inputmethod.InputMethodManagerProto.FULLSCREEN_MODE; import static android.view.inputmethod.InputMethodManagerProto.NEXT_SERVED_VIEW; import static android.view.inputmethod.InputMethodManagerProto.SERVED_CONNECTING; import static android.view.inputmethod.InputMethodManagerProto.SERVED_VIEW; import static com.android.internal.inputmethod.StartInputReason.BOUND_TO_IMMS; import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.DisplayContext; import android.annotation.DrawableRes; import android.annotation.DurationMillisLong; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresFeature; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.UiThread; import android.annotation.UserIdInt; import android.app.ActivityThread; import android.app.PropertyInvalidatedCache; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.inputmethodservice.InputMethodService; import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.ResultReceiver; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.text.style.SuggestionSpan; import android.util.Log; import android.util.Pair; import android.util.Pools.Pool; import android.util.Pools.SimplePool; import android.util.PrintWriterPrinter; import android.util.Printer; import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import android.view.Display; import android.view.ImeFocusController; import android.view.ImeInsetsSourceConsumer; import android.view.InputChannel; import android.view.InputEvent; import android.view.InputEventSender; import android.view.KeyEvent; import android.view.View; import android.view.ViewRootImpl; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowManager.LayoutParams.SoftInputModeFlags; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; import android.window.ImeOnBackInvokedDispatcher; import android.window.WindowOnBackInvokedDispatcher; import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.DirectBootAwareness; import com.android.internal.inputmethod.IBooleanListener; import com.android.internal.inputmethod.IConnectionlessHandwritingCallback; import com.android.internal.inputmethod.IInputMethodClient; import com.android.internal.inputmethod.IInputMethodSession; import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; import com.android.internal.inputmethod.ImeTracing; import com.android.internal.inputmethod.InputBindResult; import com.android.internal.inputmethod.InputMethodDebug; import com.android.internal.inputmethod.InputMethodPrivilegedOperationsRegistry; import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.inputmethod.StartInputFlags; import com.android.internal.inputmethod.StartInputReason; import com.android.internal.inputmethod.UnbindReason; import com.android.internal.os.SomeArgs; import com.android.internal.view.IInputMethodManager; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; /** * Central system API to the overall input method framework (IMF) architecture, * which arbitrates interaction between applications and the current input method. * *

Topics covered here: *

    *
  1. Architecture Overview *
  2. Applications *
  3. Input Methods *
  4. Security *
* * *

Architecture Overview

* *

There are three primary parties involved in the input method * framework (IMF) architecture:

* * * * * *

Applications

* *

In most cases, applications that are using the standard * {@link android.widget.TextView} or its subclasses will have little they need * to do to work well with soft input methods. The main things you need to * be aware of are:

* * * *

More finer-grained control is available through the APIs here to directly * interact with the IMF and its IME -- either showing or hiding the input * area, letting the user pick an input method, etc.

* *

For the rare people amongst us writing their own text editors, you * will need to implement {@link android.view.View#onCreateInputConnection} * to return a new instance of your own {@link InputConnection} interface * allowing the IME to interact with your editor.

* * * *

Input Methods

* *

An input method (IME) is implemented * as a {@link android.app.Service}, typically deriving from * {@link android.inputmethodservice.InputMethodService}. It must provide * the core {@link InputMethod} interface, though this is normally handled by * {@link android.inputmethodservice.InputMethodService} and implementors will * only need to deal with the higher-level API there.

* * See the {@link android.inputmethodservice.InputMethodService} class for * more information on implementing IMEs. * * * *

Security

* *

There are a lot of security issues associated with input methods, * since they essentially have freedom to completely drive the UI and monitor * everything the user enters. The Android input method framework also allows * arbitrary third party IMEs, so care must be taken to restrict their * selection and interactions.

* *

Here are some key points about the security architecture behind the * IMF:

* * * *

If your app targets Android 11 (API level 30) or higher, the methods in * this class each return a filtered result by the rules of * package visibility, * except for the currently connected IME. Apps having a query for the * {@link InputMethod#SERVICE_INTERFACE} see all IMEs.

*/ @SystemService(Context.INPUT_METHOD_SERVICE) @RequiresFeature(PackageManager.FEATURE_INPUT_METHODS) public final class InputMethodManager { private static final boolean DEBUG = false; private static final String TAG = "InputMethodManager"; private static final String PENDING_EVENT_COUNTER = "aq:imm"; private static final int NOT_A_SUBTYPE_ID = -1; /** * A constant that represents Voice IME. * * @see InputMethodSubtype#getMode() */ private static final String SUBTYPE_MODE_VOICE = "voice"; /** * Provide this to {@link IInputMethodManagerGlobalInvoker#startInputOrWindowGainedFocus(int, * IInputMethodClient, IBinder, int, int, int, EditorInfo, * com.android.internal.inputmethod.IRemoteInputConnection, IRemoteAccessibilityInputConnection, * int, int, ImeOnBackInvokedDispatcher)} to receive * {@link android.window.OnBackInvokedCallback} registrations from IME. */ private final ImeOnBackInvokedDispatcher mImeDispatcher = new ImeOnBackInvokedDispatcher(Handler.getMain()) { @Override public WindowOnBackInvokedDispatcher getReceivingDispatcher() { synchronized (mH) { return mCurRootView != null ? mCurRootView.getOnBackInvokedDispatcher() : null; } } }; /** * A runnable that reports {@link InputConnection} opened event for calls to * {@link IInputMethodManagerGlobalInvoker#startInputOrWindowGainedFocusAsync}. */ private abstract static class ReportInputConnectionOpenedRunner implements Runnable { /** * Sequence number to track startInput requests to * {@link IInputMethodManagerGlobalInvoker#startInputOrWindowGainedFocusAsync} */ int mSequenceNum; ReportInputConnectionOpenedRunner(int sequenceNum) { this.mSequenceNum = sequenceNum; } } private ReportInputConnectionOpenedRunner mReportInputConnectionOpenedRunner; /** * Ensures that {@link #sInstance} becomes non-{@code null} for application that have directly * or indirectly relied on {@link #sInstance} via reflection or something like that. * *

Here are scenarios we know and there could be more scenarios we are not * aware of right know.

* * * *

Since this is purely a compatibility hack, this method must be used only from * {@link android.view.WindowManagerGlobal#getWindowSession()} and {@link #getInstance()}.

* *

TODO(Bug 116157766): Remove this method once we clean up {@link UnsupportedAppUsage}.

* @hide */ public static void ensureDefaultInstanceForDefaultDisplayIfNecessary() { // Skip this call if we are in system_server, as the system code should not use this // deprecated instance. if (!ActivityThread.isSystem()) { forContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper()); } } private static final Object sLock = new Object(); /** * @deprecated This cannot be compatible with multi-display. Please do not use this. */ @Deprecated @GuardedBy("sLock") @UnsupportedAppUsage static InputMethodManager sInstance; /** * Global map between display to {@link InputMethodManager}. * *

Currently this map works like a so-called leaky singleton. Once an instance is registered * for the associated display ID, that instance will never be garbage collected.

* *

TODO(Bug 116699479): Implement instance clean up mechanism.

*/ @GuardedBy("sLock") private static final SparseArray sInstanceMap = new SparseArray<>(); /** * Timeout in milliseconds for delivering a key to an IME. */ private static final long INPUT_METHOD_NOT_RESPONDING_TIMEOUT = 2500; /** @hide */ public static final int DISPATCH_IN_PROGRESS = -1; /** @hide */ public static final int DISPATCH_NOT_HANDLED = 0; /** @hide */ public static final int DISPATCH_HANDLED = 1; /** @hide */ public static final int SHOW_IM_PICKER_MODE_AUTO = 0; /** @hide */ public static final int SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES = 1; /** @hide */ public static final int SHOW_IM_PICKER_MODE_EXCLUDE_AUXILIARY_SUBTYPES = 2; /** * Clear {@link #SHOW_FORCED} flag when the next IME focused application changed. * *

* Note that when this flag enabled in server side, {@link #SHOW_FORCED} will no longer * affect the next focused application to keep showing IME, in case of unexpected IME visible * when the next focused app isn't be the IME requester.

* * @hide */ @TestApi @ChangeId @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) public static final long CLEAR_SHOW_FORCED_FLAG_WHEN_LEAVING = 214016041L; // This is a bug id. /** * If {@code true}, avoid calling the * {@link com.android.server.inputmethod.InputMethodManagerService InputMethodManagerService} * by skipping the call to {@link IInputMethodManager#startInputOrWindowGainedFocus} * when we are switching focus between two non-editable views. This saves the cost of a binder * call into the system server. *

Note: * The default value is {@code true}. */ private static final boolean OPTIMIZE_NONEDITABLE_VIEWS = SystemProperties.getBoolean("debug.imm.optimize_noneditable_views", true); /** @hide */ @IntDef(flag = true, prefix = { "HANDWRITING_DELEGATE_FLAG_" }, value = { HANDWRITING_DELEGATE_FLAG_HOME_DELEGATOR_ALLOWED, }) @Retention(RetentionPolicy.SOURCE) public @interface HandwritingDelegateFlags {} /** * Flag indicating that views from the default home screen ({@link Intent#CATEGORY_HOME}) may * act as a handwriting delegator for the delegate editor view. If set, views from the home * screen package will be trusted for handwriting delegation, in addition to views in the {@code * delegatorPackageName} passed to * {@link #acceptStylusHandwritingDelegation(View, String, int, Executor, Consumer)} . */ @FlaggedApi(FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR) public static final int HANDWRITING_DELEGATE_FLAG_HOME_DELEGATOR_ALLOWED = 0x0001; /** * @deprecated Use {@link IInputMethodManagerGlobalInvoker} instead. */ @Deprecated @UnsupportedAppUsage final IInputMethodManager mService; private final Looper mMainLooper; // For scheduling work on the main thread. This also serves as our // global lock. // Remark on @UnsupportedAppUsage: there were context leaks on old versions // of android (b/37043700), so developers used this field to perform manual clean up. // Leaks were fixed, hacks were backported to AppCompatActivity, // so an access to the field is closed. @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) final H mH; // Our generic input connection if the current target does not have its own. private final RemoteInputConnectionImpl mFallbackInputConnection; private final int mDisplayId; /** * True if this input method client is active, initially false. */ @GuardedBy("mH") private boolean mActive = false; /** * {@code true} if next {@link ImeFocusController#onPostWindowFocus} needs to * restart input. */ @GuardedBy("mH") private boolean mRestartOnNextWindowFocus = true; /** * As reported by IME through InputConnection. */ @GuardedBy("mH") private boolean mFullscreenMode; // ----------------------------------------------------------- /** * This is the view that should currently be served by an input method, * regardless of the state of setting that up. */ @Nullable @GuardedBy("mH") private View mServedView; /** * This is the next view that will be served by the input method, when * we get around to updating things. */ @Nullable @GuardedBy("mH") private View mNextServedView; /** * The latest {@link ViewRootImpl} that has, or most recently had, input method focus. * *

This value will be cleared when it becomes inactive and no longer has window focus. */ @Nullable @GuardedBy("mH") ViewRootImpl mCurRootView; /** * Whether the {@link #mCurRootView} currently has window focus. */ @GuardedBy("mH") boolean mCurRootViewWindowFocused; /** * This is set when we are in the process of connecting, to determine * when we have actually finished. */ @GuardedBy("mH") private boolean mServedConnecting; /** * This is non-null when we have connected the served view; it holds * the attributes that were last retrieved from the served view and given * to the input connection. */ @GuardedBy("mH") private EditorInfo mCurrentEditorInfo; @GuardedBy("mH") @Nullable private ViewFocusParameterInfo mPreviousViewFocusParameters; /** * The InputConnection that was last retrieved from the served view. */ @GuardedBy("mH") private RemoteInputConnectionImpl mServedInputConnection; /** * The completions that were last provided by the served view. */ @GuardedBy("mH") private CompletionInfo[] mCompletions; // Cursor position on the screen. @GuardedBy("mH") @UnsupportedAppUsage Rect mTmpCursorRect = new Rect(); @GuardedBy("mH") @UnsupportedAppUsage Rect mCursorRect = new Rect(); /** Cached value for {@link #isStylusHandwritingAvailable} for userId. */ @GuardedBy("mH") private PropertyInvalidatedCache mStylusHandwritingAvailableCache; /** Cached value for {@link #isConnectionlessStylusHandwritingAvailable} for userId. */ @GuardedBy("mH") private PropertyInvalidatedCache mConnectionlessStylusHandwritingAvailableCache; private static final String CACHE_KEY_STYLUS_HANDWRITING_PROPERTY = "cache_key.system_server.stylus_handwriting"; private static final String CACHE_KEY_CONNECTIONLESS_STYLUS_HANDWRITING_PROPERTY = "cache_key.system_server.connectionless_stylus_handwriting"; @GuardedBy("mH") private int mCursorSelStart; @GuardedBy("mH") private int mCursorSelEnd; @GuardedBy("mH") private int mCursorCandStart; @GuardedBy("mH") private int mCursorCandEnd; @GuardedBy("mH") private int mInitialSelStart; @GuardedBy("mH") private int mInitialSelEnd; /** * Handler for {@link RemoteInputConnectionImpl#getInputConnection()}. */ @GuardedBy("mH") private Handler mServedInputConnectionHandler; /** * The instance that has previously been sent to the input method. */ @GuardedBy("mH") private CursorAnchorInfo mCursorAnchorInfo = null; // ----------------------------------------------------------- /** * ID of the method we are bound to. * * @deprecated New code should use {@code mCurBindState.mImeId}. */ @Deprecated @GuardedBy("mH") @UnsupportedAppUsage(trackingBug = 236937383, maxTargetSdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, publicAlternatives = "Apps should not change behavior based on the currently connected" + " IME. If absolutely needed, use {@link InputMethodInfo#getId()} instead.") String mCurId; /** * Kept for {@link UnsupportedAppUsage}. Not officially maintained. * * @deprecated New code should use {@code mCurBindState.mImeSession}. */ @Deprecated @GuardedBy("mH") @Nullable @UnsupportedAppUsage(trackingBug = 236937383, maxTargetSdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, publicAlternatives = "Use methods on {@link InputMethodManager} instead.") IInputMethodSession mCurMethod; /** * Encapsulates per-binding state from {@link InputBindResult}. */ @GuardedBy("mH") @Nullable private BindState mCurBindState; /** * Encapsulates IPCs to the currently connected AccessibilityServices. */ @Nullable @GuardedBy("mH") private final SparseArray mAccessibilityInputMethodSession = new SparseArray<>(); @GuardedBy("mH") private InputChannel mCurChannel; @GuardedBy("mH") private ImeInputEventSender mCurSender; private static final int REQUEST_UPDATE_CURSOR_ANCHOR_INFO_NONE = 0x0; /** * The monitor mode for {@link #updateCursorAnchorInfo(View, CursorAnchorInfo)}. * @deprecated This is kept for {@link UnsupportedAppUsage}. Must not be used. */ @Deprecated @GuardedBy("mH") private int mRequestUpdateCursorAnchorInfoMonitorMode = REQUEST_UPDATE_CURSOR_ANCHOR_INFO_NONE; /** * Applies the IME visibility and listens for other state changes. */ @GuardedBy("mH") private ImeInsetsSourceConsumer mImeInsetsConsumer; @GuardedBy("mH") private final Pool mPendingEventPool = new SimplePool<>(20); @GuardedBy("mH") private final SparseArray mPendingEvents = new SparseArray<>(20); private final DelegateImpl mDelegate = new DelegateImpl(); private static boolean sPreventImeStartupUnlessTextEditor; // ----------------------------------------------------------- private static final int MSG_DUMP = 1; private static final int MSG_BIND = 2; private static final int MSG_UNBIND = 3; private static final int MSG_SET_ACTIVE = 4; private static final int MSG_SEND_INPUT_EVENT = 5; private static final int MSG_TIMEOUT_INPUT_EVENT = 6; private static final int MSG_FLUSH_INPUT_EVENT = 7; private static final int MSG_REPORT_FULLSCREEN_MODE = 10; private static final int MSG_BIND_ACCESSIBILITY_SERVICE = 11; private static final int MSG_UNBIND_ACCESSIBILITY_SERVICE = 12; private static final int MSG_SET_INTERACTIVE = 13; private static final int MSG_ON_SHOW_REQUESTED = 31; private static final int MSG_START_INPUT_RESULT = 40; /** * Calling this will invalidate Local stylus handwriting availability Cache which * forces the next query in any process to recompute the cache. * @hide */ public static void invalidateLocalStylusHandwritingAvailabilityCaches() { PropertyInvalidatedCache.invalidateCache(CACHE_KEY_STYLUS_HANDWRITING_PROPERTY); } /** * Calling this will invalidate the local connectionless stylus handwriting availability cache, * which forces the next query in any process to recompute the cache. * * @hide */ public static void invalidateLocalConnectionlessStylusHandwritingAvailabilityCaches() { PropertyInvalidatedCache.invalidateCache( CACHE_KEY_CONNECTIONLESS_STYLUS_HANDWRITING_PROPERTY); } private static boolean isAutofillUIShowing(View servedView) { AutofillManager afm = servedView.getContext().getSystemService(AutofillManager.class); return afm != null && afm.isAutofillUiShowing(); } /** * Returns fallback {@link InputMethodManager} if the called one is not likely to be compatible * with the given {@code view}. * * @param view {@link View} to be checked. * @return {@code null} when it is unnecessary (or impossible) to use fallback * {@link InputMethodManager} to which IME API calls need to be re-dispatched. * Non-{@code null} {@link InputMethodManager} if this method believes it'd be safer to * re-dispatch IME APIs calls on it. */ @Nullable private InputMethodManager getFallbackInputMethodManagerIfNecessary(@Nullable View view) { if (view == null) { return null; } // As evidenced in Bug 118341760, view.getViewRootImpl().getDisplayId() is supposed to be // more reliable to determine with which display the given view is interacting than // view.getContext().getDisplayId() / view.getContext().getSystemService(), which can be // easily messed up by app developers (or library authors) by creating inconsistent // ContextWrapper objects that re-dispatch those methods to other Context such as // ApplicationContext. final ViewRootImpl viewRootImpl = view.getViewRootImpl(); if (viewRootImpl == null) { return null; } final int viewRootDisplayId = viewRootImpl.getDisplayId(); if (viewRootDisplayId == mDisplayId) { // Expected case. Good to go. return null; } final InputMethodManager fallbackImm = viewRootImpl.mContext.getSystemService(InputMethodManager.class); if (fallbackImm == null) { Log.v(TAG, "b/117267690: Failed to get non-null fallback IMM. view=" + view); return null; } if (fallbackImm.mDisplayId != viewRootDisplayId) { Log.v(TAG, "b/117267690: Failed to get fallback IMM with expected displayId=" + viewRootDisplayId + " actual IMM#displayId=" + fallbackImm.mDisplayId + " view=" + view); return null; } Log.v(TAG, "b/117267690: Display ID mismatch found." + " ViewRootImpl displayId=" + viewRootDisplayId + " InputMethodManager displayId=" + mDisplayId + ". Use the right InputMethodManager instance to avoid performance overhead.", new Throwable()); return fallbackImm; } /** * An internal API that returns the {@link Context} of the current served view connected to * an input method. * @hide */ Context getFallbackContextFromServedView() { synchronized (mH) { if (mCurRootView == null) { return null; } return mServedView != null ? mServedView.getContext() : null; } } private static boolean canStartInput(View servedView) { // We can start input ether the servedView has window focus // or the activity is showing autofill ui. return servedView.hasWindowFocus() || isAutofillUIShowing(servedView); } /** * Reports whether the IME is currently perceptible or not, according to the leash applied by * {@link android.view.WindowInsetsController}. * @hide */ public void reportPerceptible(@NonNull IBinder windowToken, boolean perceptible) { IInputMethodManagerGlobalInvoker.reportPerceptibleAsync(windowToken, perceptible); } private final class DelegateImpl implements ImeFocusController.InputMethodManagerDelegate { @Override public void onPreWindowGainedFocus(ViewRootImpl viewRootImpl) { synchronized (mH) { setCurrentRootViewLocked(viewRootImpl); mCurRootViewWindowFocused = true; } } @Override public void onPostWindowGainedFocus(View viewForWindowFocus, @NonNull WindowManager.LayoutParams windowAttribute) { boolean forceFocus = false; synchronized (mH) { // Update mNextServedView when focusedView changed. onViewFocusChangedInternal(viewForWindowFocus, true); // Starting new input when the next focused view is same as served view but the // currently active connection (if any) is not associated with it. final boolean nextFocusIsServedView = mServedView == viewForWindowFocus; if (nextFocusIsServedView && !hasActiveInputConnectionInternal(viewForWindowFocus)) { forceFocus = true; } } final int softInputMode = windowAttribute.softInputMode; final int windowFlags = windowAttribute.flags; int startInputFlags = getStartInputFlags(viewForWindowFocus, 0); startInputFlags |= StartInputFlags.WINDOW_GAINED_FOCUS; ImeTracing.getInstance().triggerClientDump( "InputMethodManager.DelegateImpl#startInputAsyncOnWindowFocusGain", InputMethodManager.this, null /* icProto */); boolean checkFocusResult; synchronized (mH) { if (mCurRootView == null) { return; } if (mRestartOnNextWindowFocus) { if (DEBUG) Log.v(TAG, "Restarting due to mRestartOnNextWindowFocus as true"); mRestartOnNextWindowFocus = false; forceFocus = true; } checkFocusResult = checkFocusInternalLocked(forceFocus, mCurRootView); } if (checkFocusResult) { // We need to restart input on the current focus view. This // should be done in conjunction with telling the system service // about the window gaining focus, to help make the transition // smooth. if (startInputOnWindowFocusGainInternal(StartInputReason.WINDOW_FOCUS_GAIN, viewForWindowFocus, startInputFlags, softInputMode, windowFlags)) { return; } } synchronized (mH) { // For some reason we didn't do a startInput + windowFocusGain, so // we'll just do a window focus gain and call it a day. if (DEBUG) { Log.v(TAG, "Reporting focus gain, without startInput"); } // ignore the result Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMM.startInputOrWindowGainedFocus"); IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus( StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient, viewForWindowFocus.getWindowToken(), startInputFlags, softInputMode, windowFlags, null, null, null, mCurRootView.mContext.getApplicationInfo().targetSdkVersion, UserHandle.myUserId(), mImeDispatcher); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } } @Override public void onWindowLostFocus(@NonNull ViewRootImpl viewRootImpl) { synchronized (mH) { if (mCurRootView == viewRootImpl) { mCurRootViewWindowFocused = false; clearCurRootViewIfNeeded(); } } } @Override public void onViewFocusChanged(@Nullable View view, boolean hasFocus) { onViewFocusChangedInternal(view, hasFocus); } @Override public void onScheduledCheckFocus(ViewRootImpl viewRootImpl) { synchronized (mH) { if (!checkFocusInternalLocked(false, viewRootImpl)) { return; } } startInputOnWindowFocusGainInternal(StartInputReason.SCHEDULED_CHECK_FOCUS, null /* focusedView */, 0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */); } @Override public void onViewDetachedFromWindow(View view, ViewRootImpl viewRootImpl) { synchronized (mH) { if (mCurRootView != view.getViewRootImpl()) { return; } if (mNextServedView == view) { mNextServedView = null; } if (mServedView == view) { viewRootImpl.dispatchCheckFocus(); } } } @Override public void onWindowDismissed(ViewRootImpl viewRootImpl) { synchronized (mH) { if (mCurRootView != viewRootImpl) { return; } if (mServedView != null) { finishInputLocked(); } setCurrentRootViewLocked(null); } } @GuardedBy("mH") private void setCurrentRootViewLocked(ViewRootImpl rootView) { mImeDispatcher.switchRootView(mCurRootView, rootView); mCurRootView = rootView; } } /** @hide */ public DelegateImpl getDelegate() { return mDelegate; } /** * Checks whether the active input connection (if any) is for the given view. * *

Note that {@code view} parameter does not take * {@link View#checkInputConnectionProxy(View)} into account. This method returns {@code true} * when and only when the specified {@code view} is the actual {@link View} instance that is * connected to the IME.

* * @param view {@link View} to be checked. * @return {@code true} if {@code view} is currently interacting with IME. * @hide */ @TestApi public boolean hasActiveInputConnection(@Nullable View view) { synchronized (mH) { return mCurRootView != null && view != null && mServedView == view && mServedInputConnection != null && mServedInputConnection.isAssociatedWith(view) && isImeSessionAvailableLocked(); } } /** * Checks whether the active input connection (if any) is for the given view. * * Note that this method is only intended for restarting input after focus gain * (e.g. b/160391516), DO NOT leverage this method to do another check. */ private boolean hasActiveInputConnectionInternal(@Nullable View view) { synchronized (mH) { if (!hasServedByInputMethodLocked(view) || !isImeSessionAvailableLocked()) { return false; } return mServedInputConnection != null && mServedInputConnection.isAssociatedWith(view); } } private boolean startInputOnWindowFocusGainInternal(@StartInputReason int startInputReason, View focusedView, @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode, int windowFlags) { synchronized (mH) { mCurrentEditorInfo = null; mCompletions = null; mServedConnecting = true; } return startInputInner(startInputReason, focusedView != null ? focusedView.getWindowToken() : null, startInputFlags, softInputMode, windowFlags); } @GuardedBy("mH") private View getServedViewLocked() { return mCurRootView != null ? mServedView : null; } @GuardedBy("mH") private View getNextServedViewLocked() { return mCurRootView != null ? mNextServedView : null; } /** * Returns {@code true} when the given view has been served by Input Method. */ @GuardedBy("mH") private boolean hasServedByInputMethodLocked(View view) { final View servedView = getServedViewLocked(); return (servedView == view || (servedView != null && servedView.checkInputConnectionProxy(view))); } class H extends Handler { H(Looper looper) { super(looper, null, true); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_DUMP: { SomeArgs args = (SomeArgs)msg.obj; try { doDump((FileDescriptor)args.arg1, (PrintWriter)args.arg2, (String[])args.arg3); } catch (RuntimeException e) { ((PrintWriter)args.arg2).println("Exception: " + e); } synchronized (args.arg4) { ((CountDownLatch)args.arg4).countDown(); } args.recycle(); return; } case MSG_BIND: { final InputBindResult res = (InputBindResult) msg.obj; if (DEBUG) { Log.i(TAG, "handleMessage: MSG_BIND " + res.sequence + "," + res.id); } synchronized (mH) { final int curBindSequence = getBindSequenceLocked(); if (curBindSequence < 0 || curBindSequence != res.sequence) { Log.w(TAG, "Ignoring onBind: cur seq=" + curBindSequence + ", given seq=" + res.sequence); if (res.channel != null && res.channel != mCurChannel) { res.channel.dispose(); } return; } mRequestUpdateCursorAnchorInfoMonitorMode = REQUEST_UPDATE_CURSOR_ANCHOR_INFO_NONE; updateInputChannelLocked(res.channel); mCurMethod = res.method; // for @UnsupportedAppUsage mCurBindState = new BindState(res); mCurId = res.id; // for @UnsupportedAppUsage } startInputInner(StartInputReason.BOUND_TO_IMMS, null, 0, 0, 0); return; } case MSG_START_INPUT_RESULT: { final InputBindResult res = (InputBindResult) msg.obj; final int startInputSeq = msg.arg1; if (res == null) { // IMMS logs .wtf already. return; } if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res); synchronized (mH) { if (res.id != null) { updateInputChannelLocked(res.channel); mCurMethod = res.method; // for @UnsupportedAppUsage mCurBindState = new BindState(res); mAccessibilityInputMethodSession.clear(); if (res.accessibilitySessions != null) { for (int i = 0; i < res.accessibilitySessions.size(); i++) { IAccessibilityInputMethodSessionInvoker wrapper = IAccessibilityInputMethodSessionInvoker.createOrNull( res.accessibilitySessions.valueAt(i)); if (wrapper != null) { mAccessibilityInputMethodSession.append( res.accessibilitySessions.keyAt(i), wrapper); } } } mCurId = res.id; // for @UnsupportedAppUsage } else if (res.channel != null && res.channel != mCurChannel) { res.channel.dispose(); } switch (res.result) { case InputBindResult.ResultCode.ERROR_NOT_IME_TARGET_WINDOW: mRestartOnNextWindowFocus = true; mServedView = null; break; } if (mCompletions != null) { if (isImeSessionAvailableLocked()) { mCurBindState.mImeSession.displayCompletions(mCompletions); } } if (res != null && res.method != null && mServedView != null && mReportInputConnectionOpenedRunner != null && mReportInputConnectionOpenedRunner.mSequenceNum == startInputSeq) { mReportInputConnectionOpenedRunner.run(); } mReportInputConnectionOpenedRunner = null; } return; } case MSG_UNBIND: { final int sequence = msg.arg1; @UnbindReason final int reason = msg.arg2; if (DEBUG) { Log.i(TAG, "handleMessage: MSG_UNBIND " + sequence + " reason=" + InputMethodDebug.unbindReasonToString(reason)); } final boolean startInput; synchronized (mH) { if (reason == UnbindReason.DISCONNECT_IME) { mImeDispatcher.clear(); } if (getBindSequenceLocked() != sequence) { return; } clearAllAccessibilityBindingLocked(); clearBindingLocked(); // If we were actively using the last input method, then // we would like to re-connect to the next input method. final View servedView = getServedViewLocked(); if (servedView != null && servedView.isFocused()) { mServedConnecting = true; } startInput = mActive; } if (startInput) { startInputInner( StartInputReason.UNBOUND_FROM_IMMS, null, 0, 0, 0); } return; } case MSG_BIND_ACCESSIBILITY_SERVICE: { final int id = msg.arg1; final InputBindResult res = (InputBindResult) msg.obj; if (DEBUG) { Log.i(TAG, "handleMessage: MSG_BIND_ACCESSIBILITY " + res.sequence + "," + res.id); } synchronized (mH) { final int curBindSequence = getBindSequenceLocked(); if (curBindSequence < 0 || curBindSequence != res.sequence) { Log.w(TAG, "Ignoring onBind: cur seq=" + curBindSequence + ", given seq=" + res.sequence); if (res.channel != null && res.channel != mCurChannel) { res.channel.dispose(); } return; } // Since IMM can start inputting text before a11y sessions are back, // we send a notification so that the a11y service knows the session is // registered and update the a11y service with the current cursor positions. if (res.accessibilitySessions != null) { IAccessibilityInputMethodSessionInvoker invoker = IAccessibilityInputMethodSessionInvoker.createOrNull( res.accessibilitySessions.get(id)); if (invoker != null) { mAccessibilityInputMethodSession.put(id, invoker); if (mServedInputConnection != null) { invoker.updateSelection(mInitialSelStart, mInitialSelEnd, mCursorSelStart, mCursorSelEnd, mCursorCandStart, mCursorCandEnd); } else { // If an a11y service binds before input starts, we should still // send a notification because the a11y service doesn't know it // binds before or after input starts, it may wonder if it binds // after input starts, why it doesn't receive a notification of // the current cursor positions. invoker.updateSelection(-1, -1, -1, -1, -1, -1); } } } } startInputInner(StartInputReason.BOUND_ACCESSIBILITY_SESSION_TO_IMMS, null, 0, 0, 0); return; } case MSG_UNBIND_ACCESSIBILITY_SERVICE: { final int sequence = msg.arg1; final int id = msg.arg2; if (DEBUG) { Log.i(TAG, "handleMessage: MSG_UNBIND_ACCESSIBILITY_SERVICE " + sequence + " id=" + id); } synchronized (mH) { if (getBindSequenceLocked() != sequence) { if (DEBUG) { Log.i(TAG, "current BindSequence =" + getBindSequenceLocked() + " sequence =" + sequence + " id=" + id); } return; } clearAccessibilityBindingLocked(id); } return; } case MSG_SET_ACTIVE: { final boolean active = msg.arg1 != 0; final boolean fullscreen = msg.arg2 != 0; if (DEBUG) { Log.i(TAG, "handleMessage: MSG_SET_ACTIVE " + active + ", was " + mActive); } synchronized (mH) { mActive = active; mFullscreenMode = fullscreen; if (!active) { // Some other client has starting using the IME, so note // that this happened and make sure our own editor's // state is reset. mRestartOnNextWindowFocus = true; // Note that finishComposingText() is allowed to run // even when we are not active. mFallbackInputConnection.finishComposingTextFromImm(); if (clearCurRootViewIfNeeded()) { return; } } // Check focus again in case that "onWindowFocus" is called before // handling this message. final View servedView = getServedViewLocked(); if (servedView == null || !canStartInput(servedView)) { return; } if (mCurRootView == null) { return; } if (!checkFocusInternalLocked(mRestartOnNextWindowFocus, mCurRootView)) { return; } mCurrentEditorInfo = null; mCompletions = null; mServedConnecting = true; } final int reason = active ? StartInputReason.ACTIVATED_BY_IMMS : StartInputReason.DEACTIVATED_BY_IMMS; startInputInner(reason, null, 0, 0, 0); return; } case MSG_SET_INTERACTIVE: { final boolean interactive = msg.arg1 != 0; final boolean fullscreen = msg.arg2 != 0; if (DEBUG) { Log.i(TAG, "handleMessage: MSG_SET_INTERACTIVE " + interactive + ", was " + mActive); } synchronized (mH) { mActive = interactive; mFullscreenMode = fullscreen; if (interactive) { // Find the next view focus to start the input connection when the // device was interactive. final View rootView = mCurRootView != null ? mCurRootView.getView() : null; if (rootView == null) { // No window focused or view was removed, ignore request. return; } final ViewRootImpl currentViewRootImpl = mCurRootView; // Post this on UI thread as required for view focus code. rootView.post(() -> { synchronized (mH) { if (mCurRootView != currentViewRootImpl) { // Focused window changed since posting, ignore request. return; } } final View curRootView = currentViewRootImpl.getView(); if (curRootView == null) { // View was removed, ignore request. return; } final View focusedView = curRootView.findFocus(); onViewFocusChangedInternal(focusedView, focusedView != null); }); } else { // Finish input connection when device becomes non-interactive. finishInputLocked(); if (isImeSessionAvailableLocked()) { mCurBindState.mImeSession.finishInput(); } forAccessibilitySessionsLocked( IAccessibilityInputMethodSessionInvoker::finishInput); } } return; } case MSG_SEND_INPUT_EVENT: { sendInputEventAndReportResultOnMainLooper((PendingEvent)msg.obj); return; } case MSG_TIMEOUT_INPUT_EVENT: { finishedInputEvent(msg.arg1, false, true); return; } case MSG_FLUSH_INPUT_EVENT: { finishedInputEvent(msg.arg1, false, false); return; } case MSG_REPORT_FULLSCREEN_MODE: { final boolean fullscreen = msg.arg1 != 0; RemoteInputConnectionImpl ic = null; synchronized (mH) { if (mFullscreenMode != fullscreen && mServedInputConnection != null) { ic = mServedInputConnection; mFullscreenMode = fullscreen; } } if (ic != null) { ic.dispatchReportFullscreenMode(fullscreen); } return; } case MSG_ON_SHOW_REQUESTED: { synchronized (mH) { if (mImeInsetsConsumer != null) { mImeInsetsConsumer.onShowRequested(); } } return; } } } } private final IInputMethodClient.Stub mClient = new IInputMethodClient.Stub() { @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { // No need to check for dump permission, since we only give this // interface to the system. CountDownLatch latch = new CountDownLatch(1); SomeArgs sargs = SomeArgs.obtain(); sargs.arg1 = fd; sargs.arg2 = fout; sargs.arg3 = args; sargs.arg4 = latch; mH.sendMessage(mH.obtainMessage(MSG_DUMP, sargs)); try { if (!latch.await(5, TimeUnit.SECONDS)) { fout.println("Timeout waiting for dump"); } } catch (InterruptedException e) { fout.println("Interrupted waiting for dump"); } } @Override public void onBindMethod(InputBindResult res) { mH.obtainMessage(MSG_BIND, res).sendToTarget(); } @Override public void onStartInputResult(InputBindResult res, int startInputSeq) { mH.obtainMessage(MSG_START_INPUT_RESULT, startInputSeq, -1 /* unused */, res) .sendToTarget(); } @Override public void onBindAccessibilityService(InputBindResult res, int id) { mH.obtainMessage(MSG_BIND_ACCESSIBILITY_SERVICE, id, 0, res).sendToTarget(); } @Override public void onUnbindMethod(int sequence, @UnbindReason int unbindReason) { mH.obtainMessage(MSG_UNBIND, sequence, unbindReason).sendToTarget(); } @Override public void onUnbindAccessibilityService(int sequence, int id) { mH.obtainMessage(MSG_UNBIND_ACCESSIBILITY_SERVICE, sequence, id).sendToTarget(); } @Override public void setActive(boolean active, boolean fullscreen) { mH.obtainMessage(MSG_SET_ACTIVE, active ? 1 : 0, fullscreen ? 1 : 0).sendToTarget(); } @Override public void setInteractive(boolean interactive, boolean fullscreen) { mH.obtainMessage(MSG_SET_INTERACTIVE, interactive ? 1 : 0, fullscreen ? 1 : 0) .sendToTarget(); } @Override public void scheduleStartInputIfNecessary(boolean fullscreen) { // TODO(b/149859205): See if we can optimize this by having a fused dedicated operation. mH.obtainMessage(MSG_SET_ACTIVE, 0 /* active */, fullscreen ? 1 : 0).sendToTarget(); mH.obtainMessage(MSG_SET_ACTIVE, 1 /* active */, fullscreen ? 1 : 0).sendToTarget(); } @Override public void reportFullscreenMode(boolean fullscreen) { mH.obtainMessage(MSG_REPORT_FULLSCREEN_MODE, fullscreen ? 1 : 0, 0) .sendToTarget(); } @Override public void setImeTraceEnabled(boolean enabled) { ImeTracing.getInstance().setEnabled(enabled); } @Override public void throwExceptionFromSystem(String message) { throw new RuntimeException(message); } }; /** * For layoutlib to clean up static objects inside {@link InputMethodManager}. */ static void tearDownEditMode() { if (!isInEditMode()) { throw new UnsupportedOperationException( "This method must be called only from layoutlib"); } synchronized (sLock) { sInstance = null; } } /** * For layoutlib to override this method to return {@code true}. * * @return {@code true} if the process is running for developer tools * @see View#isInEditMode() */ private static boolean isInEditMode() { return false; } static boolean isInEditModeInternal() { return isInEditMode(); } @NonNull private static InputMethodManager createInstance(int displayId, Looper looper) { return isInEditMode() ? createStubInstance(displayId, looper) : createRealInstance(displayId, looper); } @NonNull private static InputMethodManager createRealInstance(int displayId, Looper looper) { final IInputMethodManager service = IInputMethodManagerGlobalInvoker.getService(); if (service == null) { throw new IllegalStateException("IInputMethodManager is not available"); } final InputMethodManager imm = new InputMethodManager(service, displayId, looper); // InputMethodManagerService#addClient() relies on Binder.getCalling{Pid, Uid}() to // associate PID/UID with each IME client. This means: // A. if this method call will be handled as an IPC, there is no problem. // B. if this method call will be handled as an in-proc method call, we need to // ensure that Binder.getCalling{Pid, Uid}() return Process.my{Pid, Uid}() // Either ways we can always call Binder.{clear, restore}CallingIdentity() because // 1) doing so has no effect for A and 2) doing so is sufficient for B. final long identity = Binder.clearCallingIdentity(); try { IInputMethodManagerGlobalInvoker.addClient(imm.mClient, imm.mFallbackInputConnection, displayId); } finally { Binder.restoreCallingIdentity(identity); } return imm; } @NonNull private static InputMethodManager createStubInstance(int displayId, Looper looper) { // If InputMethodManager is running for layoutlib, stub out IPCs into IMMS. final Class c = IInputMethodManager.class; final IInputMethodManager stubInterface = (IInputMethodManager) Proxy.newProxyInstance(c.getClassLoader(), new Class[]{c}, (proxy, method, args) -> { final Class returnType = method.getReturnType(); if (returnType == boolean.class) { return false; } else if (returnType == int.class) { return 0; } else if (returnType == long.class) { return 0L; } else if (returnType == short.class) { return 0; } else if (returnType == char.class) { return 0; } else if (returnType == byte.class) { return 0; } else if (returnType == float.class) { return 0f; } else if (returnType == double.class) { return 0.0; } else { return null; } }); return new InputMethodManager(stubInterface, displayId, looper); } private InputMethodManager(@NonNull IInputMethodManager service, int displayId, Looper looper) { mService = service; // For @UnsupportedAppUsage mMainLooper = looper; mH = new H(looper); mDisplayId = displayId; mFallbackInputConnection = new RemoteInputConnectionImpl(looper, new BaseInputConnection(this, false), this, null); } /** * Retrieve an instance for the given {@link Context}, creating it if it doesn't already exist. * * @param context {@link Context} for which IME APIs need to work * @return {@link InputMethodManager} instance * @hide */ @NonNull public static InputMethodManager forContext(@DisplayContext Context context) { final int displayId = context.getDisplayId(); // For better backward compatibility, we always use Looper.getMainLooper() for the default // display case. final Looper looper = displayId == Display.DEFAULT_DISPLAY ? Looper.getMainLooper() : context.getMainLooper(); // Keep track of whether to expect the IME to be unavailable so as to avoid log spam in // sendInputEventOnMainLooperLocked() by not logging a verbose message on every DPAD event sPreventImeStartupUnlessTextEditor = context.getResources().getBoolean( com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor); return forContextInternal(displayId, looper); } @NonNull private static InputMethodManager forContextInternal(int displayId, Looper looper) { final boolean isDefaultDisplay = displayId == Display.DEFAULT_DISPLAY; synchronized (sLock) { InputMethodManager instance = sInstanceMap.get(displayId); if (instance != null) { return instance; } instance = createInstance(displayId, looper); // For backward compatibility, store the instance also to sInstance for default display. if (sInstance == null && isDefaultDisplay) { sInstance = instance; } sInstanceMap.put(displayId, instance); return instance; } } /** * Deprecated. Do not use. * * @return global {@link InputMethodManager} instance * @deprecated Use {@link Context#getSystemService(Class)} instead. This method cannot fully * support multi-display scenario. * @hide */ @Deprecated @UnsupportedAppUsage public static InputMethodManager getInstance() { Log.w(TAG, "InputMethodManager.getInstance() is deprecated because it cannot be" + " compatible with multi-display." + " Use context.getSystemService(InputMethodManager.class) instead.", new Throwable()); ensureDefaultInstanceForDefaultDisplayIfNecessary(); return peekInstance(); } /** * Deprecated. Do not use. * * @return {@link #sInstance} * @deprecated Use {@link Context#getSystemService(Class)} instead. This method cannot fully * support multi-display scenario. * @hide */ @Deprecated @UnsupportedAppUsage public static InputMethodManager peekInstance() { Log.w(TAG, "InputMethodManager.peekInstance() is deprecated because it cannot be" + " compatible with multi-display." + " Use context.getSystemService(InputMethodManager.class) instead.", new Throwable()); synchronized (sLock) { return sInstance; } } /** * Returns the list of installed input methods. * *

On multi user environment, this API returns a result for the calling process user.

* * @return {@link List} of {@link InputMethodInfo}. */ @NonNull public List getInputMethodList() { // We intentionally do not use UserHandle.getCallingUserId() here because for system // services InputMethodManagerInternal.getInputMethodListAsUser() should be used // instead. return IInputMethodManagerGlobalInvoker.getInputMethodList(UserHandle.myUserId(), DirectBootAwareness.AUTO); } /** * Returns {@code true} if currently selected IME supports Stylus handwriting & is enabled. * If the method returns {@code false}, {@link #startStylusHandwriting(View)} shouldn't be * called and Stylus touch should continue as normal touch input. * * @see #startStylusHandwriting(View) */ public boolean isStylusHandwritingAvailable() { return isStylusHandwritingAvailableAsUser(UserHandle.of(UserHandle.myUserId())); } /** * Returns {@code true} if currently selected IME supports Stylus handwriting & is enabled for * the given userId. * *

If the method returns {@code false}, {@link #startStylusHandwriting(View)} shouldn't be * called and Stylus touch should continue as normal touch input.

* *

{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when * {@code user} is different from the user of the current process.

* * @see #startStylusHandwriting(View) * @param user UserHandle to query. * @hide */ @NonNull @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) @TestApi @SuppressLint("UserHandle") public boolean isStylusHandwritingAvailableAsUser(@NonNull UserHandle user) { final Context fallbackContext = ActivityThread.currentApplication(); if (fallbackContext == null) { return false; } boolean isAvailable; synchronized (mH) { if (mStylusHandwritingAvailableCache == null) { mStylusHandwritingAvailableCache = new PropertyInvalidatedCache<>( 4 /* maxEntries */, CACHE_KEY_STYLUS_HANDWRITING_PROPERTY) { @Override public Boolean recompute(Integer userId) { return IInputMethodManagerGlobalInvoker.isStylusHandwritingAvailableAsUser( userId, /* connectionless= */ false); } }; } isAvailable = mStylusHandwritingAvailableCache.query(user.getIdentifier()); } return isAvailable; } /** * Returns {@code true} if the currently selected IME supports connectionless stylus handwriting * sessions and is enabled. */ @FlaggedApi(Flags.FLAG_CONNECTIONLESS_HANDWRITING) public boolean isConnectionlessStylusHandwritingAvailable() { if (ActivityThread.currentApplication() == null) { return false; } synchronized (mH) { if (mConnectionlessStylusHandwritingAvailableCache == null) { mConnectionlessStylusHandwritingAvailableCache = new PropertyInvalidatedCache<>( /* maxEntries= */ 4, CACHE_KEY_CONNECTIONLESS_STYLUS_HANDWRITING_PROPERTY) { @Override public Boolean recompute(@NonNull Integer userId) { return IInputMethodManagerGlobalInvoker.isStylusHandwritingAvailableAsUser( userId, /* connectionless= */ true); } }; } return mConnectionlessStylusHandwritingAvailableCache.query(UserHandle.myUserId()); } } /** * Returns the list of installed input methods for the specified user. * *

{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when * {@code userId} is different from the user id of the current process.

* * @param userId user ID to query * @return {@link List} of {@link InputMethodInfo}. * @hide */ @TestApi @NonNull @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) public List getInputMethodListAsUser(@UserIdInt int userId) { return IInputMethodManagerGlobalInvoker.getInputMethodList(userId, DirectBootAwareness.AUTO); } /** * Returns the list of installed input methods for the specified user. * *

{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when * {@code userId} is different from the user id of the current process.

* * @param userId user ID to query * @param directBootAwareness {@code true} if caller want to query installed input methods list * on user locked state. * @return {@link List} of {@link InputMethodInfo}. * @hide */ @NonNull @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) public List getInputMethodListAsUser(@UserIdInt int userId, @DirectBootAwareness int directBootAwareness) { return IInputMethodManagerGlobalInvoker.getInputMethodList(userId, directBootAwareness); } /** * Returns the {@link InputMethodInfo} of the currently selected input method (for the process's * user). * *

On multi user environment, this API returns a result for the calling process user.

*/ @Nullable public InputMethodInfo getCurrentInputMethodInfo() { // We intentionally do not use UserHandle.getCallingUserId() here because for system // services InputMethodManagerInternal.getCurrentInputMethodInfoForUser() should be used // instead. return IInputMethodManagerGlobalInvoker.getCurrentInputMethodInfoAsUser( UserHandle.myUserId()); } /** * Returns the {@link InputMethodInfo} for currently selected input method for the given user. * * @param user user to query. * @hide */ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL) @Nullable @SystemApi @SuppressLint("UserHandle") public InputMethodInfo getCurrentInputMethodInfoAsUser(@NonNull UserHandle user) { Objects.requireNonNull(user); return IInputMethodManagerGlobalInvoker.getCurrentInputMethodInfoAsUser( user.getIdentifier()); } /** * Returns the list of enabled input methods. * *

On multi user environment, this API returns a result for the calling process user.

* * @return {@link List} of {@link InputMethodInfo}. */ @NonNull public List getEnabledInputMethodList() { // We intentionally do not use UserHandle.getCallingUserId() here because for system // services InputMethodManagerInternal.getEnabledInputMethodListAsUser() should be used // instead. return IInputMethodManagerGlobalInvoker.getEnabledInputMethodList(UserHandle.myUserId()); } /** * Returns the list of enabled input methods for the specified user. * *

{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when * {@code user} is different from the user of the current process.

* * @param user UserHandle to query * @return {@link List} of {@link InputMethodInfo}. * @see #getEnabledInputMethodSubtypeListAsUser(String, boolean, UserHandle) * @hide */ @NonNull @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) @TestApi @SuppressLint("UserHandle") public List getEnabledInputMethodListAsUser(@NonNull UserHandle user) { return IInputMethodManagerGlobalInvoker.getEnabledInputMethodList(user.getIdentifier()); } /** * Returns a list of enabled input method subtypes for the specified input method info. * *

On multi user environment, this API returns a result for the calling process user.

* * @param imi The {@link InputMethodInfo} whose subtypes list will be returned. If {@code null}, * returns enabled subtypes for the currently selected {@link InputMethodInfo}. * @param allowsImplicitlyEnabledSubtypes A boolean flag to allow to return the implicitly * enabled subtypes. If an input method info doesn't have enabled subtypes, the framework * will implicitly enable subtypes according to the current system language. */ @NonNull public List getEnabledInputMethodSubtypeList(@Nullable InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) { return IInputMethodManagerGlobalInvoker.getEnabledInputMethodSubtypeList( imi == null ? null : imi.getId(), allowsImplicitlyEnabledSubtypes, UserHandle.myUserId()); } /** * Returns a list of enabled input method subtypes for the specified input method info for the * specified user. * * @param imeId IME ID to be queried about. * @param allowsImplicitlyEnabledSubtypes {@code true} to include implicitly enabled subtypes. * @param user UserHandle to be queried about. * {@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required if this is * different from the calling process user ID. * @return {@link List} of {@link InputMethodSubtype}. * @see #getEnabledInputMethodListAsUser(UserHandle) * @hide */ @NonNull @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) @TestApi @SuppressLint("UserHandle") public List getEnabledInputMethodSubtypeListAsUser( @NonNull String imeId, boolean allowsImplicitlyEnabledSubtypes, @NonNull UserHandle user) { return IInputMethodManagerGlobalInvoker.getEnabledInputMethodSubtypeList( Objects.requireNonNull(imeId), allowsImplicitlyEnabledSubtypes, user.getIdentifier()); } /** * @deprecated Use {@link InputMethodService#showStatusIcon(int)} instead. This method was * intended for IME developers who should be accessing APIs through the service. APIs in this * class are intended for app developers interacting with the IME. */ @Deprecated public void showStatusIcon(IBinder imeToken, String packageName, @DrawableRes int iconId) { InputMethodPrivilegedOperationsRegistry.get( imeToken).updateStatusIconAsync(packageName, iconId); } /** * @deprecated Use {@link InputMethodService#hideStatusIcon()} instead. This method was * intended for IME developers who should be accessing APIs through the service. APIs in * this class are intended for app developers interacting with the IME. */ @Deprecated public void hideStatusIcon(IBinder imeToken) { InputMethodPrivilegedOperationsRegistry.get(imeToken).updateStatusIconAsync(null, 0); } /** * This hidden API is deprecated in {@link android.os.Build.VERSION_CODES#Q}. Does nothing. * * @param spans will be ignored. * * @deprecated Do not use. * @hide */ @Deprecated @UnsupportedAppUsage public void registerSuggestionSpansForNotification(SuggestionSpan[] spans) { Log.w(TAG, "registerSuggestionSpansForNotification() is deprecated. Does nothing."); } /** * This hidden API is deprecated in {@link android.os.Build.VERSION_CODES#Q}. Does nothing. * * @deprecated Do not use. * @hide */ @Deprecated @UnsupportedAppUsage public void notifySuggestionPicked(SuggestionSpan span, String originalString, int index) { Log.w(TAG, "notifySuggestionPicked() is deprecated. Does nothing."); } /** * Allows you to discover whether the attached input method is running * in fullscreen mode. Return true if it is fullscreen, entirely covering * your UI, else returns false. */ public boolean isFullscreenMode() { synchronized (mH) { return mFullscreenMode; } } /** * Return {@code true} if the given view is the currently active view for the input method. */ public boolean isActive(View view) { // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { return fallbackImm.isActive(view); } checkFocus(); synchronized (mH) { return hasServedByInputMethodLocked(view) && mCurrentEditorInfo != null; } } /** * Return {@code true} if any view is currently active for the input method. */ public boolean isActive() { checkFocus(); synchronized (mH) { return getServedViewLocked() != null && mCurrentEditorInfo != null; } } /** * Returns {@code true} if the given view's {@link ViewRootImpl} is the currently active one * for the {@code InputMethodManager}. * * @hide */ @TestApi @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD) public boolean isCurrentRootView(@NonNull View attachedView) { synchronized (mH) { return mCurRootView == attachedView.getViewRootImpl(); } } /** * Return {@code true} if the currently served view is accepting full text edits. * If {@code false}, it has no input connection, so it can only handle raw key events. */ public boolean isAcceptingText() { checkFocus(); synchronized (mH) { return mServedInputConnection != null; } } /** * Return {@code true} if the input method is suppressing system spell checker. */ public boolean isInputMethodSuppressingSpellChecker() { synchronized (mH) { return mCurBindState != null && mCurBindState.mIsInputMethodSuppressingSpellChecker; } } /** * Reset all of the state associated with being bound to an input method. */ @GuardedBy("mH") private void clearBindingLocked() { if (DEBUG) Log.v(TAG, "Clearing binding!"); clearConnectionLocked(); updateInputChannelLocked(null); mCurId = null; // for @UnsupportedAppUsage mCurMethod = null; // for @UnsupportedAppUsage // We only reset sequence number for input method, but not accessibility. mCurBindState = null; } /** * Reset all of the state associated with being bound to an accessibility service. */ @GuardedBy("mH") private void clearAccessibilityBindingLocked(int id) { if (DEBUG) Log.v(TAG, "Clearing accessibility binding " + id); mAccessibilityInputMethodSession.remove(id); } /** * Reset all of the state associated with being bound to all accessibility services. */ @GuardedBy("mH") private void clearAllAccessibilityBindingLocked() { if (DEBUG) Log.v(TAG, "Clearing all accessibility bindings"); mAccessibilityInputMethodSession.clear(); } @GuardedBy("mH") private void updateInputChannelLocked(InputChannel channel) { if (areSameInputChannel(mCurChannel, channel)) { return; } // TODO(b/238720598) : Requirements when design a new protocol for InputChannel // channel is a dupe of 'mCurChannel', because they have the same token, and represent // the same connection. Ignore the incoming channel and keep using 'mCurChannel' to // avoid confusing the InputEventReceiver. if (mCurSender != null) { flushPendingEventsLocked(); mCurSender.dispose(); mCurSender = null; } if (mCurChannel != null) { mCurChannel.dispose(); } mCurChannel = channel; } private static boolean areSameInputChannel(@Nullable InputChannel lhs, @Nullable InputChannel rhs) { if (lhs == rhs) { return true; } if (lhs == null || rhs == null) { return false; } return lhs.getToken() == rhs.getToken(); } /** * Reset all of the state associated with a served view being connected * to an input method */ @GuardedBy("mH") private void clearConnectionLocked() { mCurrentEditorInfo = null; mPreviousViewFocusParameters = null; if (mServedInputConnection != null) { mServedInputConnection.deactivate(); mServedInputConnection = null; mServedInputConnectionHandler = null; } } /** * Disconnect any existing input connection, clearing the served view. */ @UnsupportedAppUsage @GuardedBy("mH") void finishInputLocked() { View clearedView = null; mNextServedView = null; if (mServedView != null) { clearedView = mServedView; mServedView = null; if (initiationWithoutInputConnection() && clearedView.getViewRootImpl() != null) { clearedView.getViewRootImpl().getHandwritingInitiator() .clearFocusedView(clearedView); } } if (clearedView != null) { if (DEBUG) { Log.v(TAG, "FINISH INPUT: mServedView=" + InputMethodDebug.dumpViewInfo(clearedView)); } mCompletions = null; mServedConnecting = false; clearConnectionLocked(); } mReportInputConnectionOpenedRunner = null; // Clear the back callbacks held by the ime dispatcher to avoid memory leaks. mImeDispatcher.clear(); } /** * Clears the {@link #mCurRootView} if it's no longer window focused and the connection is * no longer active. * * @return {@code} true iff it was cleared. */ @GuardedBy("mH") private boolean clearCurRootViewIfNeeded() { if (!mActive && !mCurRootViewWindowFocused) { finishInputLocked(); mDelegate.setCurrentRootViewLocked(null); return true; } return false; } public void displayCompletions(View view, CompletionInfo[] completions) { // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { fallbackImm.displayCompletions(view, completions); return; } checkFocus(); synchronized (mH) { if (!hasServedByInputMethodLocked(view)) { return; } mCompletions = completions; if (isImeSessionAvailableLocked()) { mCurBindState.mImeSession.displayCompletions(mCompletions); } } } public void updateExtractedText(View view, int token, ExtractedText text) { // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { fallbackImm.updateExtractedText(view, token, text); return; } checkFocus(); synchronized (mH) { if (!hasServedByInputMethodLocked(view)) { return; } if (isImeSessionAvailableLocked()) { mCurBindState.mImeSession.updateExtractedText(token, text); } } } /** @hide */ @IntDef(flag = true, prefix = { "SHOW_" }, value = { SHOW_IMPLICIT, SHOW_FORCED, }) @Retention(RetentionPolicy.SOURCE) public @interface ShowFlags {} /** * Flag for {@link #showSoftInput} to indicate that this is an implicit * request to show the input window, not as the result of a direct request * by the user. The window may not be shown in this case. */ public static final int SHOW_IMPLICIT = 0x0001; /** * Flag for {@link #showSoftInput} to indicate that the user has forced * the input method open (such as by long-pressing menu) so it should * not be closed until they explicitly do so. * * @deprecated Use {@link #showSoftInput} without this flag instead. Using this flag can lead * to the soft input remaining visible even when the calling application is closed. The * use of this flag can make the soft input remain visible globally. Starting in * {@link Build.VERSION_CODES#TIRAMISU Android T}, this flag only has an effect while the * caller is currently focused. */ @Deprecated public static final int SHOW_FORCED = 0x0002; /** * Synonym for {@link #showSoftInput(View, int, ResultReceiver)} without * a result receiver: explicitly request that the current input method's * soft input area be shown to the user, if needed. * * @param view The currently focused view, which would like to receive soft keyboard input. * Note that this view is only considered focused here if both it itself has * {@link View#isFocused view focus}, and its containing window has * {@link View#hasWindowFocus window focus}. Otherwise the call fails and * returns {@code false}. */ public boolean showSoftInput(View view, @ShowFlags int flags) { // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { return fallbackImm.showSoftInput(view, flags); } return showSoftInput(view, flags, null); } /** * Flag for the {@link ResultReceiver} result code from * {@link #showSoftInput(View, int, ResultReceiver)} and * {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)}: the * state of the soft input window was unchanged and remains shown. */ public static final int RESULT_UNCHANGED_SHOWN = 0; /** * Flag for the {@link ResultReceiver} result code from * {@link #showSoftInput(View, int, ResultReceiver)} and * {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)}: the * state of the soft input window was unchanged and remains hidden. */ public static final int RESULT_UNCHANGED_HIDDEN = 1; /** * Flag for the {@link ResultReceiver} result code from * {@link #showSoftInput(View, int, ResultReceiver)} and * {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)}: the * state of the soft input window changed from hidden to shown. */ public static final int RESULT_SHOWN = 2; /** * Flag for the {@link ResultReceiver} result code from * {@link #showSoftInput(View, int, ResultReceiver)} and * {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)}: the * state of the soft input window changed from shown to hidden. */ public static final int RESULT_HIDDEN = 3; /** * Explicitly request that the current input method's soft input area be * shown to the user, if needed. Call this if the user interacts with * your view in such a way that they have expressed they would like to * start performing input into it. * *

Caveat: {@link ResultReceiver} instance passed to * this method can be a long-lived object, because it may not be * garbage-collected until all the corresponding {@link ResultReceiver} * objects transferred to different processes get garbage-collected. * Follow the general patterns to avoid memory leaks in Android. * Consider to use {@link java.lang.ref.WeakReference} so that application * logic objects such as {@link android.app.Activity} and {@link Context} * can be garbage collected regardless of the lifetime of * {@link ResultReceiver}. * * @param view The currently focused view, which would like to receive soft keyboard input. * Note that this view is only considered focused here if both it itself has * {@link View#isFocused view focus}, and its containing window has * {@link View#hasWindowFocus window focus}. Otherwise the call fails and * returns {@code false}. * @param resultReceiver If non-null, this will be called by the IME when * it has processed your request to tell you what it has done. The result * code you receive may be either {@link #RESULT_UNCHANGED_SHOWN}, * {@link #RESULT_UNCHANGED_HIDDEN}, {@link #RESULT_SHOWN}, or * {@link #RESULT_HIDDEN}. */ public boolean showSoftInput(View view, @ShowFlags int flags, ResultReceiver resultReceiver) { return showSoftInput(view, flags, resultReceiver, SoftInputShowHideReason.SHOW_SOFT_INPUT); } private boolean showSoftInput(View view, @ShowFlags int flags, @Nullable ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { // TODO(b/303041796): handle tracking physical keyboard and DPAD as user interactions final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_SHOW, ImeTracker.ORIGIN_CLIENT, reason, ImeTracker.isFromUser(view)); return showSoftInput(view, statsToken, flags, resultReceiver, reason); } private boolean showSoftInput(View view, @NonNull ImeTracker.Token statsToken, @ShowFlags int flags, @Nullable ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { ImeTracker.forLatency().onRequestShow(statsToken, ImeTracker.ORIGIN_CLIENT, reason, ActivityThread::currentApplication); ImeTracing.getInstance().triggerClientDump("InputMethodManager#showSoftInput", this, null /* icProto */); // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { return fallbackImm.showSoftInput(view, statsToken, flags, resultReceiver, reason); } checkFocus(); synchronized (mH) { if (!hasServedByInputMethodLocked(view)) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); ImeTracker.forLatency().onShowFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED, ActivityThread::currentApplication); Log.w(TAG, "Ignoring showSoftInput() as view=" + view + " is not served."); return false; } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread. // TODO(b/229426865): call WindowInsetsController#show instead. mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED)); Log.d(TAG, "showSoftInput() view=" + view + " flags=" + flags + " reason=" + InputMethodDebug.softInputDisplayReasonToString(reason)); return IInputMethodManagerGlobalInvoker.showSoftInput( mClient, view.getWindowToken(), statsToken, flags, mCurRootView.getLastClickToolType(), resultReceiver, reason); } } /** * This method is still kept for a while until androidx.appcompat.widget.SearchView ver. 26.0 * is publicly released because previous implementations of that class had relied on this method * via reflection. * * @deprecated This is a hidden API. You should never use this. * @hide */ @Deprecated @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768499) public void showSoftInputUnchecked(@ShowFlags int flags, ResultReceiver resultReceiver) { synchronized (mH) { final int reason = SoftInputShowHideReason.SHOW_SOFT_INPUT; final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_SHOW, ImeTracker.ORIGIN_CLIENT, reason, false /* fromUser */); Log.w(TAG, "showSoftInputUnchecked() is a hidden method, which will be" + " removed soon. If you are using androidx.appcompat.widget.SearchView," + " please update to version 26.0 or newer version."); final View rootView = mCurRootView != null ? mCurRootView.getView() : null; if (rootView == null) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); Log.w(TAG, "No current root view, ignoring showSoftInputUnchecked()"); return; } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread. // TODO(b/229426865): call WindowInsetsController#show instead. mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED)); IInputMethodManagerGlobalInvoker.showSoftInput( mClient, rootView.getWindowToken(), statsToken, flags, mCurRootView.getLastClickToolType(), resultReceiver, reason); } } /** @hide */ @IntDef(flag = true, prefix = { "HIDE_" }, value = { HIDE_IMPLICIT_ONLY, HIDE_NOT_ALWAYS, }) @Retention(RetentionPolicy.SOURCE) public @interface HideFlags {} /** * Flag for {@link #hideSoftInputFromWindow} and {@link InputMethodService#requestHideSelf(int)} * to indicate that the soft input window should only be hidden if it was not explicitly shown * by the user. */ public static final int HIDE_IMPLICIT_ONLY = 0x0001; /** * Flag for {@link #hideSoftInputFromWindow} and {@link InputMethodService#requestShowSelf(int)} * to indicate that the soft input window should normally be hidden, unless it was originally * shown with {@link #SHOW_FORCED}. */ public static final int HIDE_NOT_ALWAYS = 0x0002; /** * Synonym for {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)} * without a result: request to hide the soft input window from the * context of the window that is currently accepting input. * * @param windowToken The token of the window that is making the request, * as returned by {@link View#getWindowToken() View.getWindowToken()}. */ public boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags) { return hideSoftInputFromWindow(windowToken, flags, null); } /** * Request to hide the soft input window from the context of the window * that is currently accepting input. This should be called as a result * of the user doing some actually than fairly explicitly requests to * have the input window hidden. * *

Caveat: {@link ResultReceiver} instance passed to * this method can be a long-lived object, because it may not be * garbage-collected until all the corresponding {@link ResultReceiver} * objects transferred to different processes get garbage-collected. * Follow the general patterns to avoid memory leaks in Android. * Consider to use {@link java.lang.ref.WeakReference} so that application * logic objects such as {@link android.app.Activity} and {@link Context} * can be garbage collected regardless of the lifetime of * {@link ResultReceiver}. * * @param windowToken The token of the window that is making the request, * as returned by {@link View#getWindowToken() View.getWindowToken()}. * @param resultReceiver If non-null, this will be called by the IME when * it has processed your request to tell you what it has done. The result * code you receive may be either {@link #RESULT_UNCHANGED_SHOWN}, * {@link #RESULT_UNCHANGED_HIDDEN}, {@link #RESULT_SHOWN}, or * {@link #RESULT_HIDDEN}. */ public boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags, ResultReceiver resultReceiver) { return hideSoftInputFromWindow(windowToken, flags, resultReceiver, SoftInputShowHideReason.HIDE_SOFT_INPUT); } private boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { // Get served view initially for statsToken creation. final View initialServedView; synchronized (mH) { initialServedView = getServedViewLocked(); } final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE, ImeTracker.ORIGIN_CLIENT, reason, ImeTracker.isFromUser(initialServedView)); ImeTracker.forLatency().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT, reason, ActivityThread::currentApplication); ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromWindow", this, null /* icProto */); checkFocus(); synchronized (mH) { // Get served view again in case it changed between the synchronized blocks. final View servedView = getServedViewLocked(); if (servedView == null || servedView.getWindowToken() != windowToken) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); ImeTracker.forLatency().onHideFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED, ActivityThread::currentApplication); return false; } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, statsToken, flags, resultReceiver, reason); } } /** * Synonym for {@link #hideSoftInputFromWindow(IBinder, int)} but takes a {@link View} as a * parameter to be a counterpart of {@link #showSoftInput(View, int)}. * * @param view {@link View} to be used to conditionally issue hide request when and only when * this {@link View} is serving as an IME target. * @hide */ public boolean hideSoftInputFromView(@NonNull View view, @HideFlags int flags) { checkFocus(); final boolean isFocusedAndWindowFocused = view.hasWindowFocus() && view.isFocused(); synchronized (mH) { final boolean hasServedByInputMethod = hasServedByInputMethodLocked(view); if (!isFocusedAndWindowFocused && !hasServedByInputMethod) { // Fail early if the view is not focused and not served // to avoid logging many erroneous calls. return false; } final int reason = SoftInputShowHideReason.HIDE_SOFT_INPUT_FROM_VIEW; final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE, ImeTracker.ORIGIN_CLIENT, reason, ImeTracker.isFromUser(view)); ImeTracker.forLatency().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT, reason, ActivityThread::currentApplication); ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromView", this, null /* icProto */); if (!hasServedByInputMethod) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); ImeTracker.forLatency().onShowFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED, ActivityThread::currentApplication); Log.w(TAG, "Ignoring hideSoftInputFromView() as view=" + view + " is not served."); return false; } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, view.getWindowToken(), statsToken, flags, null, reason); } } /** * A test API for CTS to request hiding the current soft input window, with the request origin * on the server side. * * @hide */ @SuppressLint("UnflaggedApi") // @TestApi without associated feature. @TestApi @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD) public void hideSoftInputFromServerForTest() { IInputMethodManagerGlobalInvoker.hideSoftInputFromServerForTest(); } /** * Start stylus handwriting session. * * If supported by the current input method, a stylus handwriting session is started on the * given View, capturing all stylus input and converting it to InputConnection commands. * * If handwriting mode is started successfully by the IME, any currently dispatched stylus * pointers will be {@code android.view.MotionEvent#FLAG_CANCELED} cancelled. * * If Stylus handwriting mode is not supported or cannot be fulfilled for any reason by IME, * request will be ignored and Stylus touch will continue as normal touch input. Ideally, * {@link #isStylusHandwritingAvailable()} should be called first to determine if stylus * handwriting is supported by IME. * * @param view the View for which stylus handwriting is requested. It and * {@link View#hasWindowFocus its window} must be {@link View#hasFocus focused}. * @see #isStylusHandwritingAvailable() */ public void startStylusHandwriting(@NonNull View view) { startStylusHandwritingInternal( view, /* delegatorPackageName= */ null, /* handwritingDelegateFlags= */ 0); } private void sendFailureCallback(@NonNull @CallbackExecutor Executor executor, @NonNull Consumer callback) { if (executor == null || callback == null) { return; } executor.execute(() -> callback.accept(false)); } private boolean startStylusHandwritingInternal( @NonNull View view, @Nullable String delegatorPackageName, @HandwritingDelegateFlags int handwritingDelegateFlags) { return startStylusHandwritingInternal( view, delegatorPackageName, handwritingDelegateFlags, null /* executor */, null /* callback */); } private boolean startStylusHandwritingInternal( @NonNull View view, @Nullable String delegatorPackageName, @HandwritingDelegateFlags int handwritingDelegateFlags, Executor executor, Consumer callback) { Objects.requireNonNull(view); boolean useCallback = callback != null; // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { fallbackImm.startStylusHandwritingInternal( view, delegatorPackageName, handwritingDelegateFlags, executor, callback); } boolean useDelegation = !TextUtils.isEmpty(delegatorPackageName); checkFocus(); synchronized (mH) { if (!hasServedByInputMethodLocked(view)) { Log.w(TAG, "Ignoring startStylusHandwriting as view=" + view + " is not served."); sendFailureCallback(executor, callback); return false; } if (view.getViewRootImpl() != mCurRootView) { Log.w(TAG, "Ignoring startStylusHandwriting: View's window does not have focus."); sendFailureCallback(executor, callback); return false; } if (useDelegation) { if (useCallback) { IBooleanListener listener = new IBooleanListener.Stub() { @Override public void onResult(boolean value) { executor.execute(() -> { callback.accept(value); }); } }; if (!IInputMethodManagerGlobalInvoker.acceptStylusHandwritingDelegationAsync( mClient, UserHandle.myUserId(), view.getContext().getOpPackageName(), delegatorPackageName, handwritingDelegateFlags, listener)) { sendFailureCallback(executor, callback); } return true; } else { return IInputMethodManagerGlobalInvoker.acceptStylusHandwritingDelegation( mClient, UserHandle.myUserId(), view.getContext().getOpPackageName(), delegatorPackageName, handwritingDelegateFlags); } } else { IInputMethodManagerGlobalInvoker.startStylusHandwriting(mClient); return false; } } } /** * Starts a connectionless stylus handwriting session. A connectionless session differs from a * regular stylus handwriting session in that the IME does not use an input connection to * communicate with a text editor. Instead, the IME directly returns recognised handwritten text * via a callback. * *

The {code cursorAnchorInfo} may be used by the IME to improve the handwriting recognition * accuracy and user experience of the handwriting session. Usually connectionless handwriting * is used for a view which appears like a text editor but does not really support text editing. * For best results, the {code cursorAnchorInfo} should be populated as it would be for a real * text editor (for example, the insertion marker location can be set to where the user would * expect it to be, even if there is no visible cursor). * * @param view the view receiving stylus events * @param cursorAnchorInfo positional information about the view receiving stylus events * @param callbackExecutor the executor to run the callback on * @param callback the callback to receive the result */ @FlaggedApi(Flags.FLAG_CONNECTIONLESS_HANDWRITING) public void startConnectionlessStylusHandwriting(@NonNull View view, @Nullable CursorAnchorInfo cursorAnchorInfo, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull ConnectionlessHandwritingCallback callback) { startConnectionlessStylusHandwritingInternal( view, cursorAnchorInfo, null, null, callbackExecutor, callback); } /** * Starts a connectionless stylus handwriting session (see {@link * #startConnectionlessStylusHandwriting}) and additionally enables the recognised handwritten * text to be later committed to a text editor using {@link * #acceptStylusHandwritingDelegation(View)}. * *

After a connectionless session started using this method completes successfully, a text * editor view, called the delegate view, may call {@link * #acceptStylusHandwritingDelegation(View)} which will request the IME to commit the recognised * handwritten text from the connectionless session to the delegate view. * *

The delegate view must belong to the same package as the delegator view for the delegation * to succeed. If the delegate view belongs to a different package, use {@link * #startConnectionlessStylusHandwritingForDelegation(View, CursorAnchorInfo, String, Executor, * ConnectionlessHandwritingCallback)} instead. * * @param delegatorView the view receiving stylus events * @param cursorAnchorInfo positional information about the view receiving stylus events * @param callbackExecutor the executor to run the callback on * @param callback the callback to receive the result */ @FlaggedApi(Flags.FLAG_CONNECTIONLESS_HANDWRITING) public void startConnectionlessStylusHandwritingForDelegation(@NonNull View delegatorView, @Nullable CursorAnchorInfo cursorAnchorInfo, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull ConnectionlessHandwritingCallback callback) { String delegatorPackageName = delegatorView.getContext().getOpPackageName(); startConnectionlessStylusHandwritingInternal(delegatorView, cursorAnchorInfo, delegatorPackageName, delegatorPackageName, callbackExecutor, callback); } /** * Starts a connectionless stylus handwriting session (see {@link * #startConnectionlessStylusHandwriting}) and additionally enables the recognised handwritten * text to be later committed to a text editor using {@link * #acceptStylusHandwritingDelegation(View, String)}. * *

After a connectionless session started using this method completes successfully, a text * editor view, called the delegate view, may call {@link * #acceptStylusHandwritingDelegation(View, String)} which will request the IME to commit the * recognised handwritten text from the connectionless session to the delegate view. * *

The delegate view must belong to {@code delegatePackageName} for the delegation to * succeed. * * @param delegatorView the view receiving stylus events * @param cursorAnchorInfo positional information about the view receiving stylus events * @param delegatePackageName name of the package containing the delegate view which will accept * the delegation * @param callbackExecutor the executor to run the callback on * @param callback the callback to receive the result */ @FlaggedApi(Flags.FLAG_CONNECTIONLESS_HANDWRITING) public void startConnectionlessStylusHandwritingForDelegation(@NonNull View delegatorView, @Nullable CursorAnchorInfo cursorAnchorInfo, @NonNull String delegatePackageName, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull ConnectionlessHandwritingCallback callback) { Objects.requireNonNull(delegatePackageName); String delegatorPackageName = delegatorView.getContext().getOpPackageName(); startConnectionlessStylusHandwritingInternal(delegatorView, cursorAnchorInfo, delegatorPackageName, delegatePackageName, callbackExecutor, callback); } private void startConnectionlessStylusHandwritingInternal(@NonNull View view, @Nullable CursorAnchorInfo cursorAnchorInfo, @Nullable String delegatorPackageName, @Nullable String delegatePackageName, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull ConnectionlessHandwritingCallback callback) { Objects.requireNonNull(view); Objects.requireNonNull(callbackExecutor); Objects.requireNonNull(callback); // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { fallbackImm.startConnectionlessStylusHandwritingInternal(view, cursorAnchorInfo, delegatorPackageName, delegatePackageName, callbackExecutor, callback); } checkFocus(); synchronized (mH) { if (view.getViewRootImpl() != mCurRootView) { Log.w(TAG, "Ignoring startConnectionlessStylusHandwriting: " + "View's window does not have focus."); return; } IInputMethodManagerGlobalInvoker.startConnectionlessStylusHandwriting( mClient, UserHandle.myUserId(), cursorAnchorInfo, delegatePackageName, delegatorPackageName, new ConnectionlessHandwritingCallbackProxy(callbackExecutor, callback)); } } /** * Prepares delegation of starting stylus handwriting session to a different editor in same * or different window than the view on which initial handwriting stroke was detected. * * Delegation can be used to start stylus handwriting session before the {@code Editor} view or * its {@link InputConnection} is started. Calling this method starts buffering of stylus * motion events until {@link #acceptStylusHandwritingDelegation(View)} is called, at which * point the handwriting session can be started and the buffered stylus motion events will be * delivered to the IME. * e.g. Delegation can be used when initial handwriting stroke is * on a pseudo {@code Editor} like widget (with no {@link InputConnection}) but actual * {@code Editor} is on a different window. * *

Note: If an actual {@code Editor} capable of {@link InputConnection} is being scribbled * upon using stylus, use {@link #startStylusHandwriting(View)} instead.

* * @param delegatorView the view that receives initial stylus stroke and delegates it to the * actual editor. Its window must {@link View#hasWindowFocus have focus}. * @see #prepareStylusHandwritingDelegation(View, String) * @see #acceptStylusHandwritingDelegation(View) * @see #startStylusHandwriting(View) */ public void prepareStylusHandwritingDelegation(@NonNull View delegatorView) { prepareStylusHandwritingDelegation( delegatorView, delegatorView.getContext().getOpPackageName()); } /** * Prepares delegation of starting stylus handwriting session to a different editor in same or a * different window in a different package than the view on which initial handwriting stroke * was detected. * * Delegation can be used to start stylus handwriting session before the {@code Editor} view or * its {@link InputConnection} is started. Calling this method starts buffering of stylus * motion events until {@link #acceptStylusHandwritingDelegation(View, String)} is called, at * which point the handwriting session can be started and the buffered stylus motion events will * be delivered to the IME. * e.g. Delegation can be used when initial handwriting stroke is * on a pseudo {@code Editor} like widget (with no {@link InputConnection}) but actual * {@code Editor} is on a different window in the given package. * *

Note: If delegator and delegate are in same package use * {@link #prepareStylusHandwritingDelegation(View)} instead.

* * @param delegatorView the view that receives initial stylus stroke and delegates it to the * actual editor. Its window must {@link View#hasWindowFocus have focus}. * @param delegatePackageName package name that contains actual {@code Editor} which should * start stylus handwriting session by calling {@link #acceptStylusHandwritingDelegation}. * @see #prepareStylusHandwritingDelegation(View) * @see #acceptStylusHandwritingDelegation(View, String) */ public void prepareStylusHandwritingDelegation( @NonNull View delegatorView, @NonNull String delegatePackageName) { Objects.requireNonNull(delegatorView); Objects.requireNonNull(delegatePackageName); // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(delegatorView); if (fallbackImm != null) { fallbackImm.prepareStylusHandwritingDelegation(delegatorView, delegatePackageName); } IInputMethodManagerGlobalInvoker.prepareStylusHandwritingDelegation( mClient, UserHandle.myUserId(), delegatePackageName, delegatorView.getContext().getOpPackageName()); } /** * Accepts and starts a stylus handwriting session on the delegate view, if handwriting * initiation delegation was previously requested using * {@link #prepareStylusHandwritingDelegation(View)} from the delegator. * *

Note: If delegator and delegate are in different application packages, use * {@link #acceptStylusHandwritingDelegation(View, String)} instead.

* * @param delegateView delegate view capable of receiving input via {@link InputConnection} * @return {@code true} if view belongs to same application package as used in * {@link #prepareStylusHandwritingDelegation(View)} and delegation is accepted * @see #prepareStylusHandwritingDelegation(View) * @see #acceptStylusHandwritingDelegation(View, String) */ // TODO(b/300979854): Once connectionless APIs are finalised, update documentation to add: //

Otherwise, if the delegator view previously started delegation using {@link // #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, CursorAnchorInfo)}, // requests the IME to commit the recognised handwritten text from the connectionless session to // the delegate view. // @see #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, // CursorAnchorInfo) public boolean acceptStylusHandwritingDelegation(@NonNull View delegateView) { return startStylusHandwritingInternal( delegateView, delegateView.getContext().getOpPackageName(), delegateView.getHandwritingDelegateFlags()); } /** * Accepts and starts a stylus handwriting session on the delegate view, if handwriting * initiation delegation was previously requested using * {@link #prepareStylusHandwritingDelegation(View, String)} from the delegator and the view * belongs to a specified delegate package. * *

Note: If delegator and delegate are in the same application package, use * {@link #acceptStylusHandwritingDelegation(View)} instead.

* * @param delegateView delegate view capable of receiving input via {@link InputConnection} * @param delegatorPackageName package name of the delegator that handled initial stylus stroke. * @return {@code true} if view belongs to allowed delegate package declared in {@link * #prepareStylusHandwritingDelegation(View, String)} and delegation is accepted * @see #prepareStylusHandwritingDelegation(View, String) * @see #acceptStylusHandwritingDelegation(View) * TODO (b/293640003): deprecate this method once flag is enabled. */ // TODO(b/300979854): Once connectionless APIs are finalised, update documentation to add: //

Otherwise, if the delegator view previously started delegation using {@link // #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, CursorAnchorInfo, // String)}, requests the IME to commit the recognised handwritten text from the connectionless // session to the delegate view. // @see #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, // CursorAnchorInfo, String) public boolean acceptStylusHandwritingDelegation( @NonNull View delegateView, @NonNull String delegatorPackageName) { Objects.requireNonNull(delegatorPackageName); return startStylusHandwritingInternal( delegateView, delegatorPackageName, delegateView.getHandwritingDelegateFlags()); } /** * Accepts and starts a stylus handwriting session on the delegate view, if handwriting * initiation delegation was previously requested using * {@link #prepareStylusHandwritingDelegation(View, String)} from the delegator and the view * belongs to a specified delegate package. * * @param delegateView delegate view capable of receiving input via {@link InputConnection} * on which {@link #startStylusHandwriting(View)} will be called. * @param delegatorPackageName package name of the delegator that handled initial stylus stroke. * @param executor The executor to run the callback on. * @param callback Consumer callback that provides {@code true} if view belongs to allowed * delegate package declared in * {@link #prepareStylusHandwritingDelegation(View, String)} and handwriting * session can start. * @see #prepareStylusHandwritingDelegation(View, String) * @see #acceptStylusHandwritingDelegation(View) */ @FlaggedApi(Flags.FLAG_USE_ZERO_JANK_PROXY) public void acceptStylusHandwritingDelegation( @NonNull View delegateView, @NonNull String delegatorPackageName, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer callback) { Objects.requireNonNull(delegatorPackageName); int flags = 0; if (Flags.homeScreenHandwritingDelegator()) { flags = delegateView.getHandwritingDelegateFlags(); } acceptStylusHandwritingDelegation( delegateView, delegatorPackageName, flags, executor, callback); } /** * Accepts and starts a stylus handwriting session on the delegate view, if handwriting * initiation delegation was previously requested using {@link * #prepareStylusHandwritingDelegation(View, String)} from the delegator and the view belongs to * a specified delegate package. * *

Note: If delegator and delegate are in the same application package, use {@link * #acceptStylusHandwritingDelegation(View)} instead. * * @param delegateView delegate view capable of receiving input via {@link InputConnection} * @param delegatorPackageName package name of the delegator that handled initial stylus stroke. * @param flags {@link #HANDWRITING_DELEGATE_FLAG_HOME_DELEGATOR_ALLOWED} or {@code 0} * @param executor The executor to run the callback on. * @param callback {@code true>} would be received if delegation was accepted. * @see #prepareStylusHandwritingDelegation(View, String) * @see #acceptStylusHandwritingDelegation(View) */ // TODO(b/300979854): Once connectionless APIs are finalised, update documentation to add: //

Otherwise, if the delegator view previously started delegation using {@link // #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, CursorAnchorInfo, // String)}, requests the IME to commit the recognised handwritten text from the connectionless // session to the delegate view. // @see #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, // CursorAnchorInfo, String) // @FlaggedApi(FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR) public void acceptStylusHandwritingDelegation( @NonNull View delegateView, @NonNull String delegatorPackageName, @HandwritingDelegateFlags int flags, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer callback) { Objects.requireNonNull(delegatorPackageName); Objects.requireNonNull(delegateView); Objects.requireNonNull(executor); Objects.requireNonNull(callback); startStylusHandwritingInternal( delegateView, delegatorPackageName, flags, executor, callback); } /** * This method toggles the input method window display. * If the input window is already displayed, it gets hidden. * If not the input window will be displayed. * @param windowToken The token of the window that is making the request, * as returned by {@link View#getWindowToken() View.getWindowToken()}. * * @deprecated Use {@link #showSoftInput(View, int)} or * {@link #hideSoftInputFromWindow(IBinder, int)} explicitly instead. * In particular during focus changes, the current visibility of the IME is not * well defined. Starting in {@link Build.VERSION_CODES#S Android S}, this only * has an effect if the calling app is the current IME focus. */ @Deprecated public void toggleSoftInputFromWindow(IBinder windowToken, @ShowFlags int showFlags, @HideFlags int hideFlags) { ImeTracing.getInstance().triggerClientDump( "InputMethodManager#toggleSoftInputFromWindow", InputMethodManager.this, null /* icProto */); synchronized (mH) { final View servedView = getServedViewLocked(); if (servedView == null || servedView.getWindowToken() != windowToken) { return; } toggleSoftInput(showFlags, hideFlags); } } /** * This method toggles the input method window display. * * If the input window is already displayed, it gets hidden. * If not the input window will be displayed. * * @deprecated Use {@link #showSoftInput(View, int)} or * {@link #hideSoftInputFromWindow(IBinder, int)} explicitly instead. * In particular during focus changes, the current visibility of the IME is not * well defined. Starting in {@link Build.VERSION_CODES#S Android S}, this only * has an effect if the calling app is the current IME focus. */ @Deprecated public void toggleSoftInput(@ShowFlags int showFlags, @HideFlags int hideFlags) { ImeTracing.getInstance().triggerClientDump( "InputMethodManager#toggleSoftInput", InputMethodManager.this, null /* icProto */); synchronized (mH) { final View view = getServedViewLocked(); if (view != null) { final WindowInsets rootInsets = view.getRootWindowInsets(); if (rootInsets != null && rootInsets.isVisible(WindowInsets.Type.ime())) { hideSoftInputFromWindow(view.getWindowToken(), hideFlags, null /* resultReceiver */, SoftInputShowHideReason.HIDE_TOGGLE_SOFT_INPUT); } else { showSoftInput(view, showFlags, null /* resultReceiver */, SoftInputShowHideReason.SHOW_TOGGLE_SOFT_INPUT); } } } } /** * If the input method is currently connected to the given view, * restart it with its new contents. You should call this when the text * within your view changes outside of the normal input method or key * input flow, such as when an application calls TextView.setText(). * * @param view The view whose text has changed. */ public void restartInput(View view) { // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { fallbackImm.restartInput(view); return; } checkFocus(); synchronized (mH) { if (!hasServedByInputMethodLocked(view)) { return; } mServedConnecting = true; } startInputInner(StartInputReason.APP_CALLED_RESTART_INPUT_API, null, 0, 0, 0); } /** * Sends an async signal to the IME to reset the currently served {@link InputConnection}. * * @param inputConnection the connection to be invalidated. * @param textSnapshot {@link TextSnapshot} to be used to update {@link EditorInfo}. * @param sessionId the session ID to be sent. * @return {@code true} if the operation is done. {@code false} if the caller needs to fall back * to {@link InputMethodManager#restartInput(View)}. * @hide */ public boolean doInvalidateInput(@NonNull RemoteInputConnectionImpl inputConnection, @NonNull TextSnapshot textSnapshot, int sessionId) { synchronized (mH) { if (mServedInputConnection != inputConnection || mCurrentEditorInfo == null) { // OK to ignore because the calling InputConnection is already abandoned. return true; } if (!isImeSessionAvailableLocked()) { // IME is not yet bound to the client. Need to fall back to the restartInput(). return false; } final EditorInfo editorInfo = mCurrentEditorInfo.createCopyInternal(); editorInfo.initialSelStart = mCursorSelStart = textSnapshot.getSelectionStart(); editorInfo.initialSelEnd = mCursorSelEnd = textSnapshot.getSelectionEnd(); mCursorCandStart = textSnapshot.getCompositionStart(); mCursorCandEnd = textSnapshot.getCompositionEnd(); editorInfo.initialCapsMode = textSnapshot.getCursorCapsMode(); editorInfo.setInitialSurroundingTextInternal(textSnapshot.getSurroundingText()); mCurBindState.mImeSession.invalidateInput(editorInfo, mServedInputConnection, sessionId); final IRemoteAccessibilityInputConnection accessibilityInputConnection = mServedInputConnection.asIRemoteAccessibilityInputConnection(); forAccessibilitySessionsLocked(wrapper -> wrapper.invalidateInput(editorInfo, accessibilityInputConnection, sessionId)); return true; } } /** * Gives a hint to the system that the text associated with {@code view} is updated by something * that is not an input method editor (IME), so that the system can cancel any pending text * editing requests from the IME until it receives the new editing context such as surrounding * text provided by {@link InputConnection#takeSnapshot()}. * *

When {@code view} does not support {@link InputConnection#takeSnapshot()} protocol, * calling this method may trigger {@link View#onCreateInputConnection(EditorInfo)}.

* *

Unlike {@link #restartInput(View)}, this API does not immediately interact with * {@link InputConnection}. Instead, the application may later receive * {@link InputConnection#takeSnapshot()} as needed so that the system can capture new editing * context for the IME. For instance, successive invocations of this API can be coerced into a * single (or zero) callback of {@link InputConnection#takeSnapshot()}.

* * @param view The view whose text has changed. * @see #restartInput(View) */ public void invalidateInput(@NonNull View view) { Objects.requireNonNull(view); // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { fallbackImm.invalidateInput(view); return; } synchronized (mH) { if (mServedInputConnection == null || getServedViewLocked() != view) { return; } mServedInputConnection.scheduleInvalidateInput(); } } /** * Starts an input connection from the served view that gains the window focus. * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input * background thread may blocked by other methods which already inside {@code mH} lock. * *

{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when * {@code userId} is different from the user id of the current process.

*/ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) private boolean startInputInner(@StartInputReason int startInputReason, @Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode, int windowFlags) { final View view; synchronized (mH) { view = getServedViewLocked(); // Make sure we have a window token for the served view. if (DEBUG) { Log.v(TAG, "Starting input: view=" + InputMethodDebug.dumpViewInfo(view) + " reason=" + InputMethodDebug.startInputReasonToString(startInputReason)); } if (view == null) { if (DEBUG) Log.v(TAG, "ABORT input: no served view!"); return false; } } // Now we need to get an input connection from the served view. // This is complicated in a couple ways: we can't be holding our lock // when calling out to the view, and we need to make sure we call into // the view on the same thread that is driving its view hierarchy. Handler vh = view.getHandler(); if (vh == null) { // If the view doesn't have a handler, something has changed out // from under us, so just close the current input. // If we don't close the current input, the current input method can remain on the // screen without a connection. if (DEBUG) Log.v(TAG, "ABORT input: no handler for view! Close current input."); closeCurrentInput(); return false; } if (vh.getLooper() != Looper.myLooper()) { // The view is running on a different thread than our own, so // we need to reschedule our work for over there. if (DEBUG) Log.v(TAG, "Starting input: reschedule to view thread"); vh.post(() -> startInputOnWindowFocusGainInternal(startInputReason, null, 0, 0, 0)); return false; } if (windowGainingFocus == null) { windowGainingFocus = view.getWindowToken(); if (windowGainingFocus == null) { Log.e(TAG, "ABORT input: ServedView must be attached to a Window"); return false; } startInputFlags = getStartInputFlags(view, startInputFlags); softInputMode = view.getViewRootImpl().mWindowAttributes.softInputMode; windowFlags = view.getViewRootImpl().mWindowAttributes.flags; } // Okay we are now ready to call into the served view and have it // do its stuff. // Life is good: let's hook everything up! final Pair connectionPair = createInputConnection(view); final InputConnection ic = connectionPair.first; final EditorInfo editorInfo = connectionPair.second; final Handler icHandler; InputBindResult res = null; final boolean hasServedView; synchronized (mH) { // Now that we are locked again, validate that our state hasn't // changed. final View servedView = getServedViewLocked(); if (servedView != view || !mServedConnecting) { // Something else happened, so abort. if (DEBUG) Log.v(TAG, "Starting input: finished by someone else." + " view=" + InputMethodDebug.dumpViewInfo(view) + " servedView=" + InputMethodDebug.dumpViewInfo(servedView) + " mServedConnecting=" + mServedConnecting); if (mServedInputConnection != null && startInputReason == BOUND_TO_IMMS) { // This is not an error. Once IME binds (MSG_BIND), InputConnection is fully // established. So we report this to interested recipients. reportInputConnectionOpened( mServedInputConnection.getInputConnection(), mCurrentEditorInfo, mServedInputConnectionHandler, view); } return false; } // If we already have a text box, then this view is already // connected so we want to restart it. if (mCurrentEditorInfo == null) { startInputFlags |= StartInputFlags.INITIAL_CONNECTION; } editorInfo.setInitialToolType(mCurRootView.getLastClickToolType()); // Hook 'em up and let 'er rip. mCurrentEditorInfo = editorInfo.createCopyInternal(); // Store the previously served connection so that we can determine whether it is safe // to skip the call to startInputOrWindowGainedFocus in the IMMS final RemoteInputConnectionImpl previouslyServedConnection = mServedInputConnection; mServedConnecting = false; if (mServedInputConnection != null) { mServedInputConnection.deactivate(); mServedInputConnection = null; mServedInputConnectionHandler = null; } final RemoteInputConnectionImpl servedInputConnection; if (ic != null) { mCursorSelStart = editorInfo.initialSelStart; mCursorSelEnd = editorInfo.initialSelEnd; mInitialSelStart = mCursorSelStart; mInitialSelEnd = mCursorSelEnd; mCursorCandStart = -1; mCursorCandEnd = -1; mCursorRect.setEmpty(); mCursorAnchorInfo = null; Handler handler = null; try { handler = ic.getHandler(); } catch (AbstractMethodError ignored) { // TODO(b/199934664): See if we can remove this by providing a default impl. } icHandler = handler; mServedInputConnectionHandler = icHandler; servedInputConnection = new RemoteInputConnectionImpl( icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this, view); } else { servedInputConnection = null; icHandler = null; mServedInputConnectionHandler = null; } mServedInputConnection = servedInputConnection; if (DEBUG) { Log.v(TAG, "START INPUT: view=" + InputMethodDebug.dumpViewInfo(view) + " ic=" + ic + " editorInfo=" + editorInfo + " startInputFlags=" + InputMethodDebug.startInputFlagsToString(startInputFlags)); } // When we switch between non-editable views, do not call into the IMMS. final boolean canSkip = OPTIMIZE_NONEDITABLE_VIEWS && previouslyServedConnection == null && ic == null && isSwitchingBetweenEquivalentNonEditableViews( mPreviousViewFocusParameters, startInputFlags, startInputReason, softInputMode, windowFlags); mPreviousViewFocusParameters = new ViewFocusParameterInfo(mCurrentEditorInfo, startInputFlags, startInputReason, softInputMode, windowFlags); if (canSkip) { if (DEBUG) { Log.d(TAG, "Not calling IMMS due to switching between non-editable views."); } return false; } final int targetUserId = editorInfo.targetInputMethodUser != null ? editorInfo.targetInputMethodUser.getIdentifier() : UserHandle.myUserId(); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMM.startInputOrWindowGainedFocus"); int startInputSeq = -1; if (Flags.useZeroJankProxy()) { // async result delivered via MSG_START_INPUT_RESULT. startInputSeq = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocusAsync( startInputReason, mClient, windowGainingFocus, startInputFlags, softInputMode, windowFlags, editorInfo, servedInputConnection, servedInputConnection == null ? null : servedInputConnection.asIRemoteAccessibilityInputConnection(), view.getContext().getApplicationInfo().targetSdkVersion, targetUserId, mImeDispatcher); } else { res = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus( startInputReason, mClient, windowGainingFocus, startInputFlags, softInputMode, windowFlags, editorInfo, servedInputConnection, servedInputConnection == null ? null : servedInputConnection.asIRemoteAccessibilityInputConnection(), view.getContext().getApplicationInfo().targetSdkVersion, targetUserId, mImeDispatcher); } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (Flags.useZeroJankProxy()) { // Create a runnable for delayed notification to the app that the InputConnection is // initialized and ready for use. if (ic != null) { final int seqId = startInputSeq; mReportInputConnectionOpenedRunner = new ReportInputConnectionOpenedRunner(startInputSeq) { @Override public void run() { if (DEBUG) { Log.v(TAG, "Calling View.onInputConnectionOpened: view= " + view + ", ic=" + ic + ", editorInfo=" + editorInfo + ", handler=" + icHandler + ", startInputSeq=" + seqId); } reportInputConnectionOpened(ic, editorInfo, icHandler, view); } }; } else { mReportInputConnectionOpenedRunner = null; } return true; } if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res); if (res == null) { Log.wtf(TAG, "startInputOrWindowGainedFocus must not return" + " null. startInputReason=" + InputMethodDebug.startInputReasonToString(startInputReason) + " editorInfo=" + editorInfo + " startInputFlags=" + InputMethodDebug.startInputFlagsToString(startInputFlags)); return false; } if (res.id != null) { updateInputChannelLocked(res.channel); mCurMethod = res.method; // for @UnsupportedAppUsage mCurBindState = new BindState(res); mAccessibilityInputMethodSession.clear(); if (res.accessibilitySessions != null) { for (int i = 0; i < res.accessibilitySessions.size(); i++) { IAccessibilityInputMethodSessionInvoker wrapper = IAccessibilityInputMethodSessionInvoker.createOrNull( res.accessibilitySessions.valueAt(i)); if (wrapper != null) { mAccessibilityInputMethodSession.append( res.accessibilitySessions.keyAt(i), wrapper); } } } mCurId = res.id; // for @UnsupportedAppUsage } else if (res.channel != null && res.channel != mCurChannel) { res.channel.dispose(); } switch (res.result) { case InputBindResult.ResultCode.ERROR_NOT_IME_TARGET_WINDOW: mRestartOnNextWindowFocus = true; if (initiationWithoutInputConnection()) { mServedView.getViewRootImpl().getHandwritingInitiator().clearFocusedView( mServedView); } mServedView = null; break; } if (mCompletions != null) { if (isImeSessionAvailableLocked()) { mCurBindState.mImeSession.displayCompletions(mCompletions); } } hasServedView = mServedView != null; } // Notify the app that the InputConnection is initialized and ready for use. if (ic != null && res != null && res.method != null && hasServedView) { if (DEBUG) { Log.v(TAG, "Calling View.onInputConnectionOpened: view= " + view + ", ic=" + ic + ", editorInfo=" + editorInfo + ", handler=" + icHandler); } reportInputConnectionOpened(ic, editorInfo, icHandler, view); } return true; } /** * @return {@code true} when we are switching focus between two non-editable views * so that we can avoid calling {@link IInputMethodManager#startInputOrWindowGainedFocus}. */ @GuardedBy("mH") private boolean isSwitchingBetweenEquivalentNonEditableViews( @Nullable ViewFocusParameterInfo previousViewFocusParameters, @StartInputFlags int startInputFlags, @StartInputReason int startInputReason, @SoftInputModeFlags int softInputMode, int windowFlags) { return (startInputFlags & StartInputFlags.WINDOW_GAINED_FOCUS) == 0 && (startInputFlags & StartInputFlags.IS_TEXT_EDITOR) == 0 && previousViewFocusParameters != null && previousViewFocusParameters.sameAs(mCurrentEditorInfo, startInputFlags, startInputReason, softInputMode, windowFlags); } private void reportInputConnectionOpened( InputConnection ic, EditorInfo editorInfo, Handler icHandler, View view) { view.onInputConnectionOpenedInternal(ic, editorInfo, icHandler); final ViewRootImpl viewRoot = view.getViewRootImpl(); if (viewRoot != null) { viewRoot.getHandwritingInitiator().onInputConnectionCreated(view); } } /** * * @hide */ @TestApi @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD) public void addVirtualStylusIdForTestSession() { synchronized (mH) { IInputMethodManagerGlobalInvoker.addVirtualStylusIdForTestSession(mClient); } } /** * Set a stylus idle-timeout after which handwriting {@code InkWindow} will be removed. *

This API is for tests only.

* @param timeout to set in milliseconds. To reset to default, use a value <= zero. * @hide */ @TestApi @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD) public void setStylusWindowIdleTimeoutForTest(@DurationMillisLong long timeout) { synchronized (mH) { IInputMethodManagerGlobalInvoker.setStylusWindowIdleTimeoutForTest(mClient, timeout); } } /** * An empty method only to avoid crashes of apps that call this method via reflection and do not * handle {@link NoSuchMethodException} in a graceful manner. * * @deprecated This is an empty method. No framework method must call this method. * @hide */ @Deprecated @UnsupportedAppUsage(trackingBug = 37122102, maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "{@code androidx.activity.ComponentActivity}") public void windowDismissed(IBinder appWindowToken) { // Intentionally empty. // // It seems that some applications call this method via reflection to null clear the // following fields that used to exist in InputMethodManager: // * InputMethodManager#mCurRootView // * InputMethodManager#mServedView // * InputMethodManager#mNextServedView // so that these objects can be garbage-collected when an Activity gets dismissed. // // It is indeed true that older versions of InputMethodManager had issues that prevented // these fields from being null-cleared when it should have been, but the understanding of // the engineering team is that all known issues have already been fixed as of Android 10. // // For older devices, developers can work around the object leaks by using // androidx.activity.ComponentActivity. // See https://issuetracker.google.com/u/1/issues/37122102 for details. // // If you believe InputMethodManager is leaking objects in API 24 or any later version, // please file a bug at https://issuetracker.google.com/issues/new?component=192705. } private int getStartInputFlags(View focusedView, int startInputFlags) { startInputFlags |= StartInputFlags.VIEW_HAS_FOCUS; if (focusedView.onCheckIsTextEditor()) { startInputFlags |= StartInputFlags.IS_TEXT_EDITOR; } return startInputFlags; } /** * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input * background thread may blocked by other methods which already inside {@code mH} lock. * @hide */ @UnsupportedAppUsage public void checkFocus() { synchronized (mH) { if (mCurRootView == null) { return; } if (!checkFocusInternalLocked(false /* forceNewFocus */, mCurRootView)) { return; } } startInputOnWindowFocusGainInternal(StartInputReason.CHECK_FOCUS, null /* focusedView */, 0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */); } /** * Check the next served view if needs to start input. */ @GuardedBy("mH") private boolean checkFocusInternalLocked(boolean forceNewFocus, ViewRootImpl viewRootImpl) { if (mCurRootView != viewRootImpl) { return false; } if (mServedView == mNextServedView && !forceNewFocus) { return false; } if (DEBUG) { Log.v(TAG, "checkFocus: view=" + mServedView + " next=" + mNextServedView + " force=" + forceNewFocus + " package=" + (mServedView != null ? mServedView.getContext().getPackageName() : "")); } // Close the connection when no next served view coming. if (mNextServedView == null) { finishInputLocked(); closeCurrentInput(); return false; } mServedView = mNextServedView; if (mServedInputConnection != null) { mServedInputConnection.finishComposingTextFromImm(); } return true; } @UiThread private void onViewFocusChangedInternal(@Nullable View view, boolean hasFocus) { if (view == null || view.isTemporarilyDetached()) { return; } final ViewRootImpl viewRootImpl = view.getViewRootImpl(); synchronized (mH) { if (mCurRootView != viewRootImpl) { return; } if (!view.hasImeFocus() || !view.hasWindowFocus()) { return; } if (DEBUG) { Log.d(TAG, "onViewFocusChangedInternal, view=" + InputMethodDebug.dumpViewInfo(view)); } // We don't need to track the next served view when the view lost focus here // because: // 1) The current view focus may be cleared temporary when in touch mode, closing // input at this moment isn't the right way. // 2) We only care about the served view change when it focused, since changing // input connection when the focus target changed is reasonable. // 3) Setting the next served view as null when no more served view should be // handled in other special events (e.g. view detached from window or the window // dismissed). if (hasFocus) { mNextServedView = view; } } viewRootImpl.dispatchCheckFocus(); } @UnsupportedAppUsage void closeCurrentInput() { final int reason = SoftInputShowHideReason.HIDE_CLOSE_CURRENT_SESSION; final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE, ImeTracker.ORIGIN_CLIENT, reason, false /* fromUser */); ImeTracker.forLatency().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT, reason, ActivityThread::currentApplication); synchronized (mH) { final View rootView = mCurRootView != null ? mCurRootView.getView() : null; if (rootView == null) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); ImeTracker.forLatency().onHideFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED, ActivityThread::currentApplication); Log.w(TAG, "No current root view, ignoring closeCurrentInput()"); return; } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); IInputMethodManagerGlobalInvoker.hideSoftInput( mClient, rootView.getWindowToken(), statsToken, HIDE_NOT_ALWAYS, null, reason); } } /** * Register for IME state callbacks and applying visibility in * {@link android.view.ImeInsetsSourceConsumer}. * @hide */ public void registerImeConsumer(@NonNull ImeInsetsSourceConsumer imeInsetsConsumer) { if (imeInsetsConsumer == null) { throw new IllegalStateException("ImeInsetsSourceConsumer cannot be null."); } synchronized (mH) { mImeInsetsConsumer = imeInsetsConsumer; } } /** * Unregister for IME state callbacks and applying visibility in * {@link android.view.ImeInsetsSourceConsumer}. * @hide */ public void unregisterImeConsumer(@NonNull ImeInsetsSourceConsumer imeInsetsConsumer) { if (imeInsetsConsumer == null) { throw new IllegalStateException("ImeInsetsSourceConsumer cannot be null."); } synchronized (mH) { if (mImeInsetsConsumer == imeInsetsConsumer) { mImeInsetsConsumer = null; } } } /** * Call showSoftInput with currently focused view. * * @param windowToken the window from which this request originates. If this doesn't match the * currently served view, the request is ignored and returns {@code false}. * @param statsToken the token tracking the current IME request. * * @return {@code true} if IME can (eventually) be shown, {@code false} otherwise. * @hide */ public boolean requestImeShow(IBinder windowToken, @NonNull ImeTracker.Token statsToken) { checkFocus(); synchronized (mH) { final View servedView = getServedViewLocked(); if (servedView == null || servedView.getWindowToken() != windowToken) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_CLIENT_REQUEST_IME_SHOW); return false; } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_REQUEST_IME_SHOW); showSoftInput(servedView, statsToken, 0 /* flags */, null /* resultReceiver */, SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API); return true; } } /** * Notify IMMS that IME insets are no longer visible. * * @param windowToken the window from which this request originates. If this doesn't match the * currently served view, the request is ignored. * @param statsToken the token tracking the current IME request. * @hide */ public void notifyImeHidden(IBinder windowToken, @NonNull ImeTracker.Token statsToken) { ImeTracker.forLatency().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT, SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API, ActivityThread::currentApplication); ImeTracing.getInstance().triggerClientDump("InputMethodManager#notifyImeHidden", this, null /* icProto */); synchronized (mH) { if (!isImeSessionAvailableLocked() || mCurRootView == null || mCurRootView.getWindowToken() != windowToken) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); ImeTracker.forLatency().onHideFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED, ActivityThread::currentApplication); return; } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, statsToken, 0 /* flags */, null /* resultReceiver */, SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API); } } /** * Notify IME directly to remove surface as it is no longer visible. * @param windowToken The client window token that requests the IME to remove its surface. * @hide */ public void removeImeSurface(@NonNull IBinder windowToken) { synchronized (mH) { IInputMethodManagerGlobalInvoker.removeImeSurfaceFromWindowAsync(windowToken); } } /** * Report the current selection range. * *

Editor authors, you need to call this method whenever * the cursor moves in your editor. Remember that in addition to doing this, your * editor needs to always supply current cursor values in * {@link EditorInfo#initialSelStart} and {@link EditorInfo#initialSelEnd} every * time {@link android.view.View#onCreateInputConnection(EditorInfo)} is * called, which happens whenever the keyboard shows up or the focus changes * to a text field, among other cases.

*/ public void updateSelection(View view, int selStart, int selEnd, int candidatesStart, int candidatesEnd) { // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { fallbackImm.updateSelection(view, selStart, selEnd, candidatesStart, candidatesEnd); return; } checkFocus(); synchronized (mH) { if (!hasServedByInputMethodLocked(view) || mCurrentEditorInfo == null || !isImeSessionAvailableLocked()) { return; } if (mServedInputConnection != null && mServedInputConnection.hasPendingInvalidation()) { return; } if (mCursorSelStart != selStart || mCursorSelEnd != selEnd || mCursorCandStart != candidatesStart || mCursorCandEnd != candidatesEnd) { if (DEBUG) Log.d(TAG, "updateSelection"); if (DEBUG) { Log.v(TAG, "SELECTION CHANGE: " + mCurBindState.mImeSession); } mCurBindState.mImeSession.updateSelection(mCursorSelStart, mCursorSelEnd, selStart, selEnd, candidatesStart, candidatesEnd); forAccessibilitySessionsLocked(wrapper -> wrapper.updateSelection(mCursorSelStart, mCursorSelEnd, selStart, selEnd, candidatesStart, candidatesEnd)); mCursorSelStart = selStart; mCursorSelEnd = selEnd; mCursorCandStart = candidatesStart; mCursorCandEnd = candidatesEnd; } } } /** * Notify the event when the user tapped or clicked the text view. * * @param view {@link View} which is being clicked. * @see InputMethodService#onViewClicked(boolean) * @deprecated The semantics of this method can never be defined well for composite {@link View} * that works as a giant "Canvas", which can host its own UI hierarchy and sub focus * state. {@link android.webkit.WebView} is a good example. Application / IME * developers should not rely on this method. */ @Deprecated public void viewClicked(View view) { // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { fallbackImm.viewClicked(view); return; } final View servedView; final View nextServedView; synchronized (mH) { servedView = getServedViewLocked(); nextServedView = getNextServedViewLocked(); } final boolean focusChanged = servedView != nextServedView; checkFocus(); synchronized (mH) { if (!hasServedByInputMethodLocked(view) || mCurrentEditorInfo == null || !isImeSessionAvailableLocked()) { return; } if (DEBUG) Log.v(TAG, "onViewClicked: " + focusChanged); mCurBindState.mImeSession.viewClicked(focusChanged); } } /** * Return true if the current input method wants to watch the location * of the input editor's cursor in its window. * * @deprecated Use {@link InputConnection#requestCursorUpdates(int)} instead. */ @Deprecated public boolean isWatchingCursor(View view) { return false; } /** * Return true if the current input method wants to be notified when cursor/anchor location * is changed. * * @deprecated This method is kept for {@link UnsupportedAppUsage}. Must not be used. * @hide */ @Deprecated @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public boolean isCursorAnchorInfoEnabled() { synchronized (mH) { final boolean isImmediate = (mRequestUpdateCursorAnchorInfoMonitorMode & CURSOR_UPDATE_IMMEDIATE) != 0; final boolean isMonitoring = (mRequestUpdateCursorAnchorInfoMonitorMode & CURSOR_UPDATE_MONITOR) != 0; return isImmediate || isMonitoring; } } /** * Set the requested mode for {@link #updateCursorAnchorInfo(View, CursorAnchorInfo)}. * * @deprecated This method is kept for {@link UnsupportedAppUsage}. Must not be used. * @hide */ @Deprecated @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public void setUpdateCursorAnchorInfoMode(int flags) { synchronized (mH) { mRequestUpdateCursorAnchorInfoMonitorMode = flags; } } /** * Report the current cursor location in its window. * * @deprecated Use {@link #updateCursorAnchorInfo(View, CursorAnchorInfo)} instead. */ @Deprecated public void updateCursor(View view, int left, int top, int right, int bottom) { // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { fallbackImm.updateCursor(view, left, top, right, bottom); return; } checkFocus(); synchronized (mH) { if (!hasServedByInputMethodLocked(view) || mCurrentEditorInfo == null || !isImeSessionAvailableLocked()) { return; } mTmpCursorRect.set(left, top, right, bottom); if (!mCursorRect.equals(mTmpCursorRect)) { if (DEBUG) Log.d(TAG, "updateCursor: " + mCurBindState.mImeSession); mCurBindState.mImeSession.updateCursor(mTmpCursorRect); mCursorRect.set(mTmpCursorRect); } } } /** * Report positional change of the text insertion point and/or characters in the composition * string. */ public void updateCursorAnchorInfo(View view, final CursorAnchorInfo cursorAnchorInfo) { if (view == null || cursorAnchorInfo == null) { return; } // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { fallbackImm.updateCursorAnchorInfo(view, cursorAnchorInfo); return; } checkFocus(); synchronized (mH) { if (!hasServedByInputMethodLocked(view) || mCurrentEditorInfo == null || !isImeSessionAvailableLocked()) { return; } // If immediate bit is set, we will call updateCursorAnchorInfo() even when the data has // not been changed from the previous call. final boolean isImmediate = mServedInputConnection != null && mServedInputConnection.resetHasPendingImmediateCursorAnchorInfoUpdate(); if (!isImmediate && Objects.equals(mCursorAnchorInfo, cursorAnchorInfo)) { // TODO: Consider always emitting this message once we have addressed redundant // calls of this method from android.widget.Editor. if (DEBUG) { Log.w(TAG, "Ignoring redundant updateCursorAnchorInfo: info=" + cursorAnchorInfo); } return; } if (DEBUG) Log.v(TAG, "updateCursorAnchorInfo: " + cursorAnchorInfo); mCurBindState.mImeSession.updateCursorAnchorInfo(cursorAnchorInfo); mCursorAnchorInfo = cursorAnchorInfo; } } /** * Call {@link InputMethodSession#appPrivateCommand(String, Bundle) * InputMethodSession.appPrivateCommand()} on the current Input Method. * @param view Optional View that is sending the command, or null if * you want to send the command regardless of the view that is attached * to the input method. * @param action Name of the command to be performed. This must * be a scoped name, i.e. prefixed with a package name you own, so that * different developers will not create conflicting commands. * @param data Any data to include with the command. */ public void sendAppPrivateCommand(View view, String action, Bundle data) { // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); if (fallbackImm != null) { fallbackImm.sendAppPrivateCommand(view, action, data); return; } checkFocus(); synchronized (mH) { if (!hasServedByInputMethodLocked(view) || mCurrentEditorInfo == null || !isImeSessionAvailableLocked()) { return; } if (DEBUG) Log.v(TAG, "APP PRIVATE COMMAND " + action + ": " + data); mCurBindState.mImeSession.appPrivateCommand(action, data); } } /** * Force switch to a new input method component. This can only be called * from an application or a service which has a token of the currently active input method. * *

On Android {@link Build.VERSION_CODES#Q} and later devices, the undocumented behavior that * token can be {@code null} when the caller has * {@link Manifest.permission#WRITE_SECURE_SETTINGS} is deprecated. Instead, update * {@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD} and * {@link android.provider.Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE} directly.

* * @param token Supplies the identifying token given to an input method * when it was started, which allows it to perform this operation on * itself. * @param id The unique identifier for the new input method to be switched to. * @throws IllegalArgumentException if the input method is unknown or filtered by the rules of * package visibility. * @deprecated Use {@link InputMethodService#switchInputMethod(String)} * instead. This method was intended for IME developers who should be accessing APIs through * the service. APIs in this class are intended for app developers interacting with the IME. */ @Deprecated public void setInputMethod(IBinder token, String id) { if (token == null) { // There are still some system components that rely on this undocumented behavior // regarding null IME token with WRITE_SECURE_SETTINGS. Provide a fallback logic as a // temporary remedy. if (id == null) { return; } if (Process.myUid() == Process.SYSTEM_UID) { Log.w(TAG, "System process should not be calling setInputMethod() because almost " + "always it is a bug under multi-user / multi-profile environment. " + "Consider interacting with InputMethodManagerService directly via " + "LocalServices."); return; } final Context fallbackContext = ActivityThread.currentApplication(); if (fallbackContext == null) { return; } if (fallbackContext.checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { return; } final List imis = getEnabledInputMethodList(); final int numImis = imis.size(); boolean found = false; for (int i = 0; i < numImis; ++i) { final InputMethodInfo imi = imis.get(i); if (id.equals(imi.getId())) { found = true; break; } } if (!found) { Log.e(TAG, "Ignoring setInputMethod(null, " + id + ") because the specified " + "id not found in enabled IMEs."); return; } Log.w(TAG, "The undocumented behavior that setInputMethod() accepts null token " + "when the caller has WRITE_SECURE_SETTINGS is deprecated. This behavior may " + "be completely removed in a future version. Update secure settings directly " + "instead."); final ContentResolver resolver = fallbackContext.getContentResolver(); Settings.Secure.putInt(resolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID); Settings.Secure.putString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD, id); return; } InputMethodPrivilegedOperationsRegistry.get(token).setInputMethod(id); } /** * Force switch to a new input method and subtype. This can only be called * from an application or a service which has a token of the currently active input method. * *

On Android {@link Build.VERSION_CODES#Q} and later devices, {@code token} cannot be * {@code null} even with {@link Manifest.permission#WRITE_SECURE_SETTINGS}. Instead, * update {@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD} and * {@link android.provider.Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE} directly.

* * @param token Supplies the identifying token given to an input method * when it was started, which allows it to perform this operation on * itself. * @param id The unique identifier for the new input method to be switched to. * @param subtype The new subtype of the new input method to be switched to. * @throws IllegalArgumentException if the input method is unknown or filtered by the rules of * package visibility. * @deprecated Use * {@link InputMethodService#switchInputMethod(String, InputMethodSubtype)} * instead. This method was intended for IME developers who should be accessing APIs through * the service. APIs in this class are intended for app developers interacting with the IME. */ @Deprecated public void setInputMethodAndSubtype(@NonNull IBinder token, String id, InputMethodSubtype subtype) { if (token == null) { Log.e(TAG, "setInputMethodAndSubtype() does not accept null token on Android Q " + "and later."); return; } InputMethodPrivilegedOperationsRegistry.get(token).setInputMethodAndSubtype(id, subtype); } /** * Close/hide the input method's soft input area, so the user no longer * sees it or can interact with it. This can only be called * from the currently active input method, as validated by the given token. * * @param token Supplies the identifying token given to an input method * when it was started, which allows it to perform this operation on * itself. * @deprecated Use {@link InputMethodService#requestHideSelf(int)} instead. This method was * intended for IME developers who should be accessing APIs through the service. APIs in this * class are intended for app developers interacting with the IME. */ @Deprecated public void hideSoftInputFromInputMethod(IBinder token, @HideFlags int flags) { final int reason = SoftInputShowHideReason.HIDE_SOFT_INPUT_IMM_DEPRECATION; final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE, ImeTracker.ORIGIN_CLIENT, reason, false /* fromUser */); InputMethodPrivilegedOperationsRegistry.get(token).hideMySoftInput(statsToken, flags, reason); } /** * Show the input method's soft input area, so the user * sees the input method window and can interact with it. * This can only be called from the currently active input method, * as validated by the given token. * * @param token Supplies the identifying token given to an input method * when it was started, which allows it to perform this operation on * itself. * @deprecated Use {@link InputMethodService#requestShowSelf(int)} instead. This method was * intended for IME developers who should be accessing APIs through the service. APIs in this * class are intended for app developers interacting with the IME. */ @Deprecated public void showSoftInputFromInputMethod(IBinder token, @ShowFlags int flags) { final int reason = SoftInputShowHideReason.SHOW_SOFT_INPUT_IMM_DEPRECATION; final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_SHOW, ImeTracker.ORIGIN_CLIENT, reason, false /* fromUser */); InputMethodPrivilegedOperationsRegistry.get(token).showMySoftInput(statsToken, flags, reason); } /** * Dispatches an input event to the IME. * * Returns {@link #DISPATCH_HANDLED} if the event was handled. * Returns {@link #DISPATCH_NOT_HANDLED} if the event was not handled. * Returns {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the * callback will be invoked later. * * @hide */ public int dispatchInputEvent(InputEvent event, Object token, FinishedInputEventCallback callback, Handler handler) { synchronized (mH) { if (isImeSessionAvailableLocked()) { if (event instanceof KeyEvent) { KeyEvent keyEvent = (KeyEvent)event; if (keyEvent.getAction() == KeyEvent.ACTION_DOWN && keyEvent.getKeyCode() == KeyEvent.KEYCODE_SYM && keyEvent.getRepeatCount() == 0) { showInputMethodPickerLocked(); return DISPATCH_HANDLED; } } if (DEBUG) { Log.v(TAG, "DISPATCH INPUT EVENT: " + mCurBindState.mImeSession); } PendingEvent p = obtainPendingEventLocked( event, token, mCurBindState.mImeId, callback, handler); if (mMainLooper.isCurrentThread()) { // Already running on the IMM thread so we can send the event immediately. return sendInputEventOnMainLooperLocked(p); } // Post the event to the IMM thread. Message msg = mH.obtainMessage(MSG_SEND_INPUT_EVENT, p); msg.setAsynchronous(true); mH.sendMessage(msg); return DISPATCH_IN_PROGRESS; } } return DISPATCH_NOT_HANDLED; } /** * Provides the default implementation of {@link InputConnection#sendKeyEvent(KeyEvent)}, which * is expected to dispatch an keyboard event sent from the IME to an appropriate event target * depending on the given {@link View} and the current focus state. * *

CAUTION: This method is provided only for the situation where * {@link InputConnection#sendKeyEvent(KeyEvent)} needs to be implemented without relying on * {@link BaseInputConnection}. Do not use this API for anything else.

* * @param targetView the default target view. If {@code null} is specified, then this method * tries to find a good event target based on the current focus state. * @param event the key event to be dispatched. */ public void dispatchKeyEventFromInputMethod(@Nullable View targetView, @NonNull KeyEvent event) { // Re-dispatch if there is a context mismatch. final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(targetView); if (fallbackImm != null) { fallbackImm.dispatchKeyEventFromInputMethod(targetView, event); return; } synchronized (mH) { ViewRootImpl viewRootImpl = targetView != null ? targetView.getViewRootImpl() : null; if (viewRootImpl == null) { final View servedView = getServedViewLocked(); if (servedView != null) { viewRootImpl = servedView.getViewRootImpl(); } } if (viewRootImpl != null) { viewRootImpl.dispatchKeyFromIme(event); } } } // Must be called on the main looper private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) { final boolean handled; synchronized (mH) { int result = sendInputEventOnMainLooperLocked(p); if (result == DISPATCH_IN_PROGRESS) { return; } handled = (result == DISPATCH_HANDLED); } invokeFinishedInputEventCallback(p, handled); } // Must be called on the main looper @GuardedBy("mH") private int sendInputEventOnMainLooperLocked(PendingEvent p) { if (mCurChannel != null) { if (mCurSender == null) { mCurSender = new ImeInputEventSender(mCurChannel, mH.getLooper()); } final InputEvent event = p.mEvent; final int seq = event.getSequenceNumber(); if (mCurSender.sendInputEvent(seq, event)) { mPendingEvents.put(seq, p); Trace.traceCounter(Trace.TRACE_TAG_INPUT, PENDING_EVENT_COUNTER, mPendingEvents.size()); Message msg = mH.obtainMessage(MSG_TIMEOUT_INPUT_EVENT, seq, 0, p); msg.setAsynchronous(true); mH.sendMessageDelayed(msg, INPUT_METHOD_NOT_RESPONDING_TIMEOUT); return DISPATCH_IN_PROGRESS; } if (sPreventImeStartupUnlessTextEditor) { Log.d(TAG, "Dropping event because IME is evicted: " + event); } else { Log.w(TAG, "Unable to send input event to IME: " + getImeIdLocked() + " dropping: " + event); } } return DISPATCH_NOT_HANDLED; } private void finishedInputEvent(int seq, boolean handled, boolean timeout) { final PendingEvent p; synchronized (mH) { int index = mPendingEvents.indexOfKey(seq); if (index < 0) { return; // spurious, event already finished or timed out } p = mPendingEvents.valueAt(index); mPendingEvents.removeAt(index); Trace.traceCounter(Trace.TRACE_TAG_INPUT, PENDING_EVENT_COUNTER, mPendingEvents.size()); if (timeout) { Log.w(TAG, "Timeout waiting for IME to handle input event after " + INPUT_METHOD_NOT_RESPONDING_TIMEOUT + " ms: " + p.mInputMethodId); } else { mH.removeMessages(MSG_TIMEOUT_INPUT_EVENT, p); } } invokeFinishedInputEventCallback(p, handled); } // Assumes the event has already been removed from the queue. private void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) { p.mHandled = handled; if (p.mHandler.getLooper().isCurrentThread()) { // Already running on the callback handler thread so we can send the // callback immediately. p.run(); } else { // Post the event to the callback handler thread. // In this case, the callback will be responsible for recycling the event. Message msg = Message.obtain(p.mHandler, p); msg.setAsynchronous(true); msg.sendToTarget(); } } @GuardedBy("mH") private void flushPendingEventsLocked() { mH.removeMessages(MSG_FLUSH_INPUT_EVENT); final int count = mPendingEvents.size(); for (int i = 0; i < count; i++) { int seq = mPendingEvents.keyAt(i); Message msg = mH.obtainMessage(MSG_FLUSH_INPUT_EVENT, seq, 0); msg.setAsynchronous(true); msg.sendToTarget(); } } @GuardedBy("mH") private PendingEvent obtainPendingEventLocked(InputEvent event, Object token, String inputMethodId, FinishedInputEventCallback callback, Handler handler) { PendingEvent p = mPendingEventPool.acquire(); if (p == null) { p = new PendingEvent(); } p.mEvent = event; p.mToken = token; p.mInputMethodId = inputMethodId; p.mCallback = callback; p.mHandler = handler; return p; } @GuardedBy("mH") private void recyclePendingEventLocked(PendingEvent p) { p.recycle(); mPendingEventPool.release(p); } /** * Show IME picker popup window. * *

Requires the {@link PackageManager#FEATURE_INPUT_METHODS} feature which can be detected * using {@link PackageManager#hasSystemFeature(String)}. */ public void showInputMethodPicker() { synchronized (mH) { showInputMethodPickerLocked(); } } /** * Shows the input method chooser dialog from system. * * @param showAuxiliarySubtypes Set true to show auxiliary input methods. * @param displayId The ID of the display where the chooser dialog should be shown. * @hide */ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) public void showInputMethodPickerFromSystem(boolean showAuxiliarySubtypes, int displayId) { final int mode = showAuxiliarySubtypes ? SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES : SHOW_IM_PICKER_MODE_EXCLUDE_AUXILIARY_SUBTYPES; IInputMethodManagerGlobalInvoker.showInputMethodPickerFromSystem(mode, displayId); } @GuardedBy("mH") private void showInputMethodPickerLocked() { IInputMethodManagerGlobalInvoker.showInputMethodPickerFromClient(mClient, SHOW_IM_PICKER_MODE_AUTO); } /** * A test API for CTS to make sure that {@link #showInputMethodPicker()} works as expected. * *

When customizing the implementation of {@link #showInputMethodPicker()} API, make sure * that this test API returns when and only while and only while * {@link #showInputMethodPicker()} is showing UI. Otherwise your OS implementation may not * pass CTS.

* * @return {@code true} while and only while {@link #showInputMethodPicker()} is showing UI. * @hide */ @TestApi @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD) public boolean isInputMethodPickerShown() { return IInputMethodManagerGlobalInvoker.isInputMethodPickerShownForTest(); } /** * A test API for CTS to check whether there are any pending IME visibility requests. * * @return {@code true} iff there are pending IME visibility requests. * @hide */ @TestApi @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD) public boolean hasPendingImeVisibilityRequests() { return IInputMethodManagerGlobalInvoker.hasPendingImeVisibilityRequests(); } /** * A test API for CTS to finish the tracking of any pending IME visibility requests. This * won't stop the actual requests, but allows resetting the state when starting up test runs. * * @hide */ @SuppressLint("UnflaggedApi") // @TestApi without associated feature. @TestApi @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD) public void finishTrackingPendingImeVisibilityRequests() { IInputMethodManagerGlobalInvoker.finishTrackingPendingImeVisibilityRequests(); } /** * Show the settings for enabling subtypes of the specified input method. * * @param imiId An input method, whose subtypes settings will be shown. If imiId is null, * subtypes of all input methods will be shown. */ public void showInputMethodAndSubtypeEnabler(@Nullable String imiId) { Context context = null; synchronized (mH) { if (mCurRootView != null) { context = mCurRootView.mContext; } } if (context == null) { final Context appContext = ActivityThread.currentApplication(); final DisplayManager displayManager = appContext.getSystemService(DisplayManager.class); context = appContext.createDisplayContext(displayManager.getDisplay(mDisplayId)); } final Intent intent = new Intent(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP); if (!TextUtils.isEmpty(imiId)) { intent.putExtra(Settings.EXTRA_INPUT_METHOD_ID, imiId); } context.startActivity(intent); } /** * Returns the current input method subtype. This subtype is one of the subtypes in * the current input method. This method returns null when the current input method doesn't * have any input method subtype. */ @Nullable public InputMethodSubtype getCurrentInputMethodSubtype() { return IInputMethodManagerGlobalInvoker.getCurrentInputMethodSubtype(UserHandle.myUserId()); } /** * Switch to a new input method subtype of the current input method. * @param subtype A new input method subtype to switch. * @return true if the current subtype was successfully switched. When the specified subtype is * null, this method returns false. * @deprecated If the calling process is an IME, use * {@link InputMethodService#switchInputMethod(String, InputMethodSubtype)}, which * does not require any permission as long as the caller is the current IME. * If the calling process is some privileged app that already has * {@link Manifest.permission#WRITE_SECURE_SETTINGS} permission, just * directly update {@link Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE}. */ @Deprecated @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) public boolean setCurrentInputMethodSubtype(InputMethodSubtype subtype) { if (Process.myUid() == Process.SYSTEM_UID) { Log.w(TAG, "System process should not call setCurrentInputMethodSubtype() because " + "almost always it is a bug under multi-user / multi-profile environment. " + "Consider directly interacting with InputMethodManagerService " + "via LocalServices."); return false; } if (subtype == null) { // See the JavaDoc. This is how this method has worked. return false; } final Context fallbackContext = ActivityThread.currentApplication(); if (fallbackContext == null) { return false; } if (fallbackContext.checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { return false; } final ContentResolver contentResolver = fallbackContext.getContentResolver(); final String imeId = Settings.Secure.getString(contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD); if (ComponentName.unflattenFromString(imeId) == null) { // Null or invalid IME ID format. return false; } final List enabledSubtypes = IInputMethodManagerGlobalInvoker.getEnabledInputMethodSubtypeList(imeId, true, UserHandle.myUserId()); final int numSubtypes = enabledSubtypes.size(); for (int i = 0; i < numSubtypes; ++i) { final InputMethodSubtype enabledSubtype = enabledSubtypes.get(i); if (enabledSubtype.equals(subtype)) { Settings.Secure.putInt(contentResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, enabledSubtype.hashCode()); return true; } } return false; } /** * Notify that a user took some action with this input method. * * @deprecated Just kept to avoid possible app compat issue. * @hide */ @Deprecated @UnsupportedAppUsage(trackingBug = 114740982, maxTargetSdk = Build.VERSION_CODES.P) public void notifyUserAction() { Log.w(TAG, "notifyUserAction() is a hidden method, which is now just a stub method" + " that does nothing. Leave comments in b.android.com/114740982 if your " + " application still depends on the previous behavior of this method."); } /** * Returns a map of all shortcut input method info and their subtypes. */ public Map> getShortcutInputMethodsAndSubtypes() { final List enabledImes = getEnabledInputMethodList(); // Ensure we check system IMEs first. enabledImes.sort(Comparator.comparingInt(imi -> imi.isSystem() ? 0 : 1)); final int numEnabledImes = enabledImes.size(); for (int imiIndex = 0; imiIndex < numEnabledImes; ++imiIndex) { final InputMethodInfo imi = enabledImes.get(imiIndex); final List subtypes = getEnabledInputMethodSubtypeList( imi, true); final int subtypeCount = subtypes.size(); for (int subtypeIndex = 0; subtypeIndex < subtypeCount; ++subtypeIndex) { final InputMethodSubtype subtype = imi.getSubtypeAt(subtypeIndex); if (SUBTYPE_MODE_VOICE.equals(subtype.getMode())) { return Collections.singletonMap(imi, Collections.singletonList(subtype)); } } } return Collections.emptyMap(); } /** * This is kept due to {@link android.compat.annotation.UnsupportedAppUsage}. * *

TODO(Bug 113914148): Check if we can remove this. We have accidentally exposed * WindowManagerInternal#getInputMethodWindowVisibleHeight to app developers and some of them * started relying on it.

* * @return Something that is not well-defined. * @hide */ @UnsupportedAppUsage(trackingBug = 204906124, maxTargetSdk = Build.VERSION_CODES.TIRAMISU, publicAlternatives = "Use {@link android.view.WindowInsets} instead") public int getInputMethodWindowVisibleHeight() { return IInputMethodManagerGlobalInvoker.getInputMethodWindowVisibleHeight(mClient); } /** * {@code true} means that * {@link RemoteInputConnectionImpl#requestCursorUpdatesInternal(int, int, int)} returns * {@code false} when the IME client and the IME run in different displays. */ final AtomicBoolean mRequestCursorUpdateDisplayIdCheck = new AtomicBoolean(true); /** * Controls the display ID mismatch validation in * {@link RemoteInputConnectionImpl#requestCursorUpdatesInternal(int, int, int)}. * *

{@link #updateCursorAnchorInfo(View, CursorAnchorInfo)} is not guaranteed to work * correctly when the IME client and the IME run in different displays. This is why * {@link RemoteInputConnectionImpl#requestCursorUpdatesInternal(int, int, int)} returns * {@code false} by default when the display ID does not match. This method allows special apps * to override this behavior when they are sure that it should work.

* *

By default the validation is enabled.

* * @param enabled {@code false} to disable the display ID validation. * @hide */ public void setRequestCursorUpdateDisplayIdCheck(boolean enabled) { mRequestCursorUpdateDisplayIdCheck.set(enabled); } /** * Force switch to the last used input method and subtype. If the last input method didn't have * any subtypes, the framework will simply switch to the last input method with no subtype * specified. * @param imeToken Supplies the identifying token given to an input method when it was started, * which allows it to perform this operation on itself. * @return true if the current input method and subtype was successfully switched to the last * used input method and subtype. * @deprecated Use {@link InputMethodService#switchToPreviousInputMethod()} instead. This method * was intended for IME developers who should be accessing APIs through the service. APIs in * this class are intended for app developers interacting with the IME. */ @Deprecated public boolean switchToLastInputMethod(IBinder imeToken) { return InputMethodPrivilegedOperationsRegistry.get(imeToken).switchToPreviousInputMethod(); } /** * Force switch to the next input method and subtype. If there is no IME enabled except * current IME and subtype, do nothing. * @param imeToken Supplies the identifying token given to an input method when it was started, * which allows it to perform this operation on itself. * @param onlyCurrentIme if true, the framework will find the next subtype which * belongs to the current IME * @return true if the current input method and subtype was successfully switched to the next * input method and subtype. * @deprecated Use {@link InputMethodService#switchToNextInputMethod(boolean)} instead. This * method was intended for IME developers who should be accessing APIs through the service. * APIs in this class are intended for app developers interacting with the IME. */ @Deprecated public boolean switchToNextInputMethod(IBinder imeToken, boolean onlyCurrentIme) { return InputMethodPrivilegedOperationsRegistry.get(imeToken) .switchToNextInputMethod(onlyCurrentIme); } /** * Returns true if the current IME needs to offer the users ways to switch to a next input * method (e.g. a globe key.). * When an IME sets supportsSwitchingToNextInputMethod and this method returns true, * the IME has to offer ways to to invoke {@link #switchToNextInputMethod} accordingly. *

Note that the system determines the most appropriate next input method * and subtype in order to provide the consistent user experience in switching * between IMEs and subtypes. * @param imeToken Supplies the identifying token given to an input method when it was started, * which allows it to perform this operation on itself. * @deprecated Use {@link InputMethodService#shouldOfferSwitchingToNextInputMethod()} * instead. This method was intended for IME developers who should be accessing APIs through * the service. APIs in this class are intended for app developers interacting with the IME. */ @Deprecated public boolean shouldOfferSwitchingToNextInputMethod(IBinder imeToken) { return InputMethodPrivilegedOperationsRegistry.get(imeToken) .shouldOfferSwitchingToNextInputMethod(); } /** * Set additional input method subtypes. Only a process which shares the same uid with the IME * can add additional input method subtypes to the IME. * Please note that a subtype's status is stored in the system. * For example, enabled subtypes are remembered by the framework even after they are removed * by using this method. If you re-add the same subtypes again, * they will just get enabled. If you want to avoid such conflicts, for instance, you may * want to create a "different" new subtype even with the same locale and mode, * by changing its extra value. The different subtype won't get affected by the stored past * status. (You may want to take a look at {@link InputMethodSubtype#hashCode()} to refer * to the current implementation.) * *

NOTE: If the same subtype exists in both the manifest XML file and additional subtypes * specified by {@code subtypes}, those multiple instances are automatically merged into one * instance.

* *

CAVEAT: In API Level 23 and prior, the system may do nothing if an empty * {@link InputMethodSubtype} is specified in {@code subtypes}, which prevents you from removing * the last one entry of additional subtypes. If your IME statically defines one or more * subtypes in the manifest XML file, you may be able to work around this limitation by * specifying one of those statically defined subtypes in {@code subtypes}.

* * @param imiId Id of InputMethodInfo which additional input method subtypes will be added to. * If the imiId is {@code null}, system would do nothing for this operation. * @param subtypes subtypes will be added as additional subtypes of the current input method. * If the subtypes is {@code null}, system would do nothing for this operation. * @deprecated For IMEs that have already implemented features like customizable/downloadable * keyboard layouts/languages, please start migration to other approaches. One idea * would be exposing only one unified {@link InputMethodSubtype} then implement * IME's own language switching mechanism within that unified subtype. The support * of "Additional Subtype" may be completely dropped in a future version of Android. */ @Deprecated public void setAdditionalInputMethodSubtypes(@NonNull String imiId, @NonNull InputMethodSubtype[] subtypes) { IInputMethodManagerGlobalInvoker.setAdditionalInputMethodSubtypes(imiId, subtypes, UserHandle.myUserId()); } /** * Updates the list of explicitly enabled {@link InputMethodSubtype} for a given IME owned by * the calling process. * *

By default each IME has no explicitly enabled {@link InputMethodSubtype}. In this state * the system will decide what {@link InputMethodSubtype} should be enabled by using information * available at runtime as per-user language settings. Users can, however, manually pick up one * or more {@link InputMethodSubtype} to be enabled on an Activity shown by * {@link #showInputMethodAndSubtypeEnabler(String)}. Such a manual change is stored in * {@link Settings.Secure#ENABLED_INPUT_METHODS} so that the change can persist across reboots. * {@link Settings.Secure#ENABLED_INPUT_METHODS} stores {@link InputMethodSubtype#hashCode()} as * the identifier of {@link InputMethodSubtype} for historical reasons.

* *

This API provides a safe and managed way for IME developers to modify what * {@link InputMethodSubtype} are referenced in {@link Settings.Secure#ENABLED_INPUT_METHODS} * for their own IME. One use case is when IME developers want to use their own Activity for * users to pick up {@link InputMethodSubtype}. Another use case is for IME developers to fix up * any stale and/or invalid value stored in {@link Settings.Secure#ENABLED_INPUT_METHODS} * without bothering users. Passing an empty {@code subtypeHashCodes} is guaranteed to reset * the state to default.

* *

To control the return value of {@link InputMethodSubtype#hashCode()}

*

{@link android.R.attr#subtypeId} and {@link * android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder#setSubtypeId(int)} are * available for IME developers to control the return value of * {@link InputMethodSubtype#hashCode()}. Beware that {@code -1} is not a valid value of * {@link InputMethodSubtype#hashCode()} for historical reasons.

* *

Note for Direct Boot support

*

While IME developers can call this API even before * {@link android.os.UserManager#isUserUnlocked()} becomes {@code true}, such a change is * volatile thus remains effective only until {@link android.os.UserManager#isUserUnlocked()} * becomes {@code true} or the device is rebooted. To make the change persistent IME developers * need to call this API again after receiving {@link Intent#ACTION_USER_UNLOCKED}.

* * @param imiId IME ID. The specified IME and the calling process need to belong to the same * package. Otherwise {@link SecurityException} will be thrown. * @param subtypeHashCodes An arrays of {@link InputMethodSubtype#hashCode()} to be explicitly * enabled. Entries that are found in the specified IME will be silently * ignored. Pass an empty array to reset the state to default. * @throws NullPointerException if {@code subtypeHashCodes} is {@code null}. * @throws SecurityException if the specified IME and the calling process do not belong to the * same package. */ public void setExplicitlyEnabledInputMethodSubtypes(@NonNull String imiId, @NonNull int[] subtypeHashCodes) { IInputMethodManagerGlobalInvoker.setExplicitlyEnabledInputMethodSubtypes(imiId, subtypeHashCodes, UserHandle.myUserId()); } /** * Returns the last used {@link InputMethodSubtype} in system history. * * @return the last {@link InputMethodSubtype}, {@code null} if last IME have no subtype. */ @Nullable public InputMethodSubtype getLastInputMethodSubtype() { return IInputMethodManagerGlobalInvoker.getLastInputMethodSubtype(UserHandle.myUserId()); } /** *

This is used for CTS test only. Do not use this method outside of CTS package.

* @return the ID of this display which this {@link InputMethodManager} resides * @hide */ @TestApi public int getDisplayId() { return mDisplayId; } private void doDump(FileDescriptor fd, PrintWriter fout, String[] args) { if (processDump(fd, args)) { return; } final Printer p = new PrintWriterPrinter(fout); p.println("Input method client state for " + this + ":"); p.println(" mFallbackInputConnection=" + mFallbackInputConnection); p.println(" mActive=" + mActive + " mRestartOnNextWindowFocus=" + mRestartOnNextWindowFocus + " mBindSequence=" + getBindSequenceLocked() + " mCurImeId=" + getImeIdLocked()); p.println(" mFullscreenMode=" + mFullscreenMode); if (isImeSessionAvailableLocked()) { p.println(" mCurMethod=" + mCurBindState.mImeSession); } else { p.println(" mCurMethod= null"); } for (int i = 0; i < mAccessibilityInputMethodSession.size(); i++) { p.println(" mAccessibilityInputMethodSession(" + mAccessibilityInputMethodSession.keyAt(i) + ")=" + mAccessibilityInputMethodSession.valueAt(i)); } p.println(" mCurRootView=" + mCurRootView); p.println(" mServedView=" + getServedViewLocked()); p.println(" mNextServedView=" + getNextServedViewLocked()); p.println(" mServedConnecting=" + mServedConnecting); if (mCurrentEditorInfo != null) { p.println(" mCurrentEditorInfo:"); mCurrentEditorInfo.dump(p, " ", false /* dumpExtras */); } else { p.println(" mCurrentEditorInfo: null"); } p.println(" mServedInputConnection=" + mServedInputConnection); p.println(" mServedInputConnectionHandler=" + mServedInputConnectionHandler); p.println(" mCompletions=" + Arrays.toString(mCompletions)); p.println(" mCursorRect=" + mCursorRect); p.println(" mCursorSelStart=" + mCursorSelStart + " mCursorSelEnd=" + mCursorSelEnd + " mCursorCandStart=" + mCursorCandStart + " mCursorCandEnd=" + mCursorCandEnd); } /** * Callback that is invoked when an input event that was dispatched to * the IME has been finished. * @hide */ public interface FinishedInputEventCallback { public void onFinishedInputEvent(Object token, boolean handled); } private static class ConnectionlessHandwritingCallbackProxy extends IConnectionlessHandwritingCallback.Stub { private final Object mLock = new Object(); @Nullable @GuardedBy("mLock") private Executor mExecutor; @Nullable @GuardedBy("mLock") private ConnectionlessHandwritingCallback mCallback; ConnectionlessHandwritingCallbackProxy( @NonNull Executor executor, @NonNull ConnectionlessHandwritingCallback callback) { mExecutor = executor; mCallback = callback; } @Override public void onResult(CharSequence text) { Executor executor; ConnectionlessHandwritingCallback callback; synchronized (mLock) { if (mExecutor == null || mCallback == null) { return; } executor = mExecutor; callback = mCallback; mExecutor = null; mCallback = null; } final long identity = Binder.clearCallingIdentity(); try { if (TextUtils.isEmpty(text)) { executor.execute(() -> callback.onError( ConnectionlessHandwritingCallback .CONNECTIONLESS_HANDWRITING_ERROR_NO_TEXT_RECOGNIZED)); } else { executor.execute(() -> callback.onResult(text)); } } finally { Binder.restoreCallingIdentity(identity); } } @Override public void onError(int errorCode) { Executor executor; ConnectionlessHandwritingCallback callback; synchronized (mLock) { if (mExecutor == null || mCallback == null) { return; } executor = mExecutor; callback = mCallback; mExecutor = null; mCallback = null; } final long identity = Binder.clearCallingIdentity(); try { executor.execute(() -> callback.onError(errorCode)); } finally { Binder.restoreCallingIdentity(identity); } } } private final class ImeInputEventSender extends InputEventSender { public ImeInputEventSender(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); } @Override public void onInputEventFinished(int seq, boolean handled) { finishedInputEvent(seq, handled, false); } } private final class PendingEvent implements Runnable { public InputEvent mEvent; public Object mToken; public String mInputMethodId; public FinishedInputEventCallback mCallback; public Handler mHandler; public boolean mHandled; public void recycle() { mEvent = null; mToken = null; mInputMethodId = null; mCallback = null; mHandler = null; mHandled = false; } @Override public void run() { mCallback.onFinishedInputEvent(mToken, mHandled); synchronized (mH) { recyclePendingEventLocked(this); } } } private static final class BindState { /** * Encapsulates IPCs to the currently connected InputMethodService. */ @Nullable final IInputMethodSessionInvoker mImeSession; /** * As reported by {@link InputBindResult}. This value is determined by * {@link com.android.internal.R.styleable#InputMethod_suppressesSpellChecker}. */ final boolean mIsInputMethodSuppressingSpellChecker; /** * As reported by {@link InputBindResult}. This value indicates the bound input method ID. */ @Nullable final String mImeId; /** * Sequence number of this binding, as returned by the server. */ final int mBindSequence; BindState(@NonNull InputBindResult inputBindResult) { mImeSession = IInputMethodSessionInvoker.createOrNull(inputBindResult.method); mIsInputMethodSuppressingSpellChecker = inputBindResult.isInputMethodSuppressingSpellChecker; mImeId = inputBindResult.id; mBindSequence = inputBindResult.sequence; } } @GuardedBy("mH") private boolean isImeSessionAvailableLocked() { return mCurBindState != null && mCurBindState.mImeSession != null; } @GuardedBy("mH") private String getImeIdLocked() { return mCurBindState != null ? mCurBindState.mImeId : null; } @GuardedBy("mH") private int getBindSequenceLocked() { return mCurBindState != null ? mCurBindState.mBindSequence : -1; } /** * Checks the args to see if a proto-based ime dump was requested and writes the client side * ime dump to the given {@link FileDescriptor}. * * @return {@code true} if a proto-based ime dump was requested. */ private boolean processDump(final FileDescriptor fd, final String[] args) { if (args == null) { return false; } for (String arg : args) { if (arg.equals(ImeTracing.PROTO_ARG)) { final ProtoOutputStream proto = new ProtoOutputStream(fd); dumpDebug(proto, null /* icProto */); proto.flush(); return true; } } return false; } /** * Write the proto dump of various client side components to the provided * {@link ProtoOutputStream}. * * @param proto The proto stream to which the dumps are written. * @param icProto {@link InputConnection} call data in proto format. * @hide */ public void dumpDebug(ProtoOutputStream proto, @Nullable byte[] icProto) { synchronized (mH) { if (!isImeSessionAvailableLocked()) { return; } proto.write(DISPLAY_ID, mDisplayId); final long token = proto.start(INPUT_METHOD_MANAGER); proto.write(CUR_ID, mCurBindState.mImeId); proto.write(FULLSCREEN_MODE, mFullscreenMode); proto.write(ACTIVE, mActive); proto.write(SERVED_CONNECTING, mServedConnecting); proto.write(SERVED_VIEW, Objects.toString(mServedView)); proto.write(NEXT_SERVED_VIEW, Objects.toString(mNextServedView)); proto.end(token); if (mCurRootView != null) { mCurRootView.dumpDebug(proto, VIEW_ROOT_IMPL); } if (mCurrentEditorInfo != null) { mCurrentEditorInfo.dumpDebug(proto, EDITOR_INFO); } if (mImeInsetsConsumer != null) { mImeInsetsConsumer.dumpDebug(proto, IME_INSETS_SOURCE_CONSUMER); } if (mServedInputConnection != null) { mServedInputConnection.dumpDebug(proto, INPUT_CONNECTION); } if (icProto != null) { proto.write(INPUT_CONNECTION_CALL, icProto); } } } @GuardedBy("mH") private void forAccessibilitySessionsLocked( Consumer consumer) { for (int i = 0; i < mAccessibilityInputMethodSession.size(); i++) { consumer.accept(mAccessibilityInputMethodSession.valueAt(i)); } } @UiThread private static Pair createInputConnection( @NonNull View servedView) { final EditorInfo editorInfo = new EditorInfo(); // Note: Use Context#getOpPackageName() rather than Context#getPackageName() so that the // system can verify the consistency between the uid of this process and package name passed // from here. See comment of Context#getOpPackageName() for details. editorInfo.packageName = servedView.getContext().getOpPackageName(); editorInfo.autofillId = servedView.getAutofillId(); editorInfo.fieldId = servedView.getId(); final InputConnection ic = servedView.onCreateInputConnection(editorInfo); if (DEBUG) Log.v(TAG, "Starting input: editorInfo=" + editorInfo + " ic=" + ic); // Clear autofill and field ids if a connection could not be established. // This ensures that even disconnected EditorInfos have well-defined attributes, // making them consistently and straightforwardly comparable. if (ic == null) { editorInfo.autofillId = AutofillId.NO_AUTOFILL_ID; editorInfo.fieldId = 0; } return new Pair<>(ic, editorInfo); } }