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