Merge "Integrate autofill session lifecycle with URL bar changes when on compat mode." into pi-dev
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 5bee87c..01fd090 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -274,6 +274,16 @@
     public static final int STATE_DISABLED_BY_SERVICE = 4;
 
     /**
+     * Same as {@link #STATE_UNKNOWN}, but used on
+     * {@link AutofillManagerClient#setSessionFinished(int)} when the session was finished because
+     * the URL bar changed on client mode
+     *
+     * @hide
+     */
+    public static final int STATE_UNKNOWN_COMPAT_MODE = 5;
+
+
+    /**
      * Timeout in ms for calls to the field classification service.
      * @hide
      */
@@ -1947,15 +1957,24 @@
      * Marks the state of the session as finished.
      *
      * @param newState {@link #STATE_FINISHED} (because the autofill service returned a {@code null}
-     *  FillResponse), {@link #STATE_UNKNOWN} (because the session was removed), or
-     *  {@link #STATE_DISABLED_BY_SERVICE} (because the autofill service disabled further autofill
-     *  requests for the activity).
+     *  FillResponse), {@link #STATE_UNKNOWN} (because the session was removed),
+     *  {@link #STATE_UNKNOWN_COMPAT_MODE} (beucase the session was finished when the URL bar
+     *  changed on compat mode), or {@link #STATE_DISABLED_BY_SERVICE} (because the autofill service
+     *  disabled further autofill requests for the activity).
      */
     private void setSessionFinished(int newState) {
         synchronized (mLock) {
-            if (sVerbose) Log.v(TAG, "setSessionFinished(): from " + mState + " to " + newState);
-            resetSessionLocked(/* resetEnteredIds= */ false);
-            mState = newState;
+            if (sVerbose) {
+                Log.v(TAG, "setSessionFinished(): from " + getStateAsStringLocked() + " to "
+                        + getStateAsString(newState));
+            }
+            if (newState == STATE_UNKNOWN_COMPAT_MODE) {
+                resetSessionLocked(/* resetEnteredIds= */ true);
+                mState = STATE_UNKNOWN;
+            } else {
+                resetSessionLocked(/* resetEnteredIds= */ false);
+                mState = newState;
+            }
         }
     }
 
@@ -2107,19 +2126,26 @@
 
     @GuardedBy("mLock")
     private String getStateAsStringLocked() {
-        switch (mState) {
+        return getStateAsString(mState);
+    }
+
+    @NonNull
+    private static String getStateAsString(int state) {
+        switch (state) {
             case STATE_UNKNOWN:
-                return "STATE_UNKNOWN";
+                return "UNKNOWN";
             case STATE_ACTIVE:
-                return "STATE_ACTIVE";
+                return "ACTIVE";
             case STATE_FINISHED:
-                return "STATE_FINISHED";
+                return "FINISHED";
             case STATE_SHOWING_SAVE_UI:
-                return "STATE_SHOWING_SAVE_UI";
+                return "SHOWING_SAVE_UI";
             case STATE_DISABLED_BY_SERVICE:
-                return "STATE_DISABLED_BY_SERVICE";
+                return "DISABLED_BY_SERVICE";
+            case STATE_UNKNOWN_COMPAT_MODE:
+                return "UNKNOWN_COMPAT_MODE";
             default:
-                return "INVALID:" + mState;
+                return "INVALID:" + state;
         }
     }
 
diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java
index 7bb532e..58f78ec 100644
--- a/services/autofill/java/com/android/server/autofill/Helper.java
+++ b/services/autofill/java/com/android/server/autofill/Helper.java
@@ -180,9 +180,11 @@
      *
      * @param structure Assist structure
      * @param urlBarIds list of ids; only the first id found will be sanitized.
+     *
+     * @return the node containing the URL bar
      */
     @Nullable
-    public static void sanitizeUrlBar(@NonNull AssistStructure structure,
+    public static ViewNode sanitizeUrlBar(@NonNull AssistStructure structure,
             @NonNull String[] urlBarIds) {
         final ViewNode urlBarNode = findViewNode(structure, (node) -> {
             return ArrayUtils.contains(urlBarIds, node.getIdEntry());
@@ -191,7 +193,7 @@
             final String domain = urlBarNode.getText().toString();
             if (domain.isEmpty()) {
                 if (sDebug) Slog.d(TAG, "sanitizeUrlBar(): empty on " + urlBarNode.getIdEntry());
-                return;
+                return null;
             }
             urlBarNode.setWebDomain(domain);
             if (sDebug) {
@@ -199,6 +201,7 @@
                         + urlBarNode.getWebDomain());
             }
         }
+        return urlBarNode;
     }
 
     private interface ViewNodeFilter {
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 1e1de35..bcb0bc4 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -146,6 +146,13 @@
     /** Whether the app being autofilled is running in compat mode. */
     private final boolean mCompatMode;
 
+    /** Node representing the URL bar on compat mode. */
+    @GuardedBy("mLock")
+    private ViewNode mUrlBar;
+
+    @GuardedBy("mLock")
+    private boolean mSaveOnAllViewsInvisible;
+
     @GuardedBy("mLock")
     private final ArrayMap<AutofillId, ViewState> mViewStates = new ArrayMap<>();
 
@@ -280,7 +287,17 @@
                         Slog.d(TAG, "url_bars in compat mode: " + Arrays.toString(urlBarIds));
                     }
                     if (urlBarIds != null) {
-                        Helper.sanitizeUrlBar(structure, urlBarIds);
+                        mUrlBar = Helper.sanitizeUrlBar(structure, urlBarIds);
+                        if (mUrlBar != null) {
+                            final AutofillId urlBarId = mUrlBar.getAutofillId();
+                            if (sDebug) {
+                                Slog.d(TAG, "Setting urlBar as id=" + urlBarId + " and domain "
+                                        + mUrlBar.getWebDomain());
+                            }
+                            final ViewState viewState = new ViewState(Session.this, urlBarId,
+                                    Session.this, ViewState.STATE_URL_BAR);
+                            mViewStates.put(urlBarId, viewState);
+                        }
                     }
                 }
                 structure.sanitizeForParceling(true);
@@ -1878,14 +1895,15 @@
             return;
         }
         if (sVerbose) {
-            Slog.v(TAG, "updateLocked(): id=" + id + ", action=" + action + ", flags=" + flags);
+            Slog.v(TAG, "updateLocked(): id=" + id + ", action=" + actionAsString(action)
+                    + ", flags=" + flags);
         }
         ViewState viewState = mViewStates.get(id);
 
         if (viewState == null) {
             if (action == ACTION_START_SESSION || action == ACTION_VALUE_CHANGED
                     || action == ACTION_VIEW_ENTERED) {
-                if (sVerbose) Slog.v(TAG, "Creating viewState for " + id + " on " + action);
+                if (sVerbose) Slog.v(TAG, "Creating viewState for " + id);
                 boolean isIgnored = isIgnoredLocked(id);
                 viewState = new ViewState(this, id, this,
                         isIgnored ? ViewState.STATE_IGNORED : ViewState.STATE_INITIAL);
@@ -1895,11 +1913,11 @@
                 // detectable, and batch-send them when the session is finished (but that will
                 // require tracking detectable fields on AutofillManager)
                 if (isIgnored) {
-                    if (sDebug) Slog.d(TAG, "updateLocked(): ignoring view " + id);
+                    if (sDebug) Slog.d(TAG, "updateLocked(): ignoring view " + viewState);
                     return;
                 }
             } else {
-                if (sVerbose) Slog.v(TAG, "Ignored action " + action + " for " + id);
+                if (sVerbose) Slog.v(TAG, "Ignoring specific action when viewState=null");
                 return;
             }
         }
@@ -1913,6 +1931,40 @@
                 requestNewFillResponseLocked(flags);
                 break;
             case ACTION_VALUE_CHANGED:
+                if (mCompatMode && (viewState.getState() & ViewState.STATE_URL_BAR) != 0) {
+                    // Must cancel the session if the value of the URL bar changed
+                    final String currentUrl = mUrlBar == null ? null
+                            : mUrlBar.getText().toString().trim();
+                    if (currentUrl == null) {
+                        // Sanity check - shouldn't happen.
+                        wtf(null, "URL bar value changed, but current value is null");
+                        return;
+                    }
+                    if (value == null || ! value.isText()) {
+                        // Sanity check - shouldn't happen.
+                        wtf(null, "URL bar value changed to null or non-text: %s", value);
+                        return;
+                    }
+                    final String newUrl = value.getTextValue().toString();
+                    if (newUrl.equals(currentUrl)) {
+                        if (sDebug) Slog.d(TAG, "Ignoring change on URL bar as it's the same");
+                        return;
+                    }
+                    if (mSaveOnAllViewsInvisible) {
+                        // We cannot cancel the session because it could hinder Save when all views
+                        // are finished, as the URL bar changed callback is usually called before
+                        // the virtual views become invisible.
+                        if (sDebug) {
+                            Slog.d(TAG, "Ignoring change on URL because session will finish when "
+                                    + "views are gone");
+                        }
+                        return;
+                    }
+                    if (sDebug) Slog.d(TAG, "Finishing session because URL bar changed");
+                    forceRemoveSelfLocked(AutofillManager.STATE_UNKNOWN_COMPAT_MODE);
+                    return;
+                }
+
                 if (value != null && !value.equals(viewState.getCurrentValue())) {
                     if (value.isEmpty()
                             && viewState.getCurrentValue() != null
@@ -1953,6 +2005,12 @@
                 if (sVerbose && virtualBounds != null) {
                     Slog.v(TAG, "entered on virtual child " + id + ": " + virtualBounds);
                 }
+
+                if (mCompatMode && (viewState.getState() & ViewState.STATE_URL_BAR) != 0) {
+                    if (sDebug) Slog.d(TAG, "Ignoring VIEW_ENTERED on URL BAR (id=" + id + ")");
+                    return;
+                }
+
                 requestNewFillResponseOnViewEnteredIfNecessaryLocked(id, viewState, flags);
 
                 // Remove the UI if the ViewState has changed.
@@ -2068,7 +2126,7 @@
         if (response == null) return;
 
         ArraySet<AutofillId> trackedViews = null;
-        boolean saveOnAllViewsInvisible = false;
+        mSaveOnAllViewsInvisible = false;
         boolean saveOnFinish = true;
         final SaveInfo saveInfo = response.getSaveInfo();
         final AutofillId saveTriggerId;
@@ -2081,10 +2139,10 @@
             if (mCompatMode) {
                 flags |= SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE;
             }
-            saveOnAllViewsInvisible = (flags & SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) != 0;
+            mSaveOnAllViewsInvisible = (flags & SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) != 0;
 
             // We only need to track views if we want to save once they become invisible.
-            if (saveOnAllViewsInvisible) {
+            if (mSaveOnAllViewsInvisible) {
                 if (trackedViews == null) {
                     trackedViews = new ArraySet<>();
                 }
@@ -2129,7 +2187,7 @@
                 Slog.v(TAG, "updateTrackedIdsLocked(): " + trackedViews + " => " + fillableIds
                         + " triggerId: " + saveTriggerId + " saveOnFinish:" + saveOnFinish);
             }
-            mClient.setTrackedViews(id, toArray(trackedViews), saveOnAllViewsInvisible,
+            mClient.setTrackedViews(id, toArray(trackedViews), mSaveOnAllViewsInvisible,
                     saveOnFinish, toArray(fillableIds), saveTriggerId);
         } catch (RemoteException e) {
             Slog.w(TAG, "Cannot set tracked ids", e);
@@ -2421,6 +2479,16 @@
         pw.print(prefix); pw.print("mClientState: "); pw.println(
                 Helper.bundleToString(mClientState));
         pw.print(prefix); pw.print("mCompatMode: "); pw.println(mCompatMode);
+        pw.print(prefix); pw.print("mUrlBar: ");
+        if (mUrlBar == null) {
+            pw.println("N/A");
+        } else {
+            pw.print("id="); pw.print(mUrlBar.getAutofillId());
+            pw.print(" domain="); pw.print(mUrlBar.getWebDomain());
+            pw.print(" text="); Helper.printlnRedactedText(pw, mUrlBar.getText());
+        }
+        pw.print(prefix); pw.print("mSaveOnAllViewsInvisible: "); pw.println(
+                mSaveOnAllViewsInvisible);
         pw.print(prefix); pw.print("mSelectedDatasetIds: "); pw.println(mSelectedDatasetIds);
         mRemoteFillService.dump(prefix, pw);
     }
@@ -2513,6 +2581,11 @@
      */
     @GuardedBy("mLock")
     void forceRemoveSelfLocked() {
+        forceRemoveSelfLocked(AutofillManager.STATE_UNKNOWN);
+    }
+
+    @GuardedBy("mLock")
+    void forceRemoveSelfLocked(int clientState) {
         if (sVerbose) Slog.v(TAG, "forceRemoveSelfLocked(): " + mPendingSaveUi);
 
         final boolean isPendingSaveUi = isSaveUiPendingLocked();
@@ -2521,7 +2594,7 @@
         mUi.destroyAll(mPendingSaveUi, this, false);
         if (!isPendingSaveUi) {
             try {
-                mClient.setSessionFinished(AutofillManager.STATE_UNKNOWN);
+                mClient.setSessionFinished(clientState);
             } catch (RemoteException e) {
                 Slog.e(TAG, "Error notifying client to finish session", e);
             }
@@ -2624,4 +2697,19 @@
             Slog.wtf(TAG, message);
         }
     }
+
+    private static String actionAsString(int action) {
+        switch (action) {
+            case ACTION_START_SESSION:
+                return "START_SESSION";
+            case ACTION_VIEW_ENTERED:
+                return "VIEW_ENTERED";
+            case ACTION_VIEW_EXITED:
+                return "VIEW_EXITED";
+            case ACTION_VALUE_CHANGED:
+                return "VALUE_CHANGED";
+            default:
+                return "UNKNOWN_" + action;
+        }
+    }
 }
diff --git a/services/autofill/java/com/android/server/autofill/ViewState.java b/services/autofill/java/com/android/server/autofill/ViewState.java
index 03c5850..97ab97b 100644
--- a/services/autofill/java/com/android/server/autofill/ViewState.java
+++ b/services/autofill/java/com/android/server/autofill/ViewState.java
@@ -67,6 +67,8 @@
     public static final int STATE_IGNORED = 0x080;
     /** User manually request autofill in this view, after it was already autofilled. */
     public static final int STATE_RESTARTED_SESSION = 0x100;
+    /** View is the URL bar of a package on compat mode. */
+    public  static final int STATE_URL_BAR = 0x200;
 
     public final AutofillId id;