• 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 android.inputmethodservice;
18 
19 import static android.inputmethodservice.InputMethodService.DEBUG;
20 
21 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
22 
23 import android.annotation.BinderThread;
24 import android.annotation.MainThread;
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.IBinder;
30 import android.os.RemoteException;
31 import android.util.Log;
32 import android.view.autofill.AutofillId;
33 import android.view.inputmethod.InlineSuggestionsRequest;
34 import android.view.inputmethod.InlineSuggestionsResponse;
35 
36 import com.android.internal.view.IInlineSuggestionsRequestCallback;
37 import com.android.internal.view.IInlineSuggestionsResponseCallback;
38 import com.android.internal.view.InlineSuggestionsRequestInfo;
39 
40 import java.lang.ref.WeakReference;
41 import java.util.Collections;
42 import java.util.function.Consumer;
43 import java.util.function.Function;
44 import java.util.function.Supplier;
45 
46 /**
47  * Maintains an inline suggestion session with the autofill manager service.
48  *
49  * <p> Each session correspond to one request from the Autofill manager service to create an
50  * {@link InlineSuggestionsRequest}. It's responsible for calling back to the Autofill manager
51  * service with {@link InlineSuggestionsRequest} and receiving {@link InlineSuggestionsResponse}
52  * from it.
53  * <p>
54  * TODO(b/151123764): currently the session may receive responses for different views on the same
55  * screen, but we will fix it so each session corresponds to one view.
56  *
57  * <p> All the methods are expected to be called from the main thread, to ensure thread safety.
58  */
59 class InlineSuggestionSession {
60     private static final String TAG = "ImsInlineSuggestionSession";
61 
62     static final InlineSuggestionsResponse EMPTY_RESPONSE = new InlineSuggestionsResponse(
63             Collections.emptyList());
64 
65     @NonNull
66     private final Handler mMainThreadHandler;
67     @NonNull
68     private final InlineSuggestionSessionController mInlineSuggestionSessionController;
69     @NonNull
70     private final InlineSuggestionsRequestInfo mRequestInfo;
71     @NonNull
72     private final IInlineSuggestionsRequestCallback mCallback;
73     @NonNull
74     private final Function<Bundle, InlineSuggestionsRequest> mRequestSupplier;
75     @NonNull
76     private final Supplier<IBinder> mHostInputTokenSupplier;
77     @NonNull
78     private final Consumer<InlineSuggestionsResponse> mResponseConsumer;
79     // Indicate whether the previous call to the mResponseConsumer is empty or not. If it hasn't
80     // been called yet, the value would be null.
81     @Nullable
82     private Boolean mPreviousResponseIsEmpty;
83 
84 
85     /**
86      * Indicates whether {@link #makeInlineSuggestionRequestUncheck()} has been called or not,
87      * because it should only be called at most once.
88      */
89     @Nullable
90     private boolean mCallbackInvoked = false;
91     @Nullable
92     private InlineSuggestionsResponseCallbackImpl mResponseCallback;
93 
InlineSuggestionSession(@onNull InlineSuggestionsRequestInfo requestInfo, @NonNull IInlineSuggestionsRequestCallback callback, @NonNull Function<Bundle, InlineSuggestionsRequest> requestSupplier, @NonNull Supplier<IBinder> hostInputTokenSupplier, @NonNull Consumer<InlineSuggestionsResponse> responseConsumer, @NonNull InlineSuggestionSessionController inlineSuggestionSessionController, @NonNull Handler mainThreadHandler)94     InlineSuggestionSession(@NonNull InlineSuggestionsRequestInfo requestInfo,
95             @NonNull IInlineSuggestionsRequestCallback callback,
96             @NonNull Function<Bundle, InlineSuggestionsRequest> requestSupplier,
97             @NonNull Supplier<IBinder> hostInputTokenSupplier,
98             @NonNull Consumer<InlineSuggestionsResponse> responseConsumer,
99             @NonNull InlineSuggestionSessionController inlineSuggestionSessionController,
100             @NonNull Handler mainThreadHandler) {
101         mRequestInfo = requestInfo;
102         mCallback = callback;
103         mRequestSupplier = requestSupplier;
104         mHostInputTokenSupplier = hostInputTokenSupplier;
105         mResponseConsumer = responseConsumer;
106         mInlineSuggestionSessionController = inlineSuggestionSessionController;
107         mMainThreadHandler = mainThreadHandler;
108     }
109 
110     @MainThread
getRequestInfo()111     InlineSuggestionsRequestInfo getRequestInfo() {
112         return mRequestInfo;
113     }
114 
115     @MainThread
getRequestCallback()116     IInlineSuggestionsRequestCallback getRequestCallback() {
117         return mCallback;
118     }
119 
120     /**
121      * Returns true if the session should send Ime status updates to Autofill.
122      *
123      * <p> The session only starts to send Ime status updates to Autofill after the sending back
124      * an {@link InlineSuggestionsRequest}.
125      */
126     @MainThread
shouldSendImeStatus()127     boolean shouldSendImeStatus() {
128         return mResponseCallback != null;
129     }
130 
131     /**
132      * Returns true if {@link #makeInlineSuggestionRequestUncheck()} is called. It doesn't not
133      * necessarily mean an {@link InlineSuggestionsRequest} was sent, because it may call {@link
134      * IInlineSuggestionsRequestCallback#onInlineSuggestionsUnsupported()}.
135      *
136      * <p> The callback should be invoked at most once for each session.
137      */
138     @MainThread
isCallbackInvoked()139     boolean isCallbackInvoked() {
140         return mCallbackInvoked;
141     }
142 
143     /**
144      * Invalidates the current session so it doesn't process any further {@link
145      * InlineSuggestionsResponse} from Autofill.
146      *
147      * <p> This method should be called when the session is de-referenced from the {@link
148      * InlineSuggestionSessionController}.
149      */
150     @MainThread
invalidate()151     void invalidate() {
152         try {
153             mCallback.onInlineSuggestionsSessionInvalidated();
154         } catch (RemoteException e) {
155             Log.w(TAG, "onInlineSuggestionsSessionInvalidated() remote exception", e);
156         }
157         if (mResponseCallback != null) {
158             consumeInlineSuggestionsResponse(EMPTY_RESPONSE);
159             mResponseCallback.invalidate();
160             mResponseCallback = null;
161         }
162     }
163 
164     /**
165      * Gets the {@link InlineSuggestionsRequest} from IME and send it back to the Autofill if it's
166      * not null.
167      *
168      * <p>Calling this method implies that the input is started on the view corresponding to the
169      * session.
170      */
171     @MainThread
makeInlineSuggestionRequestUncheck()172     void makeInlineSuggestionRequestUncheck() {
173         if (mCallbackInvoked) {
174             return;
175         }
176         try {
177             final InlineSuggestionsRequest request = mRequestSupplier.apply(
178                     mRequestInfo.getUiExtras());
179             if (request == null) {
180                 if (DEBUG) {
181                     Log.d(TAG, "onCreateInlineSuggestionsRequest() returned null request");
182                 }
183                 mCallback.onInlineSuggestionsUnsupported();
184             } else {
185                 request.setHostInputToken(mHostInputTokenSupplier.get());
186                 request.filterContentTypes();
187                 mResponseCallback = new InlineSuggestionsResponseCallbackImpl(this);
188                 mCallback.onInlineSuggestionsRequest(request, mResponseCallback);
189             }
190         } catch (RemoteException e) {
191             Log.w(TAG, "makeInlinedSuggestionsRequest() remote exception:" + e);
192         }
193         mCallbackInvoked = true;
194     }
195 
196     @MainThread
handleOnInlineSuggestionsResponse(@onNull AutofillId fieldId, @NonNull InlineSuggestionsResponse response)197     void handleOnInlineSuggestionsResponse(@NonNull AutofillId fieldId,
198             @NonNull InlineSuggestionsResponse response) {
199         if (!mInlineSuggestionSessionController.match(fieldId)) {
200             return;
201         }
202         if (DEBUG) {
203             Log.d(TAG, "IME receives response: " + response.getInlineSuggestions().size());
204         }
205         consumeInlineSuggestionsResponse(response);
206     }
207 
208     @MainThread
consumeInlineSuggestionsResponse(@onNull InlineSuggestionsResponse response)209     void consumeInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response) {
210         boolean isResponseEmpty = response.getInlineSuggestions().isEmpty();
211         if (isResponseEmpty && Boolean.TRUE.equals(mPreviousResponseIsEmpty)) {
212             // No-op if both the previous response and current response are empty.
213             return;
214         }
215         mPreviousResponseIsEmpty = isResponseEmpty;
216         mResponseConsumer.accept(response);
217     }
218 
219     /**
220      * Internal implementation of {@link IInlineSuggestionsResponseCallback}.
221      */
222     private static final class InlineSuggestionsResponseCallbackImpl extends
223             IInlineSuggestionsResponseCallback.Stub {
224         private final WeakReference<InlineSuggestionSession> mSession;
225         private volatile boolean mInvalid = false;
226 
InlineSuggestionsResponseCallbackImpl(InlineSuggestionSession session)227         private InlineSuggestionsResponseCallbackImpl(InlineSuggestionSession session) {
228             mSession = new WeakReference<>(session);
229         }
230 
invalidate()231         void invalidate() {
232             mInvalid = true;
233         }
234 
235         @BinderThread
236         @Override
onInlineSuggestionsResponse(AutofillId fieldId, InlineSuggestionsResponse response)237         public void onInlineSuggestionsResponse(AutofillId fieldId,
238                 InlineSuggestionsResponse response) {
239             if (mInvalid) {
240                 return;
241             }
242             final InlineSuggestionSession session = mSession.get();
243             if (session != null) {
244                 session.mMainThreadHandler.sendMessage(
245                         obtainMessage(InlineSuggestionSession::handleOnInlineSuggestionsResponse,
246                                 session, fieldId, response));
247             }
248         }
249     }
250 }
251