Home | History | Annotate | Download | only in autofill
      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.app.ActivityManagerInternal.ASSIST_KEY_RECEIVER_EXTRAS;
     20 import static android.app.ActivityManagerInternal.ASSIST_KEY_STRUCTURE;
     21 import static android.service.autofill.AutofillFieldClassificationService.EXTRA_SCORES;
     22 import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
     23 import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
     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 
     29 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
     30 import static com.android.server.autofill.Helper.getNumericValue;
     31 import static com.android.server.autofill.Helper.sDebug;
     32 import static com.android.server.autofill.Helper.sPartitionMaxCount;
     33 import static com.android.server.autofill.Helper.sVerbose;
     34 import static com.android.server.autofill.Helper.toArray;
     35 import static com.android.server.autofill.ViewState.STATE_RESTARTED_SESSION;
     36 
     37 import android.annotation.NonNull;
     38 import android.annotation.Nullable;
     39 import android.app.Activity;
     40 import android.app.ActivityManager;
     41 import android.app.IAssistDataReceiver;
     42 import android.app.assist.AssistStructure;
     43 import android.app.assist.AssistStructure.AutofillOverlay;
     44 import android.app.assist.AssistStructure.ViewNode;
     45 import android.content.ComponentName;
     46 import android.content.Context;
     47 import android.content.Intent;
     48 import android.content.IntentSender;
     49 import android.graphics.Bitmap;
     50 import android.graphics.Rect;
     51 import android.metrics.LogMaker;
     52 import android.os.Binder;
     53 import android.os.Bundle;
     54 import android.os.Handler;
     55 import android.os.IBinder;
     56 import android.os.IBinder.DeathRecipient;
     57 import android.os.Parcelable;
     58 import android.os.RemoteCallback;
     59 import android.os.RemoteException;
     60 import android.os.SystemClock;
     61 import android.service.autofill.AutofillFieldClassificationService.Scores;
     62 import android.service.autofill.AutofillService;
     63 import android.service.autofill.Dataset;
     64 import android.service.autofill.FieldClassification;
     65 import android.service.autofill.FieldClassification.Match;
     66 import android.service.autofill.FillContext;
     67 import android.service.autofill.FillRequest;
     68 import android.service.autofill.FillResponse;
     69 import android.service.autofill.InternalSanitizer;
     70 import android.service.autofill.InternalValidator;
     71 import android.service.autofill.SaveInfo;
     72 import android.service.autofill.SaveRequest;
     73 import android.service.autofill.UserData;
     74 import android.service.autofill.ValueFinder;
     75 import android.util.ArrayMap;
     76 import android.util.ArraySet;
     77 import android.util.LocalLog;
     78 import android.util.Slog;
     79 import android.util.SparseArray;
     80 import android.util.TimeUtils;
     81 import android.view.KeyEvent;
     82 import android.view.autofill.AutofillId;
     83 import android.view.autofill.AutofillManager;
     84 import android.view.autofill.AutofillValue;
     85 import android.view.autofill.IAutoFillManagerClient;
     86 import android.view.autofill.IAutofillWindowPresenter;
     87 
     88 import com.android.internal.R;
     89 import com.android.internal.annotations.GuardedBy;
     90 import com.android.internal.logging.MetricsLogger;
     91 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     92 import com.android.internal.util.ArrayUtils;
     93 import com.android.server.autofill.ui.AutoFillUI;
     94 import com.android.server.autofill.ui.PendingUi;
     95 
     96 import java.io.PrintWriter;
     97 import java.io.StringWriter;
     98 import java.util.ArrayList;
     99 import java.util.Arrays;
    100 import java.util.Collection;
    101 import java.util.Collections;
    102 import java.util.List;
    103 import java.util.Objects;
    104 import java.util.concurrent.atomic.AtomicInteger;
    105 
    106 /**
    107  * A session for a given activity.
    108  *
    109  * <p>This class manages the multiple {@link ViewState}s for each view it has, and keeps track
    110  * of the current {@link ViewState} to display the appropriate UI.
    111  *
    112  * <p>Although the autofill requests and callbacks are stateless from the service's point of
    113  * view, we need to keep state in the framework side for cases such as authentication. For
    114  * example, when service return a {@link FillResponse} that contains all the fields needed
    115  * to fill the activity but it requires authentication first, that response need to be held
    116  * until the user authenticates or it times out.
    117  */
    118 final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener,
    119         AutoFillUI.AutoFillUiCallback, ValueFinder {
    120     private static final String TAG = "AutofillSession";
    121 
    122     private static final String EXTRA_REQUEST_ID = "android.service.autofill.extra.REQUEST_ID";
    123 
    124     private final AutofillManagerServiceImpl mService;
    125     private final Handler mHandler;
    126     private final Object mLock;
    127     private final AutoFillUI mUi;
    128 
    129     private final MetricsLogger mMetricsLogger = new MetricsLogger();
    130 
    131     private static AtomicInteger sIdCounter = new AtomicInteger();
    132 
    133     /** Id of the session */
    134     public final int id;
    135 
    136     /** uid the session is for */
    137     public final int uid;
    138 
    139     /** Flags used to start the session */
    140     public final int mFlags;
    141 
    142     @GuardedBy("mLock")
    143     @NonNull private IBinder mActivityToken;
    144 
    145     /** Component that's being auto-filled */
    146     @NonNull private final ComponentName mComponentName;
    147 
    148     /** Whether the app being autofilled is running in compat mode. */
    149     private final boolean mCompatMode;
    150 
    151     /** Node representing the URL bar on compat mode. */
    152     @GuardedBy("mLock")
    153     private ViewNode mUrlBar;
    154 
    155     @GuardedBy("mLock")
    156     private boolean mSaveOnAllViewsInvisible;
    157 
    158     @GuardedBy("mLock")
    159     private final ArrayMap<AutofillId, ViewState> mViewStates = new ArrayMap<>();
    160 
    161     /**
    162      * Id of the View currently being displayed.
    163      */
    164     @GuardedBy("mLock")
    165     @Nullable private AutofillId mCurrentViewId;
    166 
    167     @GuardedBy("mLock")
    168     private IAutoFillManagerClient mClient;
    169 
    170     @GuardedBy("mLock")
    171     private DeathRecipient mClientVulture;
    172 
    173     private final RemoteFillService mRemoteFillService;
    174 
    175     @GuardedBy("mLock")
    176     private SparseArray<FillResponse> mResponses;
    177 
    178     /**
    179      * Contexts read from the app; they will be updated (sanitized, change values for save) before
    180      * sent to {@link AutofillService}. Ordered by the time they were read.
    181      */
    182     @GuardedBy("mLock")
    183     private ArrayList<FillContext> mContexts;
    184 
    185     /**
    186      * Whether the client has an {@link android.view.autofill.AutofillManager.AutofillCallback}.
    187      */
    188     private boolean mHasCallback;
    189 
    190     /**
    191      * Extras sent by service on {@code onFillRequest()} calls; the first non-null extra is saved
    192      * and used on subsequent {@code onFillRequest()} and {@code onSaveRequest()} calls.
    193      */
    194     @GuardedBy("mLock")
    195     private Bundle mClientState;
    196 
    197     @GuardedBy("mLock")
    198     private boolean mDestroyed;
    199 
    200     /** Whether the session is currently saving. */
    201     @GuardedBy("mLock")
    202     private boolean mIsSaving;
    203 
    204     /**
    205      * Helper used to handle state of Save UI when it must be hiding to show a custom description
    206      * link and later recovered.
    207      */
    208     @GuardedBy("mLock")
    209     private PendingUi mPendingSaveUi;
    210 
    211     /**
    212      * List of dataset ids selected by the user.
    213      */
    214     @GuardedBy("mLock")
    215     private ArrayList<String> mSelectedDatasetIds;
    216 
    217     /**
    218      * When the session started (using elapsed time since boot).
    219      */
    220     private final long mStartTime;
    221 
    222     /**
    223      * When the UI was shown for the first time (using elapsed time since boot).
    224      */
    225     @GuardedBy("mLock")
    226     private long mUiShownTime;
    227 
    228     @GuardedBy("mLock")
    229     private final LocalLog mUiLatencyHistory;
    230 
    231     @GuardedBy("mLock")
    232     private final LocalLog mWtfHistory;
    233 
    234     /**
    235      * Map of {@link MetricsEvent#AUTOFILL_REQUEST} metrics, keyed by fill request id.
    236      */
    237     @GuardedBy("mLock")
    238     private final SparseArray<LogMaker> mRequestLogs = new SparseArray<>(1);
    239 
    240     /**
    241      * Receiver of assist data from the app's {@link Activity}.
    242      */
    243     private final IAssistDataReceiver mAssistReceiver = new IAssistDataReceiver.Stub() {
    244         @Override
    245         public void onHandleAssistData(Bundle resultData) throws RemoteException {
    246             final AssistStructure structure = resultData.getParcelable(ASSIST_KEY_STRUCTURE);
    247             if (structure == null) {
    248                 Slog.e(TAG, "No assist structure - app might have crashed providing it");
    249                 return;
    250             }
    251 
    252             final Bundle receiverExtras = resultData.getBundle(ASSIST_KEY_RECEIVER_EXTRAS);
    253             if (receiverExtras == null) {
    254                 Slog.e(TAG, "No receiver extras - app might have crashed providing it");
    255                 return;
    256             }
    257 
    258             final int requestId = receiverExtras.getInt(EXTRA_REQUEST_ID);
    259 
    260             if (sVerbose) {
    261                 Slog.v(TAG, "New structure for requestId " + requestId + ": " + structure);
    262             }
    263 
    264             final FillRequest request;
    265             synchronized (mLock) {
    266                 // TODO(b/35708678): Must fetch the data so it's available later on handleSave(),
    267                 // even if if the activity is gone by then, but structure .ensureData() gives a
    268                 // ONE_WAY warning because system_service could block on app calls. We need to
    269                 // change AssistStructure so it provides a "one-way" writeToParcel() method that
    270                 // sends all the data
    271                 try {
    272                     structure.ensureDataForAutofill();
    273                 } catch (RuntimeException e) {
    274                     wtf(e, "Exception lazy loading assist structure for %s: %s",
    275                             structure.getActivityComponent(), e);
    276                     return;
    277                 }
    278 
    279                 // Sanitize structure before it's sent to service.
    280                 final ComponentName componentNameFromApp = structure.getActivityComponent();
    281                 if (componentNameFromApp == null || !mComponentName.getPackageName()
    282                         .equals(componentNameFromApp.getPackageName())) {
    283                     Slog.w(TAG, "Activity " + mComponentName + " forged different component on "
    284                             + "AssistStructure: " + componentNameFromApp);
    285                     structure.setActivityComponent(mComponentName);
    286                     mMetricsLogger.write(newLogMaker(MetricsEvent.AUTOFILL_FORGED_COMPONENT_ATTEMPT)
    287                             .addTaggedData(MetricsEvent.FIELD_AUTOFILL_FORGED_COMPONENT_NAME,
    288                                     componentNameFromApp == null ? "null"
    289                                             : componentNameFromApp.flattenToShortString()));
    290                 }
    291                 if (mCompatMode) {
    292                     // Sanitize URL bar, if needed
    293                     final String[] urlBarIds = mService.getUrlBarResourceIdsForCompatMode(
    294                             mComponentName.getPackageName());
    295                     if (sDebug) {
    296                         Slog.d(TAG, "url_bars in compat mode: " + Arrays.toString(urlBarIds));
    297                     }
    298                     if (urlBarIds != null) {
    299                         mUrlBar = Helper.sanitizeUrlBar(structure, urlBarIds);
    300                         if (mUrlBar != null) {
    301                             final AutofillId urlBarId = mUrlBar.getAutofillId();
    302                             if (sDebug) {
    303                                 Slog.d(TAG, "Setting urlBar as id=" + urlBarId + " and domain "
    304                                         + mUrlBar.getWebDomain());
    305                             }
    306                             final ViewState viewState = new ViewState(Session.this, urlBarId,
    307                                     Session.this, ViewState.STATE_URL_BAR);
    308                             mViewStates.put(urlBarId, viewState);
    309                         }
    310                     }
    311                 }
    312                 structure.sanitizeForParceling(true);
    313 
    314                 // Flags used to start the session.
    315                 final int flags = structure.getFlags();
    316 
    317                 if (mContexts == null) {
    318                     mContexts = new ArrayList<>(1);
    319                 }
    320                 mContexts.add(new FillContext(requestId, structure));
    321 
    322                 cancelCurrentRequestLocked();
    323 
    324                 final int numContexts = mContexts.size();
    325                 for (int i = 0; i < numContexts; i++) {
    326                     fillContextWithAllowedValuesLocked(mContexts.get(i), flags);
    327                 }
    328 
    329                 // Dispatch a snapshot of the current contexts list since it may change
    330                 // until the dispatch happens. The items in the list don't need to be cloned
    331                 // since we don't hold on them anywhere else. The client state is not touched
    332                 // by us, so no need to copy.
    333                 request = new FillRequest(requestId, new ArrayList<>(mContexts),
    334                         mClientState, flags);
    335             }
    336 
    337             mRemoteFillService.onFillRequest(request);
    338         }
    339 
    340         @Override
    341         public void onHandleAssistScreenshot(Bitmap screenshot) {
    342             // Do nothing
    343         }
    344     };
    345 
    346     /**
    347      * Returns the ids of all entries in {@link #mViewStates} in the same order.
    348      */
    349     @GuardedBy("mLock")
    350     private AutofillId[] getIdsOfAllViewStatesLocked() {
    351         final int numViewState = mViewStates.size();
    352         final AutofillId[] ids = new AutofillId[numViewState];
    353         for (int i = 0; i < numViewState; i++) {
    354             ids[i] = mViewStates.valueAt(i).id;
    355         }
    356 
    357         return ids;
    358     }
    359 
    360     @Override
    361     @Nullable
    362     public String findByAutofillId(@NonNull AutofillId id) {
    363         synchronized (mLock) {
    364             AutofillValue value = findValueLocked(id);
    365             if (value != null) {
    366                 if (value.isText()) {
    367                     return value.getTextValue().toString();
    368                 }
    369 
    370                 if (value.isList()) {
    371                     final CharSequence[] options = getAutofillOptionsFromContextsLocked(id);
    372                     if (options != null) {
    373                         final int index = value.getListValue();
    374                         final CharSequence option = options[index];
    375                         return option != null ? option.toString() : null;
    376                     } else {
    377                         Slog.w(TAG, "findByAutofillId(): no autofill options for id " + id);
    378                     }
    379                 }
    380             }
    381         }
    382         return null;
    383     }
    384 
    385     @Override
    386     public AutofillValue findRawValueByAutofillId(AutofillId id) {
    387         synchronized (mLock) {
    388             return findValueLocked(id);
    389         }
    390     }
    391 
    392     /**
    393      * <p>Gets the value of a field, using either the {@code viewStates} or the {@code mContexts},
    394      * or {@code null} when not found on either of them.
    395      */
    396     @GuardedBy("mLock")
    397     private AutofillValue findValueLocked(@NonNull AutofillId id) {
    398         final ViewState state = mViewStates.get(id);
    399         if (state == null) {
    400             if (sDebug) Slog.d(TAG, "findValueLocked(): no view state for " + id);
    401             return null;
    402         }
    403         AutofillValue value = state.getCurrentValue();
    404         if (value == null) {
    405             if (sDebug) Slog.d(TAG, "findValueLocked(): no current value for " + id);
    406             value = getValueFromContextsLocked(id);
    407         }
    408         return value;
    409     }
    410 
    411     /**
    412      * Updates values of the nodes in the context's structure so that:
    413      *
    414      * - proper node is focused
    415      * - autofillValue is sent back to service when it was previously autofilled
    416      * - autofillValue is sent in the view used to force a request
    417      *
    418      * @param fillContext The context to be filled
    419      * @param flags The flags that started the session
    420      */
    421     @GuardedBy("mLock")
    422     private void fillContextWithAllowedValuesLocked(@NonNull FillContext fillContext, int flags) {
    423         final ViewNode[] nodes = fillContext
    424                 .findViewNodesByAutofillIds(getIdsOfAllViewStatesLocked());
    425 
    426         final int numViewState = mViewStates.size();
    427         for (int i = 0; i < numViewState; i++) {
    428             final ViewState viewState = mViewStates.valueAt(i);
    429 
    430             final ViewNode node = nodes[i];
    431             if (node == null) {
    432                 if (sVerbose) {
    433                     Slog.v(TAG,
    434                             "fillContextWithAllowedValuesLocked(): no node for " + viewState.id);
    435                 }
    436                 continue;
    437             }
    438 
    439             final AutofillValue currentValue = viewState.getCurrentValue();
    440             final AutofillValue filledValue = viewState.getAutofilledValue();
    441             final AutofillOverlay overlay = new AutofillOverlay();
    442 
    443             // Sanitizes the value if the current value matches what the service sent.
    444             if (filledValue != null && filledValue.equals(currentValue)) {
    445                 overlay.value = currentValue;
    446             }
    447 
    448             if (mCurrentViewId != null) {
    449                 // Updates the focus value.
    450                 overlay.focused = mCurrentViewId.equals(viewState.id);
    451                 // Sanitizes the value of the focused field in a manual request.
    452                 if (overlay.focused && (flags & FLAG_MANUAL_REQUEST) != 0) {
    453                     overlay.value = currentValue;
    454                 }
    455             }
    456             node.setAutofillOverlay(overlay);
    457         }
    458     }
    459 
    460     /**
    461      * Cancels the last request sent to the {@link #mRemoteFillService}.
    462      */
    463     @GuardedBy("mLock")
    464     private void cancelCurrentRequestLocked() {
    465         final int canceledRequest = mRemoteFillService.cancelCurrentRequest();
    466 
    467         // Remove the FillContext as there will never be a response for the service
    468         if (canceledRequest != INVALID_REQUEST_ID && mContexts != null) {
    469             final int numContexts = mContexts.size();
    470 
    471             // It is most likely the last context, hence search backwards
    472             for (int i = numContexts - 1; i >= 0; i--) {
    473                 if (mContexts.get(i).getRequestId() == canceledRequest) {
    474                     if (sDebug) Slog.d(TAG, "cancelCurrentRequest(): id = " + canceledRequest);
    475                     mContexts.remove(i);
    476                     break;
    477                 }
    478             }
    479         }
    480     }
    481 
    482     /**
    483      * Reads a new structure and then request a new fill response from the fill service.
    484      */
    485     @GuardedBy("mLock")
    486     private void requestNewFillResponseLocked(int flags) {
    487         int requestId;
    488 
    489         do {
    490             requestId = sIdCounter.getAndIncrement();
    491         } while (requestId == INVALID_REQUEST_ID);
    492 
    493         // Create a metrics log for the request
    494         final int ordinal = mRequestLogs.size() + 1;
    495         final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_REQUEST)
    496                 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_REQUEST_ORDINAL, ordinal);
    497         if (flags != 0) {
    498             log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_FLAGS, flags);
    499         }
    500         mRequestLogs.put(requestId, log);
    501 
    502         if (sVerbose) {
    503             Slog.v(TAG, "Requesting structure for request #" + ordinal + " ,requestId="
    504                     + requestId + ", flags=" + flags);
    505         }
    506 
    507         // If the focus changes very quickly before the first request is returned each focus change
    508         // triggers a new partition and we end up with many duplicate partitions. This is
    509         // enhanced as the focus change can be much faster than the taking of the assist structure.
    510         // Hence remove the currently queued request and replace it with the one queued after the
    511         // structure is taken. This causes only one fill request per bust of focus changes.
    512         cancelCurrentRequestLocked();
    513 
    514         try {
    515             final Bundle receiverExtras = new Bundle();
    516             receiverExtras.putInt(EXTRA_REQUEST_ID, requestId);
    517             final long identity = Binder.clearCallingIdentity();
    518             try {
    519                 if (!ActivityManager.getService().requestAutofillData(mAssistReceiver,
    520                         receiverExtras, mActivityToken, flags)) {
    521                     Slog.w(TAG, "failed to request autofill data for " + mActivityToken);
    522                 }
    523             } finally {
    524                 Binder.restoreCallingIdentity(identity);
    525             }
    526         } catch (RemoteException e) {
    527             // Should not happen, it's a local call.
    528         }
    529     }
    530 
    531     Session(@NonNull AutofillManagerServiceImpl service, @NonNull AutoFillUI ui,
    532             @NonNull Context context, @NonNull Handler handler, int userId,
    533             @NonNull Object lock, int sessionId, int uid, @NonNull IBinder activityToken,
    534             @NonNull IBinder client, boolean hasCallback, @NonNull LocalLog uiLatencyHistory,
    535             @NonNull LocalLog wtfHistory,
    536             @NonNull ComponentName serviceComponentName, @NonNull ComponentName componentName,
    537             boolean compatMode, boolean bindInstantServiceAllowed, int flags) {
    538         id = sessionId;
    539         mFlags = flags;
    540         this.uid = uid;
    541         mStartTime = SystemClock.elapsedRealtime();
    542         mService = service;
    543         mLock = lock;
    544         mUi = ui;
    545         mHandler = handler;
    546         mRemoteFillService = new RemoteFillService(context, serviceComponentName, userId, this,
    547                 bindInstantServiceAllowed);
    548         mActivityToken = activityToken;
    549         mHasCallback = hasCallback;
    550         mUiLatencyHistory = uiLatencyHistory;
    551         mWtfHistory = wtfHistory;
    552         mComponentName = componentName;
    553         mCompatMode = compatMode;
    554         setClientLocked(client);
    555 
    556         mMetricsLogger.write(newLogMaker(MetricsEvent.AUTOFILL_SESSION_STARTED)
    557                 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_FLAGS, flags));
    558     }
    559 
    560     /**
    561      * Gets the currently registered activity token
    562      *
    563      * @return The activity token
    564      */
    565     @GuardedBy("mLock")
    566     @NonNull IBinder getActivityTokenLocked() {
    567         return mActivityToken;
    568     }
    569 
    570     /**
    571      * Sets new activity and client for this session.
    572      *
    573      * @param newActivity The token of the new activity
    574      * @param newClient The client receiving autofill callbacks
    575      */
    576     void switchActivity(@NonNull IBinder newActivity, @NonNull IBinder newClient) {
    577         synchronized (mLock) {
    578             if (mDestroyed) {
    579                 Slog.w(TAG, "Call to Session#switchActivity() rejected - session: "
    580                         + id + " destroyed");
    581                 return;
    582             }
    583             mActivityToken = newActivity;
    584             setClientLocked(newClient);
    585 
    586             // The tracked id are not persisted in the client, hence update them
    587             updateTrackedIdsLocked();
    588         }
    589     }
    590 
    591     @GuardedBy("mLock")
    592     private void setClientLocked(@NonNull IBinder client) {
    593         unlinkClientVultureLocked();
    594         mClient = IAutoFillManagerClient.Stub.asInterface(client);
    595         mClientVulture = () -> {
    596             Slog.d(TAG, "handling death of " + mActivityToken + " when saving=" + mIsSaving);
    597             synchronized (mLock) {
    598                 if (mIsSaving) {
    599                     mUi.hideFillUi(this);
    600                 } else {
    601                     mUi.destroyAll(mPendingSaveUi, this, false);
    602                 }
    603             }
    604         };
    605         try {
    606             mClient.asBinder().linkToDeath(mClientVulture, 0);
    607         } catch (RemoteException e) {
    608             Slog.w(TAG, "could not set binder death listener on autofill client: " + e);
    609         }
    610     }
    611 
    612     @GuardedBy("mLock")
    613     private void unlinkClientVultureLocked() {
    614         if (mClient != null && mClientVulture != null) {
    615             final boolean unlinked = mClient.asBinder().unlinkToDeath(mClientVulture, 0);
    616             if (!unlinked) {
    617                 Slog.w(TAG, "unlinking vulture from death failed for " + mActivityToken);
    618             }
    619         }
    620     }
    621 
    622     // FillServiceCallbacks
    623     @Override
    624     public void onFillRequestSuccess(int requestId, @Nullable FillResponse response,
    625             @NonNull String servicePackageName, int requestFlags) {
    626         final AutofillId[] fieldClassificationIds;
    627 
    628         final LogMaker requestLog;
    629 
    630         synchronized (mLock) {
    631             if (mDestroyed) {
    632                 Slog.w(TAG, "Call to Session#onFillRequestSuccess() rejected - session: "
    633                         + id + " destroyed");
    634                 return;
    635             }
    636 
    637             requestLog = mRequestLogs.get(requestId);
    638             if (requestLog != null) {
    639                 requestLog.setType(MetricsEvent.TYPE_SUCCESS);
    640             } else {
    641                 Slog.w(TAG, "onFillRequestSuccess(): no request log for id " + requestId);
    642             }
    643             if (response == null) {
    644                 if (requestLog != null) {
    645                     requestLog.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS, -1);
    646                 }
    647                 processNullResponseLocked(requestFlags);
    648                 return;
    649             }
    650 
    651             fieldClassificationIds = response.getFieldClassificationIds();
    652             if (fieldClassificationIds != null && !mService.isFieldClassificationEnabledLocked()) {
    653                 Slog.w(TAG, "Ignoring " + response + " because field detection is disabled");
    654                 processNullResponseLocked(requestFlags);
    655                 return;
    656             }
    657         }
    658 
    659         mService.setLastResponse(id, response);
    660 
    661         int sessionFinishedState = 0;
    662         final long disableDuration = response.getDisableDuration();
    663         if (disableDuration > 0) {
    664             final int flags = response.getFlags();
    665             if (sDebug) {
    666                 final StringBuilder message = new StringBuilder("Service disabled autofill for ")
    667                         .append(mComponentName)
    668                         .append(": flags=").append(flags)
    669                         .append(", duration=");
    670                 TimeUtils.formatDuration(disableDuration, message);
    671                 Slog.d(TAG, message.toString());
    672             }
    673             if ((flags & FillResponse.FLAG_DISABLE_ACTIVITY_ONLY) != 0) {
    674                 mService.disableAutofillForActivity(mComponentName, disableDuration,
    675                         id, mCompatMode);
    676             } else {
    677                 mService.disableAutofillForApp(mComponentName.getPackageName(), disableDuration,
    678                         id, mCompatMode);
    679             }
    680             sessionFinishedState = AutofillManager.STATE_DISABLED_BY_SERVICE;
    681         }
    682 
    683         if (((response.getDatasets() == null || response.getDatasets().isEmpty())
    684                         && response.getAuthentication() == null)
    685                 || disableDuration > 0) {
    686             // Response is "empty" from an UI point of view, need to notify client.
    687             notifyUnavailableToClient(sessionFinishedState);
    688         }
    689 
    690         if (requestLog != null) {
    691             requestLog.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS,
    692                             response.getDatasets() == null ? 0 : response.getDatasets().size());
    693             if (fieldClassificationIds != null) {
    694                 requestLog.addTaggedData(
    695                         MetricsEvent.FIELD_AUTOFILL_NUM_FIELD_CLASSIFICATION_IDS,
    696                         fieldClassificationIds.length);
    697             }
    698         }
    699 
    700         synchronized (mLock) {
    701             processResponseLocked(response, null, requestFlags);
    702         }
    703     }
    704 
    705     // FillServiceCallbacks
    706     @Override
    707     public void onFillRequestFailure(int requestId, @Nullable CharSequence message,
    708             @NonNull String servicePackageName) {
    709         onFillRequestFailureOrTimeout(requestId, false, message, servicePackageName);
    710     }
    711 
    712     // FillServiceCallbacks
    713     @Override
    714     public void onFillRequestTimeout(int requestId, @NonNull String servicePackageName) {
    715         onFillRequestFailureOrTimeout(requestId, true, null, servicePackageName);
    716     }
    717 
    718     private void onFillRequestFailureOrTimeout(int requestId, boolean timedOut,
    719             @Nullable CharSequence message, @NonNull String servicePackageName) {
    720         synchronized (mLock) {
    721             if (mDestroyed) {
    722                 Slog.w(TAG, "Call to Session#onFillRequestFailureOrTimeout(req=" + requestId
    723                         + ") rejected - session: " + id + " destroyed");
    724                 return;
    725             }
    726             mService.resetLastResponse();
    727             final LogMaker requestLog = mRequestLogs.get(requestId);
    728             if (requestLog == null) {
    729                 Slog.w(TAG, "onFillRequestFailureOrTimeout(): no log for id " + requestId);
    730             } else {
    731                 requestLog.setType(timedOut ? MetricsEvent.TYPE_CLOSE : MetricsEvent.TYPE_FAILURE);
    732             }
    733         }
    734         if (message != null) {
    735             getUiForShowing().showError(message, this);
    736         }
    737         removeSelf();
    738     }
    739 
    740     // FillServiceCallbacks
    741     @Override
    742     public void onSaveRequestSuccess(@NonNull String servicePackageName,
    743             @Nullable IntentSender intentSender) {
    744         synchronized (mLock) {
    745             mIsSaving = false;
    746 
    747             if (mDestroyed) {
    748                 Slog.w(TAG, "Call to Session#onSaveRequestSuccess() rejected - session: "
    749                         + id + " destroyed");
    750                 return;
    751             }
    752         }
    753         LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST, servicePackageName)
    754                 .setType(intentSender == null ? MetricsEvent.TYPE_SUCCESS : MetricsEvent.TYPE_OPEN);
    755         mMetricsLogger.write(log);
    756         if (intentSender != null) {
    757             if (sDebug) Slog.d(TAG, "Starting intent sender on save()");
    758             startIntentSender(intentSender);
    759         }
    760 
    761         // Nothing left to do...
    762         removeSelf();
    763     }
    764 
    765     // FillServiceCallbacks
    766     @Override
    767     public void onSaveRequestFailure(@Nullable CharSequence message,
    768             @NonNull String servicePackageName) {
    769         synchronized (mLock) {
    770             mIsSaving = false;
    771 
    772             if (mDestroyed) {
    773                 Slog.w(TAG, "Call to Session#onSaveRequestFailure() rejected - session: "
    774                         + id + " destroyed");
    775                 return;
    776             }
    777         }
    778         LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST, servicePackageName)
    779                 .setType(MetricsEvent.TYPE_FAILURE);
    780         mMetricsLogger.write(log);
    781 
    782         getUiForShowing().showError(message, this);
    783         removeSelf();
    784     }
    785 
    786     /**
    787      * Gets the {@link FillContext} for a request.
    788      *
    789      * @param requestId The id of the request
    790      *
    791      * @return The context or {@code null} if there is no context
    792      */
    793     @GuardedBy("mLock")
    794     @Nullable private FillContext getFillContextByRequestIdLocked(int requestId) {
    795         if (mContexts == null) {
    796             return null;
    797         }
    798 
    799         int numContexts = mContexts.size();
    800         for (int i = 0; i < numContexts; i++) {
    801             FillContext context = mContexts.get(i);
    802 
    803             if (context.getRequestId() == requestId) {
    804                 return context;
    805             }
    806         }
    807 
    808         return null;
    809     }
    810 
    811     // FillServiceCallbacks
    812     @Override
    813     public void authenticate(int requestId, int datasetIndex, IntentSender intent, Bundle extras) {
    814         if (sDebug) {
    815             Slog.d(TAG, "authenticate(): requestId=" + requestId + "; datasetIdx=" + datasetIndex
    816                     + "; intentSender=" + intent);
    817         }
    818         final Intent fillInIntent;
    819         synchronized (mLock) {
    820             if (mDestroyed) {
    821                 Slog.w(TAG, "Call to Session#authenticate() rejected - session: "
    822                         + id + " destroyed");
    823                 return;
    824             }
    825             fillInIntent = createAuthFillInIntentLocked(requestId, extras);
    826             if (fillInIntent == null) {
    827                 forceRemoveSelfLocked();
    828                 return;
    829             }
    830         }
    831 
    832         mService.setAuthenticationSelected(id, mClientState);
    833 
    834         final int authenticationId = AutofillManager.makeAuthenticationId(requestId, datasetIndex);
    835         mHandler.sendMessage(obtainMessage(
    836                 Session::startAuthentication,
    837                 this, authenticationId, intent, fillInIntent));
    838     }
    839 
    840     // FillServiceCallbacks
    841     @Override
    842     public void onServiceDied(RemoteFillService service) {
    843         // TODO(b/337565347): implement
    844     }
    845 
    846     // AutoFillUiCallback
    847     @Override
    848     public void fill(int requestId, int datasetIndex, Dataset dataset) {
    849         synchronized (mLock) {
    850             if (mDestroyed) {
    851                 Slog.w(TAG, "Call to Session#fill() rejected - session: "
    852                         + id + " destroyed");
    853                 return;
    854             }
    855         }
    856         mHandler.sendMessage(obtainMessage(
    857                 Session::autoFill,
    858                 this, requestId, datasetIndex, dataset, true));
    859     }
    860 
    861     // AutoFillUiCallback
    862     @Override
    863     public void save() {
    864         synchronized (mLock) {
    865             if (mDestroyed) {
    866                 Slog.w(TAG, "Call to Session#save() rejected - session: "
    867                         + id + " destroyed");
    868                 return;
    869             }
    870         }
    871         mHandler.sendMessage(obtainMessage(
    872                 AutofillManagerServiceImpl::handleSessionSave,
    873                 mService, this));
    874     }
    875 
    876     // AutoFillUiCallback
    877     @Override
    878     public void cancelSave() {
    879         synchronized (mLock) {
    880             mIsSaving = false;
    881 
    882             if (mDestroyed) {
    883                 Slog.w(TAG, "Call to Session#cancelSave() rejected - session: "
    884                         + id + " destroyed");
    885                 return;
    886             }
    887         }
    888         mHandler.sendMessage(obtainMessage(
    889                 Session::removeSelf, this));
    890     }
    891 
    892     // AutoFillUiCallback
    893     @Override
    894     public void requestShowFillUi(AutofillId id, int width, int height,
    895             IAutofillWindowPresenter presenter) {
    896         synchronized (mLock) {
    897             if (mDestroyed) {
    898                 Slog.w(TAG, "Call to Session#requestShowFillUi() rejected - session: "
    899                         + id + " destroyed");
    900                 return;
    901             }
    902             if (id.equals(mCurrentViewId)) {
    903                 try {
    904                     final ViewState view = mViewStates.get(id);
    905                     mClient.requestShowFillUi(this.id, id, width, height, view.getVirtualBounds(),
    906                             presenter);
    907                 } catch (RemoteException e) {
    908                     Slog.e(TAG, "Error requesting to show fill UI", e);
    909                 }
    910             } else {
    911                 if (sDebug) {
    912                     Slog.d(TAG, "Do not show full UI on " + id + " as it is not the current view ("
    913                             + mCurrentViewId + ") anymore");
    914                 }
    915             }
    916         }
    917     }
    918 
    919     @Override
    920     public void dispatchUnhandledKey(AutofillId id, KeyEvent keyEvent) {
    921         synchronized (mLock) {
    922             if (mDestroyed) {
    923                 Slog.w(TAG, "Call to Session#dispatchUnhandledKey() rejected - session: "
    924                         + id + " destroyed");
    925                 return;
    926             }
    927             if (id.equals(mCurrentViewId)) {
    928                 try {
    929                     mClient.dispatchUnhandledKey(this.id, id, keyEvent);
    930                 } catch (RemoteException e) {
    931                     Slog.e(TAG, "Error requesting to dispatch unhandled key", e);
    932                 }
    933             } else {
    934                 Slog.w(TAG, "Do not dispatch unhandled key on " + id
    935                         + " as it is not the current view (" + mCurrentViewId + ") anymore");
    936             }
    937         }
    938     }
    939 
    940     // AutoFillUiCallback
    941     @Override
    942     public void requestHideFillUi(AutofillId id) {
    943         synchronized (mLock) {
    944             // NOTE: We allow this call in a destroyed state as the UI is
    945             // asked to go away after we get destroyed, so let it do that.
    946             try {
    947                 mClient.requestHideFillUi(this.id, id);
    948             } catch (RemoteException e) {
    949                 Slog.e(TAG, "Error requesting to hide fill UI", e);
    950             }
    951         }
    952     }
    953 
    954     // AutoFillUiCallback
    955     @Override
    956     public void startIntentSender(IntentSender intentSender) {
    957         synchronized (mLock) {
    958             if (mDestroyed) {
    959                 Slog.w(TAG, "Call to Session#startIntentSender() rejected - session: "
    960                         + id + " destroyed");
    961                 return;
    962             }
    963             removeSelfLocked();
    964         }
    965         mHandler.sendMessage(obtainMessage(
    966                 Session::doStartIntentSender,
    967                 this, intentSender));
    968     }
    969 
    970     private void doStartIntentSender(IntentSender intentSender) {
    971         try {
    972             synchronized (mLock) {
    973                 mClient.startIntentSender(intentSender, null);
    974             }
    975         } catch (RemoteException e) {
    976             Slog.e(TAG, "Error launching auth intent", e);
    977         }
    978     }
    979 
    980     @GuardedBy("mLock")
    981     void setAuthenticationResultLocked(Bundle data, int authenticationId) {
    982         if (mDestroyed) {
    983             Slog.w(TAG, "Call to Session#setAuthenticationResultLocked() rejected - session: "
    984                     + id + " destroyed");
    985             return;
    986         }
    987         if (mResponses == null) {
    988             // Typically happens when app explicitly called cancel() while the service was showing
    989             // the auth UI.
    990             Slog.w(TAG, "setAuthenticationResultLocked(" + authenticationId + "): no responses");
    991             removeSelf();
    992             return;
    993         }
    994         final int requestId = AutofillManager.getRequestIdFromAuthenticationId(authenticationId);
    995         final FillResponse authenticatedResponse = mResponses.get(requestId);
    996         if (authenticatedResponse == null || data == null) {
    997             removeSelf();
    998             return;
    999         }
   1000 
   1001         final int datasetIdx = AutofillManager.getDatasetIdFromAuthenticationId(
   1002                 authenticationId);
   1003         // Authenticated a dataset - reset view state regardless if we got a response or a dataset
   1004         if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) {
   1005             final Dataset dataset = authenticatedResponse.getDatasets().get(datasetIdx);
   1006             if (dataset == null) {
   1007                 removeSelf();
   1008                 return;
   1009             }
   1010         }
   1011 
   1012         final Parcelable result = data.getParcelable(AutofillManager.EXTRA_AUTHENTICATION_RESULT);
   1013         final Bundle newClientState = data.getBundle(AutofillManager.EXTRA_CLIENT_STATE);
   1014         if (sDebug) {
   1015             Slog.d(TAG, "setAuthenticationResultLocked(): result=" + result
   1016                     + ", clientState=" + newClientState);
   1017         }
   1018         if (result instanceof FillResponse) {
   1019             logAuthenticationStatusLocked(requestId, MetricsEvent.AUTOFILL_AUTHENTICATED);
   1020             replaceResponseLocked(authenticatedResponse, (FillResponse) result, newClientState);
   1021         } else if (result instanceof Dataset) {
   1022             if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) {
   1023                 logAuthenticationStatusLocked(requestId,
   1024                         MetricsEvent.AUTOFILL_DATASET_AUTHENTICATED);
   1025                 if (newClientState != null) {
   1026                     if (sDebug) Slog.d(TAG,  "Updating client state from auth dataset");
   1027                     mClientState = newClientState;
   1028                 }
   1029                 final Dataset dataset = (Dataset) result;
   1030                 authenticatedResponse.getDatasets().set(datasetIdx, dataset);
   1031                 autoFill(requestId, datasetIdx, dataset, false);
   1032             } else {
   1033                 logAuthenticationStatusLocked(requestId,
   1034                         MetricsEvent.AUTOFILL_INVALID_DATASET_AUTHENTICATION);
   1035             }
   1036         } else {
   1037             if (result != null) {
   1038                 Slog.w(TAG, "service returned invalid auth type: " + result);
   1039             }
   1040             logAuthenticationStatusLocked(requestId,
   1041                     MetricsEvent.AUTOFILL_INVALID_AUTHENTICATION);
   1042             processNullResponseLocked(0);
   1043         }
   1044     }
   1045 
   1046     @GuardedBy("mLock")
   1047     void setHasCallbackLocked(boolean hasIt) {
   1048         if (mDestroyed) {
   1049             Slog.w(TAG, "Call to Session#setHasCallbackLocked() rejected - session: "
   1050                     + id + " destroyed");
   1051             return;
   1052         }
   1053         mHasCallback = hasIt;
   1054     }
   1055 
   1056     @GuardedBy("mLock")
   1057     @Nullable
   1058     private FillResponse getLastResponseLocked(@Nullable String logPrefix) {
   1059         if (mContexts == null) {
   1060             if (sDebug && logPrefix != null) Slog.d(TAG, logPrefix + ": no contexts");
   1061             return null;
   1062         }
   1063         if (mResponses == null) {
   1064             // Happens when the activity / session was finished before the service replied, or
   1065             // when the service cannot autofill it (and returned a null response).
   1066             if (sVerbose && logPrefix != null) {
   1067                 Slog.v(TAG, logPrefix + ": no responses on session");
   1068             }
   1069             return null;
   1070         }
   1071 
   1072         final int lastResponseIdx = getLastResponseIndexLocked();
   1073         if (lastResponseIdx < 0) {
   1074             if (logPrefix != null) {
   1075                 Slog.w(TAG, logPrefix + ": did not get last response. mResponses=" + mResponses
   1076                         + ", mViewStates=" + mViewStates);
   1077             }
   1078             return null;
   1079         }
   1080 
   1081         final FillResponse response = mResponses.valueAt(lastResponseIdx);
   1082         if (sVerbose && logPrefix != null) {
   1083             Slog.v(TAG, logPrefix + ": mResponses=" + mResponses + ", mContexts=" + mContexts
   1084                     + ", mViewStates=" + mViewStates);
   1085         }
   1086         return response;
   1087     }
   1088 
   1089     @GuardedBy("mLock")
   1090     @Nullable
   1091     private SaveInfo getSaveInfoLocked() {
   1092         final FillResponse response = getLastResponseLocked(null);
   1093         return response == null ? null : response.getSaveInfo();
   1094     }
   1095 
   1096     /**
   1097      * Generates a {@link android.service.autofill.FillEventHistory.Event#TYPE_CONTEXT_COMMITTED}
   1098      * when necessary.
   1099      */
   1100     public void logContextCommitted() {
   1101         mHandler.sendMessage(obtainMessage(
   1102                 Session::doLogContextCommitted, this));
   1103     }
   1104 
   1105     private void doLogContextCommitted() {
   1106         synchronized (mLock) {
   1107             logContextCommittedLocked();
   1108         }
   1109     }
   1110 
   1111     @GuardedBy("mLock")
   1112     private void logContextCommittedLocked() {
   1113         final FillResponse lastResponse = getLastResponseLocked("logContextCommited()");
   1114         if (lastResponse == null) return;
   1115 
   1116         final int flags = lastResponse.getFlags();
   1117         if ((flags & FillResponse.FLAG_TRACK_CONTEXT_COMMITED) == 0) {
   1118             if (sVerbose) Slog.v(TAG, "logContextCommittedLocked(): ignored by flags " + flags);
   1119             return;
   1120         }
   1121 
   1122         ArraySet<String> ignoredDatasets = null;
   1123         ArrayList<AutofillId> changedFieldIds = null;
   1124         ArrayList<String> changedDatasetIds = null;
   1125         ArrayMap<AutofillId, ArraySet<String>> manuallyFilledIds = null;
   1126 
   1127         boolean hasAtLeastOneDataset = false;
   1128         final int responseCount = mResponses.size();
   1129         for (int i = 0; i < responseCount; i++) {
   1130             final FillResponse response = mResponses.valueAt(i);
   1131             final List<Dataset> datasets = response.getDatasets();
   1132             if (datasets == null || datasets.isEmpty()) {
   1133                 if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + i);
   1134             } else {
   1135                 for (int j = 0; j < datasets.size(); j++) {
   1136                     final Dataset dataset = datasets.get(j);
   1137                     final String datasetId = dataset.getId();
   1138                     if (datasetId == null) {
   1139                         if (sVerbose) {
   1140                             Slog.v(TAG, "logContextCommitted() skipping idless dataset " + dataset);
   1141                         }
   1142                     } else {
   1143                         hasAtLeastOneDataset = true;
   1144                         if (mSelectedDatasetIds == null
   1145                                 || !mSelectedDatasetIds.contains(datasetId)) {
   1146                             if (sVerbose) Slog.v(TAG, "adding ignored dataset " + datasetId);
   1147                             if (ignoredDatasets == null) {
   1148                                 ignoredDatasets = new ArraySet<>();
   1149                             }
   1150                             ignoredDatasets.add(datasetId);
   1151                         }
   1152                     }
   1153                 }
   1154             }
   1155         }
   1156         final AutofillId[] fieldClassificationIds = lastResponse.getFieldClassificationIds();
   1157 
   1158         if (!hasAtLeastOneDataset && fieldClassificationIds == null) {
   1159             if (sVerbose) {
   1160                 Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets nor fields "
   1161                         + "classification ids)");
   1162             }
   1163             return;
   1164         }
   1165 
   1166         final UserData userData = mService.getUserData();
   1167 
   1168         for (int i = 0; i < mViewStates.size(); i++) {
   1169             final ViewState viewState = mViewStates.valueAt(i);
   1170             final int state = viewState.getState();
   1171 
   1172             // When value changed, we need to log if it was:
   1173             // - autofilled -> changedDatasetIds
   1174             // - not autofilled but matches a dataset value -> manuallyFilledIds
   1175             if ((state & ViewState.STATE_CHANGED) != 0) {
   1176                 // Check if autofilled value was changed
   1177                 if ((state & ViewState.STATE_AUTOFILLED) != 0) {
   1178                     final String datasetId = viewState.getDatasetId();
   1179                     if (datasetId == null) {
   1180                         // Sanity check - should never happen.
   1181                         Slog.w(TAG, "logContextCommitted(): no dataset id on " + viewState);
   1182                         continue;
   1183                     }
   1184 
   1185                     // Must first check if final changed value is not the same as value sent by
   1186                     // service.
   1187                     final AutofillValue autofilledValue = viewState.getAutofilledValue();
   1188                     final AutofillValue currentValue = viewState.getCurrentValue();
   1189                     if (autofilledValue != null && autofilledValue.equals(currentValue)) {
   1190                         if (sDebug) {
   1191                             Slog.d(TAG, "logContextCommitted(): ignoring changed " + viewState
   1192                                     + " because it has same value that was autofilled");
   1193                         }
   1194                         continue;
   1195                     }
   1196 
   1197                     if (sDebug) {
   1198                         Slog.d(TAG, "logContextCommitted() found changed state: " + viewState);
   1199                     }
   1200                     if (changedFieldIds == null) {
   1201                         changedFieldIds = new ArrayList<>();
   1202                         changedDatasetIds = new ArrayList<>();
   1203                     }
   1204                     changedFieldIds.add(viewState.id);
   1205                     changedDatasetIds.add(datasetId);
   1206                 } else {
   1207                     final AutofillValue currentValue = viewState.getCurrentValue();
   1208                     if (currentValue == null) {
   1209                         if (sDebug) {
   1210                             Slog.d(TAG, "logContextCommitted(): skipping view without current "
   1211                                     + "value ( " + viewState + ")");
   1212                         }
   1213                         continue;
   1214                     }
   1215                     // Check if value match a dataset.
   1216                     if (hasAtLeastOneDataset) {
   1217                         for (int j = 0; j < responseCount; j++) {
   1218                             final FillResponse response = mResponses.valueAt(j);
   1219                             final List<Dataset> datasets = response.getDatasets();
   1220                             if (datasets == null || datasets.isEmpty()) {
   1221                                 if (sVerbose) {
   1222                                     Slog.v(TAG,  "logContextCommitted() no datasets at " + j);
   1223                                 }
   1224                             } else {
   1225                                 for (int k = 0; k < datasets.size(); k++) {
   1226                                     final Dataset dataset = datasets.get(k);
   1227                                     final String datasetId = dataset.getId();
   1228                                     if (datasetId == null) {
   1229                                         if (sVerbose) {
   1230                                             Slog.v(TAG, "logContextCommitted() skipping idless "
   1231                                                     + "dataset " + dataset);
   1232                                         }
   1233                                     } else {
   1234                                         final ArrayList<AutofillValue> values =
   1235                                                 dataset.getFieldValues();
   1236                                         for (int l = 0; l < values.size(); l++) {
   1237                                             final AutofillValue candidate = values.get(l);
   1238                                             if (currentValue.equals(candidate)) {
   1239                                                 if (sDebug) {
   1240                                                     Slog.d(TAG, "field " + viewState.id + " was "
   1241                                                             + "manually filled with value set by "
   1242                                                             + "dataset " + datasetId);
   1243                                                 }
   1244                                                 if (manuallyFilledIds == null) {
   1245                                                     manuallyFilledIds = new ArrayMap<>();
   1246                                                 }
   1247                                                 ArraySet<String> datasetIds =
   1248                                                         manuallyFilledIds.get(viewState.id);
   1249                                                 if (datasetIds == null) {
   1250                                                     datasetIds = new ArraySet<>(1);
   1251                                                     manuallyFilledIds.put(viewState.id, datasetIds);
   1252                                                 }
   1253                                                 datasetIds.add(datasetId);
   1254                                             }
   1255                                         } // for l
   1256                                         if (mSelectedDatasetIds == null
   1257                                                 || !mSelectedDatasetIds.contains(datasetId)) {
   1258                                             if (sVerbose) {
   1259                                                 Slog.v(TAG, "adding ignored dataset " + datasetId);
   1260                                             }
   1261                                             if (ignoredDatasets == null) {
   1262                                                 ignoredDatasets = new ArraySet<>();
   1263                                             }
   1264                                             ignoredDatasets.add(datasetId);
   1265                                         } // if
   1266                                     } // if
   1267                                 } // for k
   1268                             } // else
   1269                         } // for j
   1270                     }
   1271 
   1272                 } // else
   1273             } // else
   1274         }
   1275 
   1276         ArrayList<AutofillId> manuallyFilledFieldIds = null;
   1277         ArrayList<ArrayList<String>> manuallyFilledDatasetIds = null;
   1278 
   1279         // Must "flatten" the map to the parcelable collection primitives
   1280         if (manuallyFilledIds != null) {
   1281             final int size = manuallyFilledIds.size();
   1282             manuallyFilledFieldIds = new ArrayList<>(size);
   1283             manuallyFilledDatasetIds = new ArrayList<>(size);
   1284             for (int i = 0; i < size; i++) {
   1285                 final AutofillId fieldId = manuallyFilledIds.keyAt(i);
   1286                 final ArraySet<String> datasetIds = manuallyFilledIds.valueAt(i);
   1287                 manuallyFilledFieldIds.add(fieldId);
   1288                 manuallyFilledDatasetIds.add(new ArrayList<>(datasetIds));
   1289             }
   1290         }
   1291 
   1292         // Sets field classification scores
   1293         final FieldClassificationStrategy fcStrategy = mService.getFieldClassificationStrategy();
   1294         if (userData != null && fcStrategy != null) {
   1295             logFieldClassificationScoreLocked(fcStrategy, ignoredDatasets, changedFieldIds,
   1296                     changedDatasetIds, manuallyFilledFieldIds, manuallyFilledDatasetIds,
   1297                     userData, mViewStates.values());
   1298         } else {
   1299             mService.logContextCommittedLocked(id, mClientState, mSelectedDatasetIds,
   1300                     ignoredDatasets, changedFieldIds, changedDatasetIds,
   1301                     manuallyFilledFieldIds, manuallyFilledDatasetIds,
   1302                     mComponentName, mCompatMode);
   1303         }
   1304     }
   1305 
   1306     /**
   1307      * Adds the matches to {@code detectedFieldsIds} and {@code detectedFieldClassifications} for
   1308      * {@code fieldId} based on its {@code currentValue} and {@code userData}.
   1309      */
   1310     private void logFieldClassificationScoreLocked(
   1311             @NonNull FieldClassificationStrategy fcStrategy,
   1312             @NonNull ArraySet<String> ignoredDatasets,
   1313             @NonNull ArrayList<AutofillId> changedFieldIds,
   1314             @NonNull ArrayList<String> changedDatasetIds,
   1315             @NonNull ArrayList<AutofillId> manuallyFilledFieldIds,
   1316             @NonNull ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
   1317             @NonNull UserData userData, @NonNull Collection<ViewState> viewStates) {
   1318 
   1319         final String[] userValues = userData.getValues();
   1320         final String[] categoryIds = userData.getCategoryIds();
   1321 
   1322         // Sanity check
   1323         if (userValues == null || categoryIds == null || userValues.length != categoryIds.length) {
   1324             final int valuesLength = userValues == null ? -1 : userValues.length;
   1325             final int idsLength = categoryIds == null ? -1 : categoryIds.length;
   1326             Slog.w(TAG, "setScores(): user data mismatch: values.length = "
   1327                     + valuesLength + ", ids.length = " + idsLength);
   1328             return;
   1329         }
   1330 
   1331         final int maxFieldsSize = UserData.getMaxFieldClassificationIdsSize();
   1332 
   1333         final ArrayList<AutofillId> detectedFieldIds = new ArrayList<>(maxFieldsSize);
   1334         final ArrayList<FieldClassification> detectedFieldClassifications = new ArrayList<>(
   1335                 maxFieldsSize);
   1336 
   1337         final String algorithm = userData.getFieldClassificationAlgorithm();
   1338         final Bundle algorithmArgs = userData.getAlgorithmArgs();
   1339         final int viewsSize = viewStates.size();
   1340 
   1341         // First, we get all scores.
   1342         final AutofillId[] autofillIds = new AutofillId[viewsSize];
   1343         final ArrayList<AutofillValue> currentValues = new ArrayList<>(viewsSize);
   1344         int k = 0;
   1345         for (ViewState viewState : viewStates) {
   1346             currentValues.add(viewState.getCurrentValue());
   1347             autofillIds[k++] = viewState.id;
   1348         }
   1349 
   1350         // Then use the results, asynchronously
   1351         final RemoteCallback callback = new RemoteCallback((result) -> {
   1352             if (result == null) {
   1353                 if (sDebug) Slog.d(TAG, "setFieldClassificationScore(): no results");
   1354                 mService.logContextCommittedLocked(id, mClientState, mSelectedDatasetIds,
   1355                         ignoredDatasets, changedFieldIds, changedDatasetIds,
   1356                         manuallyFilledFieldIds, manuallyFilledDatasetIds,
   1357                         mComponentName, mCompatMode);
   1358                 return;
   1359             }
   1360             final Scores scores = result.getParcelable(EXTRA_SCORES);
   1361             if (scores == null) {
   1362                 Slog.w(TAG, "No field classification score on " + result);
   1363                 return;
   1364             }
   1365             int i = 0, j = 0;
   1366             try {
   1367                 // Iteract over all autofill fields first
   1368                 for (i = 0; i < viewsSize; i++) {
   1369                     final AutofillId autofillId = autofillIds[i];
   1370 
   1371                     // Search the best scores for each category (as some categories could have
   1372                     // multiple user values
   1373                     ArrayMap<String, Float> scoresByField = null;
   1374                     for (j = 0; j < userValues.length; j++) {
   1375                         final String categoryId = categoryIds[j];
   1376                         final float score = scores.scores[i][j];
   1377                         if (score > 0) {
   1378                             if (scoresByField == null) {
   1379                                 scoresByField = new ArrayMap<>(userValues.length);
   1380                             }
   1381                             final Float currentScore = scoresByField.get(categoryId);
   1382                             if (currentScore != null && currentScore > score) {
   1383                                 if (sVerbose) {
   1384                                     Slog.v(TAG,  "skipping score " + score
   1385                                             + " because it's less than " + currentScore);
   1386                                 }
   1387                                 continue;
   1388                             }
   1389                             if (sVerbose) {
   1390                                 Slog.v(TAG, "adding score " + score + " at index " + j + " and id "
   1391                                         + autofillId);
   1392                             }
   1393                             scoresByField.put(categoryId, score);
   1394                         }
   1395                         else if (sVerbose) {
   1396                             Slog.v(TAG, "skipping score 0 at index " + j + " and id " + autofillId);
   1397                         }
   1398                     }
   1399                     if (scoresByField == null) {
   1400                         if (sVerbose) Slog.v(TAG, "no score for autofillId=" + autofillId);
   1401                         continue;
   1402                     }
   1403 
   1404                     // Then create the matches for that autofill id
   1405                     final ArrayList<Match> matches = new ArrayList<>(scoresByField.size());
   1406                     for (j = 0; j < scoresByField.size(); j++) {
   1407                         final String fieldId = scoresByField.keyAt(j);
   1408                         final float score = scoresByField.valueAt(j);
   1409                         matches.add(new Match(fieldId, score));
   1410                     }
   1411                     detectedFieldIds.add(autofillId);
   1412                     detectedFieldClassifications.add(new FieldClassification(matches));
   1413                 } // for i
   1414             } catch (ArrayIndexOutOfBoundsException e) {
   1415                 wtf(e, "Error accessing FC score at [%d, %d] (%s): %s", i, j, scores, e);
   1416                 return;
   1417             }
   1418 
   1419             mService.logContextCommittedLocked(id, mClientState, mSelectedDatasetIds,
   1420                     ignoredDatasets, changedFieldIds, changedDatasetIds, manuallyFilledFieldIds,
   1421                     manuallyFilledDatasetIds, detectedFieldIds, detectedFieldClassifications,
   1422                     mComponentName, mCompatMode);
   1423         });
   1424 
   1425         fcStrategy.getScores(callback, algorithm, algorithmArgs, currentValues, userValues);
   1426     }
   1427 
   1428     /**
   1429      * Shows the save UI, when session can be saved.
   1430      *
   1431      * @return {@code true} if session is done, or {@code false} if it's pending user action.
   1432      */
   1433     @GuardedBy("mLock")
   1434     public boolean showSaveLocked() {
   1435         if (mDestroyed) {
   1436             Slog.w(TAG, "Call to Session#showSaveLocked() rejected - session: "
   1437                     + id + " destroyed");
   1438             return false;
   1439         }
   1440         final FillResponse response = getLastResponseLocked("showSaveLocked()");
   1441         final SaveInfo saveInfo = response == null ? null : response.getSaveInfo();
   1442 
   1443         /*
   1444          * The Save dialog is only shown if all conditions below are met:
   1445          *
   1446          * - saveInfo is not null.
   1447          * - autofillValue of all required ids is not null.
   1448          * - autofillValue of at least one id (required or optional) has changed.
   1449          * - there is no Dataset in the last FillResponse whose values of all dataset fields matches
   1450          *   the current values of all fields in the screen.
   1451          */
   1452         if (saveInfo == null) {
   1453             if (sVerbose) Slog.v(TAG, "showSaveLocked(): no saveInfo from service");
   1454             return true;
   1455         }
   1456 
   1457         final ArrayMap<AutofillId, InternalSanitizer> sanitizers = createSanitizers(saveInfo);
   1458 
   1459         // Cache used to make sure changed fields do not belong to a dataset.
   1460         final ArrayMap<AutofillId, AutofillValue> currentValues = new ArrayMap<>();
   1461         final ArraySet<AutofillId> allIds = new ArraySet<>();
   1462 
   1463         final AutofillId[] requiredIds = saveInfo.getRequiredIds();
   1464         boolean allRequiredAreNotEmpty = true;
   1465         boolean atLeastOneChanged = false;
   1466         if (requiredIds != null) {
   1467             for (int i = 0; i < requiredIds.length; i++) {
   1468                 final AutofillId id = requiredIds[i];
   1469                 if (id == null) {
   1470                     Slog.w(TAG, "null autofill id on " + Arrays.toString(requiredIds));
   1471                     continue;
   1472                 }
   1473                 allIds.add(id);
   1474                 final ViewState viewState = mViewStates.get(id);
   1475                 if (viewState == null) {
   1476                     Slog.w(TAG, "showSaveLocked(): no ViewState for required " + id);
   1477                     allRequiredAreNotEmpty = false;
   1478                     break;
   1479                 }
   1480 
   1481                 AutofillValue value = viewState.getCurrentValue();
   1482                 if (value == null || value.isEmpty()) {
   1483                     final AutofillValue initialValue = getValueFromContextsLocked(id);
   1484                     if (initialValue != null) {
   1485                         if (sDebug) {
   1486                             Slog.d(TAG, "Value of required field " + id + " didn't change; "
   1487                                     + "using initial value (" + initialValue + ") instead");
   1488                         }
   1489                         value = initialValue;
   1490                     } else {
   1491                         if (sDebug) {
   1492                             Slog.d(TAG, "empty value for required " + id );
   1493                         }
   1494                         allRequiredAreNotEmpty = false;
   1495                         break;
   1496                     }
   1497                 }
   1498 
   1499                 value = getSanitizedValue(sanitizers, id, value);
   1500                 if (value == null) {
   1501                     if (sDebug) {
   1502                         Slog.d(TAG, "value of required field " + id + " failed sanitization");
   1503                     }
   1504                     allRequiredAreNotEmpty = false;
   1505                     break;
   1506                 }
   1507                 viewState.setSanitizedValue(value);
   1508                 currentValues.put(id, value);
   1509                 final AutofillValue filledValue = viewState.getAutofilledValue();
   1510 
   1511                 if (!value.equals(filledValue)) {
   1512                     boolean changed = true;
   1513                     if (filledValue == null) {
   1514                         // Dataset was not autofilled, make sure initial value didn't change.
   1515                         final AutofillValue initialValue = getValueFromContextsLocked(id);
   1516                         if (initialValue != null && initialValue.equals(value)) {
   1517                             if (sDebug) {
   1518                                 Slog.d(TAG, "id " + id + " is part of dataset but initial value "
   1519                                         + "didn't change: " + value);
   1520                             }
   1521                             changed = false;
   1522                         }
   1523                     }
   1524                     if (changed) {
   1525                         if (sDebug) {
   1526                             Slog.d(TAG, "found a change on required " + id + ": " + filledValue
   1527                                     + " => " + value);
   1528                         }
   1529                         atLeastOneChanged = true;
   1530                     }
   1531                 }
   1532             }
   1533         }
   1534 
   1535         final AutofillId[] optionalIds = saveInfo.getOptionalIds();
   1536         if (allRequiredAreNotEmpty) {
   1537             if (!atLeastOneChanged && optionalIds != null) {
   1538                 // No change on required ids yet, look for changes on optional ids.
   1539                 for (int i = 0; i < optionalIds.length; i++) {
   1540                     final AutofillId id = optionalIds[i];
   1541                     allIds.add(id);
   1542                     final ViewState viewState = mViewStates.get(id);
   1543                     if (viewState == null) {
   1544                         Slog.w(TAG, "no ViewState for optional " + id);
   1545                         continue;
   1546                     }
   1547                     if ((viewState.getState() & ViewState.STATE_CHANGED) != 0) {
   1548                         final AutofillValue currentValue = viewState.getCurrentValue();
   1549                         currentValues.put(id, currentValue);
   1550                         final AutofillValue filledValue = viewState.getAutofilledValue();
   1551                         if (currentValue != null && !currentValue.equals(filledValue)) {
   1552                             if (sDebug) {
   1553                                 Slog.d(TAG, "found a change on optional " + id + ": " + filledValue
   1554                                         + " => " + currentValue);
   1555                             }
   1556                             atLeastOneChanged = true;
   1557                             break;
   1558                         }
   1559                     } else {
   1560                         // Update current values cache based on initial value
   1561                         final AutofillValue initialValue = getValueFromContextsLocked(id);
   1562                         if (sDebug) {
   1563                             Slog.d(TAG, "no current value for " + id + "; initial value is "
   1564                                     + initialValue);
   1565                         }
   1566                         if (initialValue != null) {
   1567                             currentValues.put(id, initialValue);
   1568                         }
   1569                     }
   1570                 }
   1571             }
   1572             if (atLeastOneChanged) {
   1573                 if (sDebug) {
   1574                     Slog.d(TAG, "at least one field changed, validate fields for save UI");
   1575                 }
   1576                 final InternalValidator validator = saveInfo.getValidator();
   1577                 if (validator != null) {
   1578                     final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_SAVE_VALIDATION);
   1579                     boolean isValid;
   1580                     try {
   1581                         isValid = validator.isValid(this);
   1582                         if (sDebug) Slog.d(TAG, validator + " returned " + isValid);
   1583                         log.setType(isValid
   1584                                 ? MetricsEvent.TYPE_SUCCESS
   1585                                 : MetricsEvent.TYPE_DISMISS);
   1586                     } catch (Exception e) {
   1587                         Slog.e(TAG, "Not showing save UI because validation failed:", e);
   1588                         log.setType(MetricsEvent.TYPE_FAILURE);
   1589                         mMetricsLogger.write(log);
   1590                         return true;
   1591                     }
   1592 
   1593                     mMetricsLogger.write(log);
   1594                     if (!isValid) {
   1595                         Slog.i(TAG, "not showing save UI because fields failed validation");
   1596                         return true;
   1597                     }
   1598                 }
   1599 
   1600                 // Make sure the service doesn't have the fields already by checking the datasets
   1601                 // content.
   1602                 final List<Dataset> datasets = response.getDatasets();
   1603                 if (datasets != null) {
   1604                     datasets_loop: for (int i = 0; i < datasets.size(); i++) {
   1605                         final Dataset dataset = datasets.get(i);
   1606                         final ArrayMap<AutofillId, AutofillValue> datasetValues =
   1607                                 Helper.getFields(dataset);
   1608                         if (sVerbose) {
   1609                             Slog.v(TAG, "Checking if saved fields match contents of dataset #" + i
   1610                                     + ": " + dataset + "; allIds=" + allIds);
   1611                         }
   1612                         for (int j = 0; j < allIds.size(); j++) {
   1613                             final AutofillId id = allIds.valueAt(j);
   1614                             final AutofillValue currentValue = currentValues.get(id);
   1615                             if (currentValue == null) {
   1616                                 if (sDebug) {
   1617                                     Slog.d(TAG, "dataset has value for field that is null: " + id);
   1618                                 }
   1619                                 continue datasets_loop;
   1620                             }
   1621                             final AutofillValue datasetValue = datasetValues.get(id);
   1622                             if (!currentValue.equals(datasetValue)) {
   1623                                 if (sDebug) {
   1624                                     Slog.d(TAG, "found a dataset change on id " + id + ": from "
   1625                                             + datasetValue + " to " + currentValue);
   1626                                 }
   1627                                 continue datasets_loop;
   1628                             }
   1629                             if (sVerbose) Slog.v(TAG, "no dataset changes for id " + id);
   1630                         }
   1631                         if (sDebug) {
   1632                             Slog.d(TAG, "ignoring Save UI because all fields match contents of "
   1633                                     + "dataset #" + i + ": " + dataset);
   1634                         }
   1635                         return true;
   1636                     }
   1637                 }
   1638 
   1639                 if (sDebug) {
   1640                     Slog.d(TAG, "Good news, everyone! All checks passed, show save UI for "
   1641                             + id + "!");
   1642                 }
   1643 
   1644                 // Use handler so logContextCommitted() is logged first
   1645                 mHandler.sendMessage(obtainMessage(
   1646                         Session::logSaveShown, this));
   1647 
   1648                 final IAutoFillManagerClient client = getClient();
   1649                 mPendingSaveUi = new PendingUi(mActivityToken, id, client);
   1650                 getUiForShowing().showSaveUi(mService.getServiceLabel(), mService.getServiceIcon(),
   1651                         mService.getServicePackageName(), saveInfo, this,
   1652                         mComponentName, this, mPendingSaveUi, mCompatMode);
   1653                 if (client != null) {
   1654                     try {
   1655                         client.setSaveUiState(id, true);
   1656                     } catch (RemoteException e) {
   1657                         Slog.e(TAG, "Error notifying client to set save UI state to shown: " + e);
   1658                     }
   1659                 }
   1660                 mIsSaving = true;
   1661                 return false;
   1662             }
   1663         }
   1664         // Nothing changed...
   1665         if (sDebug) {
   1666             Slog.d(TAG, "showSaveLocked(" + id +"): with no changes, comes no responsibilities."
   1667                     + "allRequiredAreNotNull=" + allRequiredAreNotEmpty
   1668                     + ", atLeastOneChanged=" + atLeastOneChanged);
   1669         }
   1670         return true;
   1671     }
   1672 
   1673     private void logSaveShown() {
   1674         mService.logSaveShown(id, mClientState);
   1675     }
   1676 
   1677     @Nullable
   1678     private ArrayMap<AutofillId, InternalSanitizer> createSanitizers(@Nullable SaveInfo saveInfo) {
   1679         if (saveInfo == null) return null;
   1680 
   1681         final InternalSanitizer[] sanitizerKeys = saveInfo.getSanitizerKeys();
   1682         if (sanitizerKeys == null) return null;
   1683 
   1684         final int size = sanitizerKeys.length ;
   1685         final ArrayMap<AutofillId, InternalSanitizer> sanitizers = new ArrayMap<>(size);
   1686         if (sDebug) Slog.d(TAG, "Service provided " + size + " sanitizers");
   1687         final AutofillId[][] sanitizerValues = saveInfo.getSanitizerValues();
   1688         for (int i = 0; i < size; i++) {
   1689             final InternalSanitizer sanitizer = sanitizerKeys[i];
   1690             final AutofillId[] ids = sanitizerValues[i];
   1691             if (sDebug) {
   1692                 Slog.d(TAG, "sanitizer #" + i + " (" + sanitizer + ") for ids "
   1693                         + Arrays.toString(ids));
   1694             }
   1695             for (AutofillId id : ids) {
   1696                 sanitizers.put(id, sanitizer);
   1697             }
   1698         }
   1699         return sanitizers;
   1700     }
   1701 
   1702     @Nullable
   1703     private AutofillValue getSanitizedValue(
   1704             @Nullable ArrayMap<AutofillId, InternalSanitizer> sanitizers,
   1705             @NonNull AutofillId id,
   1706             @NonNull AutofillValue value) {
   1707         if (sanitizers == null) return value;
   1708 
   1709         final InternalSanitizer sanitizer = sanitizers.get(id);
   1710         if (sanitizer == null) {
   1711             return value;
   1712         }
   1713 
   1714         final AutofillValue sanitized = sanitizer.sanitize(value);
   1715         if (sDebug) Slog.d(TAG, "Value for " + id + "(" + value + ") sanitized to " + sanitized);
   1716         return sanitized;
   1717     }
   1718 
   1719     /**
   1720      * Returns whether the session is currently showing the save UI
   1721      */
   1722     @GuardedBy("mLock")
   1723     boolean isSavingLocked() {
   1724         return mIsSaving;
   1725     }
   1726 
   1727     /**
   1728      * Gets the latest non-empty value for the given id in the autofill contexts.
   1729      */
   1730     @GuardedBy("mLock")
   1731     @Nullable
   1732     private AutofillValue getValueFromContextsLocked(AutofillId id) {
   1733         final int numContexts = mContexts.size();
   1734         for (int i = numContexts - 1; i >= 0; i--) {
   1735             final FillContext context = mContexts.get(i);
   1736             final ViewNode node = Helper.findViewNodeByAutofillId(context.getStructure(), id);
   1737             if (node != null) {
   1738                 final AutofillValue value = node.getAutofillValue();
   1739                 if (sDebug) {
   1740                     Slog.d(TAG, "getValueFromContexts(" + id + ") at " + i + ": " + value);
   1741                 }
   1742                 if (value != null && !value.isEmpty()) {
   1743                     return value;
   1744                 }
   1745             }
   1746         }
   1747         return null;
   1748     }
   1749 
   1750     /**
   1751      * Gets the latest autofill options for the given id in the autofill contexts.
   1752      */
   1753     @GuardedBy("mLock")
   1754     @Nullable
   1755     private CharSequence[] getAutofillOptionsFromContextsLocked(AutofillId id) {
   1756         final int numContexts = mContexts.size();
   1757 
   1758         for (int i = numContexts - 1; i >= 0; i--) {
   1759             final FillContext context = mContexts.get(i);
   1760             final ViewNode node = Helper.findViewNodeByAutofillId(context.getStructure(), id);
   1761             if (node != null && node.getAutofillOptions() != null) {
   1762                 return node.getAutofillOptions();
   1763             }
   1764         }
   1765         return null;
   1766     }
   1767 
   1768     /**
   1769      * Calls service when user requested save.
   1770      */
   1771     @GuardedBy("mLock")
   1772     void callSaveLocked() {
   1773         if (mDestroyed) {
   1774             Slog.w(TAG, "Call to Session#callSaveLocked() rejected - session: "
   1775                     + id + " destroyed");
   1776             return;
   1777         }
   1778 
   1779         if (sVerbose) Slog.v(TAG, "callSaveLocked(): mViewStates=" + mViewStates);
   1780 
   1781         if (mContexts == null) {
   1782             Slog.w(TAG, "callSaveLocked(): no contexts");
   1783             return;
   1784         }
   1785 
   1786         final ArrayMap<AutofillId, InternalSanitizer> sanitizers =
   1787                 createSanitizers(getSaveInfoLocked());
   1788 
   1789         final int numContexts = mContexts.size();
   1790 
   1791         for (int contextNum = 0; contextNum < numContexts; contextNum++) {
   1792             final FillContext context = mContexts.get(contextNum);
   1793 
   1794             final ViewNode[] nodes =
   1795                 context.findViewNodesByAutofillIds(getIdsOfAllViewStatesLocked());
   1796 
   1797             if (sVerbose) Slog.v(TAG, "callSaveLocked(): updating " + context);
   1798 
   1799             for (int viewStateNum = 0; viewStateNum < mViewStates.size(); viewStateNum++) {
   1800                 final ViewState viewState = mViewStates.valueAt(viewStateNum);
   1801 
   1802                 final AutofillId id = viewState.id;
   1803                 final AutofillValue value = viewState.getCurrentValue();
   1804                 if (value == null) {
   1805                     if (sVerbose) Slog.v(TAG, "callSaveLocked(): skipping " + id);
   1806                     continue;
   1807                 }
   1808                 final ViewNode node = nodes[viewStateNum];
   1809                 if (node == null) {
   1810                     Slog.w(TAG, "callSaveLocked(): did not find node with id " + id);
   1811                     continue;
   1812                 }
   1813                 if (sVerbose) Slog.v(TAG, "callSaveLocked(): updating " + id + " to " + value);
   1814 
   1815                 AutofillValue sanitizedValue = viewState.getSanitizedValue();
   1816 
   1817                 if (sanitizedValue == null) {
   1818                     // Field is optional and haven't been sanitized yet.
   1819                     sanitizedValue = getSanitizedValue(sanitizers, id, value);
   1820                 }
   1821                 if (sanitizedValue != null) {
   1822                     node.updateAutofillValue(sanitizedValue);
   1823                 } else if (sDebug) {
   1824                     Slog.d(TAG, "Not updating field " + id + " because it failed sanitization");
   1825                 }
   1826             }
   1827 
   1828             // Sanitize structure before it's sent to service.
   1829             context.getStructure().sanitizeForParceling(false);
   1830 
   1831             if (sVerbose) {
   1832                 Slog.v(TAG, "Dumping structure of " + context + " before calling service.save()");
   1833                 context.getStructure().dump(false);
   1834             }
   1835         }
   1836 
   1837         // Remove pending fill requests as the session is finished.
   1838         cancelCurrentRequestLocked();
   1839 
   1840         // Dispatch a snapshot of the current contexts list since it may change
   1841         // until the dispatch happens. The items in the list don't need to be cloned
   1842         // since we don't hold on them anywhere else. The client state is not touched
   1843         // by us, so no need to copy.
   1844         final SaveRequest saveRequest = new SaveRequest(new ArrayList<>(mContexts), mClientState,
   1845                 mSelectedDatasetIds);
   1846         mRemoteFillService.onSaveRequest(saveRequest);
   1847     }
   1848 
   1849     /**
   1850      * Starts (if necessary) a new fill request upon entering a view.
   1851      *
   1852      * <p>A new request will be started in 2 scenarios:
   1853      * <ol>
   1854      *   <li>If the user manually requested autofill.
   1855      *   <li>If the view is part of a new partition.
   1856      * </ol>
   1857      *
   1858      * @param id The id of the view that is entered.
   1859      * @param viewState The view that is entered.
   1860      * @param flags The flag that was passed by the AutofillManager.
   1861      */
   1862     @GuardedBy("mLock")
   1863     private void requestNewFillResponseOnViewEnteredIfNecessaryLocked(@NonNull AutofillId id,
   1864             @NonNull ViewState viewState, int flags) {
   1865         if ((flags & FLAG_MANUAL_REQUEST) != 0) {
   1866             if (sDebug) Slog.d(TAG, "Re-starting session on view " + id + " and flags " + flags);
   1867             viewState.setState(STATE_RESTARTED_SESSION);
   1868             requestNewFillResponseLocked(flags);
   1869             return;
   1870         }
   1871 
   1872         // If it's not, then check if it it should start a partition.
   1873         if (shouldStartNewPartitionLocked(id)) {
   1874             if (sDebug) {
   1875                 Slog.d(TAG, "Starting partition for view id " + id + ": "
   1876                         + viewState.getStateAsString());
   1877             }
   1878             viewState.setState(ViewState.STATE_STARTED_PARTITION);
   1879             requestNewFillResponseLocked(flags);
   1880         } else {
   1881             if (sVerbose) {
   1882                 Slog.v(TAG, "Not starting new partition for view " + id + ": "
   1883                         + viewState.getStateAsString());
   1884             }
   1885         }
   1886     }
   1887 
   1888     /**
   1889      * Determines if a new partition should be started for an id.
   1890      *
   1891      * @param id The id of the view that is entered
   1892      *
   1893      * @return {@code true} iff a new partition should be started
   1894      */
   1895     @GuardedBy("mLock")
   1896     private boolean shouldStartNewPartitionLocked(@NonNull AutofillId id) {
   1897         if (mResponses == null) {
   1898             return true;
   1899         }
   1900 
   1901         final int numResponses = mResponses.size();
   1902         if (numResponses >= sPartitionMaxCount) {
   1903             Slog.e(TAG, "Not starting a new partition on " + id + " because session " + this.id
   1904                     + " reached maximum of " + sPartitionMaxCount);
   1905             return false;
   1906         }
   1907 
   1908         for (int responseNum = 0; responseNum < numResponses; responseNum++) {
   1909             final FillResponse response = mResponses.valueAt(responseNum);
   1910 
   1911             if (ArrayUtils.contains(response.getIgnoredIds(), id)) {
   1912                 return false;
   1913             }
   1914 
   1915             final SaveInfo saveInfo = response.getSaveInfo();
   1916             if (saveInfo != null) {
   1917                 if (ArrayUtils.contains(saveInfo.getOptionalIds(), id)
   1918                         || ArrayUtils.contains(saveInfo.getRequiredIds(), id)) {
   1919                     return false;
   1920                 }
   1921             }
   1922 
   1923             final List<Dataset> datasets = response.getDatasets();
   1924             if (datasets != null) {
   1925                 final int numDatasets = datasets.size();
   1926 
   1927                 for (int dataSetNum = 0; dataSetNum < numDatasets; dataSetNum++) {
   1928                     final ArrayList<AutofillId> fields = datasets.get(dataSetNum).getFieldIds();
   1929 
   1930                     if (fields != null && fields.contains(id)) {
   1931                         return false;
   1932                     }
   1933                 }
   1934             }
   1935 
   1936             if (ArrayUtils.contains(response.getAuthenticationIds(), id)) {
   1937                 return false;
   1938             }
   1939         }
   1940 
   1941         return true;
   1942     }
   1943 
   1944     @GuardedBy("mLock")
   1945     void updateLocked(AutofillId id, Rect virtualBounds, AutofillValue value, int action,
   1946             int flags) {
   1947         if (mDestroyed) {
   1948             Slog.w(TAG, "Call to Session#updateLocked() rejected - session: "
   1949                     + id + " destroyed");
   1950             return;
   1951         }
   1952         if (sVerbose) {
   1953             Slog.v(TAG, "updateLocked(): id=" + id + ", action=" + actionAsString(action)
   1954                     + ", flags=" + flags);
   1955         }
   1956         ViewState viewState = mViewStates.get(id);
   1957 
   1958         if (viewState == null) {
   1959             if (action == ACTION_START_SESSION || action == ACTION_VALUE_CHANGED
   1960                     || action == ACTION_VIEW_ENTERED) {
   1961                 if (sVerbose) Slog.v(TAG, "Creating viewState for " + id);
   1962                 boolean isIgnored = isIgnoredLocked(id);
   1963                 viewState = new ViewState(this, id, this,
   1964                         isIgnored ? ViewState.STATE_IGNORED : ViewState.STATE_INITIAL);
   1965                 mViewStates.put(id, viewState);
   1966 
   1967                 // TODO(b/73648631): for optimization purposes, should also ignore if change is
   1968                 // detectable, and batch-send them when the session is finished (but that will
   1969                 // require tracking detectable fields on AutofillManager)
   1970                 if (isIgnored) {
   1971                     if (sDebug) Slog.d(TAG, "updateLocked(): ignoring view " + viewState);
   1972                     return;
   1973                 }
   1974             } else {
   1975                 if (sVerbose) Slog.v(TAG, "Ignoring specific action when viewState=null");
   1976                 return;
   1977             }
   1978         }
   1979 
   1980         switch(action) {
   1981             case ACTION_START_SESSION:
   1982                 // View is triggering autofill.
   1983                 mCurrentViewId = viewState.id;
   1984                 viewState.update(value, virtualBounds, flags);
   1985                 viewState.setState(ViewState.STATE_STARTED_SESSION);
   1986                 requestNewFillResponseLocked(flags);
   1987                 break;
   1988             case ACTION_VALUE_CHANGED:
   1989                 if (mCompatMode && (viewState.getState() & ViewState.STATE_URL_BAR) != 0) {
   1990                     // Must cancel the session if the value of the URL bar changed
   1991                     final String currentUrl = mUrlBar == null ? null
   1992                             : mUrlBar.getText().toString().trim();
   1993                     if (currentUrl == null) {
   1994                         // Sanity check - shouldn't happen.
   1995                         wtf(null, "URL bar value changed, but current value is null");
   1996                         return;
   1997                     }
   1998                     if (value == null || ! value.isText()) {
   1999                         // Sanity check - shouldn't happen.
   2000                         wtf(null, "URL bar value changed to null or non-text: %s", value);
   2001                         return;
   2002                     }
   2003                     final String newUrl = value.getTextValue().toString();
   2004                     if (newUrl.equals(currentUrl)) {
   2005                         if (sDebug) Slog.d(TAG, "Ignoring change on URL bar as it's the same");
   2006                         return;
   2007                     }
   2008                     if (mSaveOnAllViewsInvisible) {
   2009                         // We cannot cancel the session because it could hinder Save when all views
   2010                         // are finished, as the URL bar changed callback is usually called before
   2011                         // the virtual views become invisible.
   2012                         if (sDebug) {
   2013                             Slog.d(TAG, "Ignoring change on URL because session will finish when "
   2014                                     + "views are gone");
   2015                         }
   2016                         return;
   2017                     }
   2018                     if (sDebug) Slog.d(TAG, "Finishing session because URL bar changed");
   2019                     forceRemoveSelfLocked(AutofillManager.STATE_UNKNOWN_COMPAT_MODE);
   2020                     return;
   2021                 }
   2022 
   2023                 if (!Objects.equals(value, viewState.getCurrentValue())) {
   2024                     if ((value == null || value.isEmpty())
   2025                             && viewState.getCurrentValue() != null
   2026                             && viewState.getCurrentValue().isText()
   2027                             && viewState.getCurrentValue().getTextValue() != null
   2028                             && getSaveInfoLocked() != null) {
   2029                         final int length = viewState.getCurrentValue().getTextValue().length();
   2030                         if (sDebug) {
   2031                             Slog.d(TAG, "updateLocked(" + id + "): resetting value that was "
   2032                                     + length + " chars long");
   2033                         }
   2034                         final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_VALUE_RESET)
   2035                                 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_PREVIOUS_LENGTH, length);
   2036                         mMetricsLogger.write(log);
   2037                     }
   2038 
   2039                     // Always update the internal state.
   2040                     viewState.setCurrentValue(value);
   2041 
   2042                     // Must check if this update was caused by autofilling the view, in which
   2043                     // case we just update the value, but not the UI.
   2044                     final AutofillValue filledValue = viewState.getAutofilledValue();
   2045                     if (filledValue != null && filledValue.equals(value)) {
   2046                         if (sVerbose) {
   2047                             Slog.v(TAG, "ignoring autofilled change on id " + id);
   2048                         }
   2049                         return;
   2050                     }
   2051                     // Update the internal state...
   2052                     viewState.setState(ViewState.STATE_CHANGED);
   2053 
   2054                     //..and the UI
   2055                     final String filterText;
   2056                     if (value == null || !value.isText()) {
   2057                         filterText = null;
   2058                     } else {
   2059                         final CharSequence text = value.getTextValue();
   2060                         // Text should never be null, but it doesn't hurt to check to avoid a
   2061                         // system crash...
   2062                         filterText = (text == null) ? null : text.toString();
   2063                     }
   2064                     getUiForShowing().filterFillUi(filterText, this);
   2065                 }
   2066                 break;
   2067             case ACTION_VIEW_ENTERED:
   2068                 if (sVerbose && virtualBounds != null) {
   2069                     Slog.v(TAG, "entered on virtual child " + id + ": " + virtualBounds);
   2070                 }
   2071 
   2072                 if (mCompatMode && (viewState.getState() & ViewState.STATE_URL_BAR) != 0) {
   2073                     if (sDebug) Slog.d(TAG, "Ignoring VIEW_ENTERED on URL BAR (id=" + id + ")");
   2074                     return;
   2075                 }
   2076 
   2077                 requestNewFillResponseOnViewEnteredIfNecessaryLocked(id, viewState, flags);
   2078 
   2079                 // Remove the UI if the ViewState has changed.
   2080                 if (mCurrentViewId != viewState.id) {
   2081                     mUi.hideFillUi(this);
   2082                     mCurrentViewId = viewState.id;
   2083                 }
   2084 
   2085                 // If the ViewState is ready to be displayed, onReady() will be called.
   2086                 viewState.update(value, virtualBounds, flags);
   2087                 break;
   2088             case ACTION_VIEW_EXITED:
   2089                 if (mCurrentViewId == viewState.id) {
   2090                     if (sVerbose) Slog.d(TAG, "Exiting view " + id);
   2091                     mUi.hideFillUi(this);
   2092                     mCurrentViewId = null;
   2093                 }
   2094                 break;
   2095             default:
   2096                 Slog.w(TAG, "updateLocked(): unknown action: " + action);
   2097         }
   2098     }
   2099 
   2100     /**
   2101      * Checks whether a view should be ignored.
   2102      */
   2103     @GuardedBy("mLock")
   2104     private boolean isIgnoredLocked(AutofillId id) {
   2105         // Always check the latest response only
   2106         final FillResponse response = getLastResponseLocked(null);
   2107         if (response == null) return false;
   2108 
   2109         return ArrayUtils.contains(response.getIgnoredIds(), id);
   2110     }
   2111 
   2112     @Override
   2113     public void onFillReady(@NonNull FillResponse response, @NonNull AutofillId filledId,
   2114             @Nullable AutofillValue value) {
   2115         synchronized (mLock) {
   2116             if (mDestroyed) {
   2117                 Slog.w(TAG, "Call to Session#onFillReady() rejected - session: "
   2118                         + id + " destroyed");
   2119                 return;
   2120             }
   2121         }
   2122 
   2123         String filterText = null;
   2124         if (value != null && value.isText()) {
   2125             filterText = value.getTextValue().toString();
   2126         }
   2127 
   2128         getUiForShowing().showFillUi(filledId, response, filterText,
   2129                 mService.getServicePackageName(), mComponentName,
   2130                 mService.getServiceLabel(), mService.getServiceIcon(), this, id, mCompatMode);
   2131 
   2132         synchronized (mLock) {
   2133             if (mUiShownTime == 0) {
   2134                 // Log first time UI is shown.
   2135                 mUiShownTime = SystemClock.elapsedRealtime();
   2136                 final long duration = mUiShownTime - mStartTime;
   2137                 if (sDebug) {
   2138                     final StringBuilder msg = new StringBuilder("1st UI for ")
   2139                             .append(mActivityToken)
   2140                             .append(" shown in ");
   2141                     TimeUtils.formatDuration(duration, msg);
   2142                     Slog.d(TAG, msg.toString());
   2143                 }
   2144                 final StringBuilder historyLog = new StringBuilder("id=").append(id)
   2145                         .append(" app=").append(mActivityToken)
   2146                         .append(" svc=").append(mService.getServicePackageName())
   2147                         .append(" latency=");
   2148                 TimeUtils.formatDuration(duration, historyLog);
   2149                 mUiLatencyHistory.log(historyLog.toString());
   2150 
   2151                 addTaggedDataToRequestLogLocked(response.getRequestId(),
   2152                         MetricsEvent.FIELD_AUTOFILL_DURATION, duration);
   2153             }
   2154         }
   2155     }
   2156 
   2157     boolean isDestroyed() {
   2158         synchronized (mLock) {
   2159             return mDestroyed;
   2160         }
   2161     }
   2162 
   2163     IAutoFillManagerClient getClient() {
   2164         synchronized (mLock) {
   2165             return mClient;
   2166         }
   2167     }
   2168 
   2169     private void notifyUnavailableToClient(int sessionFinishedState) {
   2170         synchronized (mLock) {
   2171             if (mCurrentViewId == null) return;
   2172             try {
   2173                 if (mHasCallback) {
   2174                     mClient.notifyNoFillUi(id, mCurrentViewId, sessionFinishedState);
   2175                 } else if (sessionFinishedState != 0) {
   2176                     mClient.setSessionFinished(sessionFinishedState);
   2177                 }
   2178             } catch (RemoteException e) {
   2179                 Slog.e(TAG, "Error notifying client no fill UI: id=" + mCurrentViewId, e);
   2180             }
   2181         }
   2182     }
   2183 
   2184     @GuardedBy("mLock")
   2185     private void updateTrackedIdsLocked() {
   2186         // Only track the views of the last response as only those are reported back to the
   2187         // service, see #showSaveLocked
   2188         final FillResponse response = getLastResponseLocked(null);
   2189         if (response == null) return;
   2190 
   2191         ArraySet<AutofillId> trackedViews = null;
   2192         mSaveOnAllViewsInvisible = false;
   2193         boolean saveOnFinish = true;
   2194         final SaveInfo saveInfo = response.getSaveInfo();
   2195         final AutofillId saveTriggerId;
   2196         if (saveInfo != null) {
   2197             saveTriggerId = saveInfo.getTriggerId();
   2198             if (saveTriggerId != null) {
   2199                 writeLog(MetricsEvent.AUTOFILL_EXPLICIT_SAVE_TRIGGER_DEFINITION);
   2200             }
   2201             mSaveOnAllViewsInvisible =
   2202                     (saveInfo.getFlags() & SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) != 0;
   2203 
   2204             // We only need to track views if we want to save once they become invisible.
   2205             if (mSaveOnAllViewsInvisible) {
   2206                 if (trackedViews == null) {
   2207                     trackedViews = new ArraySet<>();
   2208                 }
   2209                 if (saveInfo.getRequiredIds() != null) {
   2210                     Collections.addAll(trackedViews, saveInfo.getRequiredIds());
   2211                 }
   2212 
   2213                 if (saveInfo.getOptionalIds() != null) {
   2214                     Collections.addAll(trackedViews, saveInfo.getOptionalIds());
   2215                 }
   2216             }
   2217             if ((saveInfo.getFlags() & SaveInfo.FLAG_DONT_SAVE_ON_FINISH) != 0) {
   2218                 saveOnFinish = false;
   2219             }
   2220 
   2221         } else {
   2222             saveTriggerId = null;
   2223         }
   2224 
   2225         // Must also track that are part of datasets, otherwise the FillUI won't be hidden when
   2226         // they go away (if they're not savable).
   2227 
   2228         final List<Dataset> datasets = response.getDatasets();
   2229         ArraySet<AutofillId> fillableIds = null;
   2230         if (datasets != null) {
   2231             for (int i = 0; i < datasets.size(); i++) {
   2232                 final Dataset dataset = datasets.get(i);
   2233                 final ArrayList<AutofillId> fieldIds = dataset.getFieldIds();
   2234                 if (fieldIds == null) continue;
   2235 
   2236                 for (int j = 0; j < fieldIds.size(); j++) {
   2237                     final AutofillId id = fieldIds.get(j);
   2238                     if (trackedViews == null || !trackedViews.contains(id)) {
   2239                         fillableIds = ArrayUtils.add(fillableIds, id);
   2240                     }
   2241                 }
   2242             }
   2243         }
   2244 
   2245         try {
   2246             if (sVerbose) {
   2247                 Slog.v(TAG, "updateTrackedIdsLocked(): " + trackedViews + " => " + fillableIds
   2248                         + " triggerId: " + saveTriggerId + " saveOnFinish:" + saveOnFinish);
   2249             }
   2250             mClient.setTrackedViews(id, toArray(trackedViews), mSaveOnAllViewsInvisible,
   2251                     saveOnFinish, toArray(fillableIds), saveTriggerId);
   2252         } catch (RemoteException e) {
   2253             Slog.w(TAG, "Cannot set tracked ids", e);
   2254         }
   2255     }
   2256 
   2257     /**
   2258      * Sets the state of views that failed to autofill.
   2259      */
   2260     @GuardedBy("mLock")
   2261     void setAutofillFailureLocked(@NonNull List<AutofillId> ids) {
   2262         for (int i = 0; i < ids.size(); i++) {
   2263             final AutofillId id = ids.get(i);
   2264             final ViewState viewState = mViewStates.get(id);
   2265             if (viewState == null) {
   2266                 Slog.w(TAG, "setAutofillFailure(): no view for id " + id);
   2267                 continue;
   2268             }
   2269             viewState.resetState(ViewState.STATE_AUTOFILLED);
   2270             final int state = viewState.getState();
   2271             viewState.setState(state | ViewState.STATE_AUTOFILL_FAILED);
   2272             if (sVerbose) {
   2273                 Slog.v(TAG, "Changed state of " + id + " to " + viewState.getStateAsString());
   2274             }
   2275         }
   2276     }
   2277 
   2278     @GuardedBy("mLock")
   2279     private void replaceResponseLocked(@NonNull FillResponse oldResponse,
   2280             @NonNull FillResponse newResponse, @Nullable Bundle newClientState) {
   2281         // Disassociate view states with the old response
   2282         setViewStatesLocked(oldResponse, ViewState.STATE_INITIAL, true);
   2283         // Move over the id
   2284         newResponse.setRequestId(oldResponse.getRequestId());
   2285         // Replace the old response
   2286         mResponses.put(newResponse.getRequestId(), newResponse);
   2287         // Now process the new response
   2288         processResponseLocked(newResponse, newClientState, 0);
   2289     }
   2290 
   2291     private void processNullResponseLocked(int flags) {
   2292         if (sVerbose) Slog.v(TAG, "canceling session " + id + " when server returned null");
   2293         if ((flags & FLAG_MANUAL_REQUEST) != 0) {
   2294             getUiForShowing().showError(R.string.autofill_error_cannot_autofill, this);
   2295         }
   2296         mService.resetLastResponse();
   2297         // Nothing to be done, but need to notify client.
   2298         notifyUnavailableToClient(AutofillManager.STATE_FINISHED);
   2299         removeSelf();
   2300     }
   2301 
   2302     @GuardedBy("mLock")
   2303     private void processResponseLocked(@NonNull FillResponse newResponse,
   2304             @Nullable Bundle newClientState, int flags) {
   2305         // Make sure we are hiding the UI which will be shown
   2306         // only if handling the current response requires it.
   2307         mUi.hideAll(this);
   2308 
   2309         final int requestId = newResponse.getRequestId();
   2310         if (sVerbose) {
   2311             Slog.v(TAG, "processResponseLocked(): mCurrentViewId=" + mCurrentViewId
   2312                     + ",flags=" + flags + ", reqId=" + requestId + ", resp=" + newResponse
   2313                     + ",newClientState=" + newClientState);
   2314         }
   2315 
   2316         if (mResponses == null) {
   2317             // Set initial capacity as 2 to handle cases where service always requires auth.
   2318             // TODO: add a metric for number of responses set by server, so we can use its average
   2319             // as the initial array capacitiy.
   2320             mResponses = new SparseArray<>(2);
   2321         }
   2322         mResponses.put(requestId, newResponse);
   2323         mClientState = newClientState != null ? newClientState : newResponse.getClientState();
   2324 
   2325         setViewStatesLocked(newResponse, ViewState.STATE_FILLABLE, false);
   2326         updateTrackedIdsLocked();
   2327 
   2328         if (mCurrentViewId == null) {
   2329             return;
   2330         }
   2331 
   2332         // Updates the UI, if necessary.
   2333         final ViewState currentView = mViewStates.get(mCurrentViewId);
   2334         currentView.maybeCallOnFillReady(flags);
   2335     }
   2336 
   2337     /**
   2338      * Sets the state of all views in the given response.
   2339      */
   2340     @GuardedBy("mLock")
   2341     private void setViewStatesLocked(FillResponse response, int state, boolean clearResponse) {
   2342         final List<Dataset> datasets = response.getDatasets();
   2343         if (datasets != null) {
   2344             for (int i = 0; i < datasets.size(); i++) {
   2345                 final Dataset dataset = datasets.get(i);
   2346                 if (dataset == null) {
   2347                     Slog.w(TAG, "Ignoring null dataset on " + datasets);
   2348                     continue;
   2349                 }
   2350                 setViewStatesLocked(response, dataset, state, clearResponse);
   2351             }
   2352         } else if (response.getAuthentication() != null) {
   2353             for (AutofillId autofillId : response.getAuthenticationIds()) {
   2354                 final ViewState viewState = createOrUpdateViewStateLocked(autofillId, state, null);
   2355                 if (!clearResponse) {
   2356                     viewState.setResponse(response);
   2357                 } else {
   2358                     viewState.setResponse(null);
   2359                 }
   2360             }
   2361         }
   2362         final SaveInfo saveInfo = response.getSaveInfo();
   2363         if (saveInfo != null) {
   2364             final AutofillId[] requiredIds = saveInfo.getRequiredIds();
   2365             if (requiredIds != null) {
   2366                 for (AutofillId id : requiredIds) {
   2367                     createOrUpdateViewStateLocked(id, state, null);
   2368                 }
   2369             }
   2370             final AutofillId[] optionalIds = saveInfo.getOptionalIds();
   2371             if (optionalIds != null) {
   2372                 for (AutofillId id : optionalIds) {
   2373                     createOrUpdateViewStateLocked(id, state, null);
   2374                 }
   2375             }
   2376         }
   2377 
   2378         final AutofillId[] authIds = response.getAuthenticationIds();
   2379         if (authIds != null) {
   2380             for (AutofillId id : authIds) {
   2381                 createOrUpdateViewStateLocked(id, state, null);
   2382             }
   2383         }
   2384     }
   2385 
   2386     /**
   2387      * Sets the state of all views in the given dataset and response.
   2388      */
   2389     @GuardedBy("mLock")
   2390     private void setViewStatesLocked(@Nullable FillResponse response, @NonNull Dataset dataset,
   2391             int state, boolean clearResponse) {
   2392         final ArrayList<AutofillId> ids = dataset.getFieldIds();
   2393         final ArrayList<AutofillValue> values = dataset.getFieldValues();
   2394         for (int j = 0; j < ids.size(); j++) {
   2395             final AutofillId id = ids.get(j);
   2396             final AutofillValue value = values.get(j);
   2397             final ViewState viewState = createOrUpdateViewStateLocked(id, state, value);
   2398             final String datasetId = dataset.getId();
   2399             if (datasetId != null) {
   2400                 viewState.setDatasetId(datasetId);
   2401             }
   2402             if (response != null) {
   2403                 viewState.setResponse(response);
   2404             } else if (clearResponse) {
   2405                 viewState.setResponse(null);
   2406             }
   2407         }
   2408     }
   2409 
   2410     @GuardedBy("mLock")
   2411     private ViewState createOrUpdateViewStateLocked(@NonNull AutofillId id, int state,
   2412             @Nullable AutofillValue value) {
   2413         ViewState viewState = mViewStates.get(id);
   2414         if (viewState != null)  {
   2415             viewState.setState(state);
   2416         } else {
   2417             viewState = new ViewState(this, id, this, state);
   2418             if (sVerbose) {
   2419                 Slog.v(TAG, "Adding autofillable view with id " + id + " and state " + state);
   2420             }
   2421             mViewStates.put(id, viewState);
   2422         }
   2423         if ((state & ViewState.STATE_AUTOFILLED) != 0) {
   2424             viewState.setAutofilledValue(value);
   2425         }
   2426         return viewState;
   2427     }
   2428 
   2429     void autoFill(int requestId, int datasetIndex, Dataset dataset, boolean generateEvent) {
   2430         if (sDebug) {
   2431             Slog.d(TAG, "autoFill(): requestId=" + requestId  + "; datasetIdx=" + datasetIndex
   2432                     + "; dataset=" + dataset);
   2433         }
   2434         synchronized (mLock) {
   2435             if (mDestroyed) {
   2436                 Slog.w(TAG, "Call to Session#autoFill() rejected - session: "
   2437                         + id + " destroyed");
   2438                 return;
   2439             }
   2440             // Autofill it directly...
   2441             if (dataset.getAuthentication() == null) {
   2442                 if (generateEvent) {
   2443                     mService.logDatasetSelected(dataset.getId(), id, mClientState);
   2444                 }
   2445 
   2446                 autoFillApp(dataset);
   2447                 return;
   2448             }
   2449 
   2450             // ...or handle authentication.
   2451             mService.logDatasetAuthenticationSelected(dataset.getId(), id, mClientState);
   2452             setViewStatesLocked(null, dataset, ViewState.STATE_WAITING_DATASET_AUTH, false);
   2453             final Intent fillInIntent = createAuthFillInIntentLocked(requestId, mClientState);
   2454             if (fillInIntent == null) {
   2455                 forceRemoveSelfLocked();
   2456                 return;
   2457             }
   2458             final int authenticationId = AutofillManager.makeAuthenticationId(requestId,
   2459                     datasetIndex);
   2460             startAuthentication(authenticationId, dataset.getAuthentication(), fillInIntent);
   2461 
   2462         }
   2463     }
   2464 
   2465     CharSequence getServiceName() {
   2466         synchronized (mLock) {
   2467             return mService.getServiceName();
   2468         }
   2469     }
   2470 
   2471     // TODO: this should never be null, but we got at least one occurrence, probably due to a race.
   2472     @GuardedBy("mLock")
   2473     @Nullable
   2474     private Intent createAuthFillInIntentLocked(int requestId, Bundle extras) {
   2475         final Intent fillInIntent = new Intent();
   2476 
   2477         final FillContext context = getFillContextByRequestIdLocked(requestId);
   2478 
   2479         if (context == null) {
   2480             wtf(null, "createAuthFillInIntentLocked(): no FillContext. requestId=%d; mContexts=%s",
   2481                     requestId, mContexts);
   2482             return null;
   2483         }
   2484         fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, context.getStructure());
   2485         fillInIntent.putExtra(AutofillManager.EXTRA_CLIENT_STATE, extras);
   2486         return fillInIntent;
   2487     }
   2488 
   2489     private void startAuthentication(int authenticationId, IntentSender intent,
   2490             Intent fillInIntent) {
   2491         try {
   2492             synchronized (mLock) {
   2493                 mClient.authenticate(id, authenticationId, intent, fillInIntent);
   2494             }
   2495         } catch (RemoteException e) {
   2496             Slog.e(TAG, "Error launching auth intent", e);
   2497         }
   2498     }
   2499 
   2500     @Override
   2501     public String toString() {
   2502         return "Session: [id=" + id + ", component=" + mComponentName + "]";
   2503     }
   2504 
   2505     @GuardedBy("mLock")
   2506     void dumpLocked(String prefix, PrintWriter pw) {
   2507         final String prefix2 = prefix + "  ";
   2508         pw.print(prefix); pw.print("id: "); pw.println(id);
   2509         pw.print(prefix); pw.print("uid: "); pw.println(uid);
   2510         pw.print(prefix); pw.print("flags: "); pw.println(mFlags);
   2511         pw.print(prefix); pw.print("mComponentName: "); pw.println(mComponentName);
   2512         pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken);
   2513         pw.print(prefix); pw.print("mStartTime: "); pw.println(mStartTime);
   2514         pw.print(prefix); pw.print("Time to show UI: ");
   2515         if (mUiShownTime == 0) {
   2516             pw.println("N/A");
   2517         } else {
   2518             TimeUtils.formatDuration(mUiShownTime - mStartTime, pw);
   2519             pw.println();
   2520         }
   2521         final int requestLogsSizes = mRequestLogs.size();
   2522         pw.print(prefix); pw.print("mSessionLogs: "); pw.println(requestLogsSizes);
   2523         for (int i = 0; i < requestLogsSizes; i++) {
   2524             final int requestId = mRequestLogs.keyAt(i);
   2525             final LogMaker log = mRequestLogs.valueAt(i);
   2526             pw.print(prefix2); pw.print('#'); pw.print(i); pw.print(": req=");
   2527             pw.print(requestId); pw.print(", log=" ); dumpRequestLog(pw, log); pw.println();
   2528         }
   2529         pw.print(prefix); pw.print("mResponses: ");
   2530         if (mResponses == null) {
   2531             pw.println("null");
   2532         } else {
   2533             pw.println(mResponses.size());
   2534             for (int i = 0; i < mResponses.size(); i++) {
   2535                 pw.print(prefix2); pw.print('#'); pw.print(i);
   2536                 pw.print(' '); pw.println(mResponses.valueAt(i));
   2537             }
   2538         }
   2539         pw.print(prefix); pw.print("mCurrentViewId: "); pw.println(mCurrentViewId);
   2540         pw.print(prefix); pw.print("mDestroyed: "); pw.println(mDestroyed);
   2541         pw.print(prefix); pw.print("mIsSaving: "); pw.println(mIsSaving);
   2542         pw.print(prefix); pw.print("mPendingSaveUi: "); pw.println(mPendingSaveUi);
   2543         final int numberViews = mViewStates.size();
   2544         pw.print(prefix); pw.print("mViewStates size: "); pw.println(mViewStates.size());
   2545         for (int i = 0; i < numberViews; i++) {
   2546             pw.print(prefix); pw.print("ViewState at #"); pw.println(i);
   2547             mViewStates.valueAt(i).dump(prefix2, pw);
   2548         }
   2549 
   2550         pw.print(prefix); pw.print("mContexts: " );
   2551         if (mContexts != null) {
   2552             int numContexts = mContexts.size();
   2553             for (int i = 0; i < numContexts; i++) {
   2554                 FillContext context = mContexts.get(i);
   2555 
   2556                 pw.print(prefix2); pw.print(context);
   2557                 if (sVerbose) {
   2558                     pw.println("AssistStructure dumped at logcat)");
   2559 
   2560                     // TODO: add method on AssistStructure to dump on pw
   2561                     context.getStructure().dump(false);
   2562                 }
   2563             }
   2564         } else {
   2565             pw.println("null");
   2566         }
   2567 
   2568         pw.print(prefix); pw.print("mHasCallback: "); pw.println(mHasCallback);
   2569         if (mClientState != null) {
   2570             pw.print(prefix); pw.print("mClientState: "); pw.print(mClientState.getSize()); pw
   2571                 .println(" bytes");
   2572         }
   2573         pw.print(prefix); pw.print("mCompatMode: "); pw.println(mCompatMode);
   2574         pw.print(prefix); pw.print("mUrlBar: ");
   2575         if (mUrlBar == null) {
   2576             pw.println("N/A");
   2577         } else {
   2578             pw.print("id="); pw.print(mUrlBar.getAutofillId());
   2579             pw.print(" domain="); pw.print(mUrlBar.getWebDomain());
   2580             pw.print(" text="); Helper.printlnRedactedText(pw, mUrlBar.getText());
   2581         }
   2582         pw.print(prefix); pw.print("mSaveOnAllViewsInvisible: "); pw.println(
   2583                 mSaveOnAllViewsInvisible);
   2584         pw.print(prefix); pw.print("mSelectedDatasetIds: "); pw.println(mSelectedDatasetIds);
   2585         mRemoteFillService.dump(prefix, pw);
   2586     }
   2587 
   2588     private static void dumpRequestLog(@NonNull PrintWriter pw, @NonNull LogMaker log) {
   2589         pw.print("CAT="); pw.print(log.getCategory());
   2590         pw.print(", TYPE=");
   2591         final int type = log.getType();
   2592         switch (type) {
   2593             case MetricsEvent.TYPE_SUCCESS: pw.print("SUCCESS"); break;
   2594             case MetricsEvent.TYPE_FAILURE: pw.print("FAILURE"); break;
   2595             case MetricsEvent.TYPE_CLOSE: pw.print("CLOSE"); break;
   2596             default: pw.print("UNSUPPORTED");
   2597         }
   2598         pw.print('('); pw.print(type); pw.print(')');
   2599         pw.print(", PKG="); pw.print(log.getPackageName());
   2600         pw.print(", SERVICE="); pw.print(log
   2601                 .getTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE));
   2602         pw.print(", ORDINAL="); pw.print(log
   2603                 .getTaggedData(MetricsEvent.FIELD_AUTOFILL_REQUEST_ORDINAL));
   2604         dumpNumericValue(pw, log, "FLAGS", MetricsEvent.FIELD_AUTOFILL_FLAGS);
   2605         dumpNumericValue(pw, log, "NUM_DATASETS", MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS);
   2606         dumpNumericValue(pw, log, "UI_LATENCY", MetricsEvent.FIELD_AUTOFILL_DURATION);
   2607         final int authStatus =
   2608                 getNumericValue(log, MetricsEvent.FIELD_AUTOFILL_AUTHENTICATION_STATUS);
   2609         if (authStatus != 0) {
   2610             pw.print(", AUTH_STATUS=");
   2611             switch (authStatus) {
   2612                 case MetricsEvent.AUTOFILL_AUTHENTICATED:
   2613                     pw.print("AUTHENTICATED"); break;
   2614                 case MetricsEvent.AUTOFILL_DATASET_AUTHENTICATED:
   2615                     pw.print("DATASET_AUTHENTICATED"); break;
   2616                 case MetricsEvent.AUTOFILL_INVALID_AUTHENTICATION:
   2617                     pw.print("INVALID_AUTHENTICATION"); break;
   2618                 case MetricsEvent.AUTOFILL_INVALID_DATASET_AUTHENTICATION:
   2619                     pw.print("INVALID_DATASET_AUTHENTICATION"); break;
   2620                 default: pw.print("UNSUPPORTED");
   2621             }
   2622             pw.print('('); pw.print(authStatus); pw.print(')');
   2623         }
   2624         dumpNumericValue(pw, log, "FC_IDS",
   2625                 MetricsEvent.FIELD_AUTOFILL_NUM_FIELD_CLASSIFICATION_IDS);
   2626         dumpNumericValue(pw, log, "COMPAT_MODE",
   2627                 MetricsEvent.FIELD_AUTOFILL_COMPAT_MODE);
   2628     }
   2629 
   2630     private static void dumpNumericValue(@NonNull PrintWriter pw, @NonNull LogMaker log,
   2631             @NonNull String field, int tag) {
   2632         final int value = getNumericValue(log, tag);
   2633         if (value != 0) {
   2634             pw.print(", "); pw.print(field); pw.print('='); pw.print(value);
   2635         }
   2636     }
   2637 
   2638     void autoFillApp(Dataset dataset) {
   2639         synchronized (mLock) {
   2640             if (mDestroyed) {
   2641                 Slog.w(TAG, "Call to Session#autoFillApp() rejected - session: "
   2642                         + id + " destroyed");
   2643                 return;
   2644             }
   2645             try {
   2646                 // Skip null values as a null values means no change
   2647                 final int entryCount = dataset.getFieldIds().size();
   2648                 final List<AutofillId> ids = new ArrayList<>(entryCount);
   2649                 final List<AutofillValue> values = new ArrayList<>(entryCount);
   2650                 boolean waitingDatasetAuth = false;
   2651                 for (int i = 0; i < entryCount; i++) {
   2652                     if (dataset.getFieldValues().get(i) == null) {
   2653                         continue;
   2654                     }
   2655                     final AutofillId viewId = dataset.getFieldIds().get(i);
   2656                     ids.add(viewId);
   2657                     values.add(dataset.getFieldValues().get(i));
   2658                     final ViewState viewState = mViewStates.get(viewId);
   2659                     if (viewState != null
   2660                             && (viewState.getState() & ViewState.STATE_WAITING_DATASET_AUTH) != 0) {
   2661                         if (sVerbose) {
   2662                             Slog.v(TAG, "autofillApp(): view " + viewId + " waiting auth");
   2663                         }
   2664                         waitingDatasetAuth = true;
   2665                         viewState.resetState(ViewState.STATE_WAITING_DATASET_AUTH);
   2666                     }
   2667                 }
   2668                 if (!ids.isEmpty()) {
   2669                     if (waitingDatasetAuth) {
   2670                         mUi.hideFillUi(this);
   2671                     }
   2672                     if (sDebug) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset);
   2673 
   2674                     mClient.autofill(id, ids, values);
   2675                     if (dataset.getId() != null) {
   2676                         if (mSelectedDatasetIds == null) {
   2677                             mSelectedDatasetIds = new ArrayList<>();
   2678                         }
   2679                         mSelectedDatasetIds.add(dataset.getId());
   2680                     }
   2681                     setViewStatesLocked(null, dataset, ViewState.STATE_AUTOFILLED, false);
   2682                 }
   2683             } catch (RemoteException e) {
   2684                 Slog.w(TAG, "Error autofilling activity: " + e);
   2685             }
   2686         }
   2687     }
   2688 
   2689     private AutoFillUI getUiForShowing() {
   2690         synchronized (mLock) {
   2691             mUi.setCallback(this);
   2692             return mUi;
   2693         }
   2694     }
   2695 
   2696     /**
   2697      * Cleans up this session.
   2698      *
   2699      * <p>Typically called in 2 scenarios:
   2700      *
   2701      * <ul>
   2702      *   <li>When the session naturally finishes (i.e., from {@link #removeSelfLocked()}.
   2703      *   <li>When the service hosting the session is finished (for example, because the user
   2704      *       disabled it).
   2705      * </ul>
   2706      */
   2707     @GuardedBy("mLock")
   2708     RemoteFillService destroyLocked() {
   2709         if (mDestroyed) {
   2710             return null;
   2711         }
   2712         unlinkClientVultureLocked();
   2713         mUi.destroyAll(mPendingSaveUi, this, true);
   2714         mUi.clearCallback(this);
   2715         mDestroyed = true;
   2716 
   2717         // Log metrics
   2718         final int totalRequests = mRequestLogs.size();
   2719         if (totalRequests > 0) {
   2720             if (sVerbose) Slog.v(TAG, "destroyLocked(): logging " + totalRequests + " requests");
   2721             for (int i = 0; i < totalRequests; i++) {
   2722                 final LogMaker log = mRequestLogs.valueAt(i);
   2723                 mMetricsLogger.write(log);
   2724             }
   2725         }
   2726         mMetricsLogger.write(newLogMaker(MetricsEvent.AUTOFILL_SESSION_FINISHED)
   2727                 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUMBER_REQUESTS, totalRequests));
   2728 
   2729         return mRemoteFillService;
   2730     }
   2731 
   2732     /**
   2733      * Cleans up this session and remove it from the service always, even if it does have a pending
   2734      * Save UI.
   2735      */
   2736     @GuardedBy("mLock")
   2737     void forceRemoveSelfLocked() {
   2738         forceRemoveSelfLocked(AutofillManager.STATE_UNKNOWN);
   2739     }
   2740 
   2741     @GuardedBy("mLock")
   2742     void forceRemoveSelfLocked(int clientState) {
   2743         if (sVerbose) Slog.v(TAG, "forceRemoveSelfLocked(): " + mPendingSaveUi);
   2744 
   2745         final boolean isPendingSaveUi = isSaveUiPendingLocked();
   2746         mPendingSaveUi = null;
   2747         removeSelfLocked();
   2748         mUi.destroyAll(mPendingSaveUi, this, false);
   2749         if (!isPendingSaveUi) {
   2750             try {
   2751                 mClient.setSessionFinished(clientState);
   2752             } catch (RemoteException e) {
   2753                 Slog.e(TAG, "Error notifying client to finish session", e);
   2754             }
   2755         }
   2756     }
   2757 
   2758     /**
   2759      * Thread-safe version of {@link #removeSelfLocked()}.
   2760      */
   2761     private void removeSelf() {
   2762         synchronized (mLock) {
   2763             removeSelfLocked();
   2764         }
   2765     }
   2766 
   2767     /**
   2768      * Cleans up this session and remove it from the service, but but only if it does not have a
   2769      * pending Save UI.
   2770      */
   2771     @GuardedBy("mLock")
   2772     void removeSelfLocked() {
   2773         if (sVerbose) Slog.v(TAG, "removeSelfLocked(): " + mPendingSaveUi);
   2774         if (mDestroyed) {
   2775             Slog.w(TAG, "Call to Session#removeSelfLocked() rejected - session: "
   2776                     + id + " destroyed");
   2777             return;
   2778         }
   2779         if (isSaveUiPendingLocked()) {
   2780             Slog.i(TAG, "removeSelfLocked() ignored, waiting for pending save ui");
   2781             return;
   2782         }
   2783 
   2784         final RemoteFillService remoteFillService = destroyLocked();
   2785         mService.removeSessionLocked(id);
   2786         if (remoteFillService != null) {
   2787             remoteFillService.destroy();
   2788         }
   2789     }
   2790 
   2791     void onPendingSaveUi(int operation, @NonNull IBinder token) {
   2792         getUiForShowing().onPendingSaveUi(operation, token);
   2793     }
   2794 
   2795     /**
   2796      * Checks whether this session is hiding the Save UI to handle a custom description link for
   2797      * a specific {@code token} created by
   2798      * {@link PendingUi#PendingUi(IBinder, int, IAutoFillManagerClient)}.
   2799      */
   2800     @GuardedBy("mLock")
   2801     boolean isSaveUiPendingForTokenLocked(@NonNull IBinder token) {
   2802         return isSaveUiPendingLocked() && token.equals(mPendingSaveUi.getToken());
   2803     }
   2804 
   2805     /**
   2806      * Checks whether this session is hiding the Save UI to handle a custom description link.
   2807      */
   2808     @GuardedBy("mLock")
   2809     private boolean isSaveUiPendingLocked() {
   2810         return mPendingSaveUi != null && mPendingSaveUi.getState() == PendingUi.STATE_PENDING;
   2811     }
   2812 
   2813     @GuardedBy("mLock")
   2814     private int getLastResponseIndexLocked() {
   2815         // The response ids are monotonically increasing so
   2816         // we just find the largest id which is the last. We
   2817         // do not rely on the internal ordering in sparse
   2818         // array to avoid - wow this stopped working!?
   2819         int lastResponseIdx = -1;
   2820         int lastResponseId = -1;
   2821         if (mResponses != null) {
   2822             final int responseCount = mResponses.size();
   2823             for (int i = 0; i < responseCount; i++) {
   2824                 if (mResponses.keyAt(i) > lastResponseId) {
   2825                     lastResponseIdx = i;
   2826                 }
   2827             }
   2828         }
   2829         return lastResponseIdx;
   2830     }
   2831 
   2832     private LogMaker newLogMaker(int category) {
   2833         return newLogMaker(category, mService.getServicePackageName());
   2834     }
   2835 
   2836     private LogMaker newLogMaker(int category, String servicePackageName) {
   2837         return Helper.newLogMaker(category, mComponentName, servicePackageName, id, mCompatMode);
   2838     }
   2839 
   2840     private void writeLog(int category) {
   2841         mMetricsLogger.write(newLogMaker(category));
   2842     }
   2843 
   2844     private void logAuthenticationStatusLocked(int requestId, int status) {
   2845         addTaggedDataToRequestLogLocked(requestId,
   2846                 MetricsEvent.FIELD_AUTOFILL_AUTHENTICATION_STATUS, status);
   2847     }
   2848 
   2849     private void addTaggedDataToRequestLogLocked(int requestId, int tag, @Nullable Object value) {
   2850         final LogMaker requestLog = mRequestLogs.get(requestId);
   2851         if (requestLog == null) {
   2852             Slog.w(TAG,
   2853                     "addTaggedDataToRequestLogLocked(tag=" + tag + "): no log for id " + requestId);
   2854             return;
   2855         }
   2856         requestLog.addTaggedData(tag, value);
   2857     }
   2858 
   2859     private static String requestLogToString(@NonNull LogMaker log) {
   2860         final StringWriter sw = new StringWriter();
   2861         final PrintWriter pw = new PrintWriter(sw);
   2862         dumpRequestLog(pw, log);
   2863         pw.flush();
   2864         return sw.toString();
   2865     }
   2866 
   2867     private void wtf(@Nullable Exception e, String fmt, Object...args) {
   2868         final String message = String.format(fmt, args);
   2869         mWtfHistory.log(message);
   2870 
   2871         if (e != null) {
   2872             Slog.wtf(TAG, message, e);
   2873         } else {
   2874             Slog.wtf(TAG, message);
   2875         }
   2876     }
   2877 
   2878     private static String actionAsString(int action) {
   2879         switch (action) {
   2880             case ACTION_START_SESSION:
   2881                 return "START_SESSION";
   2882             case ACTION_VIEW_ENTERED:
   2883                 return "VIEW_ENTERED";
   2884             case ACTION_VIEW_EXITED:
   2885                 return "VIEW_EXITED";
   2886             case ACTION_VALUE_CHANGED:
   2887                 return "VALUE_CHANGED";
   2888             default:
   2889                 return "UNKNOWN_" + action;
   2890         }
   2891     }
   2892 }
   2893