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