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