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