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