• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.inputmethod;
18 
19 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.app.PendingIntent;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.ServiceConnection;
28 import android.content.pm.PackageManagerInternal;
29 import android.content.res.Resources;
30 import android.inputmethodservice.InputMethodService;
31 import android.os.Binder;
32 import android.os.IBinder;
33 import android.os.Process;
34 import android.os.RemoteException;
35 import android.os.SystemClock;
36 import android.os.Trace;
37 import android.os.UserHandle;
38 import android.provider.Settings;
39 import android.util.ArrayMap;
40 import android.util.EventLog;
41 import android.util.Slog;
42 import android.view.IWindowManager;
43 import android.view.WindowManager;
44 import android.view.inputmethod.InputMethod;
45 import android.view.inputmethod.InputMethodInfo;
46 
47 import com.android.internal.annotations.GuardedBy;
48 import com.android.internal.inputmethod.InputBindResult;
49 import com.android.internal.inputmethod.UnbindReason;
50 import com.android.internal.view.IInputMethod;
51 import com.android.server.EventLogTags;
52 import com.android.server.wm.WindowManagerInternal;
53 
54 /**
55  * A controller managing the state of the input method binding.
56  */
57 final class InputMethodBindingController {
58     static final boolean DEBUG = false;
59     private static final String TAG = InputMethodBindingController.class.getSimpleName();
60 
61     /** Time in milliseconds that the IME service has to bind before it is reconnected. */
62     static final long TIME_TO_RECONNECT = 3 * 1000;
63 
64     @NonNull private final InputMethodManagerService mService;
65     @NonNull private final Context mContext;
66     @NonNull private final ArrayMap<String, InputMethodInfo> mMethodMap;
67     @NonNull private final InputMethodUtils.InputMethodSettings mSettings;
68     @NonNull private final PackageManagerInternal mPackageManagerInternal;
69     @NonNull private final IWindowManager mIWindowManager;
70     @NonNull private final WindowManagerInternal mWindowManagerInternal;
71     @NonNull private final Resources mRes;
72 
73     @GuardedBy("ImfLock.class") private long mLastBindTime;
74     @GuardedBy("ImfLock.class") private boolean mHasConnection;
75     @GuardedBy("ImfLock.class") @Nullable private String mCurId;
76     @GuardedBy("ImfLock.class") @Nullable private String mSelectedMethodId;
77     @GuardedBy("ImfLock.class") @Nullable private Intent mCurIntent;
78     @GuardedBy("ImfLock.class") @Nullable private IInputMethodInvoker mCurMethod;
79     @GuardedBy("ImfLock.class") private int mCurMethodUid = Process.INVALID_UID;
80     @GuardedBy("ImfLock.class") private IBinder mCurToken;
81     @GuardedBy("ImfLock.class") private int mCurSeq;
82     @GuardedBy("ImfLock.class") private boolean mVisibleBound;
83     private boolean mSupportsStylusHw;
84 
85     /**
86      * Binding flags for establishing connection to the {@link InputMethodService}.
87      */
88     private static final int IME_CONNECTION_BIND_FLAGS =
89             Context.BIND_AUTO_CREATE
90                     | Context.BIND_NOT_VISIBLE
91                     | Context.BIND_NOT_FOREGROUND
92                     | Context.BIND_IMPORTANT_BACKGROUND;
93     /**
94      * Binding flags used only while the {@link InputMethodService} is showing window.
95      */
96     private static final int IME_VISIBLE_BIND_FLAGS =
97             Context.BIND_AUTO_CREATE
98                     | Context.BIND_TREAT_LIKE_ACTIVITY
99                     | Context.BIND_FOREGROUND_SERVICE
100                     | Context.BIND_INCLUDE_CAPABILITIES
101                     | Context.BIND_SHOWING_UI
102                     | Context.BIND_SCHEDULE_LIKE_TOP_APP;
103 
InputMethodBindingController(@onNull InputMethodManagerService service)104     InputMethodBindingController(@NonNull InputMethodManagerService service) {
105         mService = service;
106         mContext = mService.mContext;
107         mMethodMap = mService.mMethodMap;
108         mSettings = mService.mSettings;
109         mPackageManagerInternal = mService.mPackageManagerInternal;
110         mIWindowManager = mService.mIWindowManager;
111         mWindowManagerInternal = mService.mWindowManagerInternal;
112         mRes = mService.mRes;
113     }
114 
115     /**
116      * Time that we last initiated a bind to the input method, to determine
117      * if we should try to disconnect and reconnect to it.
118      */
119     @GuardedBy("ImfLock.class")
getLastBindTime()120     long getLastBindTime() {
121         return mLastBindTime;
122     }
123 
124     /**
125      * Set to true if our ServiceConnection is currently actively bound to
126      * a service (whether or not we have gotten its IBinder back yet).
127      */
128     @GuardedBy("ImfLock.class")
hasConnection()129     boolean hasConnection() {
130         return mHasConnection;
131     }
132 
133     /**
134      * Id obtained with {@link InputMethodInfo#getId()} for the input method that we are currently
135      * connected to or in the process of connecting to.
136      *
137      * <p>This can be {@code null} when no input method is connected.</p>
138      *
139      * @see #getSelectedMethodId()
140      */
141     @GuardedBy("ImfLock.class")
142     @Nullable
getCurId()143     String getCurId() {
144         return mCurId;
145     }
146 
147     /**
148      * Id obtained with {@link InputMethodInfo#getId()} for the currently selected input method.
149      * This is to be synchronized with the secure settings keyed with
150      * {@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD}.
151      *
152      * <p>This can be transiently {@code null} when the system is re-initializing input method
153      * settings, e.g., the system locale is just changed.</p>
154      *
155      * <p>Note that {@link #getCurId()} is used to track which IME is being connected to
156      * {@link com.android.server.inputmethod.InputMethodManagerService}.</p>
157      *
158      * @see #getCurId()
159      */
160     @GuardedBy("ImfLock.class")
161     @Nullable
getSelectedMethodId()162     String getSelectedMethodId() {
163         return mSelectedMethodId;
164     }
165 
166     @GuardedBy("ImfLock.class")
setSelectedMethodId(@ullable String selectedMethodId)167     void setSelectedMethodId(@Nullable String selectedMethodId) {
168         mSelectedMethodId = selectedMethodId;
169     }
170 
171     /**
172      * The token we have made for the currently active input method, to
173      * identify it in the future.
174      */
175     @GuardedBy("ImfLock.class")
getCurToken()176     IBinder getCurToken() {
177         return mCurToken;
178     }
179 
180     /**
181      * The Intent used to connect to the current input method.
182      */
183     @GuardedBy("ImfLock.class")
184     @Nullable
getCurIntent()185     Intent getCurIntent() {
186         return mCurIntent;
187     }
188 
189     /**
190      * The current binding sequence number, incremented every time there is
191      * a new bind performed.
192      */
193     @GuardedBy("ImfLock.class")
getSequenceNumber()194     int getSequenceNumber() {
195         return mCurSeq;
196     }
197 
198     /**
199      * Increase the current binding sequence number by one.
200      * Reset to 1 on overflow.
201      */
202     @GuardedBy("ImfLock.class")
advanceSequenceNumber()203     void advanceSequenceNumber() {
204         mCurSeq += 1;
205         if (mCurSeq <= 0) {
206             mCurSeq = 1;
207         }
208     }
209 
210     /**
211      * If non-null, this is the input method service we are currently connected
212      * to.
213      */
214     @GuardedBy("ImfLock.class")
215     @Nullable
getCurMethod()216     IInputMethodInvoker getCurMethod() {
217         return mCurMethod;
218     }
219 
220     /**
221      * If not {@link Process#INVALID_UID}, then the UID of {@link #getCurIntent()}.
222      */
223     @GuardedBy("ImfLock.class")
getCurMethodUid()224     int getCurMethodUid() {
225         return mCurMethodUid;
226     }
227 
228     /**
229      * Indicates whether {@link #mVisibleConnection} is currently in use.
230      */
231     @GuardedBy("ImfLock.class")
isVisibleBound()232     boolean isVisibleBound() {
233         return mVisibleBound;
234     }
235 
236     /**
237      * Returns {@code true} if current IME supports Stylus Handwriting.
238      */
supportsStylusHandwriting()239     boolean supportsStylusHandwriting() {
240         return mSupportsStylusHw;
241     }
242 
243     /**
244      * Used to bring IME service up to visible adjustment while it is being shown.
245      */
246     @GuardedBy("ImfLock.class")
247     private final ServiceConnection mVisibleConnection = new ServiceConnection() {
248         @Override public void onBindingDied(ComponentName name) {
249             synchronized (ImfLock.class) {
250                 mService.invalidateAutofillSessionLocked();
251                 if (mVisibleBound) {
252                     unbindVisibleConnection();
253                 }
254             }
255         }
256 
257         @Override public void onServiceConnected(ComponentName name, IBinder service) {
258         }
259 
260         @Override public void onServiceDisconnected(ComponentName name) {
261             synchronized (ImfLock.class) {
262                 mService.invalidateAutofillSessionLocked();
263             }
264         }
265     };
266 
267     /**
268      * Used to bind the IME while it is not currently being shown.
269      */
270     @GuardedBy("ImfLock.class")
271     private final ServiceConnection mMainConnection = new ServiceConnection() {
272         @Override
273         public void onServiceConnected(ComponentName name, IBinder service) {
274             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.onServiceConnected");
275             synchronized (ImfLock.class) {
276                 if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
277                     mCurMethod = IInputMethodInvoker.create(IInputMethod.Stub.asInterface(service));
278                     updateCurrentMethodUid();
279                     if (mCurToken == null) {
280                         Slog.w(TAG, "Service connected without a token!");
281                         unbindCurrentMethod();
282                         Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
283                         return;
284                     }
285                     if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);
286                     final InputMethodInfo info = mMethodMap.get(mSelectedMethodId);
287                     mSupportsStylusHw = info.supportsStylusHandwriting();
288                     mService.initializeImeLocked(mCurMethod, mCurToken, info.getConfigChanges(),
289                             mSupportsStylusHw);
290                     mService.scheduleNotifyImeUidToAudioService(mCurMethodUid);
291                     mService.reRequestCurrentClientSessionLocked();
292                     mService.performOnCreateInlineSuggestionsRequestLocked();
293                 }
294 
295                 // reset Handwriting event receiver.
296                 // always call this as it handles changes in mSupportsStylusHw. It is a noop
297                 // if unchanged.
298                 mService.scheduleResetStylusHandwriting();
299             }
300             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
301         }
302 
303         @GuardedBy("ImfLock.class")
304         private void updateCurrentMethodUid() {
305             final String curMethodPackage = mCurIntent.getComponent().getPackageName();
306             final int curMethodUid = mPackageManagerInternal.getPackageUid(
307                     curMethodPackage, 0 /* flags */, mSettings.getCurrentUserId());
308             if (curMethodUid < 0) {
309                 Slog.e(TAG, "Failed to get UID for package=" + curMethodPackage);
310                 mCurMethodUid = Process.INVALID_UID;
311             } else {
312                 mCurMethodUid = curMethodUid;
313             }
314         }
315 
316         @Override
317         public void onServiceDisconnected(@NonNull ComponentName name) {
318             // Note that mContext.unbindService(this) does not trigger this.  Hence if we are
319             // here the
320             // disconnection is not intended by IMMS (e.g. triggered because the current IMS
321             // crashed),
322             // which is irregular but can eventually happen for everyone just by continuing
323             // using the
324             // device.  Thus it is important to make sure that all the internal states are
325             // properly
326             // refreshed when this method is called back.  Running
327             //    adb install -r <APK that implements the current IME>
328             // would be a good way to trigger such a situation.
329             synchronized (ImfLock.class) {
330                 if (DEBUG) {
331                     Slog.v(TAG, "Service disconnected: " + name + " mCurIntent=" + mCurIntent);
332                 }
333                 if (mCurMethod != null && mCurIntent != null
334                         && name.equals(mCurIntent.getComponent())) {
335                     // We consider this to be a new bind attempt, since the system
336                     // should now try to restart the service for us.
337                     mLastBindTime = SystemClock.uptimeMillis();
338                     clearCurMethodAndSessions();
339                     mService.clearInputShowRequestLocked();
340                     mService.unbindCurrentClientLocked(UnbindReason.DISCONNECT_IME);
341                 }
342             }
343         }
344     };
345 
346     @GuardedBy("ImfLock.class")
unbindCurrentMethod()347     void unbindCurrentMethod() {
348         if (mVisibleBound) {
349             unbindVisibleConnection();
350         }
351 
352         if (mHasConnection) {
353             unbindMainConnection();
354         }
355 
356         if (mCurToken != null) {
357             removeCurrentToken();
358             mService.resetSystemUiLocked();
359         }
360 
361         mCurId = null;
362         clearCurMethodAndSessions();
363     }
364 
365     @GuardedBy("ImfLock.class")
clearCurMethodAndSessions()366     private void clearCurMethodAndSessions() {
367         mService.clearClientSessionsLocked();
368         mCurMethod = null;
369         mCurMethodUid = Process.INVALID_UID;
370     }
371 
372     @GuardedBy("ImfLock.class")
removeCurrentToken()373     private void removeCurrentToken() {
374         int curTokenDisplayId = mService.getCurTokenDisplayIdLocked();
375 
376         if (DEBUG) {
377             Slog.v(TAG,
378                     "Removing window token: " + mCurToken + " for display: " + curTokenDisplayId);
379         }
380         mWindowManagerInternal.removeWindowToken(mCurToken, false /* removeWindows */,
381                 false /* animateExit */, curTokenDisplayId);
382         mCurToken = null;
383     }
384 
385     @GuardedBy("ImfLock.class")
386     @NonNull
bindCurrentMethod()387     InputBindResult bindCurrentMethod() {
388         if (mSelectedMethodId == null) {
389             Slog.e(TAG, "mSelectedMethodId is null!");
390             return InputBindResult.NO_IME;
391         }
392 
393         InputMethodInfo info = mMethodMap.get(mSelectedMethodId);
394         if (info == null) {
395             throw new IllegalArgumentException("Unknown id: " + mSelectedMethodId);
396         }
397 
398         mCurIntent = createImeBindingIntent(info.getComponent());
399 
400         if (bindCurrentInputMethodServiceMainConnection()) {
401             mCurId = info.getId();
402             mLastBindTime = SystemClock.uptimeMillis();
403 
404             addFreshWindowToken();
405             return new InputBindResult(
406                     InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
407                     null, null, null, mCurId, mCurSeq, null, false);
408         }
409 
410         Slog.w(InputMethodManagerService.TAG,
411                 "Failure connecting to input method service: " + mCurIntent);
412         mCurIntent = null;
413         return InputBindResult.IME_NOT_CONNECTED;
414     }
415 
416     @NonNull
createImeBindingIntent(ComponentName component)417     private Intent createImeBindingIntent(ComponentName component) {
418         Intent intent = new Intent(InputMethod.SERVICE_INTERFACE);
419         intent.setComponent(component);
420         intent.putExtra(Intent.EXTRA_CLIENT_LABEL,
421                 com.android.internal.R.string.input_method_binding_label);
422         intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
423                 mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS),
424                 PendingIntent.FLAG_IMMUTABLE));
425         return intent;
426     }
427 
428     @GuardedBy("ImfLock.class")
addFreshWindowToken()429     private void addFreshWindowToken() {
430         int displayIdToShowIme = mService.getDisplayIdToShowImeLocked();
431         mCurToken = new Binder();
432 
433         mService.setCurTokenDisplayIdLocked(displayIdToShowIme);
434 
435         try {
436             if (DEBUG) {
437                 Slog.v(TAG, "Adding window token: " + mCurToken + " for display: "
438                         + displayIdToShowIme);
439             }
440             mIWindowManager.addWindowToken(mCurToken, WindowManager.LayoutParams.TYPE_INPUT_METHOD,
441                     displayIdToShowIme, null /* options */);
442         } catch (RemoteException e) {
443             Slog.e(TAG, "Could not add window token " + mCurToken + " for display "
444                     + displayIdToShowIme, e);
445         }
446     }
447 
448     @GuardedBy("ImfLock.class")
unbindMainConnection()449     private void unbindMainConnection() {
450         mContext.unbindService(mMainConnection);
451         mHasConnection = false;
452     }
453 
454     @GuardedBy("ImfLock.class")
unbindVisibleConnection()455     void unbindVisibleConnection() {
456         mContext.unbindService(mVisibleConnection);
457         mVisibleBound = false;
458     }
459 
460     @GuardedBy("ImfLock.class")
bindCurrentInputMethodService(ServiceConnection conn, int flags)461     private boolean bindCurrentInputMethodService(ServiceConnection conn, int flags) {
462         if (mCurIntent == null || conn == null) {
463             Slog.e(TAG, "--- bind failed: service = " + mCurIntent + ", conn = " + conn);
464             return false;
465         }
466         return mContext.bindServiceAsUser(mCurIntent, conn, flags,
467                 new UserHandle(mSettings.getCurrentUserId()));
468     }
469 
470     @GuardedBy("ImfLock.class")
bindCurrentInputMethodServiceVisibleConnection()471     private boolean bindCurrentInputMethodServiceVisibleConnection() {
472         mVisibleBound = bindCurrentInputMethodService(mVisibleConnection,
473                 IME_VISIBLE_BIND_FLAGS);
474         return mVisibleBound;
475     }
476 
477     @GuardedBy("ImfLock.class")
bindCurrentInputMethodServiceMainConnection()478     private boolean bindCurrentInputMethodServiceMainConnection() {
479         mHasConnection = bindCurrentInputMethodService(mMainConnection, IME_CONNECTION_BIND_FLAGS);
480         return mHasConnection;
481     }
482 
483     /**
484      * Bind the IME so that it can be shown.
485      *
486      * <p>
487      * Performs a rebind if no binding is achieved in {@link #TIME_TO_RECONNECT} milliseconds.
488      */
489     @GuardedBy("ImfLock.class")
setCurrentMethodVisible()490     void setCurrentMethodVisible() {
491         if (mCurMethod != null) {
492             if (DEBUG) Slog.d(TAG, "setCurrentMethodVisible: mCurToken=" + mCurToken);
493             if (mHasConnection && !mVisibleBound) {
494                 bindCurrentInputMethodServiceVisibleConnection();
495             }
496             return;
497         }
498 
499         // No IME is currently connected. Reestablish the main connection.
500         if (!mHasConnection) {
501             if (DEBUG) {
502                 Slog.d(TAG, "Cannot show input: no IME bound. Rebinding.");
503             }
504             bindCurrentMethod();
505             return;
506         }
507 
508         long bindingDuration = SystemClock.uptimeMillis() - mLastBindTime;
509         if (bindingDuration >= TIME_TO_RECONNECT) {
510             // The client has asked to have the input method shown, but
511             // we have been sitting here too long with a connection to the
512             // service and no interface received, so let's disconnect/connect
513             // to try to prod things along.
514             EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME, getSelectedMethodId(),
515                     bindingDuration, 1);
516             Slog.w(TAG, "Force disconnect/connect to the IME in setCurrentMethodVisible()");
517             unbindMainConnection();
518             bindCurrentInputMethodServiceMainConnection();
519         } else {
520             if (DEBUG) {
521                 Slog.d(TAG, "Can't show input: connection = " + mHasConnection + ", time = "
522                         + (TIME_TO_RECONNECT - bindingDuration));
523             }
524         }
525     }
526 
527     /**
528      * Remove the binding needed for the IME to be shown.
529      */
530     @GuardedBy("ImfLock.class")
setCurrentMethodNotVisible()531     void setCurrentMethodNotVisible() {
532         if (mVisibleBound) {
533             unbindVisibleConnection();
534         }
535     }
536 }
537