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