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