• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.autofill;
18 
19 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
20 import static com.android.server.autofill.Helper.sDebug;
21 import static com.android.server.autofill.Helper.sVerbose;
22 
23 import android.annotation.BinderThread;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.content.ComponentName;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.RemoteException;
30 import android.util.Slog;
31 import android.view.autofill.AutofillId;
32 import android.view.inputmethod.InlineSuggestion;
33 import android.view.inputmethod.InlineSuggestionsRequest;
34 import android.view.inputmethod.InlineSuggestionsResponse;
35 
36 import com.android.internal.annotations.GuardedBy;
37 import com.android.internal.view.IInlineSuggestionsRequestCallback;
38 import com.android.internal.view.IInlineSuggestionsResponseCallback;
39 import com.android.internal.view.InlineSuggestionsRequestInfo;
40 import com.android.server.autofill.ui.InlineFillUi;
41 import com.android.server.inputmethod.InputMethodManagerInternal;
42 
43 import java.lang.ref.WeakReference;
44 import java.util.List;
45 import java.util.Optional;
46 import java.util.function.Consumer;
47 
48 /**
49  * Maintains an inline suggestion session with the IME.
50  *
51  * <p> Each session corresponds to one request from the Autofill manager service to create an
52  * {@link InlineSuggestionsRequest}. It's responsible for receiving callbacks from the IME and
53  * sending {@link android.view.inputmethod.InlineSuggestionsResponse} to IME.
54  */
55 final class AutofillInlineSuggestionsRequestSession {
56 
57     private static final String TAG = AutofillInlineSuggestionsRequestSession.class.getSimpleName();
58 
59     @NonNull
60     private final InputMethodManagerInternal mInputMethodManagerInternal;
61     private final int mUserId;
62     @NonNull
63     private final ComponentName mComponentName;
64     @NonNull
65     private final Object mLock;
66     @NonNull
67     private final Handler mHandler;
68     @NonNull
69     private final Bundle mUiExtras;
70     @NonNull
71     private final InlineFillUi.InlineUiEventCallback mUiCallback;
72 
73     @GuardedBy("mLock")
74     @NonNull
75     private AutofillId mAutofillId;
76     @GuardedBy("mLock")
77     @Nullable
78     private Consumer<InlineSuggestionsRequest> mImeRequestConsumer;
79 
80     @GuardedBy("mLock")
81     private boolean mImeRequestReceived;
82     @GuardedBy("mLock")
83     @Nullable
84     private InlineSuggestionsRequest mImeRequest;
85     @GuardedBy("mLock")
86     @Nullable
87     private IInlineSuggestionsResponseCallback mResponseCallback;
88 
89     @GuardedBy("mLock")
90     @Nullable
91     private AutofillId mImeCurrentFieldId;
92     @GuardedBy("mLock")
93     private boolean mImeInputStarted;
94     @GuardedBy("mLock")
95     private boolean mImeInputViewStarted;
96     @GuardedBy("mLock")
97     @Nullable
98     private InlineFillUi mInlineFillUi;
99     @GuardedBy("mLock")
100     private Boolean mPreviousResponseIsNotEmpty = null;
101 
102     @GuardedBy("mLock")
103     private boolean mDestroyed = false;
104     @GuardedBy("mLock")
105     private boolean mPreviousHasNonPinSuggestionShow;
106     @GuardedBy("mLock")
107     private boolean mImeSessionInvalidated = false;
108 
AutofillInlineSuggestionsRequestSession( @onNull InputMethodManagerInternal inputMethodManagerInternal, int userId, @NonNull ComponentName componentName, @NonNull Handler handler, @NonNull Object lock, @NonNull AutofillId autofillId, @NonNull Consumer<InlineSuggestionsRequest> requestConsumer, @NonNull Bundle uiExtras, @NonNull InlineFillUi.InlineUiEventCallback callback)109     AutofillInlineSuggestionsRequestSession(
110             @NonNull InputMethodManagerInternal inputMethodManagerInternal, int userId,
111             @NonNull ComponentName componentName, @NonNull Handler handler, @NonNull Object lock,
112             @NonNull AutofillId autofillId,
113             @NonNull Consumer<InlineSuggestionsRequest> requestConsumer, @NonNull Bundle uiExtras,
114             @NonNull InlineFillUi.InlineUiEventCallback callback) {
115         mInputMethodManagerInternal = inputMethodManagerInternal;
116         mUserId = userId;
117         mComponentName = componentName;
118         mHandler = handler;
119         mLock = lock;
120         mUiExtras = uiExtras;
121         mUiCallback = callback;
122 
123         mAutofillId = autofillId;
124         mImeRequestConsumer = requestConsumer;
125     }
126 
127     @GuardedBy("mLock")
128     @NonNull
getAutofillIdLocked()129     AutofillId getAutofillIdLocked() {
130         return mAutofillId;
131     }
132 
133     /**
134      * Returns the {@link InlineSuggestionsRequest} provided by IME.
135      *
136      * <p> The caller is responsible for making sure Autofill hears back from IME before calling
137      * this method, using the {@link #mImeRequestConsumer}.
138      */
139     @GuardedBy("mLock")
getInlineSuggestionsRequestLocked()140     Optional<InlineSuggestionsRequest> getInlineSuggestionsRequestLocked() {
141         if (mDestroyed) {
142             return Optional.empty();
143         }
144         return Optional.ofNullable(mImeRequest);
145     }
146 
147     /**
148      * Requests showing the inline suggestion in the IME when the IME becomes visible and is focused
149      * on the {@code autofillId}.
150      *
151      * @return false if the IME callback is not available.
152      */
153     @GuardedBy("mLock")
onInlineSuggestionsResponseLocked(@onNull InlineFillUi inlineFillUi)154     boolean onInlineSuggestionsResponseLocked(@NonNull InlineFillUi inlineFillUi) {
155         if (mDestroyed) {
156             return false;
157         }
158         if (sDebug) {
159             Slog.d(TAG,
160                     "onInlineSuggestionsResponseLocked called for:" + inlineFillUi.getAutofillId());
161         }
162         if (mImeRequest == null || mResponseCallback == null || mImeSessionInvalidated) {
163             return false;
164         }
165         // TODO(b/151123764): each session should only correspond to one field.
166         mAutofillId = inlineFillUi.getAutofillId();
167         mInlineFillUi = inlineFillUi;
168         maybeUpdateResponseToImeLocked();
169         return true;
170     }
171 
172     /**
173      * Prevents further interaction with the IME. Must be called before starting a new request
174      * session to avoid unwanted behavior from two overlapping requests.
175      */
176     @GuardedBy("mLock")
destroySessionLocked()177     void destroySessionLocked() {
178         mDestroyed = true;
179 
180         if (!mImeRequestReceived) {
181             Slog.w(TAG,
182                     "Never received an InlineSuggestionsRequest from the IME for " + mAutofillId);
183         }
184     }
185 
186     /**
187      * Requests the IME to create an {@link InlineSuggestionsRequest}.
188      *
189      * <p> This method should only be called once per session.
190      */
191     @GuardedBy("mLock")
onCreateInlineSuggestionsRequestLocked()192     void onCreateInlineSuggestionsRequestLocked() {
193         if (mDestroyed) {
194             return;
195         }
196         mImeSessionInvalidated = false;
197         if (sDebug) Slog.d(TAG, "onCreateInlineSuggestionsRequestLocked called: " + mAutofillId);
198         mInputMethodManagerInternal.onCreateInlineSuggestionsRequest(mUserId,
199                 new InlineSuggestionsRequestInfo(mComponentName, mAutofillId, mUiExtras),
200                 new InlineSuggestionsRequestCallbackImpl(this));
201     }
202 
203     /**
204      * Clear the locally cached inline fill UI, but don't clear the suggestion in IME.
205      *
206      * See also {@link AutofillInlineSessionController#resetInlineFillUiLocked()}
207      */
208     @GuardedBy("mLock")
resetInlineFillUiLocked()209     void resetInlineFillUiLocked() {
210         mInlineFillUi = null;
211     }
212 
213     /**
214      * Optionally sends inline response to the IME, depending on the current state.
215      */
216     @GuardedBy("mLock")
maybeUpdateResponseToImeLocked()217     private void maybeUpdateResponseToImeLocked() {
218         if (sVerbose) Slog.v(TAG, "maybeUpdateResponseToImeLocked called");
219         if (mDestroyed || mResponseCallback == null) {
220             return;
221         }
222         if (mImeInputViewStarted && mInlineFillUi != null && match(mAutofillId,
223                 mImeCurrentFieldId)) {
224             // if IME is visible, and response is not null, send the response
225             InlineSuggestionsResponse response = mInlineFillUi.getInlineSuggestionsResponse();
226             boolean isEmptyResponse = response.getInlineSuggestions().isEmpty();
227             if (isEmptyResponse && Boolean.FALSE.equals(mPreviousResponseIsNotEmpty)) {
228                 // No-op if both the previous response and current response are empty.
229                 return;
230             }
231             maybeNotifyFillUiEventLocked(response.getInlineSuggestions());
232             updateResponseToImeUncheckLocked(response);
233             mPreviousResponseIsNotEmpty = !isEmptyResponse;
234         }
235     }
236 
237     /**
238      * Sends the {@code response} to the IME, assuming all the relevant checks are already done.
239      */
240     @GuardedBy("mLock")
updateResponseToImeUncheckLocked(InlineSuggestionsResponse response)241     private void updateResponseToImeUncheckLocked(InlineSuggestionsResponse response) {
242         if (mDestroyed) {
243             return;
244         }
245         if (sDebug) Slog.d(TAG, "Send inline response: " + response.getInlineSuggestions().size());
246         try {
247             mResponseCallback.onInlineSuggestionsResponse(mAutofillId, response);
248         } catch (RemoteException e) {
249             Slog.e(TAG, "RemoteException sending InlineSuggestionsResponse to IME");
250         }
251     }
252 
253     @GuardedBy("mLock")
maybeNotifyFillUiEventLocked(@onNull List<InlineSuggestion> suggestions)254     private void maybeNotifyFillUiEventLocked(@NonNull List<InlineSuggestion> suggestions) {
255         if (mDestroyed) {
256             return;
257         }
258         boolean hasSuggestionToShow = false;
259         for (int i = 0; i < suggestions.size(); i++) {
260             InlineSuggestion suggestion = suggestions.get(i);
261             // It is possible we don't have any match result but we still have pinned
262             // suggestions. Only notify we have non-pinned suggestions to show
263             if (!suggestion.getInfo().isPinned()) {
264                 hasSuggestionToShow = true;
265                 break;
266             }
267         }
268         if (sDebug) {
269             Slog.d(TAG, "maybeNotifyFillUiEventLoked(): hasSuggestionToShow=" + hasSuggestionToShow
270                     + ", mPreviousHasNonPinSuggestionShow=" + mPreviousHasNonPinSuggestionShow);
271         }
272         // Use mPreviousHasNonPinSuggestionShow to save previous status, if the display status
273         // change, we can notify the event.
274         if (hasSuggestionToShow && !mPreviousHasNonPinSuggestionShow) {
275             // From no suggestion to has suggestions to show
276             mUiCallback.notifyInlineUiShown(mAutofillId);
277         } else if (!hasSuggestionToShow && mPreviousHasNonPinSuggestionShow) {
278             // From has suggestions to no suggestions to show
279             mUiCallback.notifyInlineUiHidden(mAutofillId);
280         }
281         // Update the latest status
282         mPreviousHasNonPinSuggestionShow = hasSuggestionToShow;
283     }
284 
285     /**
286      * Handles the {@code request} and {@code callback} received from the IME.
287      *
288      * <p> Should only invoked in the {@link #mHandler} thread.
289      */
handleOnReceiveImeRequest(@ullable InlineSuggestionsRequest request, @Nullable IInlineSuggestionsResponseCallback callback)290     private void handleOnReceiveImeRequest(@Nullable InlineSuggestionsRequest request,
291             @Nullable IInlineSuggestionsResponseCallback callback) {
292         synchronized (mLock) {
293             if (mDestroyed || mImeRequestReceived) {
294                 return;
295             }
296             mImeRequestReceived = true;
297             mImeSessionInvalidated = false;
298 
299             if (request != null && callback != null) {
300                 mImeRequest = request;
301                 mResponseCallback = callback;
302                 handleOnReceiveImeStatusUpdated(mAutofillId, true, false);
303             }
304             if (mImeRequestConsumer != null) {
305                 // Note that mImeRequest is only set if both request and callback are non-null.
306                 mImeRequestConsumer.accept(mImeRequest);
307                 mImeRequestConsumer = null;
308             }
309         }
310     }
311 
312     /**
313      * Handles the IME status updates received from the IME.
314      *
315      * <p> Should only be invoked in the {@link #mHandler} thread.
316      */
handleOnReceiveImeStatusUpdated(boolean imeInputStarted, boolean imeInputViewStarted)317     private void handleOnReceiveImeStatusUpdated(boolean imeInputStarted,
318             boolean imeInputViewStarted) {
319         synchronized (mLock) {
320             if (mDestroyed) {
321                 return;
322             }
323             if (mImeCurrentFieldId != null) {
324                 boolean imeInputStartedChanged = (mImeInputStarted != imeInputStarted);
325                 boolean imeInputViewStartedChanged = (mImeInputViewStarted != imeInputViewStarted);
326                 mImeInputStarted = imeInputStarted;
327                 mImeInputViewStarted = imeInputViewStarted;
328                 if (imeInputStartedChanged || imeInputViewStartedChanged) {
329                     maybeUpdateResponseToImeLocked();
330                 }
331             }
332         }
333     }
334 
335     /**
336      * Handles the IME status updates received from the IME.
337      *
338      * <p> Should only be invoked in the {@link #mHandler} thread.
339      */
handleOnReceiveImeStatusUpdated(@ullable AutofillId imeFieldId, boolean imeInputStarted, boolean imeInputViewStarted)340     private void handleOnReceiveImeStatusUpdated(@Nullable AutofillId imeFieldId,
341             boolean imeInputStarted, boolean imeInputViewStarted) {
342         synchronized (mLock) {
343             if (mDestroyed) {
344                 return;
345             }
346             if (imeFieldId != null) {
347                 mImeCurrentFieldId = imeFieldId;
348             }
349             handleOnReceiveImeStatusUpdated(imeInputStarted, imeInputViewStarted);
350         }
351     }
352 
353     /**
354      * Handles the IME session status received from the IME.
355      *
356      * <p> Should only be invoked in the {@link #mHandler} thread.
357      */
handleOnReceiveImeSessionInvalidated()358     private void handleOnReceiveImeSessionInvalidated() {
359         synchronized (mLock) {
360             if (mDestroyed) {
361                 return;
362             }
363             mImeSessionInvalidated = true;
364         }
365     }
366 
367     /**
368      * Internal implementation of {@link IInlineSuggestionsRequestCallback}.
369      */
370     private static final class InlineSuggestionsRequestCallbackImpl extends
371             IInlineSuggestionsRequestCallback.Stub {
372 
373         private final WeakReference<AutofillInlineSuggestionsRequestSession> mSession;
374 
InlineSuggestionsRequestCallbackImpl( AutofillInlineSuggestionsRequestSession session)375         private InlineSuggestionsRequestCallbackImpl(
376                 AutofillInlineSuggestionsRequestSession session) {
377             mSession = new WeakReference<>(session);
378         }
379 
380         @BinderThread
381         @Override
onInlineSuggestionsUnsupported()382         public void onInlineSuggestionsUnsupported() throws RemoteException {
383             if (sDebug) Slog.d(TAG, "onInlineSuggestionsUnsupported() called.");
384             final AutofillInlineSuggestionsRequestSession session = mSession.get();
385             if (session != null) {
386                 session.mHandler.sendMessage(obtainMessage(
387                         AutofillInlineSuggestionsRequestSession::handleOnReceiveImeRequest, session,
388                         null, null));
389             }
390         }
391 
392         @BinderThread
393         @Override
onInlineSuggestionsRequest(InlineSuggestionsRequest request, IInlineSuggestionsResponseCallback callback)394         public void onInlineSuggestionsRequest(InlineSuggestionsRequest request,
395                 IInlineSuggestionsResponseCallback callback) {
396             if (sDebug) Slog.d(TAG, "onInlineSuggestionsRequest() received: " + request);
397             final AutofillInlineSuggestionsRequestSession session = mSession.get();
398             if (session != null) {
399                 session.mHandler.sendMessage(obtainMessage(
400                         AutofillInlineSuggestionsRequestSession::handleOnReceiveImeRequest, session,
401                         request, callback));
402             }
403         }
404 
405         @Override
onInputMethodStartInput(AutofillId imeFieldId)406         public void onInputMethodStartInput(AutofillId imeFieldId) throws RemoteException {
407             if (sVerbose) Slog.v(TAG, "onInputMethodStartInput() received on " + imeFieldId);
408             final AutofillInlineSuggestionsRequestSession session = mSession.get();
409             if (session != null) {
410                 session.mHandler.sendMessage(obtainMessage(
411                         AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated,
412                         session, imeFieldId, true, false));
413             }
414         }
415 
416         @Override
onInputMethodShowInputRequested(boolean requestResult)417         public void onInputMethodShowInputRequested(boolean requestResult) throws RemoteException {
418             if (sVerbose) {
419                 Slog.v(TAG, "onInputMethodShowInputRequested() received: " + requestResult);
420             }
421         }
422 
423         @BinderThread
424         @Override
onInputMethodStartInputView()425         public void onInputMethodStartInputView() {
426             if (sVerbose) Slog.v(TAG, "onInputMethodStartInputView() received");
427             final AutofillInlineSuggestionsRequestSession session = mSession.get();
428             if (session != null) {
429                 session.mHandler.sendMessage(obtainMessage(
430                         AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated,
431                         session, true, true));
432             }
433         }
434 
435         @BinderThread
436         @Override
onInputMethodFinishInputView()437         public void onInputMethodFinishInputView() {
438             if (sVerbose) Slog.v(TAG, "onInputMethodFinishInputView() received");
439             final AutofillInlineSuggestionsRequestSession session = mSession.get();
440             if (session != null) {
441                 session.mHandler.sendMessage(obtainMessage(
442                         AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated,
443                         session, true, false));
444             }
445         }
446 
447         @Override
onInputMethodFinishInput()448         public void onInputMethodFinishInput() throws RemoteException {
449             if (sVerbose) Slog.v(TAG, "onInputMethodFinishInput() received");
450             final AutofillInlineSuggestionsRequestSession session = mSession.get();
451             if (session != null) {
452                 session.mHandler.sendMessage(obtainMessage(
453                         AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated,
454                         session, false, false));
455             }
456         }
457 
458         @BinderThread
459         @Override
onInlineSuggestionsSessionInvalidated()460         public void onInlineSuggestionsSessionInvalidated() throws RemoteException {
461             if (sDebug) Slog.d(TAG, "onInlineSuggestionsSessionInvalidated() called.");
462             final AutofillInlineSuggestionsRequestSession session = mSession.get();
463             if (session != null) {
464                 session.mHandler.sendMessage(obtainMessage(
465                         AutofillInlineSuggestionsRequestSession
466                                 ::handleOnReceiveImeSessionInvalidated, session));
467             }
468         }
469     }
470 
match(@ullable AutofillId autofillId, @Nullable AutofillId imeClientFieldId)471     private static boolean match(@Nullable AutofillId autofillId,
472             @Nullable AutofillId imeClientFieldId) {
473         // The IME doesn't have information about the virtual view id for the child views in the
474         // web view, so we are only comparing the parent view id here. This means that for cases
475         // where there are two input fields in the web view, they will have the same view id
476         // (although different virtual child id), and we will not be able to distinguish them.
477         return autofillId != null && imeClientFieldId != null
478                 && autofillId.getViewId() == imeClientFieldId.getViewId();
479     }
480 }
481