• 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.ui;
18 
19 import static com.android.server.autofill.Helper.sVerbose;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.UserIdInt;
24 import android.content.IntentSender;
25 import android.service.autofill.Dataset;
26 import android.service.autofill.FillResponse;
27 import android.service.autofill.InlinePresentation;
28 import android.text.TextUtils;
29 import android.util.Pair;
30 import android.util.Slog;
31 import android.util.SparseArray;
32 import android.view.autofill.AutofillId;
33 import android.view.autofill.AutofillValue;
34 import android.view.inputmethod.InlineSuggestion;
35 import android.view.inputmethod.InlineSuggestionInfo;
36 import android.view.inputmethod.InlineSuggestionsRequest;
37 import android.view.inputmethod.InlineSuggestionsResponse;
38 
39 import com.android.internal.view.inline.IInlineContentProvider;
40 import com.android.server.autofill.RemoteInlineSuggestionRenderService;
41 
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.List;
45 import java.util.regex.Pattern;
46 
47 
48 /**
49  * UI for a particular field (i.e. {@link AutofillId}) based on an inline autofill response from
50  * the autofill service or the augmented autofill service. It wraps multiple inline suggestions.
51  *
52  * <p> This class is responsible for filtering the suggestions based on the filtered text.
53  * It'll create {@link InlineSuggestion} instances by reusing the backing remote views (from the
54  * renderer service) if possible.
55  */
56 public final class InlineFillUi {
57 
58     private static final String TAG = "InlineFillUi";
59 
60     /**
61      * The id of the field which the current Ui is for.
62      */
63     @NonNull
64     final AutofillId mAutofillId;
65 
66     /**
67      * The list of inline suggestions, before applying any filtering
68      */
69     @NonNull
70     private final ArrayList<InlineSuggestion> mInlineSuggestions;
71 
72     /**
73      * The corresponding data sets for the inline suggestions. The list may be null if the current
74      * Ui is the authentication UI for the response. If non-null, the size of data sets should equal
75      * that of  inline suggestions.
76      */
77     @Nullable
78     private final ArrayList<Dataset> mDatasets;
79 
80     /**
81      * The filter text which will be applied on the inline suggestion list before they are returned
82      * as a response.
83      */
84     @Nullable
85     private String mFilterText;
86 
87     /**
88      * Whether prefix/regex based filtering is disabled.
89      */
90     private boolean mFilterMatchingDisabled;
91 
92     /**
93      * Returns an empty inline autofill UI.
94      */
95     @NonNull
emptyUi(@onNull AutofillId autofillId)96     public static InlineFillUi emptyUi(@NonNull AutofillId autofillId) {
97         return new InlineFillUi(autofillId);
98     }
99 
100     /**
101      * Encapsulates various arguments used by {@link #forAutofill} and {@link #forAugmentedAutofill}
102      */
103     public static class InlineFillUiInfo {
104 
105         public int mUserId;
106         public int mSessionId;
107         public InlineSuggestionsRequest mInlineRequest;
108         public AutofillId mFocusedId;
109         public String mFilterText;
110         public RemoteInlineSuggestionRenderService mRemoteRenderService;
111 
InlineFillUiInfo(@onNull InlineSuggestionsRequest inlineRequest, @NonNull AutofillId focusedId, @NonNull String filterText, @NonNull RemoteInlineSuggestionRenderService remoteRenderService, @UserIdInt int userId, int sessionId)112         public InlineFillUiInfo(@NonNull InlineSuggestionsRequest inlineRequest,
113                 @NonNull AutofillId focusedId, @NonNull String filterText,
114                 @NonNull RemoteInlineSuggestionRenderService remoteRenderService,
115                 @UserIdInt int userId, int sessionId) {
116             mUserId = userId;
117             mSessionId = sessionId;
118             mInlineRequest = inlineRequest;
119             mFocusedId = focusedId;
120             mFilterText = filterText;
121             mRemoteRenderService = remoteRenderService;
122         }
123     }
124 
125     /**
126      * Returns an inline autofill UI for a field based on an Autofilll response.
127      */
128     @NonNull
forAutofill(@onNull InlineFillUiInfo inlineFillUiInfo, @NonNull FillResponse response, @NonNull InlineSuggestionUiCallback uiCallback)129     public static InlineFillUi forAutofill(@NonNull InlineFillUiInfo inlineFillUiInfo,
130             @NonNull FillResponse response,
131             @NonNull InlineSuggestionUiCallback uiCallback) {
132         if (response.getAuthentication() != null && response.getInlinePresentation() != null) {
133             InlineSuggestion inlineAuthentication =
134                     InlineSuggestionFactory.createInlineAuthentication(inlineFillUiInfo, response,
135                             uiCallback);
136             return new InlineFillUi(inlineFillUiInfo, inlineAuthentication);
137         } else if (response.getDatasets() != null) {
138             SparseArray<Pair<Dataset, InlineSuggestion>> inlineSuggestions =
139                     InlineSuggestionFactory.createInlineSuggestions(inlineFillUiInfo,
140                             InlineSuggestionInfo.SOURCE_AUTOFILL, response.getDatasets(),
141                             uiCallback);
142             return new InlineFillUi(inlineFillUiInfo, inlineSuggestions);
143         }
144         return new InlineFillUi(inlineFillUiInfo, new SparseArray<>());
145     }
146 
147     /**
148      * Returns an inline autofill UI for a field based on an Autofilll response.
149      */
150     @NonNull
forAugmentedAutofill(@onNull InlineFillUiInfo inlineFillUiInfo, @NonNull List<Dataset> datasets, @NonNull InlineSuggestionUiCallback uiCallback)151     public static InlineFillUi forAugmentedAutofill(@NonNull InlineFillUiInfo inlineFillUiInfo,
152             @NonNull List<Dataset> datasets,
153             @NonNull InlineSuggestionUiCallback uiCallback) {
154         SparseArray<Pair<Dataset, InlineSuggestion>> inlineSuggestions =
155                 InlineSuggestionFactory.createInlineSuggestions(inlineFillUiInfo,
156                         InlineSuggestionInfo.SOURCE_PLATFORM, datasets, uiCallback);
157         return new InlineFillUi(inlineFillUiInfo, inlineSuggestions);
158     }
159 
InlineFillUi(@ullable InlineFillUiInfo inlineFillUiInfo, @NonNull SparseArray<Pair<Dataset, InlineSuggestion>> inlineSuggestions)160     private InlineFillUi(@Nullable InlineFillUiInfo inlineFillUiInfo,
161             @NonNull SparseArray<Pair<Dataset, InlineSuggestion>> inlineSuggestions) {
162         mAutofillId = inlineFillUiInfo.mFocusedId;
163         int size = inlineSuggestions.size();
164         mDatasets = new ArrayList<>(size);
165         mInlineSuggestions = new ArrayList<>(size);
166         for (int i = 0; i < size; i++) {
167             Pair<Dataset, InlineSuggestion> value = inlineSuggestions.valueAt(i);
168             mDatasets.add(value.first);
169             mInlineSuggestions.add(value.second);
170         }
171         mFilterText = inlineFillUiInfo.mFilterText;
172     }
173 
InlineFillUi(@onNull InlineFillUiInfo inlineFillUiInfo, @NonNull InlineSuggestion inlineSuggestion)174     private InlineFillUi(@NonNull InlineFillUiInfo inlineFillUiInfo,
175             @NonNull InlineSuggestion inlineSuggestion) {
176         mAutofillId = inlineFillUiInfo.mFocusedId;
177         mDatasets = null;
178         mInlineSuggestions = new ArrayList<>();
179         mInlineSuggestions.add(inlineSuggestion);
180         mFilterText = inlineFillUiInfo.mFilterText;
181     }
182 
183     /**
184      * Only used for constructing an empty InlineFillUi with {@link #emptyUi}
185      */
InlineFillUi(@onNull AutofillId focusedId)186     private InlineFillUi(@NonNull AutofillId focusedId) {
187         mAutofillId = focusedId;
188         mDatasets = new ArrayList<>(0);
189         mInlineSuggestions = new ArrayList<>(0);
190         mFilterText = null;
191     }
192 
193     @NonNull
getAutofillId()194     public AutofillId getAutofillId() {
195         return mAutofillId;
196     }
197 
setFilterText(@ullable String filterText)198     public void setFilterText(@Nullable String filterText) {
199         mFilterText = filterText;
200     }
201 
202     /**
203      * Returns the list of filtered inline suggestions suitable for being sent to the IME.
204      */
205     @NonNull
getInlineSuggestionsResponse()206     public InlineSuggestionsResponse getInlineSuggestionsResponse() {
207         final int size = mInlineSuggestions.size();
208         if (size == 0) {
209             return new InlineSuggestionsResponse(Collections.emptyList());
210         }
211         final List<InlineSuggestion> inlineSuggestions = new ArrayList<>();
212         if (mDatasets == null || mDatasets.size() != size) {
213             // authentication case
214             for (int i = 0; i < size; i++) {
215                 inlineSuggestions.add(copy(i, mInlineSuggestions.get(i)));
216             }
217             return new InlineSuggestionsResponse(inlineSuggestions);
218         }
219         for (int i = 0; i < size; i++) {
220             final Dataset dataset = mDatasets.get(i);
221             final int fieldIndex = dataset.getFieldIds().indexOf(mAutofillId);
222             if (fieldIndex < 0) {
223                 Slog.w(TAG, "AutofillId=" + mAutofillId + " not found in dataset");
224                 continue;
225             }
226             final InlinePresentation inlinePresentation = dataset.getFieldInlinePresentation(
227                     fieldIndex);
228             if (inlinePresentation == null) {
229                 Slog.w(TAG, "InlinePresentation not found in dataset");
230                 continue;
231             }
232             if (!inlinePresentation.isPinned()  // don't filter pinned suggestions
233                     && !includeDataset(dataset, fieldIndex)) {
234                 continue;
235             }
236             inlineSuggestions.add(copy(i, mInlineSuggestions.get(i)));
237         }
238         return new InlineSuggestionsResponse(inlineSuggestions);
239     }
240 
241     /**
242      * Returns a copy of the suggestion, that internally copies the {@link IInlineContentProvider}
243      * so that it's not reused by the remote IME process across different inline suggestions.
244      * See {@link InlineContentProviderImpl} for why this is needed.
245      *
246      * <p>Note that although it copies the {@link IInlineContentProvider}, the underlying remote
247      * view (in the renderer service) is still reused.
248      */
249     @NonNull
copy(int index, @NonNull InlineSuggestion inlineSuggestion)250     private InlineSuggestion copy(int index, @NonNull InlineSuggestion inlineSuggestion) {
251         final IInlineContentProvider contentProvider = inlineSuggestion.getContentProvider();
252         if (contentProvider instanceof InlineContentProviderImpl) {
253             // We have to create a new inline suggestion instance to ensure we don't reuse the
254             // same {@link IInlineContentProvider}, but the underlying views are reused when
255             // calling {@link InlineContentProviderImpl#copy()}.
256             InlineSuggestion newInlineSuggestion = new InlineSuggestion(inlineSuggestion
257                     .getInfo(), ((InlineContentProviderImpl) contentProvider).copy());
258             // The remote view is only set when the content provider is called to inflate the view,
259             // which happens after it's sent to the IME (i.e. not now), so we keep the latest
260             // content provider (through newInlineSuggestion) to make sure the next time we copy it,
261             // we get to reuse the view.
262             mInlineSuggestions.set(index, newInlineSuggestion);
263             return newInlineSuggestion;
264         }
265         return inlineSuggestion;
266     }
267 
268     // TODO: Extract the shared filtering logic here and in FillUi to a common method.
includeDataset(Dataset dataset, int fieldIndex)269     private boolean includeDataset(Dataset dataset, int fieldIndex) {
270         // Show everything when the user input is empty.
271         if (TextUtils.isEmpty(mFilterText)) {
272             return true;
273         }
274 
275         final String constraintLowerCase = mFilterText.toString().toLowerCase();
276 
277         // Use the filter provided by the service, if available.
278         final Dataset.DatasetFieldFilter filter = dataset.getFilter(fieldIndex);
279         if (filter != null) {
280             Pattern filterPattern = filter.pattern;
281             if (filterPattern == null) {
282                 if (sVerbose) {
283                     Slog.v(TAG, "Explicitly disabling filter for dataset id" + dataset.getId());
284                 }
285                 return false;
286             }
287             if (mFilterMatchingDisabled) {
288                 return false;
289             }
290             return filterPattern.matcher(constraintLowerCase).matches();
291         }
292 
293         final AutofillValue value = dataset.getFieldValues().get(fieldIndex);
294         if (value == null || !value.isText()) {
295             return dataset.getAuthentication() == null;
296         }
297         if (mFilterMatchingDisabled) {
298             return false;
299         }
300         final String valueText = value.getTextValue().toString().toLowerCase();
301         return valueText.toLowerCase().startsWith(constraintLowerCase);
302     }
303 
304     /**
305      * Disables prefix/regex based filtering. Other filtering rules (see {@link
306      * android.service.autofill.Dataset}) still apply.
307      */
disableFilterMatching()308     public void disableFilterMatching() {
309         mFilterMatchingDisabled = true;
310     }
311 
312     /**
313      * Callback from the inline suggestion Ui.
314      */
315     public interface InlineSuggestionUiCallback {
316         /**
317          * Callback to autofill a dataset to the client app.
318          */
autofill(@onNull Dataset dataset, int datasetIndex)319         void autofill(@NonNull Dataset dataset, int datasetIndex);
320 
321         /**
322          * Callback to authenticate a dataset.
323          *
324          * <p>Only implemented by regular autofill for now.</p>
325          */
authenticate(int requestId, int datasetIndex)326         void authenticate(int requestId, int datasetIndex);
327 
328         /**
329          * Callback to start Intent in client app.
330          */
startIntentSender(@onNull IntentSender intentSender)331         void startIntentSender(@NonNull IntentSender intentSender);
332 
333         /**
334          * Callback on errors.
335          */
onError()336         void onError();
337     }
338 
339     /**
340      * Callback for inline suggestion Ui related events.
341      */
342     public interface InlineUiEventCallback {
343         /**
344          * Callback to notify inline ui is shown.
345          */
notifyInlineUiShown(@onNull AutofillId autofillId)346         void notifyInlineUiShown(@NonNull AutofillId autofillId);
347 
348         /**
349          * Callback to notify inline ui is hidden.
350          */
notifyInlineUiHidden(@onNull AutofillId autofillId)351         void notifyInlineUiHidden(@NonNull AutofillId autofillId);
352     }
353 }
354