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