1 /*
2  * Copyright (C) 2022 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 android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.UserIdInt;
22 import android.os.IBinder;
23 import android.os.RemoteException;
24 import android.util.ArrayMap;
25 import android.util.Slog;
26 import android.view.autofill.AutofillId;
27 import android.view.inputmethod.InlineSuggestionsRequest;
28 import android.view.inputmethod.InputMethodInfo;
29 
30 import com.android.internal.annotations.GuardedBy;
31 import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
32 import com.android.internal.inputmethod.IInlineSuggestionsResponseCallback;
33 import com.android.internal.inputmethod.InlineSuggestionsRequestInfo;
34 
35 /**
36  * A controller managing autofill suggestion requests.
37  */
38 final class AutofillSuggestionsController {
39     private static final boolean DEBUG = false;
40     private static final String TAG = AutofillSuggestionsController.class.getSimpleName();
41 
42     @NonNull private final InputMethodManagerService mService;
43     @NonNull private final ArrayMap<String, InputMethodInfo> mMethodMap;
44     @NonNull private final InputMethodUtils.InputMethodSettings mSettings;
45 
46     private static final class CreateInlineSuggestionsRequest {
47         @NonNull final InlineSuggestionsRequestInfo mRequestInfo;
48         @NonNull final IInlineSuggestionsRequestCallback mCallback;
49         @NonNull final String mPackageName;
50 
CreateInlineSuggestionsRequest( @onNull InlineSuggestionsRequestInfo requestInfo, @NonNull IInlineSuggestionsRequestCallback callback, @NonNull String packageName)51         CreateInlineSuggestionsRequest(
52                 @NonNull InlineSuggestionsRequestInfo requestInfo,
53                 @NonNull IInlineSuggestionsRequestCallback callback,
54                 @NonNull String packageName) {
55             mRequestInfo = requestInfo;
56             mCallback = callback;
57             mPackageName = packageName;
58         }
59     }
60 
61     /**
62      * If a request to create inline autofill suggestions comes in while the IME is unbound
63      * due to {@link InputMethodManagerService#mPreventImeStartupUnlessTextEditor},
64      * this is where it is stored, so that it may be fulfilled once the IME rebinds.
65      */
66     @GuardedBy("ImfLock.class")
67     @Nullable
68     private CreateInlineSuggestionsRequest mPendingInlineSuggestionsRequest;
69 
70     /**
71      * A callback into the autofill service obtained from the latest call to
72      * {@link #onCreateInlineSuggestionsRequest}, which can be used to invalidate an
73      * autofill session in case the IME process dies.
74      */
75     @GuardedBy("ImfLock.class")
76     @Nullable
77     private IInlineSuggestionsRequestCallback mInlineSuggestionsRequestCallback;
78 
AutofillSuggestionsController(@onNull InputMethodManagerService service)79     AutofillSuggestionsController(@NonNull InputMethodManagerService service) {
80         mService = service;
81         mMethodMap = mService.mMethodMap;
82         mSettings = mService.mSettings;
83     }
84 
85     @GuardedBy("ImfLock.class")
onCreateInlineSuggestionsRequest(@serIdInt int userId, InlineSuggestionsRequestInfo requestInfo, IInlineSuggestionsRequestCallback callback, boolean touchExplorationEnabled)86     void onCreateInlineSuggestionsRequest(@UserIdInt int userId,
87             InlineSuggestionsRequestInfo requestInfo, IInlineSuggestionsRequestCallback callback,
88             boolean touchExplorationEnabled) {
89         clearPendingInlineSuggestionsRequest();
90         mInlineSuggestionsRequestCallback = callback;
91         final InputMethodInfo imi = mMethodMap.get(mService.getSelectedMethodIdLocked());
92         try {
93             if (userId == mSettings.getCurrentUserId()
94                     && imi != null && isInlineSuggestionsEnabled(imi, touchExplorationEnabled)) {
95                 mPendingInlineSuggestionsRequest = new CreateInlineSuggestionsRequest(
96                         requestInfo, callback, imi.getPackageName());
97                 if (mService.getCurMethodLocked() != null) {
98                     // In the normal case when the IME is connected, we can make the request here.
99                     performOnCreateInlineSuggestionsRequest();
100                 } else {
101                     // Otherwise, the next time the IME connection is established,
102                     // InputMethodBindingController.mMainConnection#onServiceConnected() will call
103                     // into #performOnCreateInlineSuggestionsRequestLocked() to make the request.
104                     if (DEBUG) {
105                         Slog.d(TAG, "IME not connected. Delaying inline suggestions request.");
106                     }
107                 }
108             } else {
109                 callback.onInlineSuggestionsUnsupported();
110             }
111         } catch (RemoteException e) {
112             Slog.w(TAG, "RemoteException calling onCreateInlineSuggestionsRequest(): " + e);
113         }
114     }
115 
116     @GuardedBy("ImfLock.class")
performOnCreateInlineSuggestionsRequest()117     void performOnCreateInlineSuggestionsRequest() {
118         if (mPendingInlineSuggestionsRequest == null) {
119             return;
120         }
121         IInputMethodInvoker curMethod = mService.getCurMethodLocked();
122         if (DEBUG) {
123             Slog.d(TAG, "Performing onCreateInlineSuggestionsRequest. mCurMethod = " + curMethod);
124         }
125         if (curMethod != null) {
126             final IInlineSuggestionsRequestCallback callback =
127                     new InlineSuggestionsRequestCallbackDecorator(
128                             mPendingInlineSuggestionsRequest.mCallback,
129                             mPendingInlineSuggestionsRequest.mPackageName,
130                             mService.getCurTokenDisplayIdLocked(),
131                             mService.getCurTokenLocked(),
132                             mService);
133             curMethod.onCreateInlineSuggestionsRequest(
134                     mPendingInlineSuggestionsRequest.mRequestInfo, callback);
135         } else {
136             Slog.w(TAG, "No IME connected! Abandoning inline suggestions creation request.");
137         }
138         clearPendingInlineSuggestionsRequest();
139     }
140 
141     @GuardedBy("ImfLock.class")
clearPendingInlineSuggestionsRequest()142     private void clearPendingInlineSuggestionsRequest() {
143         mPendingInlineSuggestionsRequest = null;
144     }
145 
isInlineSuggestionsEnabled(InputMethodInfo imi, boolean touchExplorationEnabled)146     private static boolean isInlineSuggestionsEnabled(InputMethodInfo imi,
147             boolean touchExplorationEnabled) {
148         return imi.isInlineSuggestionsEnabled()
149                 && (!touchExplorationEnabled
150                 || imi.supportsInlineSuggestionsWithTouchExploration());
151     }
152 
153     @GuardedBy("ImfLock.class")
invalidateAutofillSession()154     void invalidateAutofillSession() {
155         if (mInlineSuggestionsRequestCallback != null) {
156             try {
157                 mInlineSuggestionsRequestCallback.onInlineSuggestionsSessionInvalidated();
158             } catch (RemoteException e) {
159                 Slog.e(TAG, "Cannot invalidate autofill session.", e);
160             }
161         }
162     }
163 
164     /**
165      * The decorator which validates the host package name in the
166      * {@link InlineSuggestionsRequest} argument to make sure it matches the IME package name.
167      */
168     private static final class InlineSuggestionsRequestCallbackDecorator
169             extends IInlineSuggestionsRequestCallback.Stub {
170         @NonNull private final IInlineSuggestionsRequestCallback mCallback;
171         @NonNull private final String mImePackageName;
172         private final int mImeDisplayId;
173         @NonNull private final IBinder mImeToken;
174         @NonNull private final InputMethodManagerService mImms;
175 
InlineSuggestionsRequestCallbackDecorator( @onNull IInlineSuggestionsRequestCallback callback, @NonNull String imePackageName, int displayId, @NonNull IBinder imeToken, @NonNull InputMethodManagerService imms)176         InlineSuggestionsRequestCallbackDecorator(
177                 @NonNull IInlineSuggestionsRequestCallback callback, @NonNull String imePackageName,
178                 int displayId, @NonNull IBinder imeToken, @NonNull InputMethodManagerService imms) {
179             mCallback = callback;
180             mImePackageName = imePackageName;
181             mImeDisplayId = displayId;
182             mImeToken = imeToken;
183             mImms = imms;
184         }
185 
186         @Override
onInlineSuggestionsUnsupported()187         public void onInlineSuggestionsUnsupported() throws RemoteException {
188             mCallback.onInlineSuggestionsUnsupported();
189         }
190 
191         @Override
onInlineSuggestionsRequest(InlineSuggestionsRequest request, IInlineSuggestionsResponseCallback callback)192         public void onInlineSuggestionsRequest(InlineSuggestionsRequest request,
193                 IInlineSuggestionsResponseCallback callback)
194                 throws RemoteException {
195             if (!mImePackageName.equals(request.getHostPackageName())) {
196                 throw new SecurityException(
197                         "Host package name in the provide request=[" + request.getHostPackageName()
198                                 + "] doesn't match the IME package name=[" + mImePackageName
199                                 + "].");
200             }
201             request.setHostDisplayId(mImeDisplayId);
202             mImms.setCurHostInputToken(mImeToken, request.getHostInputToken());
203             mCallback.onInlineSuggestionsRequest(request, callback);
204         }
205 
206         @Override
onInputMethodStartInput(AutofillId imeFieldId)207         public void onInputMethodStartInput(AutofillId imeFieldId) throws RemoteException {
208             mCallback.onInputMethodStartInput(imeFieldId);
209         }
210 
211         @Override
onInputMethodShowInputRequested(boolean requestResult)212         public void onInputMethodShowInputRequested(boolean requestResult) throws RemoteException {
213             mCallback.onInputMethodShowInputRequested(requestResult);
214         }
215 
216         @Override
onInputMethodStartInputView()217         public void onInputMethodStartInputView() throws RemoteException {
218             mCallback.onInputMethodStartInputView();
219         }
220 
221         @Override
onInputMethodFinishInputView()222         public void onInputMethodFinishInputView() throws RemoteException {
223             mCallback.onInputMethodFinishInputView();
224         }
225 
226         @Override
onInputMethodFinishInput()227         public void onInputMethodFinishInput() throws RemoteException {
228             mCallback.onInputMethodFinishInput();
229         }
230 
231         @Override
onInlineSuggestionsSessionInvalidated()232         public void onInlineSuggestionsSessionInvalidated() throws RemoteException {
233             mCallback.onInlineSuggestionsSessionInvalidated();
234         }
235     }
236 }
237