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