Alan Viverette | 3da604b | 2020-06-10 18:34:39 +0000 | [diff] [blame^] | 1 | /* |
| 2 | * Copyright (C) 2017 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; |
| 18 | |
| 19 | import static android.service.autofill.AutofillFieldClassificationService.EXTRA_SCORES; |
| 20 | import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; |
| 21 | import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE; |
| 22 | import static android.service.autofill.FillRequest.INVALID_REQUEST_ID; |
| 23 | import static android.view.autofill.AutofillManager.ACTION_RESPONSE_EXPIRED; |
| 24 | import static android.view.autofill.AutofillManager.ACTION_START_SESSION; |
| 25 | import static android.view.autofill.AutofillManager.ACTION_VALUE_CHANGED; |
| 26 | import static android.view.autofill.AutofillManager.ACTION_VIEW_ENTERED; |
| 27 | import static android.view.autofill.AutofillManager.ACTION_VIEW_EXITED; |
| 28 | import static android.view.autofill.AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM; |
| 29 | import static android.view.autofill.AutofillManager.getSmartSuggestionModeToString; |
| 30 | |
| 31 | import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; |
| 32 | import static com.android.server.autofill.Helper.getNumericValue; |
| 33 | import static com.android.server.autofill.Helper.sDebug; |
| 34 | import static com.android.server.autofill.Helper.sVerbose; |
| 35 | import static com.android.server.autofill.Helper.toArray; |
| 36 | import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_RECEIVER_EXTRAS; |
| 37 | import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE; |
| 38 | |
| 39 | import android.annotation.NonNull; |
| 40 | import android.annotation.Nullable; |
| 41 | import android.app.Activity; |
| 42 | import android.app.ActivityTaskManager; |
| 43 | import android.app.IAssistDataReceiver; |
| 44 | import android.app.assist.AssistStructure; |
| 45 | import android.app.assist.AssistStructure.AutofillOverlay; |
| 46 | import android.app.assist.AssistStructure.ViewNode; |
| 47 | import android.content.ComponentName; |
| 48 | import android.content.Context; |
| 49 | import android.content.Intent; |
| 50 | import android.content.IntentSender; |
| 51 | import android.graphics.Bitmap; |
| 52 | import android.graphics.Rect; |
| 53 | import android.graphics.drawable.Drawable; |
| 54 | import android.metrics.LogMaker; |
| 55 | import android.os.Binder; |
| 56 | import android.os.Build; |
| 57 | import android.os.Bundle; |
| 58 | import android.os.Handler; |
| 59 | import android.os.IBinder; |
| 60 | import android.os.IBinder.DeathRecipient; |
| 61 | import android.os.Parcelable; |
| 62 | import android.os.RemoteCallback; |
| 63 | import android.os.RemoteException; |
| 64 | import android.os.SystemClock; |
| 65 | import android.service.autofill.AutofillFieldClassificationService.Scores; |
| 66 | import android.service.autofill.AutofillService; |
| 67 | import android.service.autofill.CompositeUserData; |
| 68 | import android.service.autofill.Dataset; |
| 69 | import android.service.autofill.FieldClassification; |
| 70 | import android.service.autofill.FieldClassification.Match; |
| 71 | import android.service.autofill.FieldClassificationUserData; |
| 72 | import android.service.autofill.FillContext; |
| 73 | import android.service.autofill.FillRequest; |
| 74 | import android.service.autofill.FillResponse; |
| 75 | import android.service.autofill.InternalSanitizer; |
| 76 | import android.service.autofill.InternalValidator; |
| 77 | import android.service.autofill.SaveInfo; |
| 78 | import android.service.autofill.SaveRequest; |
| 79 | import android.service.autofill.UserData; |
| 80 | import android.service.autofill.ValueFinder; |
| 81 | import android.text.TextUtils; |
| 82 | import android.util.ArrayMap; |
| 83 | import android.util.ArraySet; |
| 84 | import android.util.LocalLog; |
| 85 | import android.util.Log; |
| 86 | import android.util.Slog; |
| 87 | import android.util.SparseArray; |
| 88 | import android.util.TimeUtils; |
| 89 | import android.view.KeyEvent; |
| 90 | import android.view.autofill.AutofillId; |
| 91 | import android.view.autofill.AutofillManager; |
| 92 | import android.view.autofill.AutofillManager.SmartSuggestionMode; |
| 93 | import android.view.autofill.AutofillValue; |
| 94 | import android.view.autofill.IAutoFillManagerClient; |
| 95 | import android.view.autofill.IAutofillWindowPresenter; |
| 96 | import android.view.inputmethod.InlineSuggestionsRequest; |
| 97 | import android.view.inputmethod.InlineSuggestionsResponse; |
| 98 | |
| 99 | import com.android.internal.R; |
| 100 | import com.android.internal.annotations.GuardedBy; |
| 101 | import com.android.internal.logging.MetricsLogger; |
| 102 | import com.android.internal.logging.nano.MetricsProto.MetricsEvent; |
| 103 | import com.android.internal.util.ArrayUtils; |
| 104 | import com.android.server.autofill.ui.AutoFillUI; |
| 105 | import com.android.server.autofill.ui.InlineSuggestionFactory; |
| 106 | import com.android.server.autofill.ui.PendingUi; |
| 107 | import com.android.server.inputmethod.InputMethodManagerInternal; |
| 108 | |
| 109 | import java.io.PrintWriter; |
| 110 | import java.util.ArrayList; |
| 111 | import java.util.Arrays; |
| 112 | import java.util.Collection; |
| 113 | import java.util.Collections; |
| 114 | import java.util.List; |
| 115 | import java.util.Objects; |
| 116 | import java.util.Optional; |
| 117 | import java.util.concurrent.CountDownLatch; |
| 118 | import java.util.concurrent.atomic.AtomicInteger; |
| 119 | import java.util.function.Consumer; |
| 120 | import java.util.function.Function; |
| 121 | |
| 122 | /** |
| 123 | * A session for a given activity. |
| 124 | * |
| 125 | * <p>This class manages the multiple {@link ViewState}s for each view it has, and keeps track |
| 126 | * of the current {@link ViewState} to display the appropriate UI. |
| 127 | * |
| 128 | * <p>Although the autofill requests and callbacks are stateless from the service's point of |
| 129 | * view, we need to keep state in the framework side for cases such as authentication. For |
| 130 | * example, when service return a {@link FillResponse} that contains all the fields needed |
| 131 | * to fill the activity but it requires authentication first, that response need to be held |
| 132 | * until the user authenticates or it times out. |
| 133 | */ |
| 134 | final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener, |
| 135 | AutoFillUI.AutoFillUiCallback, ValueFinder { |
| 136 | private static final String TAG = "AutofillSession"; |
| 137 | |
| 138 | private static final String EXTRA_REQUEST_ID = "android.service.autofill.extra.REQUEST_ID"; |
| 139 | |
| 140 | private final AutofillManagerServiceImpl mService; |
| 141 | private final Handler mHandler; |
| 142 | private final Object mLock; |
| 143 | private final AutoFillUI mUi; |
| 144 | |
| 145 | private final MetricsLogger mMetricsLogger = new MetricsLogger(); |
| 146 | |
| 147 | private static AtomicInteger sIdCounter = new AtomicInteger(); |
| 148 | |
| 149 | /** |
| 150 | * ID of the session. |
| 151 | * |
| 152 | * <p>It's always a positive number, to make it easier to embed it in a long. |
| 153 | */ |
| 154 | public final int id; |
| 155 | |
| 156 | /** uid the session is for */ |
| 157 | public final int uid; |
| 158 | |
| 159 | /** ID of the task associated with this session's activity */ |
| 160 | public final int taskId; |
| 161 | |
| 162 | /** Flags used to start the session */ |
| 163 | public final int mFlags; |
| 164 | |
| 165 | @GuardedBy("mLock") |
| 166 | @NonNull private IBinder mActivityToken; |
| 167 | |
| 168 | /** Component that's being auto-filled */ |
| 169 | @NonNull private final ComponentName mComponentName; |
| 170 | |
| 171 | /** Whether the app being autofilled is running in compat mode. */ |
| 172 | private final boolean mCompatMode; |
| 173 | |
| 174 | /** Node representing the URL bar on compat mode. */ |
| 175 | @GuardedBy("mLock") |
| 176 | private ViewNode mUrlBar; |
| 177 | |
| 178 | @GuardedBy("mLock") |
| 179 | private boolean mSaveOnAllViewsInvisible; |
| 180 | |
| 181 | @GuardedBy("mLock") |
| 182 | private final ArrayMap<AutofillId, ViewState> mViewStates = new ArrayMap<>(); |
| 183 | |
| 184 | /** |
| 185 | * Id of the View currently being displayed. |
| 186 | */ |
| 187 | @GuardedBy("mLock") |
| 188 | @Nullable private AutofillId mCurrentViewId; |
| 189 | |
| 190 | @GuardedBy("mLock") |
| 191 | private IAutoFillManagerClient mClient; |
| 192 | |
| 193 | @GuardedBy("mLock") |
| 194 | private DeathRecipient mClientVulture; |
| 195 | |
| 196 | /** |
| 197 | * Reference to the remote service. |
| 198 | * |
| 199 | * <p>Only {@code null} when the session is for augmented autofill only. |
| 200 | */ |
| 201 | @Nullable |
| 202 | private final RemoteFillService mRemoteFillService; |
| 203 | |
| 204 | @GuardedBy("mLock") |
| 205 | private SparseArray<FillResponse> mResponses; |
| 206 | |
| 207 | /** |
| 208 | * Contexts read from the app; they will be updated (sanitized, change values for save) before |
| 209 | * sent to {@link AutofillService}. Ordered by the time they were read. |
| 210 | */ |
| 211 | @GuardedBy("mLock") |
| 212 | private ArrayList<FillContext> mContexts; |
| 213 | |
| 214 | /** |
| 215 | * Whether the client has an {@link android.view.autofill.AutofillManager.AutofillCallback}. |
| 216 | */ |
| 217 | private boolean mHasCallback; |
| 218 | |
| 219 | /** |
| 220 | * Extras sent by service on {@code onFillRequest()} calls; the first non-null extra is saved |
| 221 | * and used on subsequent {@code onFillRequest()} and {@code onSaveRequest()} calls. |
| 222 | */ |
| 223 | @GuardedBy("mLock") |
| 224 | private Bundle mClientState; |
| 225 | |
| 226 | @GuardedBy("mLock") |
| 227 | private boolean mDestroyed; |
| 228 | |
| 229 | /** Whether the session is currently saving. */ |
| 230 | @GuardedBy("mLock") |
| 231 | private boolean mIsSaving; |
| 232 | |
| 233 | /** |
| 234 | * Helper used to handle state of Save UI when it must be hiding to show a custom description |
| 235 | * link and later recovered. |
| 236 | */ |
| 237 | @GuardedBy("mLock") |
| 238 | private PendingUi mPendingSaveUi; |
| 239 | |
| 240 | /** |
| 241 | * List of dataset ids selected by the user. |
| 242 | */ |
| 243 | @GuardedBy("mLock") |
| 244 | private ArrayList<String> mSelectedDatasetIds; |
| 245 | |
| 246 | /** |
| 247 | * When the session started (using elapsed time since boot). |
| 248 | */ |
| 249 | private final long mStartTime; |
| 250 | |
| 251 | /** |
| 252 | * When the UI was shown for the first time (using elapsed time since boot). |
| 253 | */ |
| 254 | @GuardedBy("mLock") |
| 255 | private long mUiShownTime; |
| 256 | |
| 257 | @GuardedBy("mLock") |
| 258 | private final LocalLog mUiLatencyHistory; |
| 259 | |
| 260 | @GuardedBy("mLock") |
| 261 | private final LocalLog mWtfHistory; |
| 262 | |
| 263 | @GuardedBy("mLock") |
| 264 | private boolean mExpiredResponse; |
| 265 | |
| 266 | /** |
| 267 | * Map of {@link MetricsEvent#AUTOFILL_REQUEST} metrics, keyed by fill request id. |
| 268 | */ |
| 269 | @GuardedBy("mLock") |
| 270 | private final SparseArray<LogMaker> mRequestLogs = new SparseArray<>(1); |
| 271 | |
| 272 | /** |
| 273 | * Destroys the augmented Autofill UI. |
| 274 | */ |
| 275 | // TODO(b/123099468): this runnable is called when the Autofill session is destroyed, the |
| 276 | // main reason being the cases where user tap HOME. |
| 277 | // Right now it's completely destroying the UI, but we need to decide whether / how to |
| 278 | // properly recover it later (for example, if the user switches back to the activity, |
| 279 | // should it be restored? Right now it kind of is, because Autofill's Session trigger a |
| 280 | // new FillRequest, which in turn triggers the Augmented Autofill request again) |
| 281 | @GuardedBy("mLock") |
| 282 | @Nullable |
| 283 | private Runnable mAugmentedAutofillDestroyer; |
| 284 | |
| 285 | /** |
| 286 | * List of {@link MetricsEvent#AUTOFILL_AUGMENTED_REQUEST} metrics. |
| 287 | */ |
| 288 | @GuardedBy("mLock") |
| 289 | private ArrayList<LogMaker> mAugmentedRequestsLogs; |
| 290 | |
| 291 | |
| 292 | /** |
| 293 | * List of autofill ids of autofillable fields present in the AssistStructure that can be used |
| 294 | * to trigger new augmented autofill requests (because the "standard" service was not interested |
| 295 | * on autofilling the app. |
| 296 | */ |
| 297 | @GuardedBy("mLock") |
| 298 | private ArrayList<AutofillId> mAugmentedAutofillableIds; |
| 299 | |
| 300 | /** |
| 301 | * When {@code true}, the session was created only to handle Augmented Autofill requests (i.e., |
| 302 | * the session would not have existed otherwsie). |
| 303 | */ |
| 304 | @GuardedBy("mLock") |
| 305 | private boolean mForAugmentedAutofillOnly; |
| 306 | |
| 307 | @Nullable |
| 308 | private final AutofillInlineSessionController mInlineSessionController; |
| 309 | |
| 310 | /** |
| 311 | * Receiver of assist data from the app's {@link Activity}. |
| 312 | */ |
| 313 | private final AssistDataReceiverImpl mAssistReceiver = new AssistDataReceiverImpl(); |
| 314 | |
| 315 | /** |
| 316 | * TODO(b/151867668): improve how asynchronous data dependencies are handled, without using |
| 317 | * CountDownLatch. |
| 318 | */ |
| 319 | private final class AssistDataReceiverImpl extends IAssistDataReceiver.Stub { |
| 320 | |
| 321 | @GuardedBy("mLock") |
| 322 | private InlineSuggestionsRequest mPendingInlineSuggestionsRequest; |
| 323 | @GuardedBy("mLock") |
| 324 | private FillRequest mPendingFillRequest; |
| 325 | @GuardedBy("mLock") |
| 326 | private CountDownLatch mCountDownLatch = new CountDownLatch(0); |
| 327 | |
| 328 | @Nullable Consumer<InlineSuggestionsRequest> newAutofillRequestLocked(ViewState viewState, |
| 329 | boolean isInlineRequest) { |
| 330 | mCountDownLatch = new CountDownLatch(isInlineRequest ? 2 : 1); |
| 331 | mPendingFillRequest = null; |
| 332 | mPendingInlineSuggestionsRequest = null; |
| 333 | return isInlineRequest ? (inlineSuggestionsRequest) -> { |
| 334 | synchronized (mLock) { |
| 335 | if (mCountDownLatch.getCount() == 0) { |
| 336 | return; |
| 337 | } |
| 338 | mPendingInlineSuggestionsRequest = inlineSuggestionsRequest; |
| 339 | mCountDownLatch.countDown(); |
| 340 | maybeRequestFillLocked(); |
| 341 | viewState.resetState(ViewState.STATE_PENDING_CREATE_INLINE_REQUEST); |
| 342 | } |
| 343 | } : null; |
| 344 | } |
| 345 | |
| 346 | void maybeRequestFillLocked() { |
| 347 | if (mCountDownLatch.getCount() > 0 || mPendingFillRequest == null) { |
| 348 | return; |
| 349 | } |
| 350 | if (mPendingInlineSuggestionsRequest != null) { |
| 351 | mPendingFillRequest = new FillRequest(mPendingFillRequest.getId(), |
| 352 | mPendingFillRequest.getFillContexts(), mPendingFillRequest.getClientState(), |
| 353 | mPendingFillRequest.getFlags(), mPendingInlineSuggestionsRequest); |
| 354 | } |
| 355 | mRemoteFillService.onFillRequest(mPendingFillRequest); |
| 356 | mPendingInlineSuggestionsRequest = null; |
| 357 | mPendingFillRequest = null; |
| 358 | } |
| 359 | |
| 360 | @Override |
| 361 | public void onHandleAssistData(Bundle resultData) throws RemoteException { |
| 362 | if (mRemoteFillService == null) { |
| 363 | wtf(null, "onHandleAssistData() called without a remote service. " |
| 364 | + "mForAugmentedAutofillOnly: %s", mForAugmentedAutofillOnly); |
| 365 | return; |
| 366 | } |
| 367 | // Keeps to prevent it is cleared on multiple threads. |
| 368 | final AutofillId currentViewId = mCurrentViewId; |
| 369 | if (currentViewId == null) { |
| 370 | Slog.w(TAG, "No current view id - session might have finished"); |
| 371 | return; |
| 372 | } |
| 373 | |
| 374 | final AssistStructure structure = resultData.getParcelable(ASSIST_KEY_STRUCTURE); |
| 375 | if (structure == null) { |
| 376 | Slog.e(TAG, "No assist structure - app might have crashed providing it"); |
| 377 | return; |
| 378 | } |
| 379 | |
| 380 | final Bundle receiverExtras = resultData.getBundle(ASSIST_KEY_RECEIVER_EXTRAS); |
| 381 | if (receiverExtras == null) { |
| 382 | Slog.e(TAG, "No receiver extras - app might have crashed providing it"); |
| 383 | return; |
| 384 | } |
| 385 | |
| 386 | final int requestId = receiverExtras.getInt(EXTRA_REQUEST_ID); |
| 387 | |
| 388 | if (sVerbose) { |
| 389 | Slog.v(TAG, "New structure for requestId " + requestId + ": " + structure); |
| 390 | } |
| 391 | |
| 392 | final FillRequest request; |
| 393 | synchronized (mLock) { |
| 394 | // TODO(b/35708678): Must fetch the data so it's available later on handleSave(), |
| 395 | // even if if the activity is gone by then, but structure .ensureData() gives a |
| 396 | // ONE_WAY warning because system_service could block on app calls. We need to |
| 397 | // change AssistStructure so it provides a "one-way" writeToParcel() method that |
| 398 | // sends all the data |
| 399 | try { |
| 400 | structure.ensureDataForAutofill(); |
| 401 | } catch (RuntimeException e) { |
| 402 | wtf(e, "Exception lazy loading assist structure for %s: %s", |
| 403 | structure.getActivityComponent(), e); |
| 404 | return; |
| 405 | } |
| 406 | |
| 407 | final ArrayList<AutofillId> ids = Helper.getAutofillIds(structure, |
| 408 | /* autofillableOnly= */false); |
| 409 | for (int i = 0; i < ids.size(); i++) { |
| 410 | ids.get(i).setSessionId(Session.this.id); |
| 411 | } |
| 412 | |
| 413 | // Flags used to start the session. |
| 414 | int flags = structure.getFlags(); |
| 415 | |
| 416 | if (mCompatMode) { |
| 417 | // Sanitize URL bar, if needed |
| 418 | final String[] urlBarIds = mService.getUrlBarResourceIdsForCompatMode( |
| 419 | mComponentName.getPackageName()); |
| 420 | if (sDebug) { |
| 421 | Slog.d(TAG, "url_bars in compat mode: " + Arrays.toString(urlBarIds)); |
| 422 | } |
| 423 | if (urlBarIds != null) { |
| 424 | mUrlBar = Helper.sanitizeUrlBar(structure, urlBarIds); |
| 425 | if (mUrlBar != null) { |
| 426 | final AutofillId urlBarId = mUrlBar.getAutofillId(); |
| 427 | if (sDebug) { |
| 428 | Slog.d(TAG, "Setting urlBar as id=" + urlBarId + " and domain " |
| 429 | + mUrlBar.getWebDomain()); |
| 430 | } |
| 431 | final ViewState viewState = new ViewState(urlBarId, Session.this, |
| 432 | ViewState.STATE_URL_BAR); |
| 433 | mViewStates.put(urlBarId, viewState); |
| 434 | } |
| 435 | } |
| 436 | flags |= FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST; |
| 437 | } |
| 438 | structure.sanitizeForParceling(true); |
| 439 | |
| 440 | if (mContexts == null) { |
| 441 | mContexts = new ArrayList<>(1); |
| 442 | } |
| 443 | mContexts.add(new FillContext(requestId, structure, currentViewId)); |
| 444 | |
| 445 | cancelCurrentRequestLocked(); |
| 446 | |
| 447 | final int numContexts = mContexts.size(); |
| 448 | for (int i = 0; i < numContexts; i++) { |
| 449 | fillContextWithAllowedValuesLocked(mContexts.get(i), flags); |
| 450 | } |
| 451 | |
| 452 | final ArrayList<FillContext> contexts = |
| 453 | mergePreviousSessionLocked(/* forSave= */ false); |
| 454 | request = new FillRequest(requestId, contexts, mClientState, flags, |
| 455 | /*inlineSuggestionsRequest=*/null); |
| 456 | |
| 457 | if (mCountDownLatch.getCount() > 0) { |
| 458 | mPendingFillRequest = request; |
| 459 | mCountDownLatch.countDown(); |
| 460 | maybeRequestFillLocked(); |
| 461 | } else { |
| 462 | // TODO(b/151867668): ideally this case should not happen, but it was |
| 463 | // observed, we should figure out why and fix. |
| 464 | mRemoteFillService.onFillRequest(request); |
| 465 | } |
| 466 | } |
| 467 | |
| 468 | if (mActivityToken != null) { |
| 469 | mService.sendActivityAssistDataToContentCapture(mActivityToken, resultData); |
| 470 | } |
| 471 | } |
| 472 | |
| 473 | @Override |
| 474 | public void onHandleAssistScreenshot(Bitmap screenshot) { |
| 475 | // Do nothing |
| 476 | } |
| 477 | }; |
| 478 | |
| 479 | /** |
| 480 | * Returns the ids of all entries in {@link #mViewStates} in the same order. |
| 481 | */ |
| 482 | @GuardedBy("mLock") |
| 483 | private AutofillId[] getIdsOfAllViewStatesLocked() { |
| 484 | final int numViewState = mViewStates.size(); |
| 485 | final AutofillId[] ids = new AutofillId[numViewState]; |
| 486 | for (int i = 0; i < numViewState; i++) { |
| 487 | ids[i] = mViewStates.valueAt(i).id; |
| 488 | } |
| 489 | |
| 490 | return ids; |
| 491 | } |
| 492 | |
| 493 | @Override |
| 494 | @Nullable |
| 495 | public String findByAutofillId(@NonNull AutofillId id) { |
| 496 | synchronized (mLock) { |
| 497 | AutofillValue value = findValueLocked(id); |
| 498 | if (value != null) { |
| 499 | if (value.isText()) { |
| 500 | return value.getTextValue().toString(); |
| 501 | } |
| 502 | |
| 503 | if (value.isList()) { |
| 504 | final CharSequence[] options = getAutofillOptionsFromContextsLocked(id); |
| 505 | if (options != null) { |
| 506 | final int index = value.getListValue(); |
| 507 | final CharSequence option = options[index]; |
| 508 | return option != null ? option.toString() : null; |
| 509 | } else { |
| 510 | Slog.w(TAG, "findByAutofillId(): no autofill options for id " + id); |
| 511 | } |
| 512 | } |
| 513 | } |
| 514 | } |
| 515 | return null; |
| 516 | } |
| 517 | |
| 518 | @Override |
| 519 | public AutofillValue findRawValueByAutofillId(AutofillId id) { |
| 520 | synchronized (mLock) { |
| 521 | return findValueLocked(id); |
| 522 | } |
| 523 | } |
| 524 | |
| 525 | /** |
| 526 | * <p>Gets the value of a field, using either the {@code viewStates} or the {@code mContexts}, |
| 527 | * or {@code null} when not found on either of them. |
| 528 | */ |
| 529 | @GuardedBy("mLock") |
| 530 | @Nullable |
| 531 | private AutofillValue findValueLocked(@NonNull AutofillId autofillId) { |
| 532 | final AutofillValue value = findValueFromThisSessionOnlyLocked(autofillId); |
| 533 | if (value != null) { |
| 534 | return getSanitizedValue(createSanitizers(getSaveInfoLocked()), autofillId, value); |
| 535 | } |
| 536 | |
| 537 | // TODO(b/113281366): rather than explicitly look for previous session, it might be better |
| 538 | // to merge the sessions when created (see note on mergePreviousSessionLocked()) |
| 539 | final ArrayList<Session> previousSessions = mService.getPreviousSessionsLocked(this); |
| 540 | if (previousSessions != null) { |
| 541 | if (sDebug) { |
| 542 | Slog.d(TAG, "findValueLocked(): looking on " + previousSessions.size() |
| 543 | + " previous sessions for autofillId " + autofillId); |
| 544 | } |
| 545 | for (int i = 0; i < previousSessions.size(); i++) { |
| 546 | final Session previousSession = previousSessions.get(i); |
| 547 | final AutofillValue previousValue = previousSession |
| 548 | .findValueFromThisSessionOnlyLocked(autofillId); |
| 549 | if (previousValue != null) { |
| 550 | return getSanitizedValue(createSanitizers(previousSession.getSaveInfoLocked()), |
| 551 | autofillId, previousValue); |
| 552 | } |
| 553 | } |
| 554 | } |
| 555 | return null; |
| 556 | } |
| 557 | |
| 558 | @Nullable |
| 559 | private AutofillValue findValueFromThisSessionOnlyLocked(@NonNull AutofillId autofillId) { |
| 560 | final ViewState state = mViewStates.get(autofillId); |
| 561 | if (state == null) { |
| 562 | if (sDebug) Slog.d(TAG, "findValueLocked(): no view state for " + autofillId); |
| 563 | return null; |
| 564 | } |
| 565 | AutofillValue value = state.getCurrentValue(); |
| 566 | if (value == null) { |
| 567 | if (sDebug) Slog.d(TAG, "findValueLocked(): no current value for " + autofillId); |
| 568 | value = getValueFromContextsLocked(autofillId); |
| 569 | } |
| 570 | return value; |
| 571 | } |
| 572 | |
| 573 | /** |
| 574 | * Updates values of the nodes in the context's structure so that: |
| 575 | * |
| 576 | * - proper node is focused |
| 577 | * - autofillValue is sent back to service when it was previously autofilled |
| 578 | * - autofillValue is sent in the view used to force a request |
| 579 | * |
| 580 | * @param fillContext The context to be filled |
| 581 | * @param flags The flags that started the session |
| 582 | */ |
| 583 | @GuardedBy("mLock") |
| 584 | private void fillContextWithAllowedValuesLocked(@NonNull FillContext fillContext, int flags) { |
| 585 | final ViewNode[] nodes = fillContext |
| 586 | .findViewNodesByAutofillIds(getIdsOfAllViewStatesLocked()); |
| 587 | |
| 588 | final int numViewState = mViewStates.size(); |
| 589 | for (int i = 0; i < numViewState; i++) { |
| 590 | final ViewState viewState = mViewStates.valueAt(i); |
| 591 | |
| 592 | final ViewNode node = nodes[i]; |
| 593 | if (node == null) { |
| 594 | if (sVerbose) { |
| 595 | Slog.v(TAG, |
| 596 | "fillContextWithAllowedValuesLocked(): no node for " + viewState.id); |
| 597 | } |
| 598 | continue; |
| 599 | } |
| 600 | |
| 601 | final AutofillValue currentValue = viewState.getCurrentValue(); |
| 602 | final AutofillValue filledValue = viewState.getAutofilledValue(); |
| 603 | final AutofillOverlay overlay = new AutofillOverlay(); |
| 604 | |
| 605 | // Sanitizes the value if the current value matches what the service sent. |
| 606 | if (filledValue != null && filledValue.equals(currentValue)) { |
| 607 | overlay.value = currentValue; |
| 608 | } |
| 609 | |
| 610 | if (mCurrentViewId != null) { |
| 611 | // Updates the focus value. |
| 612 | overlay.focused = mCurrentViewId.equals(viewState.id); |
| 613 | // Sanitizes the value of the focused field in a manual request. |
| 614 | if (overlay.focused && (flags & FLAG_MANUAL_REQUEST) != 0) { |
| 615 | overlay.value = currentValue; |
| 616 | } |
| 617 | } |
| 618 | node.setAutofillOverlay(overlay); |
| 619 | } |
| 620 | } |
| 621 | |
| 622 | /** |
| 623 | * Cancels the last request sent to the {@link #mRemoteFillService}. |
| 624 | */ |
| 625 | @GuardedBy("mLock") |
| 626 | private void cancelCurrentRequestLocked() { |
| 627 | if (mRemoteFillService == null) { |
| 628 | wtf(null, "cancelCurrentRequestLocked() called without a remote service. " |
| 629 | + "mForAugmentedAutofillOnly: %s", mForAugmentedAutofillOnly); |
| 630 | return; |
| 631 | } |
| 632 | final int canceledRequest = mRemoteFillService.cancelCurrentRequest(); |
| 633 | |
| 634 | // Remove the FillContext as there will never be a response for the service |
| 635 | if (canceledRequest != INVALID_REQUEST_ID && mContexts != null) { |
| 636 | final int numContexts = mContexts.size(); |
| 637 | |
| 638 | // It is most likely the last context, hence search backwards |
| 639 | for (int i = numContexts - 1; i >= 0; i--) { |
| 640 | if (mContexts.get(i).getRequestId() == canceledRequest) { |
| 641 | if (sDebug) Slog.d(TAG, "cancelCurrentRequest(): id = " + canceledRequest); |
| 642 | mContexts.remove(i); |
| 643 | break; |
| 644 | } |
| 645 | } |
| 646 | } |
| 647 | } |
| 648 | |
| 649 | /** |
| 650 | * Returns whether inline suggestions are supported by Autofill provider (not augmented |
| 651 | * Autofill provider). |
| 652 | */ |
| 653 | private boolean isInlineSuggestionsEnabledByAutofillProviderLocked() { |
| 654 | return mService.isInlineSuggestionsEnabled(); |
| 655 | } |
| 656 | |
| 657 | /** |
| 658 | * Clears the existing response for the partition, reads a new structure, and then requests a |
| 659 | * new fill response from the fill service. |
| 660 | * |
| 661 | * <p> Also asks the IME to make an inline suggestions request if it's enabled. |
| 662 | */ |
| 663 | @GuardedBy("mLock") |
| 664 | private void requestNewFillResponseLocked(@NonNull ViewState viewState, int newState, |
| 665 | int flags) { |
| 666 | final FillResponse existingResponse = viewState.getResponse(); |
| 667 | if (existingResponse != null) { |
| 668 | setViewStatesLocked( |
| 669 | existingResponse, |
| 670 | ViewState.STATE_INITIAL, |
| 671 | /* clearResponse= */ true); |
| 672 | } |
| 673 | mExpiredResponse = false; |
| 674 | if (mForAugmentedAutofillOnly || mRemoteFillService == null) { |
| 675 | if (sVerbose) { |
| 676 | Slog.v(TAG, "requestNewFillResponse(): triggering augmented autofill instead " |
| 677 | + "(mForAugmentedAutofillOnly=" + mForAugmentedAutofillOnly |
| 678 | + ", flags=" + flags + ")"); |
| 679 | } |
| 680 | mForAugmentedAutofillOnly = true; |
| 681 | triggerAugmentedAutofillLocked(flags); |
| 682 | return; |
| 683 | } |
| 684 | |
| 685 | viewState.setState(newState); |
| 686 | |
| 687 | int requestId; |
| 688 | |
| 689 | do { |
| 690 | requestId = sIdCounter.getAndIncrement(); |
| 691 | } while (requestId == INVALID_REQUEST_ID); |
| 692 | |
| 693 | // Create a metrics log for the request |
| 694 | final int ordinal = mRequestLogs.size() + 1; |
| 695 | final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_REQUEST) |
| 696 | .addTaggedData(MetricsEvent.FIELD_AUTOFILL_REQUEST_ORDINAL, ordinal); |
| 697 | if (flags != 0) { |
| 698 | log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_FLAGS, flags); |
| 699 | } |
| 700 | mRequestLogs.put(requestId, log); |
| 701 | |
| 702 | if (sVerbose) { |
| 703 | Slog.v(TAG, "Requesting structure for request #" + ordinal + " ,requestId=" + requestId |
| 704 | + ", flags=" + flags); |
| 705 | } |
| 706 | |
| 707 | // If the focus changes very quickly before the first request is returned each focus change |
| 708 | // triggers a new partition and we end up with many duplicate partitions. This is |
| 709 | // enhanced as the focus change can be much faster than the taking of the assist structure. |
| 710 | // Hence remove the currently queued request and replace it with the one queued after the |
| 711 | // structure is taken. This causes only one fill request per bust of focus changes. |
| 712 | cancelCurrentRequestLocked(); |
| 713 | |
| 714 | // Only ask IME to create inline suggestions request if Autofill provider supports it and |
| 715 | // the render service is available. |
| 716 | final RemoteInlineSuggestionRenderService remoteRenderService = |
| 717 | mService.getRemoteInlineSuggestionRenderServiceLocked(); |
| 718 | if (isInlineSuggestionsEnabledByAutofillProviderLocked() && remoteRenderService != null) { |
| 719 | Consumer<InlineSuggestionsRequest> inlineSuggestionsRequestConsumer = |
| 720 | mAssistReceiver.newAutofillRequestLocked(viewState, |
| 721 | /*isInlineRequest=*/ true); |
| 722 | if (inlineSuggestionsRequestConsumer != null) { |
| 723 | final AutofillId focusedId = mCurrentViewId; |
| 724 | remoteRenderService.getInlineSuggestionsRendererInfo( |
| 725 | new RemoteCallback((extras) -> { |
| 726 | synchronized (mLock) { |
| 727 | mInlineSessionController.onCreateInlineSuggestionsRequestLocked( |
| 728 | focusedId, inlineSuggestionsRequestConsumer, extras); |
| 729 | } |
| 730 | }, mHandler) |
| 731 | ); |
| 732 | viewState.setState(ViewState.STATE_PENDING_CREATE_INLINE_REQUEST); |
| 733 | } |
| 734 | } else { |
| 735 | mAssistReceiver.newAutofillRequestLocked(viewState, |
| 736 | /*isInlineRequest=*/ false); |
| 737 | } |
| 738 | |
| 739 | // Now request the assist structure data. |
| 740 | try { |
| 741 | final Bundle receiverExtras = new Bundle(); |
| 742 | receiverExtras.putInt(EXTRA_REQUEST_ID, requestId); |
| 743 | final long identity = Binder.clearCallingIdentity(); |
| 744 | try { |
| 745 | if (!ActivityTaskManager.getService().requestAutofillData(mAssistReceiver, |
| 746 | receiverExtras, mActivityToken, flags)) { |
| 747 | Slog.w(TAG, "failed to request autofill data for " + mActivityToken); |
| 748 | } |
| 749 | } finally { |
| 750 | Binder.restoreCallingIdentity(identity); |
| 751 | } |
| 752 | } catch (RemoteException e) { |
| 753 | // Should not happen, it's a local call. |
| 754 | } |
| 755 | } |
| 756 | |
| 757 | Session(@NonNull AutofillManagerServiceImpl service, @NonNull AutoFillUI ui, |
| 758 | @NonNull Context context, @NonNull Handler handler, int userId, @NonNull Object lock, |
| 759 | int sessionId, int taskId, int uid, @NonNull IBinder activityToken, |
| 760 | @NonNull IBinder client, boolean hasCallback, @NonNull LocalLog uiLatencyHistory, |
| 761 | @NonNull LocalLog wtfHistory, @Nullable ComponentName serviceComponentName, |
| 762 | @NonNull ComponentName componentName, boolean compatMode, |
| 763 | boolean bindInstantServiceAllowed, boolean forAugmentedAutofillOnly, int flags, |
| 764 | @NonNull InputMethodManagerInternal inputMethodManagerInternal) { |
| 765 | if (sessionId < 0) { |
| 766 | wtf(null, "Non-positive sessionId: %s", sessionId); |
| 767 | } |
| 768 | id = sessionId; |
| 769 | mFlags = flags; |
| 770 | this.taskId = taskId; |
| 771 | this.uid = uid; |
| 772 | mStartTime = SystemClock.elapsedRealtime(); |
| 773 | mService = service; |
| 774 | mLock = lock; |
| 775 | mUi = ui; |
| 776 | mHandler = handler; |
| 777 | mRemoteFillService = serviceComponentName == null ? null |
| 778 | : new RemoteFillService(context, serviceComponentName, userId, this, |
| 779 | bindInstantServiceAllowed); |
| 780 | mActivityToken = activityToken; |
| 781 | mHasCallback = hasCallback; |
| 782 | mUiLatencyHistory = uiLatencyHistory; |
| 783 | mWtfHistory = wtfHistory; |
| 784 | mComponentName = componentName; |
| 785 | mCompatMode = compatMode; |
| 786 | mForAugmentedAutofillOnly = forAugmentedAutofillOnly; |
| 787 | setClientLocked(client); |
| 788 | |
| 789 | mInlineSessionController = new AutofillInlineSessionController(inputMethodManagerInternal, |
| 790 | userId, componentName, handler, mLock); |
| 791 | |
| 792 | mMetricsLogger.write(newLogMaker(MetricsEvent.AUTOFILL_SESSION_STARTED) |
| 793 | .addTaggedData(MetricsEvent.FIELD_AUTOFILL_FLAGS, flags)); |
| 794 | } |
| 795 | |
| 796 | /** |
| 797 | * Gets the currently registered activity token |
| 798 | * |
| 799 | * @return The activity token |
| 800 | */ |
| 801 | @GuardedBy("mLock") |
| 802 | @NonNull IBinder getActivityTokenLocked() { |
| 803 | return mActivityToken; |
| 804 | } |
| 805 | |
| 806 | /** |
| 807 | * Sets new activity and client for this session. |
| 808 | * |
| 809 | * @param newActivity The token of the new activity |
| 810 | * @param newClient The client receiving autofill callbacks |
| 811 | */ |
| 812 | void switchActivity(@NonNull IBinder newActivity, @NonNull IBinder newClient) { |
| 813 | synchronized (mLock) { |
| 814 | if (mDestroyed) { |
| 815 | Slog.w(TAG, "Call to Session#switchActivity() rejected - session: " |
| 816 | + id + " destroyed"); |
| 817 | return; |
| 818 | } |
| 819 | mActivityToken = newActivity; |
| 820 | setClientLocked(newClient); |
| 821 | |
| 822 | // The tracked id are not persisted in the client, hence update them |
| 823 | updateTrackedIdsLocked(); |
| 824 | } |
| 825 | } |
| 826 | |
| 827 | @GuardedBy("mLock") |
| 828 | private void setClientLocked(@NonNull IBinder client) { |
| 829 | unlinkClientVultureLocked(); |
| 830 | mClient = IAutoFillManagerClient.Stub.asInterface(client); |
| 831 | mClientVulture = () -> { |
| 832 | Slog.d(TAG, "handling death of " + mActivityToken + " when saving=" + mIsSaving); |
| 833 | synchronized (mLock) { |
| 834 | if (mIsSaving) { |
| 835 | mUi.hideFillUi(this); |
| 836 | } else { |
| 837 | mUi.destroyAll(mPendingSaveUi, this, false); |
| 838 | } |
| 839 | } |
| 840 | }; |
| 841 | try { |
| 842 | mClient.asBinder().linkToDeath(mClientVulture, 0); |
| 843 | } catch (RemoteException e) { |
| 844 | Slog.w(TAG, "could not set binder death listener on autofill client: " + e); |
| 845 | mClientVulture = null; |
| 846 | } |
| 847 | } |
| 848 | |
| 849 | @GuardedBy("mLock") |
| 850 | private void unlinkClientVultureLocked() { |
| 851 | if (mClient != null && mClientVulture != null) { |
| 852 | final boolean unlinked = mClient.asBinder().unlinkToDeath(mClientVulture, 0); |
| 853 | if (!unlinked) { |
| 854 | Slog.w(TAG, "unlinking vulture from death failed for " + mActivityToken); |
| 855 | } |
| 856 | mClientVulture = null; |
| 857 | } |
| 858 | } |
| 859 | |
| 860 | // FillServiceCallbacks |
| 861 | @Override |
| 862 | public void onFillRequestSuccess(int requestId, @Nullable FillResponse response, |
| 863 | @NonNull String servicePackageName, int requestFlags) { |
| 864 | final AutofillId[] fieldClassificationIds; |
| 865 | |
| 866 | final LogMaker requestLog; |
| 867 | |
| 868 | synchronized (mLock) { |
| 869 | if (mDestroyed) { |
| 870 | Slog.w(TAG, "Call to Session#onFillRequestSuccess() rejected - session: " |
| 871 | + id + " destroyed"); |
| 872 | return; |
| 873 | } |
| 874 | |
| 875 | requestLog = mRequestLogs.get(requestId); |
| 876 | if (requestLog != null) { |
| 877 | requestLog.setType(MetricsEvent.TYPE_SUCCESS); |
| 878 | } else { |
| 879 | Slog.w(TAG, "onFillRequestSuccess(): no request log for id " + requestId); |
| 880 | } |
| 881 | if (response == null) { |
| 882 | if (requestLog != null) { |
| 883 | requestLog.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS, -1); |
| 884 | } |
| 885 | processNullResponseLocked(requestId, requestFlags); |
| 886 | return; |
| 887 | } |
| 888 | |
| 889 | fieldClassificationIds = response.getFieldClassificationIds(); |
| 890 | if (fieldClassificationIds != null && !mService.isFieldClassificationEnabledLocked()) { |
| 891 | Slog.w(TAG, "Ignoring " + response + " because field detection is disabled"); |
| 892 | processNullResponseLocked(requestId, requestFlags); |
| 893 | return; |
| 894 | } |
| 895 | } |
| 896 | |
| 897 | mService.setLastResponse(id, response); |
| 898 | |
| 899 | int sessionFinishedState = 0; |
| 900 | final long disableDuration = response.getDisableDuration(); |
| 901 | if (disableDuration > 0) { |
| 902 | final int flags = response.getFlags(); |
| 903 | final boolean disableActivityOnly = |
| 904 | (flags & FillResponse.FLAG_DISABLE_ACTIVITY_ONLY) != 0; |
| 905 | notifyDisableAutofillToClient(disableDuration, |
| 906 | disableActivityOnly ? mComponentName : null); |
| 907 | |
| 908 | if (disableActivityOnly) { |
| 909 | mService.disableAutofillForActivity(mComponentName, disableDuration, |
| 910 | id, mCompatMode); |
| 911 | } else { |
| 912 | mService.disableAutofillForApp(mComponentName.getPackageName(), disableDuration, |
| 913 | id, mCompatMode); |
| 914 | } |
| 915 | |
| 916 | // Although "standard" autofill is disabled, it might still trigger augmented autofill |
| 917 | if (triggerAugmentedAutofillLocked(requestFlags) != null) { |
| 918 | mForAugmentedAutofillOnly = true; |
| 919 | if (sDebug) { |
| 920 | Slog.d(TAG, "Service disabled autofill for " + mComponentName |
| 921 | + ", but session is kept for augmented autofill only"); |
| 922 | } |
| 923 | return; |
| 924 | } |
| 925 | if (sDebug) { |
| 926 | final StringBuilder message = new StringBuilder("Service disabled autofill for ") |
| 927 | .append(mComponentName) |
| 928 | .append(": flags=").append(flags) |
| 929 | .append(", duration="); |
| 930 | TimeUtils.formatDuration(disableDuration, message); |
| 931 | Slog.d(TAG, message.toString()); |
| 932 | } |
| 933 | sessionFinishedState = AutofillManager.STATE_DISABLED_BY_SERVICE; |
| 934 | } |
| 935 | |
| 936 | if (((response.getDatasets() == null || response.getDatasets().isEmpty()) |
| 937 | && response.getAuthentication() == null) |
| 938 | || disableDuration > 0) { |
| 939 | // Response is "empty" from an UI point of view, need to notify client. |
| 940 | notifyUnavailableToClient(sessionFinishedState, /* autofillableIds= */ null); |
| 941 | } |
| 942 | |
| 943 | if (requestLog != null) { |
| 944 | requestLog.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS, |
| 945 | response.getDatasets() == null ? 0 : response.getDatasets().size()); |
| 946 | if (fieldClassificationIds != null) { |
| 947 | requestLog.addTaggedData( |
| 948 | MetricsEvent.FIELD_AUTOFILL_NUM_FIELD_CLASSIFICATION_IDS, |
| 949 | fieldClassificationIds.length); |
| 950 | } |
| 951 | } |
| 952 | |
| 953 | synchronized (mLock) { |
| 954 | processResponseLocked(response, null, requestFlags); |
| 955 | } |
| 956 | } |
| 957 | |
| 958 | // FillServiceCallbacks |
| 959 | @Override |
| 960 | public void onFillRequestFailure(int requestId, @Nullable CharSequence message) { |
| 961 | onFillRequestFailureOrTimeout(requestId, false, message); |
| 962 | } |
| 963 | |
| 964 | // FillServiceCallbacks |
| 965 | @Override |
| 966 | public void onFillRequestTimeout(int requestId) { |
| 967 | onFillRequestFailureOrTimeout(requestId, true, null); |
| 968 | } |
| 969 | |
| 970 | private void onFillRequestFailureOrTimeout(int requestId, boolean timedOut, |
| 971 | @Nullable CharSequence message) { |
| 972 | boolean showMessage = !TextUtils.isEmpty(message); |
| 973 | synchronized (mLock) { |
| 974 | if (mDestroyed) { |
| 975 | Slog.w(TAG, "Call to Session#onFillRequestFailureOrTimeout(req=" + requestId |
| 976 | + ") rejected - session: " + id + " destroyed"); |
| 977 | return; |
| 978 | } |
| 979 | if (sDebug) { |
| 980 | Slog.d(TAG, "finishing session due to service " |
| 981 | + (timedOut ? "timeout" : "failure")); |
| 982 | } |
| 983 | mService.resetLastResponse(); |
| 984 | final LogMaker requestLog = mRequestLogs.get(requestId); |
| 985 | if (requestLog == null) { |
| 986 | Slog.w(TAG, "onFillRequestFailureOrTimeout(): no log for id " + requestId); |
| 987 | } else { |
| 988 | requestLog.setType(timedOut ? MetricsEvent.TYPE_CLOSE : MetricsEvent.TYPE_FAILURE); |
| 989 | } |
| 990 | if (showMessage) { |
| 991 | final int targetSdk = mService.getTargedSdkLocked(); |
| 992 | if (targetSdk >= Build.VERSION_CODES.Q) { |
| 993 | showMessage = false; |
| 994 | Slog.w(TAG, "onFillRequestFailureOrTimeout(): not showing '" + message |
| 995 | + "' because service's targetting API " + targetSdk); |
| 996 | } |
| 997 | if (message != null) { |
| 998 | requestLog.addTaggedData(MetricsEvent.FIELD_AUTOFILL_TEXT_LEN, |
| 999 | message.length()); |
| 1000 | } |
| 1001 | } |
| 1002 | } |
| 1003 | notifyUnavailableToClient(AutofillManager.STATE_UNKNOWN_FAILED, |
| 1004 | /* autofillableIds= */ null); |
| 1005 | if (showMessage) { |
| 1006 | getUiForShowing().showError(message, this); |
| 1007 | } |
| 1008 | removeSelf(); |
| 1009 | } |
| 1010 | |
| 1011 | // FillServiceCallbacks |
| 1012 | @Override |
| 1013 | public void onSaveRequestSuccess(@NonNull String servicePackageName, |
| 1014 | @Nullable IntentSender intentSender) { |
| 1015 | synchronized (mLock) { |
| 1016 | mIsSaving = false; |
| 1017 | |
| 1018 | if (mDestroyed) { |
| 1019 | Slog.w(TAG, "Call to Session#onSaveRequestSuccess() rejected - session: " |
| 1020 | + id + " destroyed"); |
| 1021 | return; |
| 1022 | } |
| 1023 | } |
| 1024 | LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST, servicePackageName) |
| 1025 | .setType(intentSender == null ? MetricsEvent.TYPE_SUCCESS : MetricsEvent.TYPE_OPEN); |
| 1026 | mMetricsLogger.write(log); |
| 1027 | if (intentSender != null) { |
| 1028 | if (sDebug) Slog.d(TAG, "Starting intent sender on save()"); |
| 1029 | startIntentSenderAndFinishSession(intentSender); |
| 1030 | } |
| 1031 | |
| 1032 | // Nothing left to do... |
| 1033 | removeSelf(); |
| 1034 | } |
| 1035 | |
| 1036 | // FillServiceCallbacks |
| 1037 | @Override |
| 1038 | public void onSaveRequestFailure(@Nullable CharSequence message, |
| 1039 | @NonNull String servicePackageName) { |
| 1040 | boolean showMessage = !TextUtils.isEmpty(message); |
| 1041 | synchronized (mLock) { |
| 1042 | mIsSaving = false; |
| 1043 | |
| 1044 | if (mDestroyed) { |
| 1045 | Slog.w(TAG, "Call to Session#onSaveRequestFailure() rejected - session: " |
| 1046 | + id + " destroyed"); |
| 1047 | return; |
| 1048 | } |
| 1049 | if (showMessage) { |
| 1050 | final int targetSdk = mService.getTargedSdkLocked(); |
| 1051 | if (targetSdk >= Build.VERSION_CODES.Q) { |
| 1052 | showMessage = false; |
| 1053 | Slog.w(TAG, "onSaveRequestFailure(): not showing '" + message |
| 1054 | + "' because service's targetting API " + targetSdk); |
| 1055 | } |
| 1056 | } |
| 1057 | } |
| 1058 | final LogMaker log = |
| 1059 | newLogMaker(MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST, servicePackageName) |
| 1060 | .setType(MetricsEvent.TYPE_FAILURE); |
| 1061 | if (message != null) { |
| 1062 | log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_TEXT_LEN, message.length()); |
| 1063 | } |
| 1064 | mMetricsLogger.write(log); |
| 1065 | |
| 1066 | if (showMessage) { |
| 1067 | getUiForShowing().showError(message, this); |
| 1068 | } |
| 1069 | removeSelf(); |
| 1070 | } |
| 1071 | |
| 1072 | /** |
| 1073 | * Gets the {@link FillContext} for a request. |
| 1074 | * |
| 1075 | * @param requestId The id of the request |
| 1076 | * |
| 1077 | * @return The context or {@code null} if there is no context |
| 1078 | */ |
| 1079 | @GuardedBy("mLock") |
| 1080 | @Nullable private FillContext getFillContextByRequestIdLocked(int requestId) { |
| 1081 | if (mContexts == null) { |
| 1082 | return null; |
| 1083 | } |
| 1084 | |
| 1085 | int numContexts = mContexts.size(); |
| 1086 | for (int i = 0; i < numContexts; i++) { |
| 1087 | FillContext context = mContexts.get(i); |
| 1088 | |
| 1089 | if (context.getRequestId() == requestId) { |
| 1090 | return context; |
| 1091 | } |
| 1092 | } |
| 1093 | |
| 1094 | return null; |
| 1095 | } |
| 1096 | |
| 1097 | // FillServiceCallbacks |
| 1098 | @Override |
| 1099 | public void authenticate(int requestId, int datasetIndex, IntentSender intent, Bundle extras, |
| 1100 | boolean authenticateInline) { |
| 1101 | if (sDebug) { |
| 1102 | Slog.d(TAG, "authenticate(): requestId=" + requestId + "; datasetIdx=" + datasetIndex |
| 1103 | + "; intentSender=" + intent); |
| 1104 | } |
| 1105 | final Intent fillInIntent; |
| 1106 | synchronized (mLock) { |
| 1107 | if (mDestroyed) { |
| 1108 | Slog.w(TAG, "Call to Session#authenticate() rejected - session: " |
| 1109 | + id + " destroyed"); |
| 1110 | return; |
| 1111 | } |
| 1112 | fillInIntent = createAuthFillInIntentLocked(requestId, extras); |
| 1113 | if (fillInIntent == null) { |
| 1114 | forceRemoveSelfLocked(); |
| 1115 | return; |
| 1116 | } |
| 1117 | } |
| 1118 | |
| 1119 | mService.setAuthenticationSelected(id, mClientState); |
| 1120 | |
| 1121 | final int authenticationId = AutofillManager.makeAuthenticationId(requestId, datasetIndex); |
| 1122 | mHandler.sendMessage(obtainMessage( |
| 1123 | Session::startAuthentication, |
| 1124 | this, authenticationId, intent, fillInIntent, authenticateInline)); |
| 1125 | } |
| 1126 | |
| 1127 | // VultureCallback |
| 1128 | @Override |
| 1129 | public void onServiceDied(@NonNull RemoteFillService service) { |
| 1130 | Slog.w(TAG, "removing session because service died"); |
| 1131 | forceRemoveSelfLocked(); |
| 1132 | } |
| 1133 | |
| 1134 | // AutoFillUiCallback |
| 1135 | @Override |
| 1136 | public void fill(int requestId, int datasetIndex, Dataset dataset) { |
| 1137 | synchronized (mLock) { |
| 1138 | if (mDestroyed) { |
| 1139 | Slog.w(TAG, "Call to Session#fill() rejected - session: " |
| 1140 | + id + " destroyed"); |
| 1141 | return; |
| 1142 | } |
| 1143 | } |
| 1144 | mHandler.sendMessage(obtainMessage( |
| 1145 | Session::autoFill, |
| 1146 | this, requestId, datasetIndex, dataset, true)); |
| 1147 | } |
| 1148 | |
| 1149 | // AutoFillUiCallback |
| 1150 | @Override |
| 1151 | public void save() { |
| 1152 | synchronized (mLock) { |
| 1153 | if (mDestroyed) { |
| 1154 | Slog.w(TAG, "Call to Session#save() rejected - session: " |
| 1155 | + id + " destroyed"); |
| 1156 | return; |
| 1157 | } |
| 1158 | } |
| 1159 | mHandler.sendMessage(obtainMessage( |
| 1160 | AutofillManagerServiceImpl::handleSessionSave, |
| 1161 | mService, this)); |
| 1162 | } |
| 1163 | |
| 1164 | // AutoFillUiCallback |
| 1165 | @Override |
| 1166 | public void cancelSave() { |
| 1167 | synchronized (mLock) { |
| 1168 | mIsSaving = false; |
| 1169 | |
| 1170 | if (mDestroyed) { |
| 1171 | Slog.w(TAG, "Call to Session#cancelSave() rejected - session: " |
| 1172 | + id + " destroyed"); |
| 1173 | return; |
| 1174 | } |
| 1175 | } |
| 1176 | mHandler.sendMessage(obtainMessage( |
| 1177 | Session::removeSelf, this)); |
| 1178 | } |
| 1179 | |
| 1180 | // AutoFillUiCallback |
| 1181 | @Override |
| 1182 | public void requestShowFillUi(AutofillId id, int width, int height, |
| 1183 | IAutofillWindowPresenter presenter) { |
| 1184 | synchronized (mLock) { |
| 1185 | if (mDestroyed) { |
| 1186 | Slog.w(TAG, "Call to Session#requestShowFillUi() rejected - session: " |
| 1187 | + id + " destroyed"); |
| 1188 | return; |
| 1189 | } |
| 1190 | if (id.equals(mCurrentViewId)) { |
| 1191 | try { |
| 1192 | final ViewState view = mViewStates.get(id); |
| 1193 | mClient.requestShowFillUi(this.id, id, width, height, view.getVirtualBounds(), |
| 1194 | presenter); |
| 1195 | } catch (RemoteException e) { |
| 1196 | Slog.e(TAG, "Error requesting to show fill UI", e); |
| 1197 | } |
| 1198 | } else { |
| 1199 | if (sDebug) { |
| 1200 | Slog.d(TAG, "Do not show full UI on " + id + " as it is not the current view (" |
| 1201 | + mCurrentViewId + ") anymore"); |
| 1202 | } |
| 1203 | } |
| 1204 | } |
| 1205 | } |
| 1206 | |
| 1207 | @Override |
| 1208 | public void dispatchUnhandledKey(AutofillId id, KeyEvent keyEvent) { |
| 1209 | synchronized (mLock) { |
| 1210 | if (mDestroyed) { |
| 1211 | Slog.w(TAG, "Call to Session#dispatchUnhandledKey() rejected - session: " |
| 1212 | + id + " destroyed"); |
| 1213 | return; |
| 1214 | } |
| 1215 | if (id.equals(mCurrentViewId)) { |
| 1216 | try { |
| 1217 | mClient.dispatchUnhandledKey(this.id, id, keyEvent); |
| 1218 | } catch (RemoteException e) { |
| 1219 | Slog.e(TAG, "Error requesting to dispatch unhandled key", e); |
| 1220 | } |
| 1221 | } else { |
| 1222 | Slog.w(TAG, "Do not dispatch unhandled key on " + id |
| 1223 | + " as it is not the current view (" + mCurrentViewId + ") anymore"); |
| 1224 | } |
| 1225 | } |
| 1226 | } |
| 1227 | |
| 1228 | // AutoFillUiCallback |
| 1229 | @Override |
| 1230 | public void requestHideFillUi(AutofillId id) { |
| 1231 | synchronized (mLock) { |
| 1232 | // NOTE: We allow this call in a destroyed state as the UI is |
| 1233 | // asked to go away after we get destroyed, so let it do that. |
| 1234 | try { |
| 1235 | mClient.requestHideFillUi(this.id, id); |
| 1236 | } catch (RemoteException e) { |
| 1237 | Slog.e(TAG, "Error requesting to hide fill UI", e); |
| 1238 | } |
| 1239 | |
| 1240 | mInlineSessionController.hideInlineSuggestionsUiLocked(id); |
| 1241 | } |
| 1242 | } |
| 1243 | |
| 1244 | // AutoFillUiCallback |
| 1245 | @Override |
| 1246 | public void cancelSession() { |
| 1247 | synchronized (mLock) { |
| 1248 | removeSelfLocked(); |
| 1249 | } |
| 1250 | } |
| 1251 | |
| 1252 | // AutoFillUiCallback |
| 1253 | @Override |
| 1254 | public void startIntentSenderAndFinishSession(IntentSender intentSender) { |
| 1255 | startIntentSender(intentSender, null); |
| 1256 | } |
| 1257 | |
| 1258 | // AutoFillUiCallback |
| 1259 | @Override |
| 1260 | public void startIntentSender(IntentSender intentSender, Intent intent) { |
| 1261 | synchronized (mLock) { |
| 1262 | if (mDestroyed) { |
| 1263 | Slog.w(TAG, "Call to Session#startIntentSender() rejected - session: " |
| 1264 | + id + " destroyed"); |
| 1265 | return; |
| 1266 | } |
| 1267 | if (intent == null) { |
| 1268 | removeSelfLocked(); |
| 1269 | } |
| 1270 | } |
| 1271 | mHandler.sendMessage(obtainMessage( |
| 1272 | Session::doStartIntentSender, |
| 1273 | this, intentSender, intent)); |
| 1274 | } |
| 1275 | |
| 1276 | private void doStartIntentSender(IntentSender intentSender, Intent intent) { |
| 1277 | try { |
| 1278 | synchronized (mLock) { |
| 1279 | mClient.startIntentSender(intentSender, intent); |
| 1280 | } |
| 1281 | } catch (RemoteException e) { |
| 1282 | Slog.e(TAG, "Error launching auth intent", e); |
| 1283 | } |
| 1284 | } |
| 1285 | |
| 1286 | @GuardedBy("mLock") |
| 1287 | void setAuthenticationResultLocked(Bundle data, int authenticationId) { |
| 1288 | if (mDestroyed) { |
| 1289 | Slog.w(TAG, "Call to Session#setAuthenticationResultLocked() rejected - session: " |
| 1290 | + id + " destroyed"); |
| 1291 | return; |
| 1292 | } |
| 1293 | if (mResponses == null) { |
| 1294 | // Typically happens when app explicitly called cancel() while the service was showing |
| 1295 | // the auth UI. |
| 1296 | Slog.w(TAG, "setAuthenticationResultLocked(" + authenticationId + "): no responses"); |
| 1297 | removeSelf(); |
| 1298 | return; |
| 1299 | } |
| 1300 | final int requestId = AutofillManager.getRequestIdFromAuthenticationId(authenticationId); |
| 1301 | final FillResponse authenticatedResponse = mResponses.get(requestId); |
| 1302 | if (authenticatedResponse == null || data == null) { |
| 1303 | Slog.w(TAG, "no authenticated response"); |
| 1304 | removeSelf(); |
| 1305 | return; |
| 1306 | } |
| 1307 | |
| 1308 | final int datasetIdx = AutofillManager.getDatasetIdFromAuthenticationId( |
| 1309 | authenticationId); |
| 1310 | // Authenticated a dataset - reset view state regardless if we got a response or a dataset |
| 1311 | if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) { |
| 1312 | final Dataset dataset = authenticatedResponse.getDatasets().get(datasetIdx); |
| 1313 | if (dataset == null) { |
| 1314 | Slog.w(TAG, "no dataset with index " + datasetIdx + " on fill response"); |
| 1315 | removeSelf(); |
| 1316 | return; |
| 1317 | } |
| 1318 | } |
| 1319 | |
| 1320 | // The client becomes invisible for the authentication, the response is effective. |
| 1321 | mExpiredResponse = false; |
| 1322 | |
| 1323 | final Parcelable result = data.getParcelable(AutofillManager.EXTRA_AUTHENTICATION_RESULT); |
| 1324 | final Bundle newClientState = data.getBundle(AutofillManager.EXTRA_CLIENT_STATE); |
| 1325 | if (sDebug) { |
| 1326 | Slog.d(TAG, "setAuthenticationResultLocked(): result=" + result |
| 1327 | + ", clientState=" + newClientState + ", authenticationId=" + authenticationId); |
| 1328 | } |
| 1329 | if (result instanceof FillResponse) { |
| 1330 | logAuthenticationStatusLocked(requestId, MetricsEvent.AUTOFILL_AUTHENTICATED); |
| 1331 | replaceResponseLocked(authenticatedResponse, (FillResponse) result, newClientState); |
| 1332 | } else if (result instanceof Dataset) { |
| 1333 | if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) { |
| 1334 | logAuthenticationStatusLocked(requestId, |
| 1335 | MetricsEvent.AUTOFILL_DATASET_AUTHENTICATED); |
| 1336 | if (newClientState != null) { |
| 1337 | if (sDebug) Slog.d(TAG, "Updating client state from auth dataset"); |
| 1338 | mClientState = newClientState; |
| 1339 | } |
| 1340 | final Dataset dataset = (Dataset) result; |
| 1341 | authenticatedResponse.getDatasets().set(datasetIdx, dataset); |
| 1342 | autoFill(requestId, datasetIdx, dataset, false); |
| 1343 | } else { |
| 1344 | Slog.w(TAG, "invalid index (" + datasetIdx + ") for authentication id " |
| 1345 | + authenticationId); |
| 1346 | logAuthenticationStatusLocked(requestId, |
| 1347 | MetricsEvent.AUTOFILL_INVALID_DATASET_AUTHENTICATION); |
| 1348 | } |
| 1349 | } else { |
| 1350 | if (result != null) { |
| 1351 | Slog.w(TAG, "service returned invalid auth type: " + result); |
| 1352 | } |
| 1353 | logAuthenticationStatusLocked(requestId, |
| 1354 | MetricsEvent.AUTOFILL_INVALID_AUTHENTICATION); |
| 1355 | processNullResponseLocked(requestId, 0); |
| 1356 | } |
| 1357 | } |
| 1358 | |
| 1359 | @GuardedBy("mLock") |
| 1360 | void setHasCallbackLocked(boolean hasIt) { |
| 1361 | if (mDestroyed) { |
| 1362 | Slog.w(TAG, "Call to Session#setHasCallbackLocked() rejected - session: " |
| 1363 | + id + " destroyed"); |
| 1364 | return; |
| 1365 | } |
| 1366 | mHasCallback = hasIt; |
| 1367 | } |
| 1368 | |
| 1369 | @GuardedBy("mLock") |
| 1370 | @Nullable |
| 1371 | private FillResponse getLastResponseLocked(@Nullable String logPrefixFmt) { |
| 1372 | final String logPrefix = sDebug && logPrefixFmt != null |
| 1373 | ? String.format(logPrefixFmt, this.id) |
| 1374 | : null; |
| 1375 | if (mContexts == null) { |
| 1376 | if (logPrefix != null) Slog.d(TAG, logPrefix + ": no contexts"); |
| 1377 | return null; |
| 1378 | } |
| 1379 | if (mResponses == null) { |
| 1380 | // Happens when the activity / session was finished before the service replied, or |
| 1381 | // when the service cannot autofill it (and returned a null response). |
| 1382 | if (sVerbose && logPrefix != null) { |
| 1383 | Slog.v(TAG, logPrefix + ": no responses on session"); |
| 1384 | } |
| 1385 | return null; |
| 1386 | } |
| 1387 | |
| 1388 | final int lastResponseIdx = getLastResponseIndexLocked(); |
| 1389 | if (lastResponseIdx < 0) { |
| 1390 | if (logPrefix != null) { |
| 1391 | Slog.w(TAG, logPrefix + ": did not get last response. mResponses=" + mResponses |
| 1392 | + ", mViewStates=" + mViewStates); |
| 1393 | } |
| 1394 | return null; |
| 1395 | } |
| 1396 | |
| 1397 | final FillResponse response = mResponses.valueAt(lastResponseIdx); |
| 1398 | if (sVerbose && logPrefix != null) { |
| 1399 | Slog.v(TAG, logPrefix + ": mResponses=" + mResponses + ", mContexts=" + mContexts |
| 1400 | + ", mViewStates=" + mViewStates); |
| 1401 | } |
| 1402 | return response; |
| 1403 | } |
| 1404 | |
| 1405 | @GuardedBy("mLock") |
| 1406 | @Nullable |
| 1407 | private SaveInfo getSaveInfoLocked() { |
| 1408 | final FillResponse response = getLastResponseLocked(null); |
| 1409 | return response == null ? null : response.getSaveInfo(); |
| 1410 | } |
| 1411 | |
| 1412 | @GuardedBy("mLock") |
| 1413 | int getSaveInfoFlagsLocked() { |
| 1414 | final SaveInfo saveInfo = getSaveInfoLocked(); |
| 1415 | return saveInfo == null ? 0 : saveInfo.getFlags(); |
| 1416 | } |
| 1417 | |
| 1418 | /** |
| 1419 | * Generates a {@link android.service.autofill.FillEventHistory.Event#TYPE_CONTEXT_COMMITTED} |
| 1420 | * when necessary. |
| 1421 | */ |
| 1422 | public void logContextCommitted() { |
| 1423 | mHandler.sendMessage(obtainMessage(Session::handleLogContextCommitted, this)); |
| 1424 | } |
| 1425 | |
| 1426 | private void handleLogContextCommitted() { |
| 1427 | final FillResponse lastResponse; |
| 1428 | synchronized (mLock) { |
| 1429 | lastResponse = getLastResponseLocked("logContextCommited(%s)"); |
| 1430 | } |
| 1431 | |
| 1432 | if (lastResponse == null) { |
| 1433 | Slog.w(TAG, "handleLogContextCommitted(): last response is null"); |
| 1434 | return; |
| 1435 | } |
| 1436 | |
| 1437 | // Merge UserData if necessary. |
| 1438 | // Fields in packageUserData will override corresponding fields in genericUserData. |
| 1439 | final UserData genericUserData = mService.getUserData(); |
| 1440 | final UserData packageUserData = lastResponse.getUserData(); |
| 1441 | final FieldClassificationUserData userData; |
| 1442 | if (packageUserData == null && genericUserData == null) { |
| 1443 | userData = null; |
| 1444 | } else if (packageUserData != null && genericUserData != null) { |
| 1445 | userData = new CompositeUserData(genericUserData, packageUserData); |
| 1446 | } else if (packageUserData != null) { |
| 1447 | userData = packageUserData; |
| 1448 | } else { |
| 1449 | userData = mService.getUserData(); |
| 1450 | } |
| 1451 | |
| 1452 | final FieldClassificationStrategy fcStrategy = mService.getFieldClassificationStrategy(); |
| 1453 | |
| 1454 | // Sets field classification scores |
| 1455 | if (userData != null && fcStrategy != null) { |
| 1456 | logFieldClassificationScore(fcStrategy, userData); |
| 1457 | } else { |
| 1458 | logContextCommitted(null, null); |
| 1459 | } |
| 1460 | } |
| 1461 | |
| 1462 | private void logContextCommitted(@Nullable ArrayList<AutofillId> detectedFieldIds, |
| 1463 | @Nullable ArrayList<FieldClassification> detectedFieldClassifications) { |
| 1464 | synchronized (mLock) { |
| 1465 | logContextCommittedLocked(detectedFieldIds, detectedFieldClassifications); |
| 1466 | } |
| 1467 | } |
| 1468 | |
| 1469 | @GuardedBy("mLock") |
| 1470 | private void logContextCommittedLocked(@Nullable ArrayList<AutofillId> detectedFieldIds, |
| 1471 | @Nullable ArrayList<FieldClassification> detectedFieldClassifications) { |
| 1472 | final FillResponse lastResponse = getLastResponseLocked("logContextCommited(%s)"); |
| 1473 | if (lastResponse == null) return; |
| 1474 | |
| 1475 | final int flags = lastResponse.getFlags(); |
| 1476 | if ((flags & FillResponse.FLAG_TRACK_CONTEXT_COMMITED) == 0) { |
| 1477 | if (sVerbose) Slog.v(TAG, "logContextCommittedLocked(): ignored by flags " + flags); |
| 1478 | return; |
| 1479 | } |
| 1480 | |
| 1481 | ArraySet<String> ignoredDatasets = null; |
| 1482 | ArrayList<AutofillId> changedFieldIds = null; |
| 1483 | ArrayList<String> changedDatasetIds = null; |
| 1484 | ArrayMap<AutofillId, ArraySet<String>> manuallyFilledIds = null; |
| 1485 | |
| 1486 | boolean hasAtLeastOneDataset = false; |
| 1487 | final int responseCount = mResponses.size(); |
| 1488 | for (int i = 0; i < responseCount; i++) { |
| 1489 | final FillResponse response = mResponses.valueAt(i); |
| 1490 | final List<Dataset> datasets = response.getDatasets(); |
| 1491 | if (datasets == null || datasets.isEmpty()) { |
| 1492 | if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + i); |
| 1493 | } else { |
| 1494 | for (int j = 0; j < datasets.size(); j++) { |
| 1495 | final Dataset dataset = datasets.get(j); |
| 1496 | final String datasetId = dataset.getId(); |
| 1497 | if (datasetId == null) { |
| 1498 | if (sVerbose) { |
| 1499 | Slog.v(TAG, "logContextCommitted() skipping idless dataset " + dataset); |
| 1500 | } |
| 1501 | } else { |
| 1502 | hasAtLeastOneDataset = true; |
| 1503 | if (mSelectedDatasetIds == null |
| 1504 | || !mSelectedDatasetIds.contains(datasetId)) { |
| 1505 | if (sVerbose) Slog.v(TAG, "adding ignored dataset " + datasetId); |
| 1506 | if (ignoredDatasets == null) { |
| 1507 | ignoredDatasets = new ArraySet<>(); |
| 1508 | } |
| 1509 | ignoredDatasets.add(datasetId); |
| 1510 | } |
| 1511 | } |
| 1512 | } |
| 1513 | } |
| 1514 | } |
| 1515 | final AutofillId[] fieldClassificationIds = lastResponse.getFieldClassificationIds(); |
| 1516 | |
| 1517 | if (!hasAtLeastOneDataset && fieldClassificationIds == null) { |
| 1518 | if (sVerbose) { |
| 1519 | Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets nor fields " |
| 1520 | + "classification ids)"); |
| 1521 | } |
| 1522 | return; |
| 1523 | } |
| 1524 | |
| 1525 | for (int i = 0; i < mViewStates.size(); i++) { |
| 1526 | final ViewState viewState = mViewStates.valueAt(i); |
| 1527 | final int state = viewState.getState(); |
| 1528 | |
| 1529 | // When value changed, we need to log if it was: |
| 1530 | // - autofilled -> changedDatasetIds |
| 1531 | // - not autofilled but matches a dataset value -> manuallyFilledIds |
| 1532 | if ((state & ViewState.STATE_CHANGED) != 0) { |
| 1533 | // Check if autofilled value was changed |
| 1534 | if ((state & ViewState.STATE_AUTOFILLED_ONCE) != 0) { |
| 1535 | final String datasetId = viewState.getDatasetId(); |
| 1536 | if (datasetId == null) { |
| 1537 | // Sanity check - should never happen. |
| 1538 | Slog.w(TAG, "logContextCommitted(): no dataset id on " + viewState); |
| 1539 | continue; |
| 1540 | } |
| 1541 | |
| 1542 | // Must first check if final changed value is not the same as value sent by |
| 1543 | // service. |
| 1544 | final AutofillValue autofilledValue = viewState.getAutofilledValue(); |
| 1545 | final AutofillValue currentValue = viewState.getCurrentValue(); |
| 1546 | if (autofilledValue != null && autofilledValue.equals(currentValue)) { |
| 1547 | if (sDebug) { |
| 1548 | Slog.d(TAG, "logContextCommitted(): ignoring changed " + viewState |
| 1549 | + " because it has same value that was autofilled"); |
| 1550 | } |
| 1551 | continue; |
| 1552 | } |
| 1553 | |
| 1554 | if (sDebug) { |
| 1555 | Slog.d(TAG, "logContextCommitted() found changed state: " + viewState); |
| 1556 | } |
| 1557 | if (changedFieldIds == null) { |
| 1558 | changedFieldIds = new ArrayList<>(); |
| 1559 | changedDatasetIds = new ArrayList<>(); |
| 1560 | } |
| 1561 | changedFieldIds.add(viewState.id); |
| 1562 | changedDatasetIds.add(datasetId); |
| 1563 | } else { |
| 1564 | final AutofillValue currentValue = viewState.getCurrentValue(); |
| 1565 | if (currentValue == null) { |
| 1566 | if (sDebug) { |
| 1567 | Slog.d(TAG, "logContextCommitted(): skipping view without current " |
| 1568 | + "value ( " + viewState + ")"); |
| 1569 | } |
| 1570 | continue; |
| 1571 | } |
| 1572 | // Check if value match a dataset. |
| 1573 | if (hasAtLeastOneDataset) { |
| 1574 | for (int j = 0; j < responseCount; j++) { |
| 1575 | final FillResponse response = mResponses.valueAt(j); |
| 1576 | final List<Dataset> datasets = response.getDatasets(); |
| 1577 | if (datasets == null || datasets.isEmpty()) { |
| 1578 | if (sVerbose) { |
| 1579 | Slog.v(TAG, "logContextCommitted() no datasets at " + j); |
| 1580 | } |
| 1581 | } else { |
| 1582 | for (int k = 0; k < datasets.size(); k++) { |
| 1583 | final Dataset dataset = datasets.get(k); |
| 1584 | final String datasetId = dataset.getId(); |
| 1585 | if (datasetId == null) { |
| 1586 | if (sVerbose) { |
| 1587 | Slog.v(TAG, "logContextCommitted() skipping idless " |
| 1588 | + "dataset " + dataset); |
| 1589 | } |
| 1590 | } else { |
| 1591 | final ArrayList<AutofillValue> values = |
| 1592 | dataset.getFieldValues(); |
| 1593 | for (int l = 0; l < values.size(); l++) { |
| 1594 | final AutofillValue candidate = values.get(l); |
| 1595 | if (currentValue.equals(candidate)) { |
| 1596 | if (sDebug) { |
| 1597 | Slog.d(TAG, "field " + viewState.id + " was " |
| 1598 | + "manually filled with value set by " |
| 1599 | + "dataset " + datasetId); |
| 1600 | } |
| 1601 | if (manuallyFilledIds == null) { |
| 1602 | manuallyFilledIds = new ArrayMap<>(); |
| 1603 | } |
| 1604 | ArraySet<String> datasetIds = |
| 1605 | manuallyFilledIds.get(viewState.id); |
| 1606 | if (datasetIds == null) { |
| 1607 | datasetIds = new ArraySet<>(1); |
| 1608 | manuallyFilledIds.put(viewState.id, datasetIds); |
| 1609 | } |
| 1610 | datasetIds.add(datasetId); |
| 1611 | } |
| 1612 | } // for l |
| 1613 | if (mSelectedDatasetIds == null |
| 1614 | || !mSelectedDatasetIds.contains(datasetId)) { |
| 1615 | if (sVerbose) { |
| 1616 | Slog.v(TAG, "adding ignored dataset " + datasetId); |
| 1617 | } |
| 1618 | if (ignoredDatasets == null) { |
| 1619 | ignoredDatasets = new ArraySet<>(); |
| 1620 | } |
| 1621 | ignoredDatasets.add(datasetId); |
| 1622 | } // if |
| 1623 | } // if |
| 1624 | } // for k |
| 1625 | } // else |
| 1626 | } // for j |
| 1627 | } |
| 1628 | |
| 1629 | } // else |
| 1630 | } // else |
| 1631 | } |
| 1632 | |
| 1633 | ArrayList<AutofillId> manuallyFilledFieldIds = null; |
| 1634 | ArrayList<ArrayList<String>> manuallyFilledDatasetIds = null; |
| 1635 | |
| 1636 | // Must "flatten" the map to the parcelable collection primitives |
| 1637 | if (manuallyFilledIds != null) { |
| 1638 | final int size = manuallyFilledIds.size(); |
| 1639 | manuallyFilledFieldIds = new ArrayList<>(size); |
| 1640 | manuallyFilledDatasetIds = new ArrayList<>(size); |
| 1641 | for (int i = 0; i < size; i++) { |
| 1642 | final AutofillId fieldId = manuallyFilledIds.keyAt(i); |
| 1643 | final ArraySet<String> datasetIds = manuallyFilledIds.valueAt(i); |
| 1644 | manuallyFilledFieldIds.add(fieldId); |
| 1645 | manuallyFilledDatasetIds.add(new ArrayList<>(datasetIds)); |
| 1646 | } |
| 1647 | } |
| 1648 | |
| 1649 | mService.logContextCommittedLocked(id, mClientState, mSelectedDatasetIds, |
| 1650 | ignoredDatasets, changedFieldIds, changedDatasetIds, |
| 1651 | manuallyFilledFieldIds, manuallyFilledDatasetIds, detectedFieldIds, |
| 1652 | detectedFieldClassifications, mComponentName, mCompatMode); |
| 1653 | } |
| 1654 | |
| 1655 | /** |
| 1656 | * Adds the matches to {@code detectedFieldsIds} and {@code detectedFieldClassifications} for |
| 1657 | * {@code fieldId} based on its {@code currentValue} and {@code userData}. |
| 1658 | */ |
| 1659 | private void logFieldClassificationScore(@NonNull FieldClassificationStrategy fcStrategy, |
| 1660 | @NonNull FieldClassificationUserData userData) { |
| 1661 | |
| 1662 | final String[] userValues = userData.getValues(); |
| 1663 | final String[] categoryIds = userData.getCategoryIds(); |
| 1664 | |
| 1665 | final String defaultAlgorithm = userData.getFieldClassificationAlgorithm(); |
| 1666 | final Bundle defaultArgs = userData.getDefaultFieldClassificationArgs(); |
| 1667 | |
| 1668 | final ArrayMap<String, String> algorithms = userData.getFieldClassificationAlgorithms(); |
| 1669 | final ArrayMap<String, Bundle> args = userData.getFieldClassificationArgs(); |
| 1670 | |
| 1671 | // Sanity check |
| 1672 | if (userValues == null || categoryIds == null || userValues.length != categoryIds.length) { |
| 1673 | final int valuesLength = userValues == null ? -1 : userValues.length; |
| 1674 | final int idsLength = categoryIds == null ? -1 : categoryIds.length; |
| 1675 | Slog.w(TAG, "setScores(): user data mismatch: values.length = " |
| 1676 | + valuesLength + ", ids.length = " + idsLength); |
| 1677 | return; |
| 1678 | } |
| 1679 | |
| 1680 | final int maxFieldsSize = UserData.getMaxFieldClassificationIdsSize(); |
| 1681 | |
| 1682 | final ArrayList<AutofillId> detectedFieldIds = new ArrayList<>(maxFieldsSize); |
| 1683 | final ArrayList<FieldClassification> detectedFieldClassifications = new ArrayList<>( |
| 1684 | maxFieldsSize); |
| 1685 | |
| 1686 | final Collection<ViewState> viewStates; |
| 1687 | synchronized (mLock) { |
| 1688 | viewStates = mViewStates.values(); |
| 1689 | } |
| 1690 | |
| 1691 | final int viewsSize = viewStates.size(); |
| 1692 | |
| 1693 | // First, we get all scores. |
| 1694 | final AutofillId[] autofillIds = new AutofillId[viewsSize]; |
| 1695 | final ArrayList<AutofillValue> currentValues = new ArrayList<>(viewsSize); |
| 1696 | int k = 0; |
| 1697 | for (ViewState viewState : viewStates) { |
| 1698 | currentValues.add(viewState.getCurrentValue()); |
| 1699 | autofillIds[k++] = viewState.id; |
| 1700 | } |
| 1701 | |
| 1702 | // Then use the results, asynchronously |
| 1703 | final RemoteCallback callback = new RemoteCallback((result) -> { |
| 1704 | if (result == null) { |
| 1705 | if (sDebug) Slog.d(TAG, "setFieldClassificationScore(): no results"); |
| 1706 | logContextCommitted(null, null); |
| 1707 | return; |
| 1708 | } |
| 1709 | final Scores scores = result.getParcelable(EXTRA_SCORES); |
| 1710 | if (scores == null) { |
| 1711 | Slog.w(TAG, "No field classification score on " + result); |
| 1712 | return; |
| 1713 | } |
| 1714 | int i = 0, j = 0; |
| 1715 | try { |
| 1716 | // Iteract over all autofill fields first |
| 1717 | for (i = 0; i < viewsSize; i++) { |
| 1718 | final AutofillId autofillId = autofillIds[i]; |
| 1719 | |
| 1720 | // Search the best scores for each category (as some categories could have |
| 1721 | // multiple user values |
| 1722 | ArrayMap<String, Float> scoresByField = null; |
| 1723 | for (j = 0; j < userValues.length; j++) { |
| 1724 | final String categoryId = categoryIds[j]; |
| 1725 | final float score = scores.scores[i][j]; |
| 1726 | if (score > 0) { |
| 1727 | if (scoresByField == null) { |
| 1728 | scoresByField = new ArrayMap<>(userValues.length); |
| 1729 | } |
| 1730 | final Float currentScore = scoresByField.get(categoryId); |
| 1731 | if (currentScore != null && currentScore > score) { |
| 1732 | if (sVerbose) { |
| 1733 | Slog.v(TAG, "skipping score " + score |
| 1734 | + " because it's less than " + currentScore); |
| 1735 | } |
| 1736 | continue; |
| 1737 | } |
| 1738 | if (sVerbose) { |
| 1739 | Slog.v(TAG, "adding score " + score + " at index " + j + " and id " |
| 1740 | + autofillId); |
| 1741 | } |
| 1742 | scoresByField.put(categoryId, score); |
| 1743 | } else if (sVerbose) { |
| 1744 | Slog.v(TAG, "skipping score 0 at index " + j + " and id " + autofillId); |
| 1745 | } |
| 1746 | } |
| 1747 | if (scoresByField == null) { |
| 1748 | if (sVerbose) Slog.v(TAG, "no score for autofillId=" + autofillId); |
| 1749 | continue; |
| 1750 | } |
| 1751 | |
| 1752 | // Then create the matches for that autofill id |
| 1753 | final ArrayList<Match> matches = new ArrayList<>(scoresByField.size()); |
| 1754 | for (j = 0; j < scoresByField.size(); j++) { |
| 1755 | final String fieldId = scoresByField.keyAt(j); |
| 1756 | final float score = scoresByField.valueAt(j); |
| 1757 | matches.add(new Match(fieldId, score)); |
| 1758 | } |
| 1759 | detectedFieldIds.add(autofillId); |
| 1760 | detectedFieldClassifications.add(new FieldClassification(matches)); |
| 1761 | } // for i |
| 1762 | } catch (ArrayIndexOutOfBoundsException e) { |
| 1763 | wtf(e, "Error accessing FC score at [%d, %d] (%s): %s", i, j, scores, e); |
| 1764 | return; |
| 1765 | } |
| 1766 | |
| 1767 | logContextCommitted(detectedFieldIds, detectedFieldClassifications); |
| 1768 | }); |
| 1769 | |
| 1770 | fcStrategy.calculateScores(callback, currentValues, userValues, categoryIds, |
| 1771 | defaultAlgorithm, defaultArgs, algorithms, args); |
| 1772 | } |
| 1773 | |
| 1774 | /** |
| 1775 | * Shows the save UI, when session can be saved. |
| 1776 | * |
| 1777 | * @return {@code true} if session is done and could be removed, or {@code false} if it's |
| 1778 | * pending user action or the service asked to keep it alive (for multi-screens workflow). |
| 1779 | */ |
| 1780 | @GuardedBy("mLock") |
| 1781 | public boolean showSaveLocked() { |
| 1782 | if (mDestroyed) { |
| 1783 | Slog.w(TAG, "Call to Session#showSaveLocked() rejected - session: " |
| 1784 | + id + " destroyed"); |
| 1785 | return false; |
| 1786 | } |
| 1787 | final FillResponse response = getLastResponseLocked("showSaveLocked(%s)"); |
| 1788 | final SaveInfo saveInfo = response == null ? null : response.getSaveInfo(); |
| 1789 | |
| 1790 | /* |
| 1791 | * The Save dialog is only shown if all conditions below are met: |
| 1792 | * |
| 1793 | * - saveInfo is not null. |
| 1794 | * - autofillValue of all required ids is not null. |
| 1795 | * - autofillValue of at least one id (required or optional) has changed. |
| 1796 | * - there is no Dataset in the last FillResponse whose values of all dataset fields matches |
| 1797 | * the current values of all fields in the screen. |
| 1798 | * - server didn't ask to keep session alive |
| 1799 | */ |
| 1800 | if (saveInfo == null) { |
| 1801 | if (sVerbose) Slog.v(TAG, "showSaveLocked(" + this.id + "): no saveInfo from service"); |
| 1802 | return true; |
| 1803 | } |
| 1804 | |
| 1805 | if ((saveInfo.getFlags() & SaveInfo.FLAG_DELAY_SAVE) != 0) { |
| 1806 | // TODO(b/113281366): log metrics |
| 1807 | if (sDebug) Slog.v(TAG, "showSaveLocked(" + this.id + "): service asked to delay save"); |
| 1808 | return false; |
| 1809 | } |
| 1810 | |
| 1811 | final ArrayMap<AutofillId, InternalSanitizer> sanitizers = createSanitizers(saveInfo); |
| 1812 | |
| 1813 | // Cache used to make sure changed fields do not belong to a dataset. |
| 1814 | final ArrayMap<AutofillId, AutofillValue> currentValues = new ArrayMap<>(); |
| 1815 | // Savable (optional or required) ids that will be checked against the dataset ids. |
| 1816 | final ArraySet<AutofillId> savableIds = new ArraySet<>(); |
| 1817 | |
| 1818 | final AutofillId[] requiredIds = saveInfo.getRequiredIds(); |
| 1819 | boolean allRequiredAreNotEmpty = true; |
| 1820 | boolean atLeastOneChanged = false; |
| 1821 | // If an autofilled field is changed, we need to change isUpdate to true so the proper UI is |
| 1822 | // shown. |
| 1823 | boolean isUpdate = false; |
| 1824 | if (requiredIds != null) { |
| 1825 | for (int i = 0; i < requiredIds.length; i++) { |
| 1826 | final AutofillId id = requiredIds[i]; |
| 1827 | if (id == null) { |
| 1828 | Slog.w(TAG, "null autofill id on " + Arrays.toString(requiredIds)); |
| 1829 | continue; |
| 1830 | } |
| 1831 | savableIds.add(id); |
| 1832 | final ViewState viewState = mViewStates.get(id); |
| 1833 | if (viewState == null) { |
| 1834 | Slog.w(TAG, "showSaveLocked(): no ViewState for required " + id); |
| 1835 | allRequiredAreNotEmpty = false; |
| 1836 | break; |
| 1837 | } |
| 1838 | |
| 1839 | AutofillValue value = viewState.getCurrentValue(); |
| 1840 | if (value == null || value.isEmpty()) { |
| 1841 | final AutofillValue initialValue = getValueFromContextsLocked(id); |
| 1842 | if (initialValue != null) { |
| 1843 | if (sDebug) { |
| 1844 | Slog.d(TAG, "Value of required field " + id + " didn't change; " |
| 1845 | + "using initial value (" + initialValue + ") instead"); |
| 1846 | } |
| 1847 | value = initialValue; |
| 1848 | } else { |
| 1849 | if (sDebug) { |
| 1850 | Slog.d(TAG, "empty value for required " + id ); |
| 1851 | } |
| 1852 | allRequiredAreNotEmpty = false; |
| 1853 | break; |
| 1854 | } |
| 1855 | } |
| 1856 | |
| 1857 | value = getSanitizedValue(sanitizers, id, value); |
| 1858 | if (value == null) { |
| 1859 | if (sDebug) { |
| 1860 | Slog.d(TAG, "value of required field " + id + " failed sanitization"); |
| 1861 | } |
| 1862 | allRequiredAreNotEmpty = false; |
| 1863 | break; |
| 1864 | } |
| 1865 | viewState.setSanitizedValue(value); |
| 1866 | currentValues.put(id, value); |
| 1867 | final AutofillValue filledValue = viewState.getAutofilledValue(); |
| 1868 | |
| 1869 | if (!value.equals(filledValue)) { |
| 1870 | boolean changed = true; |
| 1871 | if (filledValue == null) { |
| 1872 | // Dataset was not autofilled, make sure initial value didn't change. |
| 1873 | final AutofillValue initialValue = getValueFromContextsLocked(id); |
| 1874 | if (initialValue != null && initialValue.equals(value)) { |
| 1875 | if (sDebug) { |
| 1876 | Slog.d(TAG, "id " + id + " is part of dataset but initial value " |
| 1877 | + "didn't change: " + value); |
| 1878 | } |
| 1879 | changed = false; |
| 1880 | } |
| 1881 | } else { |
| 1882 | isUpdate = true; |
| 1883 | } |
| 1884 | if (changed) { |
| 1885 | if (sDebug) { |
| 1886 | Slog.d(TAG, "found a change on required " + id + ": " + filledValue |
| 1887 | + " => " + value); |
| 1888 | } |
| 1889 | atLeastOneChanged = true; |
| 1890 | } |
| 1891 | } |
| 1892 | } |
| 1893 | } |
| 1894 | |
| 1895 | final AutofillId[] optionalIds = saveInfo.getOptionalIds(); |
| 1896 | if (sVerbose) { |
| 1897 | Slog.v(TAG, "allRequiredAreNotEmpty: " + allRequiredAreNotEmpty + " hasOptional: " |
| 1898 | + (optionalIds != null)); |
| 1899 | } |
| 1900 | if (allRequiredAreNotEmpty) { |
| 1901 | // Must look up all optional ids in 2 scenarios: |
| 1902 | // - if no required id changed but an optional id did, it should trigger save / update |
| 1903 | // - if at least one required id changed but it was not part of a filled dataset, we |
| 1904 | // need to check if an optional id is part of a filled datased (in which case we show |
| 1905 | // Update instead of Save) |
| 1906 | if (optionalIds!= null && (!atLeastOneChanged || !isUpdate)) { |
| 1907 | // No change on required ids yet, look for changes on optional ids. |
| 1908 | for (int i = 0; i < optionalIds.length; i++) { |
| 1909 | final AutofillId id = optionalIds[i]; |
| 1910 | savableIds.add(id); |
| 1911 | final ViewState viewState = mViewStates.get(id); |
| 1912 | if (viewState == null) { |
| 1913 | Slog.w(TAG, "no ViewState for optional " + id); |
| 1914 | continue; |
| 1915 | } |
| 1916 | if ((viewState.getState() & ViewState.STATE_CHANGED) != 0) { |
| 1917 | final AutofillValue currentValue = viewState.getCurrentValue(); |
| 1918 | final AutofillValue value = getSanitizedValue(sanitizers, id, currentValue); |
| 1919 | if (value == null) { |
| 1920 | if (sDebug) { |
| 1921 | Slog.d(TAG, "value of opt. field " + id + " failed sanitization"); |
| 1922 | } |
| 1923 | continue; |
| 1924 | } |
| 1925 | |
| 1926 | currentValues.put(id, value); |
| 1927 | final AutofillValue filledValue = viewState.getAutofilledValue(); |
| 1928 | if (value != null && !value.equals(filledValue)) { |
| 1929 | if (sDebug) { |
| 1930 | Slog.d(TAG, "found a change on optional " + id + ": " + filledValue |
| 1931 | + " => " + value); |
| 1932 | } |
| 1933 | if (filledValue != null) { |
| 1934 | isUpdate = true; |
| 1935 | } |
| 1936 | atLeastOneChanged = true; |
| 1937 | } |
| 1938 | } else { |
| 1939 | // Update current values cache based on initial value |
| 1940 | final AutofillValue initialValue = getValueFromContextsLocked(id); |
| 1941 | if (sDebug) { |
| 1942 | Slog.d(TAG, "no current value for " + id + "; initial value is " |
| 1943 | + initialValue); |
| 1944 | } |
| 1945 | if (initialValue != null) { |
| 1946 | currentValues.put(id, initialValue); |
| 1947 | } |
| 1948 | } |
| 1949 | } |
| 1950 | } |
| 1951 | if (atLeastOneChanged) { |
| 1952 | if (sDebug) { |
| 1953 | Slog.d(TAG, "at least one field changed, validate fields for save UI"); |
| 1954 | } |
| 1955 | final InternalValidator validator = saveInfo.getValidator(); |
| 1956 | if (validator != null) { |
| 1957 | final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_SAVE_VALIDATION); |
| 1958 | boolean isValid; |
| 1959 | try { |
| 1960 | isValid = validator.isValid(this); |
| 1961 | if (sDebug) Slog.d(TAG, validator + " returned " + isValid); |
| 1962 | log.setType(isValid |
| 1963 | ? MetricsEvent.TYPE_SUCCESS |
| 1964 | : MetricsEvent.TYPE_DISMISS); |
| 1965 | } catch (Exception e) { |
| 1966 | Slog.e(TAG, "Not showing save UI because validation failed:", e); |
| 1967 | log.setType(MetricsEvent.TYPE_FAILURE); |
| 1968 | mMetricsLogger.write(log); |
| 1969 | return true; |
| 1970 | } |
| 1971 | |
| 1972 | mMetricsLogger.write(log); |
| 1973 | if (!isValid) { |
| 1974 | Slog.i(TAG, "not showing save UI because fields failed validation"); |
| 1975 | return true; |
| 1976 | } |
| 1977 | } |
| 1978 | |
| 1979 | // Make sure the service doesn't have the fields already by checking the datasets |
| 1980 | // content. |
| 1981 | final List<Dataset> datasets = response.getDatasets(); |
| 1982 | if (datasets != null) { |
| 1983 | datasets_loop: for (int i = 0; i < datasets.size(); i++) { |
| 1984 | final Dataset dataset = datasets.get(i); |
| 1985 | final ArrayMap<AutofillId, AutofillValue> datasetValues = |
| 1986 | Helper.getFields(dataset); |
| 1987 | if (sVerbose) { |
| 1988 | Slog.v(TAG, "Checking if saved fields match contents of dataset #" + i |
| 1989 | + ": " + dataset + "; savableIds=" + savableIds); |
| 1990 | } |
| 1991 | savable_ids_loop: for (int j = 0; j < savableIds.size(); j++) { |
| 1992 | final AutofillId id = savableIds.valueAt(j); |
| 1993 | final AutofillValue currentValue = currentValues.get(id); |
| 1994 | if (currentValue == null) { |
| 1995 | if (sDebug) { |
| 1996 | Slog.d(TAG, "dataset has value for field that is null: " + id); |
| 1997 | } |
| 1998 | continue savable_ids_loop; |
| 1999 | } |
| 2000 | final AutofillValue datasetValue = datasetValues.get(id); |
| 2001 | if (!currentValue.equals(datasetValue)) { |
| 2002 | if (sDebug) { |
| 2003 | Slog.d(TAG, "found a dataset change on id " + id + ": from " |
| 2004 | + datasetValue + " to " + currentValue); |
| 2005 | } |
| 2006 | continue datasets_loop; |
| 2007 | } |
| 2008 | if (sVerbose) Slog.v(TAG, "no dataset changes for id " + id); |
| 2009 | } |
| 2010 | if (sDebug) { |
| 2011 | Slog.d(TAG, "ignoring Save UI because all fields match contents of " |
| 2012 | + "dataset #" + i + ": " + dataset); |
| 2013 | } |
| 2014 | return true; |
| 2015 | } |
| 2016 | } |
| 2017 | |
| 2018 | if (sDebug) { |
| 2019 | Slog.d(TAG, "Good news, everyone! All checks passed, show save UI for " |
| 2020 | + id + "!"); |
| 2021 | } |
| 2022 | |
| 2023 | // Use handler so logContextCommitted() is logged first |
| 2024 | mHandler.sendMessage(obtainMessage(Session::logSaveShown, this)); |
| 2025 | |
| 2026 | final IAutoFillManagerClient client = getClient(); |
| 2027 | mPendingSaveUi = new PendingUi(new Binder(), id, client); |
| 2028 | |
| 2029 | final CharSequence serviceLabel; |
| 2030 | final Drawable serviceIcon; |
| 2031 | synchronized (mLock) { |
| 2032 | serviceLabel = mService.getServiceLabelLocked(); |
| 2033 | serviceIcon = mService.getServiceIconLocked(); |
| 2034 | } |
| 2035 | if (serviceLabel == null || serviceIcon == null) { |
| 2036 | wtf(null, "showSaveLocked(): no service label or icon"); |
| 2037 | return true; |
| 2038 | } |
| 2039 | getUiForShowing().showSaveUi(serviceLabel, serviceIcon, |
| 2040 | mService.getServicePackageName(), saveInfo, this, |
| 2041 | mComponentName, this, mPendingSaveUi, isUpdate, mCompatMode); |
| 2042 | if (client != null) { |
| 2043 | try { |
| 2044 | client.setSaveUiState(id, true); |
| 2045 | } catch (RemoteException e) { |
| 2046 | Slog.e(TAG, "Error notifying client to set save UI state to shown: " + e); |
| 2047 | } |
| 2048 | } |
| 2049 | mIsSaving = true; |
| 2050 | return false; |
| 2051 | } |
| 2052 | } |
| 2053 | // Nothing changed... |
| 2054 | if (sDebug) { |
| 2055 | Slog.d(TAG, "showSaveLocked(" + id +"): with no changes, comes no responsibilities." |
| 2056 | + "allRequiredAreNotNull=" + allRequiredAreNotEmpty |
| 2057 | + ", atLeastOneChanged=" + atLeastOneChanged); |
| 2058 | } |
| 2059 | return true; |
| 2060 | } |
| 2061 | |
| 2062 | private void logSaveShown() { |
| 2063 | mService.logSaveShown(id, mClientState); |
| 2064 | } |
| 2065 | |
| 2066 | @Nullable |
| 2067 | private ArrayMap<AutofillId, InternalSanitizer> createSanitizers(@Nullable SaveInfo saveInfo) { |
| 2068 | if (saveInfo == null) return null; |
| 2069 | |
| 2070 | final InternalSanitizer[] sanitizerKeys = saveInfo.getSanitizerKeys(); |
| 2071 | if (sanitizerKeys == null) return null; |
| 2072 | |
| 2073 | final int size = sanitizerKeys.length ; |
| 2074 | final ArrayMap<AutofillId, InternalSanitizer> sanitizers = new ArrayMap<>(size); |
| 2075 | if (sDebug) Slog.d(TAG, "Service provided " + size + " sanitizers"); |
| 2076 | final AutofillId[][] sanitizerValues = saveInfo.getSanitizerValues(); |
| 2077 | for (int i = 0; i < size; i++) { |
| 2078 | final InternalSanitizer sanitizer = sanitizerKeys[i]; |
| 2079 | final AutofillId[] ids = sanitizerValues[i]; |
| 2080 | if (sDebug) { |
| 2081 | Slog.d(TAG, "sanitizer #" + i + " (" + sanitizer + ") for ids " |
| 2082 | + Arrays.toString(ids)); |
| 2083 | } |
| 2084 | for (AutofillId id : ids) { |
| 2085 | sanitizers.put(id, sanitizer); |
| 2086 | } |
| 2087 | } |
| 2088 | return sanitizers; |
| 2089 | } |
| 2090 | |
| 2091 | @Nullable |
| 2092 | private AutofillValue getSanitizedValue( |
| 2093 | @Nullable ArrayMap<AutofillId, InternalSanitizer> sanitizers, |
| 2094 | @NonNull AutofillId id, |
| 2095 | @Nullable AutofillValue value) { |
| 2096 | if (sanitizers == null || value == null) return value; |
| 2097 | |
| 2098 | final ViewState state = mViewStates.get(id); |
| 2099 | AutofillValue sanitized = state == null ? null : state.getSanitizedValue(); |
| 2100 | if (sanitized == null) { |
| 2101 | final InternalSanitizer sanitizer = sanitizers.get(id); |
| 2102 | if (sanitizer == null) { |
| 2103 | return value; |
| 2104 | } |
| 2105 | |
| 2106 | sanitized = sanitizer.sanitize(value); |
| 2107 | if (sDebug) { |
| 2108 | Slog.d(TAG, "Value for " + id + "(" + value + ") sanitized to " + sanitized); |
| 2109 | } |
| 2110 | if (state != null) { |
| 2111 | state.setSanitizedValue(sanitized); |
| 2112 | } |
| 2113 | } |
| 2114 | return sanitized; |
| 2115 | } |
| 2116 | |
| 2117 | /** |
| 2118 | * Returns whether the session is currently showing the save UI |
| 2119 | */ |
| 2120 | @GuardedBy("mLock") |
| 2121 | boolean isSavingLocked() { |
| 2122 | return mIsSaving; |
| 2123 | } |
| 2124 | |
| 2125 | /** |
| 2126 | * Gets the latest non-empty value for the given id in the autofill contexts. |
| 2127 | */ |
| 2128 | @GuardedBy("mLock") |
| 2129 | @Nullable |
| 2130 | private AutofillValue getValueFromContextsLocked(@NonNull AutofillId autofillId) { |
| 2131 | final int numContexts = mContexts.size(); |
| 2132 | for (int i = numContexts - 1; i >= 0; i--) { |
| 2133 | final FillContext context = mContexts.get(i); |
| 2134 | final ViewNode node = Helper.findViewNodeByAutofillId(context.getStructure(), |
| 2135 | autofillId); |
| 2136 | if (node != null) { |
| 2137 | final AutofillValue value = node.getAutofillValue(); |
| 2138 | if (sDebug) { |
| 2139 | Slog.d(TAG, "getValueFromContexts(" + this.id + "/" + autofillId + ") at " |
| 2140 | + i + ": " + value); |
| 2141 | } |
| 2142 | if (value != null && !value.isEmpty()) { |
| 2143 | return value; |
| 2144 | } |
| 2145 | } |
| 2146 | } |
| 2147 | return null; |
| 2148 | } |
| 2149 | |
| 2150 | /** |
| 2151 | * Gets the latest autofill options for the given id in the autofill contexts. |
| 2152 | */ |
| 2153 | @GuardedBy("mLock") |
| 2154 | @Nullable |
| 2155 | private CharSequence[] getAutofillOptionsFromContextsLocked(AutofillId id) { |
| 2156 | final int numContexts = mContexts.size(); |
| 2157 | |
| 2158 | for (int i = numContexts - 1; i >= 0; i--) { |
| 2159 | final FillContext context = mContexts.get(i); |
| 2160 | final ViewNode node = Helper.findViewNodeByAutofillId(context.getStructure(), id); |
| 2161 | if (node != null && node.getAutofillOptions() != null) { |
| 2162 | return node.getAutofillOptions(); |
| 2163 | } |
| 2164 | } |
| 2165 | return null; |
| 2166 | } |
| 2167 | |
| 2168 | /** |
| 2169 | * Update the {@link AutofillValue values} of the {@link AssistStructure} before sending it to |
| 2170 | * the service on save(). |
| 2171 | */ |
| 2172 | private void updateValuesForSaveLocked() { |
| 2173 | final ArrayMap<AutofillId, InternalSanitizer> sanitizers = |
| 2174 | createSanitizers(getSaveInfoLocked()); |
| 2175 | |
| 2176 | final int numContexts = mContexts.size(); |
| 2177 | for (int contextNum = 0; contextNum < numContexts; contextNum++) { |
| 2178 | final FillContext context = mContexts.get(contextNum); |
| 2179 | |
| 2180 | final ViewNode[] nodes = |
| 2181 | context.findViewNodesByAutofillIds(getIdsOfAllViewStatesLocked()); |
| 2182 | |
| 2183 | if (sVerbose) Slog.v(TAG, "updateValuesForSaveLocked(): updating " + context); |
| 2184 | |
| 2185 | for (int viewStateNum = 0; viewStateNum < mViewStates.size(); viewStateNum++) { |
| 2186 | final ViewState viewState = mViewStates.valueAt(viewStateNum); |
| 2187 | |
| 2188 | final AutofillId id = viewState.id; |
| 2189 | final AutofillValue value = viewState.getCurrentValue(); |
| 2190 | if (value == null) { |
| 2191 | if (sVerbose) Slog.v(TAG, "updateValuesForSaveLocked(): skipping " + id); |
| 2192 | continue; |
| 2193 | } |
| 2194 | final ViewNode node = nodes[viewStateNum]; |
| 2195 | if (node == null) { |
| 2196 | Slog.w(TAG, "callSaveLocked(): did not find node with id " + id); |
| 2197 | continue; |
| 2198 | } |
| 2199 | if (sVerbose) { |
| 2200 | Slog.v(TAG, "updateValuesForSaveLocked(): updating " + id + " to " + value); |
| 2201 | } |
| 2202 | |
| 2203 | AutofillValue sanitizedValue = viewState.getSanitizedValue(); |
| 2204 | |
| 2205 | if (sanitizedValue == null) { |
| 2206 | // Field is optional and haven't been sanitized yet. |
| 2207 | sanitizedValue = getSanitizedValue(sanitizers, id, value); |
| 2208 | } |
| 2209 | if (sanitizedValue != null) { |
| 2210 | node.updateAutofillValue(sanitizedValue); |
| 2211 | } else if (sDebug) { |
| 2212 | Slog.d(TAG, "updateValuesForSaveLocked(): not updating field " + id |
| 2213 | + " because it failed sanitization"); |
| 2214 | } |
| 2215 | } |
| 2216 | |
| 2217 | // Sanitize structure before it's sent to service. |
| 2218 | context.getStructure().sanitizeForParceling(false); |
| 2219 | |
| 2220 | if (sVerbose) { |
| 2221 | Slog.v(TAG, "updateValuesForSaveLocked(): dumping structure of " + context |
| 2222 | + " before calling service.save()"); |
| 2223 | context.getStructure().dump(false); |
| 2224 | } |
| 2225 | } |
| 2226 | } |
| 2227 | |
| 2228 | /** |
| 2229 | * Calls service when user requested save. |
| 2230 | */ |
| 2231 | @GuardedBy("mLock") |
| 2232 | void callSaveLocked() { |
| 2233 | if (mDestroyed) { |
| 2234 | Slog.w(TAG, "Call to Session#callSaveLocked() rejected - session: " |
| 2235 | + id + " destroyed"); |
| 2236 | return; |
| 2237 | } |
| 2238 | if (mRemoteFillService == null) { |
| 2239 | wtf(null, "callSaveLocked() called without a remote service. " |
| 2240 | + "mForAugmentedAutofillOnly: %s", mForAugmentedAutofillOnly); |
| 2241 | return; |
| 2242 | } |
| 2243 | |
| 2244 | if (sVerbose) Slog.v(TAG, "callSaveLocked(" + this.id + "): mViewStates=" + mViewStates); |
| 2245 | |
| 2246 | if (mContexts == null) { |
| 2247 | Slog.w(TAG, "callSaveLocked(): no contexts"); |
| 2248 | return; |
| 2249 | } |
| 2250 | |
| 2251 | updateValuesForSaveLocked(); |
| 2252 | |
| 2253 | // Remove pending fill requests as the session is finished. |
| 2254 | cancelCurrentRequestLocked(); |
| 2255 | |
| 2256 | final ArrayList<FillContext> contexts = mergePreviousSessionLocked( /* forSave= */ true); |
| 2257 | |
| 2258 | final SaveRequest saveRequest = |
| 2259 | new SaveRequest(contexts, mClientState, mSelectedDatasetIds); |
| 2260 | mRemoteFillService.onSaveRequest(saveRequest); |
| 2261 | } |
| 2262 | |
| 2263 | // TODO(b/113281366): rather than merge it here, it might be better to simply reuse the old |
| 2264 | // session instead of creating a new one. But we need to consider what would happen on corner |
| 2265 | // cases such as "Main Activity M -> activity A with username -> activity B with password" |
| 2266 | // If user follows the normal workflow, than session A would be merged with session B as |
| 2267 | // expected. But if when on Activity A the user taps back or somehow launches another activity, |
| 2268 | // session A could be merged with the wrong session. |
| 2269 | /** |
| 2270 | * Gets a list of contexts that includes not only this session's contexts but also the contexts |
| 2271 | * from previous sessions that were asked by the service to be delayed (if any). |
| 2272 | * |
| 2273 | * <p>As a side-effect: |
| 2274 | * <ul> |
| 2275 | * <li>If the current {@link #mClientState} is {@code null}, sets it with the last non- |
| 2276 | * {@code null} client state from previous sessions. |
| 2277 | * <li>When {@code forSave} is {@code true}, calls {@link #updateValuesForSaveLocked()} in the |
| 2278 | * previous sessions. |
| 2279 | * </ul> |
| 2280 | */ |
| 2281 | @NonNull |
| 2282 | private ArrayList<FillContext> mergePreviousSessionLocked(boolean forSave) { |
| 2283 | final ArrayList<Session> previousSessions = mService.getPreviousSessionsLocked(this); |
| 2284 | final ArrayList<FillContext> contexts; |
| 2285 | if (previousSessions != null) { |
| 2286 | if (sDebug) { |
| 2287 | Slog.d(TAG, "mergeSessions(" + this.id + "): Merging the content of " |
| 2288 | + previousSessions.size() + " sessions for task " + taskId); |
| 2289 | } |
| 2290 | contexts = new ArrayList<>(); |
| 2291 | for (int i = 0; i < previousSessions.size(); i++) { |
| 2292 | final Session previousSession = previousSessions.get(i); |
| 2293 | final ArrayList<FillContext> previousContexts = previousSession.mContexts; |
| 2294 | if (previousContexts == null) { |
| 2295 | Slog.w(TAG, "mergeSessions(" + this.id + "): Not merging null contexts from " |
| 2296 | + previousSession.id); |
| 2297 | continue; |
| 2298 | } |
| 2299 | if (forSave) { |
| 2300 | previousSession.updateValuesForSaveLocked(); |
| 2301 | } |
| 2302 | if (sDebug) { |
| 2303 | Slog.d(TAG, "mergeSessions(" + this.id + "): adding " + previousContexts.size() |
| 2304 | + " context from previous session #" + previousSession.id); |
| 2305 | } |
| 2306 | contexts.addAll(previousContexts); |
| 2307 | if (mClientState == null && previousSession.mClientState != null) { |
| 2308 | if (sDebug) { |
| 2309 | Slog.d(TAG, "mergeSessions(" + this.id + "): setting client state from " |
| 2310 | + "previous session" + previousSession.id); |
| 2311 | } |
| 2312 | mClientState = previousSession.mClientState; |
| 2313 | } |
| 2314 | } |
| 2315 | contexts.addAll(mContexts); |
| 2316 | } else { |
| 2317 | // Dispatch a snapshot of the current contexts list since it may change |
| 2318 | // until the dispatch happens. The items in the list don't need to be cloned |
| 2319 | // since we don't hold on them anywhere else. The client state is not touched |
| 2320 | // by us, so no need to copy. |
| 2321 | contexts = new ArrayList<>(mContexts); |
| 2322 | } |
| 2323 | return contexts; |
| 2324 | } |
| 2325 | |
| 2326 | /** |
| 2327 | * Starts (if necessary) a new fill request upon entering a view. |
| 2328 | * |
| 2329 | * <p>A new request will be started in 2 scenarios: |
| 2330 | * <ol> |
| 2331 | * <li>If the user manually requested autofill. |
| 2332 | * <li>If the view is part of a new partition. |
| 2333 | * </ol> |
| 2334 | * |
| 2335 | * @param id The id of the view that is entered. |
| 2336 | * @param viewState The view that is entered. |
| 2337 | * @param flags The flag that was passed by the AutofillManager. |
| 2338 | * |
| 2339 | * @return {@code true} if a new fill response is requested. |
| 2340 | */ |
| 2341 | @GuardedBy("mLock") |
| 2342 | private boolean requestNewFillResponseOnViewEnteredIfNecessaryLocked(@NonNull AutofillId id, |
| 2343 | @NonNull ViewState viewState, int flags) { |
| 2344 | if ((flags & FLAG_MANUAL_REQUEST) != 0) { |
| 2345 | mForAugmentedAutofillOnly = false; |
| 2346 | if (sDebug) Slog.d(TAG, "Re-starting session on view " + id + " and flags " + flags); |
| 2347 | requestNewFillResponseLocked(viewState, ViewState.STATE_RESTARTED_SESSION, flags); |
| 2348 | return true; |
| 2349 | } |
| 2350 | |
| 2351 | // If it's not, then check if it it should start a partition. |
| 2352 | if (shouldStartNewPartitionLocked(id)) { |
| 2353 | if (sDebug) { |
| 2354 | Slog.d(TAG, "Starting partition or augmented request for view id " + id + ": " |
| 2355 | + viewState.getStateAsString()); |
| 2356 | } |
| 2357 | requestNewFillResponseLocked(viewState, ViewState.STATE_STARTED_PARTITION, flags); |
| 2358 | return true; |
| 2359 | } else { |
| 2360 | if (sVerbose) { |
| 2361 | Slog.v(TAG, "Not starting new partition for view " + id + ": " |
| 2362 | + viewState.getStateAsString()); |
| 2363 | } |
| 2364 | } |
| 2365 | return false; |
| 2366 | } |
| 2367 | |
| 2368 | /** |
| 2369 | * Determines if a new partition should be started for an id. |
| 2370 | * |
| 2371 | * @param id The id of the view that is entered |
| 2372 | * |
| 2373 | * @return {@code true} if a new partition should be started |
| 2374 | */ |
| 2375 | @GuardedBy("mLock") |
| 2376 | private boolean shouldStartNewPartitionLocked(@NonNull AutofillId id) { |
| 2377 | final ViewState currentView = mViewStates.get(id); |
| 2378 | if (mResponses == null && currentView != null |
| 2379 | && (currentView.getState() & ViewState.STATE_PENDING_CREATE_INLINE_REQUEST) == 0) { |
| 2380 | return true; |
| 2381 | } |
| 2382 | |
| 2383 | if (mExpiredResponse) { |
| 2384 | if (sDebug) { |
| 2385 | Slog.d(TAG, "Starting a new partition because the response has expired."); |
| 2386 | } |
| 2387 | return true; |
| 2388 | } |
| 2389 | |
| 2390 | final int numResponses = mResponses.size(); |
| 2391 | if (numResponses >= AutofillManagerService.getPartitionMaxCount()) { |
| 2392 | Slog.e(TAG, "Not starting a new partition on " + id + " because session " + this.id |
| 2393 | + " reached maximum of " + AutofillManagerService.getPartitionMaxCount()); |
| 2394 | return false; |
| 2395 | } |
| 2396 | |
| 2397 | for (int responseNum = 0; responseNum < numResponses; responseNum++) { |
| 2398 | final FillResponse response = mResponses.valueAt(responseNum); |
| 2399 | |
| 2400 | if (ArrayUtils.contains(response.getIgnoredIds(), id)) { |
| 2401 | return false; |
| 2402 | } |
| 2403 | |
| 2404 | final SaveInfo saveInfo = response.getSaveInfo(); |
| 2405 | if (saveInfo != null) { |
| 2406 | if (ArrayUtils.contains(saveInfo.getOptionalIds(), id) |
| 2407 | || ArrayUtils.contains(saveInfo.getRequiredIds(), id)) { |
| 2408 | return false; |
| 2409 | } |
| 2410 | } |
| 2411 | |
| 2412 | final List<Dataset> datasets = response.getDatasets(); |
| 2413 | if (datasets != null) { |
| 2414 | final int numDatasets = datasets.size(); |
| 2415 | |
| 2416 | for (int dataSetNum = 0; dataSetNum < numDatasets; dataSetNum++) { |
| 2417 | final ArrayList<AutofillId> fields = datasets.get(dataSetNum).getFieldIds(); |
| 2418 | |
| 2419 | if (fields != null && fields.contains(id)) { |
| 2420 | return false; |
| 2421 | } |
| 2422 | } |
| 2423 | } |
| 2424 | |
| 2425 | if (ArrayUtils.contains(response.getAuthenticationIds(), id)) { |
| 2426 | return false; |
| 2427 | } |
| 2428 | } |
| 2429 | |
| 2430 | return true; |
| 2431 | } |
| 2432 | |
| 2433 | @GuardedBy("mLock") |
| 2434 | void updateLocked(AutofillId id, Rect virtualBounds, AutofillValue value, int action, |
| 2435 | int flags) { |
| 2436 | if (mDestroyed) { |
| 2437 | Slog.w(TAG, "Call to Session#updateLocked() rejected - session: " |
| 2438 | + id + " destroyed"); |
| 2439 | return; |
| 2440 | } |
| 2441 | if (action == ACTION_RESPONSE_EXPIRED) { |
| 2442 | mExpiredResponse = true; |
| 2443 | if (sDebug) { |
| 2444 | Slog.d(TAG, "Set the response has expired."); |
| 2445 | } |
| 2446 | return; |
| 2447 | } |
| 2448 | |
| 2449 | id.setSessionId(this.id); |
| 2450 | if (sVerbose) { |
| 2451 | Slog.v(TAG, "updateLocked(" + this.id + "): id=" + id + ", action=" |
| 2452 | + actionAsString(action) + ", flags=" + flags); |
| 2453 | } |
| 2454 | ViewState viewState = mViewStates.get(id); |
| 2455 | |
| 2456 | if (viewState == null) { |
| 2457 | if (action == ACTION_START_SESSION || action == ACTION_VALUE_CHANGED |
| 2458 | || action == ACTION_VIEW_ENTERED) { |
| 2459 | if (sVerbose) Slog.v(TAG, "Creating viewState for " + id); |
| 2460 | boolean isIgnored = isIgnoredLocked(id); |
| 2461 | viewState = new ViewState(id, this, |
| 2462 | isIgnored ? ViewState.STATE_IGNORED : ViewState.STATE_INITIAL); |
| 2463 | mViewStates.put(id, viewState); |
| 2464 | |
| 2465 | // TODO(b/73648631): for optimization purposes, should also ignore if change is |
| 2466 | // detectable, and batch-send them when the session is finished (but that will |
| 2467 | // require tracking detectable fields on AutofillManager) |
| 2468 | if (isIgnored) { |
| 2469 | if (sDebug) Slog.d(TAG, "updateLocked(): ignoring view " + viewState); |
| 2470 | return; |
| 2471 | } |
| 2472 | } else { |
| 2473 | if (sVerbose) Slog.v(TAG, "Ignoring specific action when viewState=null"); |
| 2474 | return; |
| 2475 | } |
| 2476 | } |
| 2477 | |
| 2478 | switch(action) { |
| 2479 | case ACTION_START_SESSION: |
| 2480 | // View is triggering autofill. |
| 2481 | mCurrentViewId = viewState.id; |
| 2482 | viewState.update(value, virtualBounds, flags); |
| 2483 | requestNewFillResponseLocked(viewState, ViewState.STATE_STARTED_SESSION, flags); |
| 2484 | break; |
| 2485 | case ACTION_VALUE_CHANGED: |
| 2486 | if (mCompatMode && (viewState.getState() & ViewState.STATE_URL_BAR) != 0) { |
| 2487 | // Must cancel the session if the value of the URL bar changed |
| 2488 | final String currentUrl = mUrlBar == null ? null |
| 2489 | : mUrlBar.getText().toString().trim(); |
| 2490 | if (currentUrl == null) { |
| 2491 | // Sanity check - shouldn't happen. |
| 2492 | wtf(null, "URL bar value changed, but current value is null"); |
| 2493 | return; |
| 2494 | } |
| 2495 | if (value == null || ! value.isText()) { |
| 2496 | // Sanity check - shouldn't happen. |
| 2497 | wtf(null, "URL bar value changed to null or non-text: %s", value); |
| 2498 | return; |
| 2499 | } |
| 2500 | final String newUrl = value.getTextValue().toString(); |
| 2501 | if (newUrl.equals(currentUrl)) { |
| 2502 | if (sDebug) Slog.d(TAG, "Ignoring change on URL bar as it's the same"); |
| 2503 | return; |
| 2504 | } |
| 2505 | if (mSaveOnAllViewsInvisible) { |
| 2506 | // We cannot cancel the session because it could hinder Save when all views |
| 2507 | // are finished, as the URL bar changed callback is usually called before |
| 2508 | // the virtual views become invisible. |
| 2509 | if (sDebug) { |
| 2510 | Slog.d(TAG, "Ignoring change on URL because session will finish when " |
| 2511 | + "views are gone"); |
| 2512 | } |
| 2513 | return; |
| 2514 | } |
| 2515 | if (sDebug) Slog.d(TAG, "Finishing session because URL bar changed"); |
| 2516 | forceRemoveSelfLocked(AutofillManager.STATE_UNKNOWN_COMPAT_MODE); |
| 2517 | return; |
| 2518 | } |
| 2519 | if (!Objects.equals(value, viewState.getCurrentValue())) { |
| 2520 | logIfViewClearedLocked(id, value, viewState); |
| 2521 | updateViewStateAndUiOnValueChangedLocked(id, value, viewState, flags); |
| 2522 | } |
| 2523 | break; |
| 2524 | case ACTION_VIEW_ENTERED: |
| 2525 | if (sVerbose && virtualBounds != null) { |
| 2526 | Slog.v(TAG, "entered on virtual child " + id + ": " + virtualBounds); |
| 2527 | } |
| 2528 | |
| 2529 | final boolean isSameViewEntered = Objects.equals(mCurrentViewId, viewState.id); |
| 2530 | // Update the view states first... |
| 2531 | mCurrentViewId = viewState.id; |
| 2532 | if (value != null) { |
| 2533 | viewState.setCurrentValue(value); |
| 2534 | } |
| 2535 | |
| 2536 | if (mCompatMode && (viewState.getState() & ViewState.STATE_URL_BAR) != 0) { |
| 2537 | if (sDebug) Slog.d(TAG, "Ignoring VIEW_ENTERED on URL BAR (id=" + id + ")"); |
| 2538 | return; |
| 2539 | } |
| 2540 | |
| 2541 | if ((flags & FLAG_MANUAL_REQUEST) == 0) { |
| 2542 | // Not a manual request |
| 2543 | if (mAugmentedAutofillableIds != null && mAugmentedAutofillableIds.contains( |
| 2544 | id)) { |
| 2545 | // Regular autofill handled the view and returned null response, but it |
| 2546 | // triggered augmented autofill |
| 2547 | if (!isSameViewEntered) { |
| 2548 | if (sDebug) Slog.d(TAG, "trigger augmented autofill."); |
| 2549 | triggerAugmentedAutofillLocked(flags); |
| 2550 | } else { |
| 2551 | if (sDebug) Slog.d(TAG, "skip augmented autofill for same view."); |
| 2552 | } |
| 2553 | return; |
| 2554 | } else if (mForAugmentedAutofillOnly && isSameViewEntered) { |
| 2555 | // Regular autofill is disabled. |
| 2556 | if (sDebug) Slog.d(TAG, "skip augmented autofill for same view."); |
| 2557 | return; |
| 2558 | } |
| 2559 | } |
| 2560 | |
| 2561 | if (requestNewFillResponseOnViewEnteredIfNecessaryLocked(id, viewState, flags)) { |
| 2562 | return; |
| 2563 | } |
| 2564 | |
| 2565 | if (isSameViewEntered) { |
| 2566 | return; |
| 2567 | } |
| 2568 | |
| 2569 | // If the ViewState is ready to be displayed, onReady() will be called. |
| 2570 | viewState.update(value, virtualBounds, flags); |
| 2571 | break; |
| 2572 | case ACTION_VIEW_EXITED: |
| 2573 | if (Objects.equals(mCurrentViewId, viewState.id)) { |
| 2574 | if (sVerbose) Slog.v(TAG, "Exiting view " + id); |
| 2575 | mUi.hideFillUi(this); |
| 2576 | hideAugmentedAutofillLocked(viewState); |
| 2577 | mInlineSessionController.hideInlineSuggestionsUiLocked(mCurrentViewId); |
| 2578 | mCurrentViewId = null; |
| 2579 | } |
| 2580 | break; |
| 2581 | default: |
| 2582 | Slog.w(TAG, "updateLocked(): unknown action: " + action); |
| 2583 | } |
| 2584 | } |
| 2585 | |
| 2586 | @GuardedBy("mLock") |
| 2587 | private void hideAugmentedAutofillLocked(@NonNull ViewState viewState) { |
| 2588 | if ((viewState.getState() |
| 2589 | & ViewState.STATE_TRIGGERED_AUGMENTED_AUTOFILL) != 0) { |
| 2590 | viewState.resetState(ViewState.STATE_TRIGGERED_AUGMENTED_AUTOFILL); |
| 2591 | cancelAugmentedAutofillLocked(); |
| 2592 | } |
| 2593 | } |
| 2594 | |
| 2595 | /** |
| 2596 | * Checks whether a view should be ignored. |
| 2597 | */ |
| 2598 | @GuardedBy("mLock") |
| 2599 | private boolean isIgnoredLocked(AutofillId id) { |
| 2600 | // Always check the latest response only |
| 2601 | final FillResponse response = getLastResponseLocked(null); |
| 2602 | if (response == null) return false; |
| 2603 | |
| 2604 | return ArrayUtils.contains(response.getIgnoredIds(), id); |
| 2605 | } |
| 2606 | |
| 2607 | @GuardedBy("mLock") |
| 2608 | private void logIfViewClearedLocked(AutofillId id, AutofillValue value, ViewState viewState) { |
| 2609 | if ((value == null || value.isEmpty()) |
| 2610 | && viewState.getCurrentValue() != null |
| 2611 | && viewState.getCurrentValue().isText() |
| 2612 | && viewState.getCurrentValue().getTextValue() != null |
| 2613 | && getSaveInfoLocked() != null) { |
| 2614 | final int length = viewState.getCurrentValue().getTextValue().length(); |
| 2615 | if (sDebug) { |
| 2616 | Slog.d(TAG, "updateLocked(" + id + "): resetting value that was " |
| 2617 | + length + " chars long"); |
| 2618 | } |
| 2619 | final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_VALUE_RESET) |
| 2620 | .addTaggedData(MetricsEvent.FIELD_AUTOFILL_PREVIOUS_LENGTH, length); |
| 2621 | mMetricsLogger.write(log); |
| 2622 | } |
| 2623 | } |
| 2624 | |
| 2625 | @GuardedBy("mLock") |
| 2626 | private void updateViewStateAndUiOnValueChangedLocked(AutofillId id, AutofillValue value, |
| 2627 | ViewState viewState, int flags) { |
| 2628 | final String textValue; |
| 2629 | if (value == null || !value.isText()) { |
| 2630 | textValue = null; |
| 2631 | } else { |
| 2632 | final CharSequence text = value.getTextValue(); |
| 2633 | // Text should never be null, but it doesn't hurt to check to avoid a |
| 2634 | // system crash... |
| 2635 | textValue = (text == null) ? null : text.toString(); |
| 2636 | } |
| 2637 | updateFilteringStateOnValueChangedLocked(textValue, viewState); |
| 2638 | |
| 2639 | viewState.setCurrentValue(value); |
| 2640 | |
| 2641 | final String filterText = textValue; |
| 2642 | |
| 2643 | final AutofillValue filledValue = viewState.getAutofilledValue(); |
| 2644 | if (filledValue != null) { |
| 2645 | if (filledValue.equals(value)) { |
| 2646 | // When the update is caused by autofilling the view, just update the |
| 2647 | // value, not the UI. |
| 2648 | if (sVerbose) { |
| 2649 | Slog.v(TAG, "ignoring autofilled change on id " + id); |
| 2650 | } |
| 2651 | viewState.resetState(ViewState.STATE_CHANGED); |
| 2652 | return; |
| 2653 | } else if ((viewState.id.equals(this.mCurrentViewId)) |
| 2654 | && (viewState.getState() & ViewState.STATE_AUTOFILLED) != 0) { |
| 2655 | // Remove autofilled state once field is changed after autofilling. |
| 2656 | if (sVerbose) { |
| 2657 | Slog.v(TAG, "field changed after autofill on id " + id); |
| 2658 | } |
| 2659 | viewState.resetState(ViewState.STATE_AUTOFILLED); |
| 2660 | final ViewState currentView = mViewStates.get(mCurrentViewId); |
| 2661 | currentView.maybeCallOnFillReady(flags); |
| 2662 | } |
| 2663 | } else if (viewState.id.equals(this.mCurrentViewId) |
| 2664 | && (viewState.getState() & ViewState.STATE_INLINE_SHOWN) != 0) { |
| 2665 | requestShowInlineSuggestionsLocked(viewState.getResponse(), filterText); |
| 2666 | } else if (viewState.id.equals(this.mCurrentViewId) |
| 2667 | && (viewState.getState() & ViewState.STATE_TRIGGERED_AUGMENTED_AUTOFILL) != 0) { |
| 2668 | if (!TextUtils.isEmpty(filterText)) { |
| 2669 | mInlineSessionController.hideInlineSuggestionsUiLocked(mCurrentViewId); |
| 2670 | } |
| 2671 | } |
| 2672 | |
| 2673 | viewState.setState(ViewState.STATE_CHANGED); |
| 2674 | getUiForShowing().filterFillUi(filterText, this); |
| 2675 | } |
| 2676 | |
| 2677 | /** |
| 2678 | * Disable filtering of inline suggestions for further text changes in this view if any |
| 2679 | * character was removed earlier and now any character is being added. Such behaviour may |
| 2680 | * indicate the IME attempting to probe the potentially sensitive content of inline suggestions. |
| 2681 | */ |
| 2682 | @GuardedBy("mLock") |
| 2683 | private void updateFilteringStateOnValueChangedLocked(@Nullable String newTextValue, |
| 2684 | ViewState viewState) { |
| 2685 | if (newTextValue == null) { |
| 2686 | // Don't just return here, otherwise the IME can circumvent this logic using non-text |
| 2687 | // values. |
| 2688 | newTextValue = ""; |
| 2689 | } |
| 2690 | final AutofillValue currentValue = viewState.getCurrentValue(); |
| 2691 | final String currentTextValue; |
| 2692 | if (currentValue == null || !currentValue.isText()) { |
| 2693 | currentTextValue = ""; |
| 2694 | } else { |
| 2695 | currentTextValue = currentValue.getTextValue().toString(); |
| 2696 | } |
| 2697 | |
| 2698 | if ((viewState.getState() & ViewState.STATE_CHAR_REMOVED) == 0) { |
| 2699 | if (!containsCharsInOrder(newTextValue, currentTextValue)) { |
| 2700 | viewState.setState(ViewState.STATE_CHAR_REMOVED); |
| 2701 | } |
| 2702 | } else if (!containsCharsInOrder(currentTextValue, newTextValue)) { |
| 2703 | // Characters were added or replaced. |
| 2704 | viewState.setState(ViewState.STATE_INLINE_DISABLED); |
| 2705 | } |
| 2706 | } |
| 2707 | |
| 2708 | /** |
| 2709 | * Returns true if {@code s1} contains all characters of {@code s2}, in order. |
| 2710 | */ |
| 2711 | private static boolean containsCharsInOrder(String s1, String s2) { |
| 2712 | int prevIndex = -1; |
| 2713 | for (char ch : s2.toCharArray()) { |
| 2714 | int index = TextUtils.indexOf(s1, ch, prevIndex + 1); |
| 2715 | if (index == -1) { |
| 2716 | return false; |
| 2717 | } |
| 2718 | prevIndex = index; |
| 2719 | } |
| 2720 | return true; |
| 2721 | } |
| 2722 | |
| 2723 | @Override |
| 2724 | public void onFillReady(@NonNull FillResponse response, @NonNull AutofillId filledId, |
| 2725 | @Nullable AutofillValue value) { |
| 2726 | synchronized (mLock) { |
| 2727 | if (mDestroyed) { |
| 2728 | Slog.w(TAG, "Call to Session#onFillReady() rejected - session: " |
| 2729 | + id + " destroyed"); |
| 2730 | return; |
| 2731 | } |
| 2732 | } |
| 2733 | |
| 2734 | String filterText = null; |
| 2735 | if (value != null && value.isText()) { |
| 2736 | filterText = value.getTextValue().toString(); |
| 2737 | } |
| 2738 | |
| 2739 | final CharSequence serviceLabel; |
| 2740 | final Drawable serviceIcon; |
| 2741 | synchronized (mLock) { |
| 2742 | serviceLabel = mService.getServiceLabelLocked(); |
| 2743 | serviceIcon = mService.getServiceIconLocked(); |
| 2744 | } |
| 2745 | if (serviceLabel == null || serviceIcon == null) { |
| 2746 | wtf(null, "onFillReady(): no service label or icon"); |
| 2747 | return; |
| 2748 | } |
| 2749 | |
| 2750 | if (response.supportsInlineSuggestions()) { |
| 2751 | synchronized (mLock) { |
| 2752 | if (requestShowInlineSuggestionsLocked(response, filterText)) { |
| 2753 | final ViewState currentView = mViewStates.get(mCurrentViewId); |
| 2754 | currentView.setState(ViewState.STATE_INLINE_SHOWN); |
| 2755 | //TODO(b/137800469): Fix it to log showed only when IME asks for inflation, |
| 2756 | // rather than here where framework sends back the response. |
| 2757 | mService.logDatasetShown(id, mClientState); |
| 2758 | return; |
| 2759 | } |
| 2760 | } |
| 2761 | } |
| 2762 | |
| 2763 | getUiForShowing().showFillUi(filledId, response, filterText, |
| 2764 | mService.getServicePackageName(), mComponentName, |
| 2765 | serviceLabel, serviceIcon, this, id, mCompatMode); |
| 2766 | |
| 2767 | mService.logDatasetShown(id, mClientState); |
| 2768 | |
| 2769 | synchronized (mLock) { |
| 2770 | if (mUiShownTime == 0) { |
| 2771 | // Log first time UI is shown. |
| 2772 | mUiShownTime = SystemClock.elapsedRealtime(); |
| 2773 | final long duration = mUiShownTime - mStartTime; |
| 2774 | if (sDebug) { |
| 2775 | final StringBuilder msg = new StringBuilder("1st UI for ") |
| 2776 | .append(mActivityToken) |
| 2777 | .append(" shown in "); |
| 2778 | TimeUtils.formatDuration(duration, msg); |
| 2779 | Slog.d(TAG, msg.toString()); |
| 2780 | } |
| 2781 | final StringBuilder historyLog = new StringBuilder("id=").append(id) |
| 2782 | .append(" app=").append(mActivityToken) |
| 2783 | .append(" svc=").append(mService.getServicePackageName()) |
| 2784 | .append(" latency="); |
| 2785 | TimeUtils.formatDuration(duration, historyLog); |
| 2786 | mUiLatencyHistory.log(historyLog.toString()); |
| 2787 | |
| 2788 | addTaggedDataToRequestLogLocked(response.getRequestId(), |
| 2789 | MetricsEvent.FIELD_AUTOFILL_DURATION, duration); |
| 2790 | } |
| 2791 | } |
| 2792 | } |
| 2793 | |
| 2794 | /** |
| 2795 | * Returns whether we made a request to show inline suggestions. |
| 2796 | */ |
| 2797 | private boolean requestShowInlineSuggestionsLocked(@NonNull FillResponse response, |
| 2798 | @Nullable String filterText) { |
| 2799 | if (mCurrentViewId == null) { |
| 2800 | Log.w(TAG, "requestShowInlineSuggestionsLocked(): no view currently focused"); |
| 2801 | return false; |
| 2802 | } |
| 2803 | final AutofillId focusedId = mCurrentViewId; |
| 2804 | |
| 2805 | final Optional<InlineSuggestionsRequest> inlineSuggestionsRequest = |
| 2806 | mInlineSessionController.getInlineSuggestionsRequestLocked(); |
| 2807 | if (!inlineSuggestionsRequest.isPresent()) { |
| 2808 | Log.w(TAG, "InlineSuggestionsRequest unavailable"); |
| 2809 | return false; |
| 2810 | } |
| 2811 | |
| 2812 | final RemoteInlineSuggestionRenderService remoteRenderService = |
| 2813 | mService.getRemoteInlineSuggestionRenderServiceLocked(); |
| 2814 | if (remoteRenderService == null) { |
| 2815 | Log.w(TAG, "RemoteInlineSuggestionRenderService not found"); |
| 2816 | return false; |
| 2817 | } |
| 2818 | |
| 2819 | final ViewState currentView = mViewStates.get(focusedId); |
| 2820 | if ((currentView.getState() & ViewState.STATE_INLINE_DISABLED) != 0) { |
| 2821 | response.getDatasets().clear(); |
| 2822 | } |
| 2823 | InlineSuggestionsResponse inlineSuggestionsResponse = |
| 2824 | InlineSuggestionFactory.createInlineSuggestionsResponse( |
| 2825 | inlineSuggestionsRequest.get(), response, filterText, focusedId, |
| 2826 | this, () -> { |
| 2827 | synchronized (mLock) { |
| 2828 | mInlineSessionController.hideInlineSuggestionsUiLocked( |
| 2829 | focusedId); |
| 2830 | } |
| 2831 | }, remoteRenderService); |
| 2832 | if (inlineSuggestionsResponse == null) { |
| 2833 | Slog.w(TAG, "InlineSuggestionFactory created null response"); |
| 2834 | return false; |
| 2835 | } |
| 2836 | |
| 2837 | return mInlineSessionController.onInlineSuggestionsResponseLocked(focusedId, |
| 2838 | inlineSuggestionsResponse); |
| 2839 | } |
| 2840 | |
| 2841 | boolean isDestroyed() { |
| 2842 | synchronized (mLock) { |
| 2843 | return mDestroyed; |
| 2844 | } |
| 2845 | } |
| 2846 | |
| 2847 | IAutoFillManagerClient getClient() { |
| 2848 | synchronized (mLock) { |
| 2849 | return mClient; |
| 2850 | } |
| 2851 | } |
| 2852 | |
| 2853 | private void notifyUnavailableToClient(int sessionFinishedState, |
| 2854 | @Nullable ArrayList<AutofillId> autofillableIds) { |
| 2855 | synchronized (mLock) { |
| 2856 | if (mCurrentViewId == null) return; |
| 2857 | try { |
| 2858 | if (mHasCallback) { |
| 2859 | mClient.notifyNoFillUi(id, mCurrentViewId, sessionFinishedState); |
| 2860 | } else if (sessionFinishedState != AutofillManager.STATE_UNKNOWN) { |
| 2861 | mClient.setSessionFinished(sessionFinishedState, autofillableIds); |
| 2862 | } |
| 2863 | } catch (RemoteException e) { |
| 2864 | Slog.e(TAG, "Error notifying client no fill UI: id=" + mCurrentViewId, e); |
| 2865 | } |
| 2866 | } |
| 2867 | } |
| 2868 | |
| 2869 | private void notifyDisableAutofillToClient(long disableDuration, ComponentName componentName) { |
| 2870 | synchronized (mLock) { |
| 2871 | if (mCurrentViewId == null) return; |
| 2872 | try { |
| 2873 | mClient.notifyDisableAutofill(disableDuration, componentName); |
| 2874 | } catch (RemoteException e) { |
| 2875 | Slog.e(TAG, "Error notifying client disable autofill: id=" + mCurrentViewId, e); |
| 2876 | } |
| 2877 | } |
| 2878 | } |
| 2879 | |
| 2880 | @GuardedBy("mLock") |
| 2881 | private void updateTrackedIdsLocked() { |
| 2882 | // Only track the views of the last response as only those are reported back to the |
| 2883 | // service, see #showSaveLocked |
| 2884 | final FillResponse response = getLastResponseLocked(null); |
| 2885 | if (response == null) return; |
| 2886 | |
| 2887 | ArraySet<AutofillId> trackedViews = null; |
| 2888 | mSaveOnAllViewsInvisible = false; |
| 2889 | boolean saveOnFinish = true; |
| 2890 | final SaveInfo saveInfo = response.getSaveInfo(); |
| 2891 | final AutofillId saveTriggerId; |
| 2892 | final int flags; |
| 2893 | if (saveInfo != null) { |
| 2894 | saveTriggerId = saveInfo.getTriggerId(); |
| 2895 | if (saveTriggerId != null) { |
| 2896 | writeLog(MetricsEvent.AUTOFILL_EXPLICIT_SAVE_TRIGGER_DEFINITION); |
| 2897 | } |
| 2898 | flags = saveInfo.getFlags(); |
| 2899 | mSaveOnAllViewsInvisible = (flags & SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) != 0; |
| 2900 | |
| 2901 | // We only need to track views if we want to save once they become invisible. |
| 2902 | if (mSaveOnAllViewsInvisible) { |
| 2903 | if (trackedViews == null) { |
| 2904 | trackedViews = new ArraySet<>(); |
| 2905 | } |
| 2906 | if (saveInfo.getRequiredIds() != null) { |
| 2907 | Collections.addAll(trackedViews, saveInfo.getRequiredIds()); |
| 2908 | } |
| 2909 | |
| 2910 | if (saveInfo.getOptionalIds() != null) { |
| 2911 | Collections.addAll(trackedViews, saveInfo.getOptionalIds()); |
| 2912 | } |
| 2913 | } |
| 2914 | if ((flags & SaveInfo.FLAG_DONT_SAVE_ON_FINISH) != 0) { |
| 2915 | saveOnFinish = false; |
| 2916 | } |
| 2917 | |
| 2918 | } else { |
| 2919 | flags = 0; |
| 2920 | saveTriggerId = null; |
| 2921 | } |
| 2922 | |
| 2923 | // Must also track that are part of datasets, otherwise the FillUI won't be hidden when |
| 2924 | // they go away (if they're not savable). |
| 2925 | |
| 2926 | final List<Dataset> datasets = response.getDatasets(); |
| 2927 | ArraySet<AutofillId> fillableIds = null; |
| 2928 | if (datasets != null) { |
| 2929 | for (int i = 0; i < datasets.size(); i++) { |
| 2930 | final Dataset dataset = datasets.get(i); |
| 2931 | final ArrayList<AutofillId> fieldIds = dataset.getFieldIds(); |
| 2932 | if (fieldIds == null) continue; |
| 2933 | |
| 2934 | for (int j = 0; j < fieldIds.size(); j++) { |
| 2935 | final AutofillId id = fieldIds.get(j); |
| 2936 | if (trackedViews == null || !trackedViews.contains(id)) { |
| 2937 | fillableIds = ArrayUtils.add(fillableIds, id); |
| 2938 | } |
| 2939 | } |
| 2940 | } |
| 2941 | } |
| 2942 | |
| 2943 | try { |
| 2944 | if (sVerbose) { |
| 2945 | Slog.v(TAG, "updateTrackedIdsLocked(): " + trackedViews + " => " + fillableIds |
| 2946 | + " triggerId: " + saveTriggerId + " saveOnFinish:" + saveOnFinish |
| 2947 | + " flags: " + flags + " hasSaveInfo: " + (saveInfo != null)); |
| 2948 | } |
| 2949 | mClient.setTrackedViews(id, toArray(trackedViews), mSaveOnAllViewsInvisible, |
| 2950 | saveOnFinish, toArray(fillableIds), saveTriggerId); |
| 2951 | } catch (RemoteException e) { |
| 2952 | Slog.w(TAG, "Cannot set tracked ids", e); |
| 2953 | } |
| 2954 | } |
| 2955 | |
| 2956 | /** |
| 2957 | * Sets the state of views that failed to autofill. |
| 2958 | */ |
| 2959 | @GuardedBy("mLock") |
| 2960 | void setAutofillFailureLocked(@NonNull List<AutofillId> ids) { |
| 2961 | for (int i = 0; i < ids.size(); i++) { |
| 2962 | final AutofillId id = ids.get(i); |
| 2963 | final ViewState viewState = mViewStates.get(id); |
| 2964 | if (viewState == null) { |
| 2965 | Slog.w(TAG, "setAutofillFailure(): no view for id " + id); |
| 2966 | continue; |
| 2967 | } |
| 2968 | viewState.resetState(ViewState.STATE_AUTOFILLED); |
| 2969 | final int state = viewState.getState(); |
| 2970 | viewState.setState(state | ViewState.STATE_AUTOFILL_FAILED); |
| 2971 | if (sVerbose) { |
| 2972 | Slog.v(TAG, "Changed state of " + id + " to " + viewState.getStateAsString()); |
| 2973 | } |
| 2974 | } |
| 2975 | } |
| 2976 | |
| 2977 | @GuardedBy("mLock") |
| 2978 | private void replaceResponseLocked(@NonNull FillResponse oldResponse, |
| 2979 | @NonNull FillResponse newResponse, @Nullable Bundle newClientState) { |
| 2980 | // Disassociate view states with the old response |
| 2981 | setViewStatesLocked(oldResponse, ViewState.STATE_INITIAL, true); |
| 2982 | // Move over the id |
| 2983 | newResponse.setRequestId(oldResponse.getRequestId()); |
| 2984 | // Replace the old response |
| 2985 | mResponses.put(newResponse.getRequestId(), newResponse); |
| 2986 | // Now process the new response |
| 2987 | processResponseLocked(newResponse, newClientState, 0); |
| 2988 | } |
| 2989 | |
| 2990 | @GuardedBy("mLock") |
| 2991 | private void processNullResponseLocked(int requestId, int flags) { |
| 2992 | if ((flags & FLAG_MANUAL_REQUEST) != 0) { |
| 2993 | getUiForShowing().showError(R.string.autofill_error_cannot_autofill, this); |
| 2994 | } |
| 2995 | |
| 2996 | final FillContext context = getFillContextByRequestIdLocked(requestId); |
| 2997 | |
| 2998 | final ArrayList<AutofillId> autofillableIds; |
| 2999 | if (context != null) { |
| 3000 | final AssistStructure structure = context.getStructure(); |
| 3001 | autofillableIds = Helper.getAutofillIds(structure, /* autofillableOnly= */true); |
| 3002 | } else { |
| 3003 | Slog.w(TAG, "processNullResponseLocked(): no context for req " + requestId); |
| 3004 | autofillableIds = null; |
| 3005 | } |
| 3006 | |
| 3007 | mService.resetLastResponse(); |
| 3008 | |
| 3009 | // The default autofill service cannot fullfill the request, let's check if the augmented |
| 3010 | // autofill service can. |
| 3011 | mAugmentedAutofillDestroyer = triggerAugmentedAutofillLocked(flags); |
| 3012 | if (mAugmentedAutofillDestroyer == null && ((flags & FLAG_PASSWORD_INPUT_TYPE) == 0)) { |
| 3013 | if (sVerbose) { |
| 3014 | Slog.v(TAG, "canceling session " + id + " when service returned null and it cannot " |
| 3015 | + "be augmented. AutofillableIds: " + autofillableIds); |
| 3016 | } |
| 3017 | // Nothing to be done, but need to notify client. |
| 3018 | notifyUnavailableToClient(AutofillManager.STATE_FINISHED, autofillableIds); |
| 3019 | removeSelf(); |
| 3020 | } else { |
| 3021 | if (sVerbose) { |
| 3022 | if ((flags & FLAG_PASSWORD_INPUT_TYPE) != 0) { |
| 3023 | Slog.v(TAG, "keeping session " + id + " when service returned null and " |
| 3024 | + "augmented service is disabled for password fields. " |
| 3025 | + "AutofillableIds: " + autofillableIds); |
| 3026 | } else { |
| 3027 | Slog.v(TAG, "keeping session " + id + " when service returned null but " |
| 3028 | + "it can be augmented. AutofillableIds: " + autofillableIds); |
| 3029 | } |
| 3030 | } |
| 3031 | mAugmentedAutofillableIds = autofillableIds; |
| 3032 | try { |
| 3033 | mClient.setState(AutofillManager.SET_STATE_FLAG_FOR_AUTOFILL_ONLY); |
| 3034 | } catch (RemoteException e) { |
| 3035 | Slog.e(TAG, "Error setting client to autofill-only", e); |
| 3036 | } |
| 3037 | } |
| 3038 | } |
| 3039 | |
| 3040 | /** |
| 3041 | * Tries to trigger Augmented Autofill when the standard service could not fulfill a request. |
| 3042 | * |
| 3043 | * <p> The request may not have been sent when this method returns as it may be waiting for |
| 3044 | * the inline suggestion request asynchronously. |
| 3045 | * |
| 3046 | * @return callback to destroy the autofill UI, or {@code null} if not supported. |
| 3047 | */ |
| 3048 | // TODO(b/123099468): might need to call it in other places, like when the service returns a |
| 3049 | // non-null response but without datasets (for example, just SaveInfo) |
| 3050 | @GuardedBy("mLock") |
| 3051 | private Runnable triggerAugmentedAutofillLocked(int flags) { |
| 3052 | // (TODO: b/141703197) Fix later by passing info to service. |
| 3053 | if ((flags & FLAG_PASSWORD_INPUT_TYPE) != 0) { |
| 3054 | return null; |
| 3055 | } |
| 3056 | |
| 3057 | // Check if Smart Suggestions is supported... |
| 3058 | final @SmartSuggestionMode int supportedModes = mService |
| 3059 | .getSupportedSmartSuggestionModesLocked(); |
| 3060 | if (supportedModes == 0) { |
| 3061 | if (sVerbose) Slog.v(TAG, "triggerAugmentedAutofillLocked(): no supported modes"); |
| 3062 | return null; |
| 3063 | } |
| 3064 | |
| 3065 | // ...then if the service is set for the user |
| 3066 | |
| 3067 | final RemoteAugmentedAutofillService remoteService = mService |
| 3068 | .getRemoteAugmentedAutofillServiceLocked(); |
| 3069 | if (remoteService == null) { |
| 3070 | if (sVerbose) Slog.v(TAG, "triggerAugmentedAutofillLocked(): no service for user"); |
| 3071 | return null; |
| 3072 | } |
| 3073 | |
| 3074 | // Define which mode will be used |
| 3075 | final int mode; |
| 3076 | if ((supportedModes & FLAG_SMART_SUGGESTION_SYSTEM) != 0) { |
| 3077 | mode = FLAG_SMART_SUGGESTION_SYSTEM; |
| 3078 | } else { |
| 3079 | Slog.w(TAG, "Unsupported Smart Suggestion mode: " + supportedModes); |
| 3080 | return null; |
| 3081 | } |
| 3082 | |
| 3083 | if (mCurrentViewId == null) { |
| 3084 | Slog.w(TAG, "triggerAugmentedAutofillLocked(): no view currently focused"); |
| 3085 | return null; |
| 3086 | } |
| 3087 | |
| 3088 | final boolean isWhitelisted = mService |
| 3089 | .isWhitelistedForAugmentedAutofillLocked(mComponentName); |
| 3090 | |
| 3091 | if (!isWhitelisted) { |
| 3092 | if (sVerbose) { |
| 3093 | Slog.v(TAG, "triggerAugmentedAutofillLocked(): " |
| 3094 | + ComponentName.flattenToShortString(mComponentName) + " not whitelisted "); |
| 3095 | } |
| 3096 | logAugmentedAutofillRequestLocked(mode, remoteService.getComponentName(), |
| 3097 | mCurrentViewId, isWhitelisted, /*isInline*/null); |
| 3098 | return null; |
| 3099 | } |
| 3100 | |
| 3101 | if (sVerbose) { |
| 3102 | Slog.v(TAG, "calling Augmented Autofill Service (" |
| 3103 | + ComponentName.flattenToShortString(remoteService.getComponentName()) |
| 3104 | + ") on view " + mCurrentViewId + " using suggestion mode " |
| 3105 | + getSmartSuggestionModeToString(mode) |
| 3106 | + " when server returned null for session " + this.id); |
| 3107 | } |
| 3108 | |
| 3109 | final ViewState viewState = mViewStates.get(mCurrentViewId); |
| 3110 | viewState.setState(ViewState.STATE_TRIGGERED_AUGMENTED_AUTOFILL); |
| 3111 | final AutofillValue currentValue = viewState.getCurrentValue(); |
| 3112 | |
| 3113 | if (mAugmentedRequestsLogs == null) { |
| 3114 | mAugmentedRequestsLogs = new ArrayList<>(); |
| 3115 | } |
| 3116 | final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_AUGMENTED_REQUEST, |
| 3117 | remoteService.getComponentName().getPackageName()); |
| 3118 | mAugmentedRequestsLogs.add(log); |
| 3119 | |
| 3120 | final AutofillId focusedId = mCurrentViewId; |
| 3121 | |
| 3122 | final Function<InlineSuggestionsResponse, Boolean> inlineSuggestionsResponseCallback = |
| 3123 | response -> { |
| 3124 | synchronized (mLock) { |
| 3125 | return mInlineSessionController.onInlineSuggestionsResponseLocked( |
| 3126 | focusedId, response); |
| 3127 | } |
| 3128 | }; |
| 3129 | final Consumer<InlineSuggestionsRequest> requestAugmentedAutofill = |
| 3130 | (inlineSuggestionsRequest) -> { |
| 3131 | synchronized (mLock) { |
| 3132 | logAugmentedAutofillRequestLocked(mode, remoteService.getComponentName(), |
| 3133 | focusedId, isWhitelisted, inlineSuggestionsRequest != null); |
| 3134 | remoteService.onRequestAutofillLocked(id, mClient, taskId, mComponentName, |
| 3135 | AutofillId.withoutSession(focusedId), currentValue, |
| 3136 | inlineSuggestionsRequest, inlineSuggestionsResponseCallback, |
| 3137 | /*onErrorCallback=*/ () -> { |
| 3138 | synchronized (mLock) { |
| 3139 | cancelAugmentedAutofillLocked(); |
| 3140 | } |
| 3141 | }, mService.getRemoteInlineSuggestionRenderServiceLocked()); |
| 3142 | } |
| 3143 | }; |
| 3144 | |
| 3145 | // When the inline suggestion render service is available, there are 2 cases when |
| 3146 | // augmented autofill should ask IME for inline suggestion request, because standard |
| 3147 | // autofill flow didn't: |
| 3148 | // 1. the field is augmented autofill only (when standard autofill provider is None or |
| 3149 | // when it returns null response) |
| 3150 | // 2. standard autofill provider doesn't support inline suggestion |
| 3151 | final RemoteInlineSuggestionRenderService remoteRenderService = |
| 3152 | mService.getRemoteInlineSuggestionRenderServiceLocked(); |
| 3153 | if (remoteRenderService != null |
| 3154 | && (mForAugmentedAutofillOnly |
| 3155 | || !isInlineSuggestionsEnabledByAutofillProviderLocked())) { |
| 3156 | if (sDebug) Slog.d(TAG, "Create inline request for augmented autofill"); |
| 3157 | remoteRenderService.getInlineSuggestionsRendererInfo(new RemoteCallback( |
| 3158 | (extras) -> { |
| 3159 | synchronized (mLock) { |
| 3160 | mInlineSessionController.onCreateInlineSuggestionsRequestLocked( |
| 3161 | focusedId, /*requestConsumer=*/ requestAugmentedAutofill, |
| 3162 | extras); |
| 3163 | } |
| 3164 | }, mHandler)); |
| 3165 | } else { |
| 3166 | requestAugmentedAutofill.accept( |
| 3167 | mInlineSessionController.getInlineSuggestionsRequestLocked().orElse(null)); |
| 3168 | } |
| 3169 | if (mAugmentedAutofillDestroyer == null) { |
| 3170 | mAugmentedAutofillDestroyer = () -> remoteService.onDestroyAutofillWindowsRequest(); |
| 3171 | } |
| 3172 | return mAugmentedAutofillDestroyer; |
| 3173 | } |
| 3174 | |
| 3175 | @GuardedBy("mLock") |
| 3176 | private void logAugmentedAutofillRequestLocked(int mode, |
| 3177 | ComponentName augmentedRemoteServiceName, AutofillId focusedId, boolean isWhitelisted, |
| 3178 | Boolean isInline) { |
| 3179 | final String historyItem = |
| 3180 | "aug:id=" + id + " u=" + uid + " m=" + mode |
| 3181 | + " a=" + ComponentName.flattenToShortString(mComponentName) |
| 3182 | + " f=" + focusedId |
| 3183 | + " s=" + augmentedRemoteServiceName |
| 3184 | + " w=" + isWhitelisted |
| 3185 | + " i=" + isInline; |
| 3186 | mService.getMaster().logRequestLocked(historyItem); |
| 3187 | } |
| 3188 | |
| 3189 | @GuardedBy("mLock") |
| 3190 | private void cancelAugmentedAutofillLocked() { |
| 3191 | final RemoteAugmentedAutofillService remoteService = mService |
| 3192 | .getRemoteAugmentedAutofillServiceLocked(); |
| 3193 | if (remoteService == null) { |
| 3194 | Slog.w(TAG, "cancelAugmentedAutofillLocked(): no service for user"); |
| 3195 | return; |
| 3196 | } |
| 3197 | if (sVerbose) Slog.v(TAG, "cancelAugmentedAutofillLocked() on " + mCurrentViewId); |
| 3198 | remoteService.onDestroyAutofillWindowsRequest(); |
| 3199 | } |
| 3200 | |
| 3201 | @GuardedBy("mLock") |
| 3202 | private void processResponseLocked(@NonNull FillResponse newResponse, |
| 3203 | @Nullable Bundle newClientState, int flags) { |
| 3204 | // Make sure we are hiding the UI which will be shown |
| 3205 | // only if handling the current response requires it. |
| 3206 | mUi.hideAll(this); |
| 3207 | |
| 3208 | final int requestId = newResponse.getRequestId(); |
| 3209 | if (sVerbose) { |
| 3210 | Slog.v(TAG, "processResponseLocked(): mCurrentViewId=" + mCurrentViewId |
| 3211 | + ",flags=" + flags + ", reqId=" + requestId + ", resp=" + newResponse |
| 3212 | + ",newClientState=" + newClientState); |
| 3213 | } |
| 3214 | |
| 3215 | if (mResponses == null) { |
| 3216 | // Set initial capacity as 2 to handle cases where service always requires auth. |
| 3217 | // TODO: add a metric for number of responses set by server, so we can use its average |
| 3218 | // as the initial array capacitiy. |
| 3219 | mResponses = new SparseArray<>(2); |
| 3220 | } |
| 3221 | mResponses.put(requestId, newResponse); |
| 3222 | mClientState = newClientState != null ? newClientState : newResponse.getClientState(); |
| 3223 | |
| 3224 | setViewStatesLocked(newResponse, ViewState.STATE_FILLABLE, false); |
| 3225 | updateTrackedIdsLocked(); |
| 3226 | |
| 3227 | if (mCurrentViewId == null) { |
| 3228 | return; |
| 3229 | } |
| 3230 | |
| 3231 | // Updates the UI, if necessary. |
| 3232 | final ViewState currentView = mViewStates.get(mCurrentViewId); |
| 3233 | currentView.maybeCallOnFillReady(flags); |
| 3234 | } |
| 3235 | |
| 3236 | /** |
| 3237 | * Sets the state of all views in the given response. |
| 3238 | */ |
| 3239 | @GuardedBy("mLock") |
| 3240 | private void setViewStatesLocked(FillResponse response, int state, boolean clearResponse) { |
| 3241 | final List<Dataset> datasets = response.getDatasets(); |
| 3242 | if (datasets != null) { |
| 3243 | for (int i = 0; i < datasets.size(); i++) { |
| 3244 | final Dataset dataset = datasets.get(i); |
| 3245 | if (dataset == null) { |
| 3246 | Slog.w(TAG, "Ignoring null dataset on " + datasets); |
| 3247 | continue; |
| 3248 | } |
| 3249 | setViewStatesLocked(response, dataset, state, clearResponse); |
| 3250 | } |
| 3251 | } else if (response.getAuthentication() != null) { |
| 3252 | for (AutofillId autofillId : response.getAuthenticationIds()) { |
| 3253 | final ViewState viewState = createOrUpdateViewStateLocked(autofillId, state, null); |
| 3254 | if (!clearResponse) { |
| 3255 | viewState.setResponse(response); |
| 3256 | } else { |
| 3257 | viewState.setResponse(null); |
| 3258 | } |
| 3259 | } |
| 3260 | } |
| 3261 | final SaveInfo saveInfo = response.getSaveInfo(); |
| 3262 | if (saveInfo != null) { |
| 3263 | final AutofillId[] requiredIds = saveInfo.getRequiredIds(); |
| 3264 | if (requiredIds != null) { |
| 3265 | for (AutofillId id : requiredIds) { |
| 3266 | createOrUpdateViewStateLocked(id, state, null); |
| 3267 | } |
| 3268 | } |
| 3269 | final AutofillId[] optionalIds = saveInfo.getOptionalIds(); |
| 3270 | if (optionalIds != null) { |
| 3271 | for (AutofillId id : optionalIds) { |
| 3272 | createOrUpdateViewStateLocked(id, state, null); |
| 3273 | } |
| 3274 | } |
| 3275 | } |
| 3276 | |
| 3277 | final AutofillId[] authIds = response.getAuthenticationIds(); |
| 3278 | if (authIds != null) { |
| 3279 | for (AutofillId id : authIds) { |
| 3280 | createOrUpdateViewStateLocked(id, state, null); |
| 3281 | } |
| 3282 | } |
| 3283 | } |
| 3284 | |
| 3285 | /** |
| 3286 | * Sets the state and response of all views in the given dataset. |
| 3287 | */ |
| 3288 | @GuardedBy("mLock") |
| 3289 | private void setViewStatesLocked(@Nullable FillResponse response, @NonNull Dataset dataset, |
| 3290 | int state, boolean clearResponse) { |
| 3291 | final ArrayList<AutofillId> ids = dataset.getFieldIds(); |
| 3292 | final ArrayList<AutofillValue> values = dataset.getFieldValues(); |
| 3293 | for (int j = 0; j < ids.size(); j++) { |
| 3294 | final AutofillId id = ids.get(j); |
| 3295 | final AutofillValue value = values.get(j); |
| 3296 | final ViewState viewState = createOrUpdateViewStateLocked(id, state, value); |
| 3297 | final String datasetId = dataset.getId(); |
| 3298 | if (datasetId != null) { |
| 3299 | viewState.setDatasetId(datasetId); |
| 3300 | } |
| 3301 | if (clearResponse) { |
| 3302 | viewState.setResponse(null); |
| 3303 | } else if (response != null) { |
| 3304 | viewState.setResponse(response); |
| 3305 | } |
| 3306 | } |
| 3307 | } |
| 3308 | |
| 3309 | @GuardedBy("mLock") |
| 3310 | private ViewState createOrUpdateViewStateLocked(@NonNull AutofillId id, int state, |
| 3311 | @Nullable AutofillValue value) { |
| 3312 | ViewState viewState = mViewStates.get(id); |
| 3313 | if (viewState != null) { |
| 3314 | viewState.setState(state); |
| 3315 | } else { |
| 3316 | viewState = new ViewState(id, this, state); |
| 3317 | if (sVerbose) { |
| 3318 | Slog.v(TAG, "Adding autofillable view with id " + id + " and state " + state); |
| 3319 | } |
| 3320 | viewState.setCurrentValue(findValueLocked(id)); |
| 3321 | mViewStates.put(id, viewState); |
| 3322 | } |
| 3323 | if ((state & ViewState.STATE_AUTOFILLED) != 0) { |
| 3324 | viewState.setAutofilledValue(value); |
| 3325 | } |
| 3326 | return viewState; |
| 3327 | } |
| 3328 | |
| 3329 | void autoFill(int requestId, int datasetIndex, Dataset dataset, boolean generateEvent) { |
| 3330 | if (sDebug) { |
| 3331 | Slog.d(TAG, "autoFill(): requestId=" + requestId + "; datasetIdx=" + datasetIndex |
| 3332 | + "; dataset=" + dataset); |
| 3333 | } |
| 3334 | synchronized (mLock) { |
| 3335 | if (mDestroyed) { |
| 3336 | Slog.w(TAG, "Call to Session#autoFill() rejected - session: " |
| 3337 | + id + " destroyed"); |
| 3338 | return; |
| 3339 | } |
| 3340 | // Autofill it directly... |
| 3341 | if (dataset.getAuthentication() == null) { |
| 3342 | if (generateEvent) { |
| 3343 | mService.logDatasetSelected(dataset.getId(), id, mClientState); |
| 3344 | } |
| 3345 | |
| 3346 | autoFillApp(dataset); |
| 3347 | return; |
| 3348 | } |
| 3349 | |
| 3350 | // ...or handle authentication. |
| 3351 | mService.logDatasetAuthenticationSelected(dataset.getId(), id, mClientState); |
| 3352 | setViewStatesLocked(null, dataset, ViewState.STATE_WAITING_DATASET_AUTH, false); |
| 3353 | final Intent fillInIntent = createAuthFillInIntentLocked(requestId, mClientState); |
| 3354 | if (fillInIntent == null) { |
| 3355 | forceRemoveSelfLocked(); |
| 3356 | return; |
| 3357 | } |
| 3358 | final int authenticationId = AutofillManager.makeAuthenticationId(requestId, |
| 3359 | datasetIndex); |
| 3360 | startAuthentication(authenticationId, dataset.getAuthentication(), fillInIntent, |
| 3361 | /* authenticateInline= */false); |
| 3362 | |
| 3363 | } |
| 3364 | } |
| 3365 | |
| 3366 | // TODO: this should never be null, but we got at least one occurrence, probably due to a race. |
| 3367 | @GuardedBy("mLock") |
| 3368 | @Nullable |
| 3369 | private Intent createAuthFillInIntentLocked(int requestId, Bundle extras) { |
| 3370 | final Intent fillInIntent = new Intent(); |
| 3371 | |
| 3372 | final FillContext context = getFillContextByRequestIdLocked(requestId); |
| 3373 | |
| 3374 | if (context == null) { |
| 3375 | wtf(null, "createAuthFillInIntentLocked(): no FillContext. requestId=%d; mContexts=%s", |
| 3376 | requestId, mContexts); |
| 3377 | return null; |
| 3378 | } |
| 3379 | fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, context.getStructure()); |
| 3380 | fillInIntent.putExtra(AutofillManager.EXTRA_CLIENT_STATE, extras); |
| 3381 | return fillInIntent; |
| 3382 | } |
| 3383 | |
| 3384 | private void startAuthentication(int authenticationId, IntentSender intent, |
| 3385 | Intent fillInIntent, boolean authenticateInline) { |
| 3386 | try { |
| 3387 | synchronized (mLock) { |
| 3388 | mClient.authenticate(id, authenticationId, intent, fillInIntent, |
| 3389 | authenticateInline); |
| 3390 | } |
| 3391 | } catch (RemoteException e) { |
| 3392 | Slog.e(TAG, "Error launching auth intent", e); |
| 3393 | } |
| 3394 | } |
| 3395 | |
| 3396 | @Override |
| 3397 | public String toString() { |
| 3398 | return "Session: [id=" + id + ", component=" + mComponentName + "]"; |
| 3399 | } |
| 3400 | |
| 3401 | @GuardedBy("mLock") |
| 3402 | void dumpLocked(String prefix, PrintWriter pw) { |
| 3403 | final String prefix2 = prefix + " "; |
| 3404 | pw.print(prefix); pw.print("id: "); pw.println(id); |
| 3405 | pw.print(prefix); pw.print("uid: "); pw.println(uid); |
| 3406 | pw.print(prefix); pw.print("taskId: "); pw.println(taskId); |
| 3407 | pw.print(prefix); pw.print("flags: "); pw.println(mFlags); |
| 3408 | pw.print(prefix); pw.print("mComponentName: "); pw.println(mComponentName); |
| 3409 | pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken); |
| 3410 | pw.print(prefix); pw.print("mStartTime: "); pw.println(mStartTime); |
| 3411 | pw.print(prefix); pw.print("Time to show UI: "); |
| 3412 | if (mUiShownTime == 0) { |
| 3413 | pw.println("N/A"); |
| 3414 | } else { |
| 3415 | TimeUtils.formatDuration(mUiShownTime - mStartTime, pw); |
| 3416 | pw.println(); |
| 3417 | } |
| 3418 | final int requestLogsSizes = mRequestLogs.size(); |
| 3419 | pw.print(prefix); pw.print("mSessionLogs: "); pw.println(requestLogsSizes); |
| 3420 | for (int i = 0; i < requestLogsSizes; i++) { |
| 3421 | final int requestId = mRequestLogs.keyAt(i); |
| 3422 | final LogMaker log = mRequestLogs.valueAt(i); |
| 3423 | pw.print(prefix2); pw.print('#'); pw.print(i); pw.print(": req="); |
| 3424 | pw.print(requestId); pw.print(", log=" ); dumpRequestLog(pw, log); pw.println(); |
| 3425 | } |
| 3426 | pw.print(prefix); pw.print("mResponses: "); |
| 3427 | if (mResponses == null) { |
| 3428 | pw.println("null"); |
| 3429 | } else { |
| 3430 | pw.println(mResponses.size()); |
| 3431 | for (int i = 0; i < mResponses.size(); i++) { |
| 3432 | pw.print(prefix2); pw.print('#'); pw.print(i); |
| 3433 | pw.print(' '); pw.println(mResponses.valueAt(i)); |
| 3434 | } |
| 3435 | } |
| 3436 | pw.print(prefix); pw.print("mCurrentViewId: "); pw.println(mCurrentViewId); |
| 3437 | pw.print(prefix); pw.print("mDestroyed: "); pw.println(mDestroyed); |
| 3438 | pw.print(prefix); pw.print("mIsSaving: "); pw.println(mIsSaving); |
| 3439 | pw.print(prefix); pw.print("mPendingSaveUi: "); pw.println(mPendingSaveUi); |
| 3440 | final int numberViews = mViewStates.size(); |
| 3441 | pw.print(prefix); pw.print("mViewStates size: "); pw.println(mViewStates.size()); |
| 3442 | for (int i = 0; i < numberViews; i++) { |
| 3443 | pw.print(prefix); pw.print("ViewState at #"); pw.println(i); |
| 3444 | mViewStates.valueAt(i).dump(prefix2, pw); |
| 3445 | } |
| 3446 | |
| 3447 | pw.print(prefix); pw.print("mContexts: " ); |
| 3448 | if (mContexts != null) { |
| 3449 | int numContexts = mContexts.size(); |
| 3450 | for (int i = 0; i < numContexts; i++) { |
| 3451 | FillContext context = mContexts.get(i); |
| 3452 | |
| 3453 | pw.print(prefix2); pw.print(context); |
| 3454 | if (sVerbose) { |
| 3455 | pw.println("AssistStructure dumped at logcat)"); |
| 3456 | |
| 3457 | // TODO: add method on AssistStructure to dump on pw |
| 3458 | context.getStructure().dump(false); |
| 3459 | } |
| 3460 | } |
| 3461 | } else { |
| 3462 | pw.println("null"); |
| 3463 | } |
| 3464 | |
| 3465 | pw.print(prefix); pw.print("mHasCallback: "); pw.println(mHasCallback); |
| 3466 | if (mClientState != null) { |
| 3467 | pw.print(prefix); pw.print("mClientState: "); pw.print(mClientState.getSize()); pw |
| 3468 | .println(" bytes"); |
| 3469 | } |
| 3470 | pw.print(prefix); pw.print("mCompatMode: "); pw.println(mCompatMode); |
| 3471 | pw.print(prefix); pw.print("mUrlBar: "); |
| 3472 | if (mUrlBar == null) { |
| 3473 | pw.println("N/A"); |
| 3474 | } else { |
| 3475 | pw.print("id="); pw.print(mUrlBar.getAutofillId()); |
| 3476 | pw.print(" domain="); pw.print(mUrlBar.getWebDomain()); |
| 3477 | pw.print(" text="); Helper.printlnRedactedText(pw, mUrlBar.getText()); |
| 3478 | } |
| 3479 | pw.print(prefix); pw.print("mSaveOnAllViewsInvisible: "); pw.println( |
| 3480 | mSaveOnAllViewsInvisible); |
| 3481 | pw.print(prefix); pw.print("mSelectedDatasetIds: "); pw.println(mSelectedDatasetIds); |
| 3482 | if (mForAugmentedAutofillOnly) { |
| 3483 | pw.print(prefix); pw.println("For Augmented Autofill Only"); |
| 3484 | } |
| 3485 | if (mAugmentedAutofillDestroyer != null) { |
| 3486 | pw.print(prefix); pw.println("has mAugmentedAutofillDestroyer"); |
| 3487 | } |
| 3488 | if (mAugmentedRequestsLogs != null) { |
| 3489 | pw.print(prefix); pw.print("number augmented requests: "); |
| 3490 | pw.println(mAugmentedRequestsLogs.size()); |
| 3491 | } |
| 3492 | |
| 3493 | if (mAugmentedAutofillableIds != null) { |
| 3494 | pw.print(prefix); pw.print("mAugmentedAutofillableIds: "); |
| 3495 | pw.println(mAugmentedAutofillableIds); |
| 3496 | } |
| 3497 | if (mRemoteFillService != null) { |
| 3498 | mRemoteFillService.dump(prefix, pw); |
| 3499 | } |
| 3500 | } |
| 3501 | |
| 3502 | private static void dumpRequestLog(@NonNull PrintWriter pw, @NonNull LogMaker log) { |
| 3503 | pw.print("CAT="); pw.print(log.getCategory()); |
| 3504 | pw.print(", TYPE="); |
| 3505 | final int type = log.getType(); |
| 3506 | switch (type) { |
| 3507 | case MetricsEvent.TYPE_SUCCESS: pw.print("SUCCESS"); break; |
| 3508 | case MetricsEvent.TYPE_FAILURE: pw.print("FAILURE"); break; |
| 3509 | case MetricsEvent.TYPE_CLOSE: pw.print("CLOSE"); break; |
| 3510 | default: pw.print("UNSUPPORTED"); |
| 3511 | } |
| 3512 | pw.print('('); pw.print(type); pw.print(')'); |
| 3513 | pw.print(", PKG="); pw.print(log.getPackageName()); |
| 3514 | pw.print(", SERVICE="); pw.print(log |
| 3515 | .getTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE)); |
| 3516 | pw.print(", ORDINAL="); pw.print(log |
| 3517 | .getTaggedData(MetricsEvent.FIELD_AUTOFILL_REQUEST_ORDINAL)); |
| 3518 | dumpNumericValue(pw, log, "FLAGS", MetricsEvent.FIELD_AUTOFILL_FLAGS); |
| 3519 | dumpNumericValue(pw, log, "NUM_DATASETS", MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS); |
| 3520 | dumpNumericValue(pw, log, "UI_LATENCY", MetricsEvent.FIELD_AUTOFILL_DURATION); |
| 3521 | final int authStatus = |
| 3522 | getNumericValue(log, MetricsEvent.FIELD_AUTOFILL_AUTHENTICATION_STATUS); |
| 3523 | if (authStatus != 0) { |
| 3524 | pw.print(", AUTH_STATUS="); |
| 3525 | switch (authStatus) { |
| 3526 | case MetricsEvent.AUTOFILL_AUTHENTICATED: |
| 3527 | pw.print("AUTHENTICATED"); break; |
| 3528 | case MetricsEvent.AUTOFILL_DATASET_AUTHENTICATED: |
| 3529 | pw.print("DATASET_AUTHENTICATED"); break; |
| 3530 | case MetricsEvent.AUTOFILL_INVALID_AUTHENTICATION: |
| 3531 | pw.print("INVALID_AUTHENTICATION"); break; |
| 3532 | case MetricsEvent.AUTOFILL_INVALID_DATASET_AUTHENTICATION: |
| 3533 | pw.print("INVALID_DATASET_AUTHENTICATION"); break; |
| 3534 | default: pw.print("UNSUPPORTED"); |
| 3535 | } |
| 3536 | pw.print('('); pw.print(authStatus); pw.print(')'); |
| 3537 | } |
| 3538 | dumpNumericValue(pw, log, "FC_IDS", |
| 3539 | MetricsEvent.FIELD_AUTOFILL_NUM_FIELD_CLASSIFICATION_IDS); |
| 3540 | dumpNumericValue(pw, log, "COMPAT_MODE", |
| 3541 | MetricsEvent.FIELD_AUTOFILL_COMPAT_MODE); |
| 3542 | } |
| 3543 | |
| 3544 | private static void dumpNumericValue(@NonNull PrintWriter pw, @NonNull LogMaker log, |
| 3545 | @NonNull String field, int tag) { |
| 3546 | final int value = getNumericValue(log, tag); |
| 3547 | if (value != 0) { |
| 3548 | pw.print(", "); pw.print(field); pw.print('='); pw.print(value); |
| 3549 | } |
| 3550 | } |
| 3551 | |
| 3552 | void autoFillApp(Dataset dataset) { |
| 3553 | synchronized (mLock) { |
| 3554 | if (mDestroyed) { |
| 3555 | Slog.w(TAG, "Call to Session#autoFillApp() rejected - session: " |
| 3556 | + id + " destroyed"); |
| 3557 | return; |
| 3558 | } |
| 3559 | try { |
| 3560 | // Skip null values as a null values means no change |
| 3561 | final int entryCount = dataset.getFieldIds().size(); |
| 3562 | final List<AutofillId> ids = new ArrayList<>(entryCount); |
| 3563 | final List<AutofillValue> values = new ArrayList<>(entryCount); |
| 3564 | boolean waitingDatasetAuth = false; |
| 3565 | boolean hideHighlight = (entryCount == 1 |
| 3566 | && dataset.getFieldIds().get(0).equals(mCurrentViewId)); |
| 3567 | for (int i = 0; i < entryCount; i++) { |
| 3568 | if (dataset.getFieldValues().get(i) == null) { |
| 3569 | continue; |
| 3570 | } |
| 3571 | final AutofillId viewId = dataset.getFieldIds().get(i); |
| 3572 | ids.add(viewId); |
| 3573 | values.add(dataset.getFieldValues().get(i)); |
| 3574 | final ViewState viewState = mViewStates.get(viewId); |
| 3575 | if (viewState != null |
| 3576 | && (viewState.getState() & ViewState.STATE_WAITING_DATASET_AUTH) != 0) { |
| 3577 | if (sVerbose) { |
| 3578 | Slog.v(TAG, "autofillApp(): view " + viewId + " waiting auth"); |
| 3579 | } |
| 3580 | waitingDatasetAuth = true; |
| 3581 | viewState.resetState(ViewState.STATE_WAITING_DATASET_AUTH); |
| 3582 | } |
| 3583 | } |
| 3584 | if (!ids.isEmpty()) { |
| 3585 | if (waitingDatasetAuth) { |
| 3586 | mUi.hideFillUi(this); |
| 3587 | } |
| 3588 | if (sDebug) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset); |
| 3589 | |
| 3590 | mClient.autofill(id, ids, values, hideHighlight); |
| 3591 | if (dataset.getId() != null) { |
| 3592 | if (mSelectedDatasetIds == null) { |
| 3593 | mSelectedDatasetIds = new ArrayList<>(); |
| 3594 | } |
| 3595 | mSelectedDatasetIds.add(dataset.getId()); |
| 3596 | } |
| 3597 | setViewStatesLocked(null, dataset, ViewState.STATE_AUTOFILLED, false); |
| 3598 | } |
| 3599 | } catch (RemoteException e) { |
| 3600 | Slog.w(TAG, "Error autofilling activity: " + e); |
| 3601 | } |
| 3602 | } |
| 3603 | } |
| 3604 | |
| 3605 | private AutoFillUI getUiForShowing() { |
| 3606 | synchronized (mLock) { |
| 3607 | mUi.setCallback(this); |
| 3608 | return mUi; |
| 3609 | } |
| 3610 | } |
| 3611 | |
| 3612 | /** |
| 3613 | * Cleans up this session. |
| 3614 | * |
| 3615 | * <p>Typically called in 2 scenarios: |
| 3616 | * |
| 3617 | * <ul> |
| 3618 | * <li>When the session naturally finishes (i.e., from {@link #removeSelfLocked()}. |
| 3619 | * <li>When the service hosting the session is finished (for example, because the user |
| 3620 | * disabled it). |
| 3621 | * </ul> |
| 3622 | */ |
| 3623 | @GuardedBy("mLock") |
| 3624 | RemoteFillService destroyLocked() { |
| 3625 | if (mDestroyed) { |
| 3626 | return null; |
| 3627 | } |
| 3628 | |
| 3629 | unlinkClientVultureLocked(); |
| 3630 | mUi.destroyAll(mPendingSaveUi, this, true); |
| 3631 | mUi.clearCallback(this); |
| 3632 | mDestroyed = true; |
| 3633 | |
| 3634 | // Log metrics |
| 3635 | final int totalRequests = mRequestLogs.size(); |
| 3636 | if (totalRequests > 0) { |
| 3637 | if (sVerbose) Slog.v(TAG, "destroyLocked(): logging " + totalRequests + " requests"); |
| 3638 | for (int i = 0; i < totalRequests; i++) { |
| 3639 | final LogMaker log = mRequestLogs.valueAt(i); |
| 3640 | mMetricsLogger.write(log); |
| 3641 | } |
| 3642 | } |
| 3643 | |
| 3644 | final int totalAugmentedRequests = mAugmentedRequestsLogs == null ? 0 |
| 3645 | : mAugmentedRequestsLogs.size(); |
| 3646 | if (totalAugmentedRequests > 0) { |
| 3647 | if (sVerbose) { |
| 3648 | Slog.v(TAG, "destroyLocked(): logging " + totalRequests + " augmented requests"); |
| 3649 | } |
| 3650 | for (int i = 0; i < totalAugmentedRequests; i++) { |
| 3651 | final LogMaker log = mAugmentedRequestsLogs.get(i); |
| 3652 | mMetricsLogger.write(log); |
| 3653 | } |
| 3654 | } |
| 3655 | |
| 3656 | final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_SESSION_FINISHED) |
| 3657 | .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUMBER_REQUESTS, totalRequests); |
| 3658 | if (totalAugmentedRequests > 0) { |
| 3659 | log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUMBER_AUGMENTED_REQUESTS, |
| 3660 | totalAugmentedRequests); |
| 3661 | } |
| 3662 | if (mForAugmentedAutofillOnly) { |
| 3663 | log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_AUGMENTED_ONLY, 1); |
| 3664 | } |
| 3665 | mMetricsLogger.write(log); |
| 3666 | |
| 3667 | return mRemoteFillService; |
| 3668 | } |
| 3669 | |
| 3670 | /** |
| 3671 | * Cleans up this session and remove it from the service always, even if it does have a pending |
| 3672 | * Save UI. |
| 3673 | */ |
| 3674 | @GuardedBy("mLock") |
| 3675 | void forceRemoveSelfLocked() { |
| 3676 | forceRemoveSelfLocked(AutofillManager.STATE_UNKNOWN); |
| 3677 | } |
| 3678 | |
| 3679 | @GuardedBy("mLock") |
| 3680 | void forceRemoveSelfIfForAugmentedAutofillOnlyLocked() { |
| 3681 | if (sVerbose) { |
| 3682 | Slog.v(TAG, "forceRemoveSelfIfForAugmentedAutofillOnly(" + this.id + "): " |
| 3683 | + mForAugmentedAutofillOnly); |
| 3684 | } |
| 3685 | if (!mForAugmentedAutofillOnly) return; |
| 3686 | |
| 3687 | forceRemoveSelfLocked(); |
| 3688 | } |
| 3689 | |
| 3690 | @GuardedBy("mLock") |
| 3691 | void forceRemoveSelfLocked(int clientState) { |
| 3692 | if (sVerbose) Slog.v(TAG, "forceRemoveSelfLocked(): " + mPendingSaveUi); |
| 3693 | |
| 3694 | final boolean isPendingSaveUi = isSaveUiPendingLocked(); |
| 3695 | mPendingSaveUi = null; |
| 3696 | removeSelfLocked(); |
| 3697 | mUi.destroyAll(mPendingSaveUi, this, false); |
| 3698 | if (!isPendingSaveUi) { |
| 3699 | try { |
| 3700 | mClient.setSessionFinished(clientState, /* autofillableIds= */ null); |
| 3701 | } catch (RemoteException e) { |
| 3702 | Slog.e(TAG, "Error notifying client to finish session", e); |
| 3703 | } |
| 3704 | } |
| 3705 | destroyAugmentedAutofillWindowsLocked(); |
| 3706 | } |
| 3707 | |
| 3708 | @GuardedBy("mLock") |
| 3709 | void destroyAugmentedAutofillWindowsLocked() { |
| 3710 | if (mAugmentedAutofillDestroyer != null) { |
| 3711 | mAugmentedAutofillDestroyer.run(); |
| 3712 | mAugmentedAutofillDestroyer = null; |
| 3713 | } |
| 3714 | } |
| 3715 | |
| 3716 | /** |
| 3717 | * Thread-safe version of {@link #removeSelfLocked()}. |
| 3718 | */ |
| 3719 | private void removeSelf() { |
| 3720 | synchronized (mLock) { |
| 3721 | removeSelfLocked(); |
| 3722 | } |
| 3723 | } |
| 3724 | |
| 3725 | /** |
| 3726 | * Cleans up this session and remove it from the service, but but only if it does not have a |
| 3727 | * pending Save UI. |
| 3728 | */ |
| 3729 | @GuardedBy("mLock") |
| 3730 | void removeSelfLocked() { |
| 3731 | if (sVerbose) Slog.v(TAG, "removeSelfLocked(" + this.id + "): " + mPendingSaveUi); |
| 3732 | if (mDestroyed) { |
| 3733 | Slog.w(TAG, "Call to Session#removeSelfLocked() rejected - session: " |
| 3734 | + id + " destroyed"); |
| 3735 | return; |
| 3736 | } |
| 3737 | if (isSaveUiPendingLocked()) { |
| 3738 | Slog.i(TAG, "removeSelfLocked() ignored, waiting for pending save ui"); |
| 3739 | return; |
| 3740 | } |
| 3741 | |
| 3742 | final RemoteFillService remoteFillService = destroyLocked(); |
| 3743 | mService.removeSessionLocked(id); |
| 3744 | if (remoteFillService != null) { |
| 3745 | remoteFillService.destroy(); |
| 3746 | } |
| 3747 | } |
| 3748 | |
| 3749 | void onPendingSaveUi(int operation, @NonNull IBinder token) { |
| 3750 | getUiForShowing().onPendingSaveUi(operation, token); |
| 3751 | } |
| 3752 | |
| 3753 | /** |
| 3754 | * Checks whether this session is hiding the Save UI to handle a custom description link for |
| 3755 | * a specific {@code token} created by |
| 3756 | * {@link PendingUi#PendingUi(IBinder, int, IAutoFillManagerClient)}. |
| 3757 | */ |
| 3758 | @GuardedBy("mLock") |
| 3759 | boolean isSaveUiPendingForTokenLocked(@NonNull IBinder token) { |
| 3760 | return isSaveUiPendingLocked() && token.equals(mPendingSaveUi.getToken()); |
| 3761 | } |
| 3762 | |
| 3763 | /** |
| 3764 | * Checks whether this session is hiding the Save UI to handle a custom description link. |
| 3765 | */ |
| 3766 | @GuardedBy("mLock") |
| 3767 | private boolean isSaveUiPendingLocked() { |
| 3768 | return mPendingSaveUi != null && mPendingSaveUi.getState() == PendingUi.STATE_PENDING; |
| 3769 | } |
| 3770 | |
| 3771 | @GuardedBy("mLock") |
| 3772 | private int getLastResponseIndexLocked() { |
| 3773 | // The response ids are monotonically increasing so |
| 3774 | // we just find the largest id which is the last. We |
| 3775 | // do not rely on the internal ordering in sparse |
| 3776 | // array to avoid - wow this stopped working!? |
| 3777 | int lastResponseIdx = -1; |
| 3778 | int lastResponseId = -1; |
| 3779 | if (mResponses != null) { |
| 3780 | final int responseCount = mResponses.size(); |
| 3781 | for (int i = 0; i < responseCount; i++) { |
| 3782 | if (mResponses.keyAt(i) > lastResponseId) { |
| 3783 | lastResponseIdx = i; |
| 3784 | } |
| 3785 | } |
| 3786 | } |
| 3787 | return lastResponseIdx; |
| 3788 | } |
| 3789 | |
| 3790 | private LogMaker newLogMaker(int category) { |
| 3791 | return newLogMaker(category, mService.getServicePackageName()); |
| 3792 | } |
| 3793 | |
| 3794 | private LogMaker newLogMaker(int category, String servicePackageName) { |
| 3795 | return Helper.newLogMaker(category, mComponentName, servicePackageName, id, mCompatMode); |
| 3796 | } |
| 3797 | |
| 3798 | private void writeLog(int category) { |
| 3799 | mMetricsLogger.write(newLogMaker(category)); |
| 3800 | } |
| 3801 | |
| 3802 | @GuardedBy("mLock") |
| 3803 | private void logAuthenticationStatusLocked(int requestId, int status) { |
| 3804 | addTaggedDataToRequestLogLocked(requestId, |
| 3805 | MetricsEvent.FIELD_AUTOFILL_AUTHENTICATION_STATUS, status); |
| 3806 | } |
| 3807 | |
| 3808 | @GuardedBy("mLock") |
| 3809 | private void addTaggedDataToRequestLogLocked(int requestId, int tag, @Nullable Object value) { |
| 3810 | final LogMaker requestLog = mRequestLogs.get(requestId); |
| 3811 | if (requestLog == null) { |
| 3812 | Slog.w(TAG, |
| 3813 | "addTaggedDataToRequestLogLocked(tag=" + tag + "): no log for id " + requestId); |
| 3814 | return; |
| 3815 | } |
| 3816 | requestLog.addTaggedData(tag, value); |
| 3817 | } |
| 3818 | |
| 3819 | private void wtf(@Nullable Exception e, String fmt, Object...args) { |
| 3820 | final String message = String.format(fmt, args); |
| 3821 | synchronized (mLock) { |
| 3822 | mWtfHistory.log(message); |
| 3823 | } |
| 3824 | |
| 3825 | if (e != null) { |
| 3826 | Slog.wtf(TAG, message, e); |
| 3827 | } else { |
| 3828 | Slog.wtf(TAG, message); |
| 3829 | } |
| 3830 | } |
| 3831 | |
| 3832 | private static String actionAsString(int action) { |
| 3833 | switch (action) { |
| 3834 | case ACTION_START_SESSION: |
| 3835 | return "START_SESSION"; |
| 3836 | case ACTION_VIEW_ENTERED: |
| 3837 | return "VIEW_ENTERED"; |
| 3838 | case ACTION_VIEW_EXITED: |
| 3839 | return "VIEW_EXITED"; |
| 3840 | case ACTION_VALUE_CHANGED: |
| 3841 | return "VALUE_CHANGED"; |
| 3842 | case ACTION_RESPONSE_EXPIRED: |
| 3843 | return "RESPONSE_EXPIRED"; |
| 3844 | default: |
| 3845 | return "UNKNOWN_" + action; |
| 3846 | } |
| 3847 | } |
| 3848 | } |