Merge "Moved Session and ViewState to its own classes."
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 4d78350..3d1c251 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -19,15 +19,10 @@
 import static android.service.autofill.AutofillService.EXTRA_ACTIVITY_TOKEN;
 import static android.service.voice.VoiceInteractionSession.KEY_RECEIVER_EXTRAS;
 import static android.service.voice.VoiceInteractionSession.KEY_STRUCTURE;
-import static android.view.autofill.AutofillManager.FLAG_VIEW_ENTERED;
-import static android.view.autofill.AutofillManager.FLAG_VIEW_EXITED;
 import static android.view.autofill.AutofillManager.FLAG_START_SESSION;
-import static android.view.autofill.AutofillManager.FLAG_VALUE_CHANGED;
-import static android.view.autofill.AutofillManager.FLAG_MANUAL_REQUEST;
 
 import static com.android.server.autofill.Helper.DEBUG;
 import static com.android.server.autofill.Helper.VERBOSE;
-import static com.android.server.autofill.Helper.findValue;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -35,32 +30,23 @@
 import android.app.ActivityManager;
 import android.app.AppGlobals;
 import android.app.assist.AssistStructure;
-import android.app.assist.AssistStructure.ViewNode;
-import android.app.assist.AssistStructure.WindowNode;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentSender;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
 import android.graphics.Rect;
-import android.metrics.LogMaker;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Looper;
-import android.os.Parcelable;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.UserManager;
 import android.provider.Settings;
 import android.service.autofill.AutofillService;
 import android.service.autofill.AutofillServiceInfo;
-import android.service.autofill.Dataset;
-import android.service.autofill.FillResponse;
 import android.service.autofill.IAutoFillService;
-import android.service.autofill.SaveInfo;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.LocalLog;
@@ -68,21 +54,16 @@
 import android.util.PrintWriterPrinter;
 import android.util.Slog;
 import android.view.autofill.AutofillId;
-import android.view.autofill.AutofillManager;
 import android.view.autofill.AutofillValue;
 import android.view.autofill.IAutoFillManagerClient;
-import android.view.autofill.IAutofillWindowPresenter;
+
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto;
 import com.android.internal.os.HandlerCaller;
 import com.android.internal.os.IResultReceiver;
 import com.android.server.autofill.ui.AutoFillUI;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Map;
-import java.util.Map.Entry;
 
 /**
  * Bridge between the {@code system_server}'s {@link AutofillManagerService} and the
@@ -93,13 +74,12 @@
 
     private static final String TAG = "AutofillManagerServiceImpl";
 
-    private static final int MSG_SERVICE_SAVE = 1;
+    static final int MSG_SERVICE_SAVE = 1;
 
     private final int mUserId;
     private final Context mContext;
     private final Object mLock;
     private final AutoFillUI mUi;
-    private final MetricsLogger mMetricsLogger = new MetricsLogger();
 
     private RemoteCallbackList<IAutoFillManagerClient> mClients;
     private AutofillServiceInfo mInfo;
@@ -358,8 +338,9 @@
     private Session createSessionByTokenLocked(@NonNull IBinder activityToken,
             @Nullable IBinder windowToken, @NonNull IBinder appCallbackToken, boolean hasCallback,
             int flags, @NonNull String packageName) {
-        final Session newSession = new Session(mContext, activityToken,
-                windowToken, appCallbackToken, hasCallback, flags, packageName);
+        final Session newSession = new Session(this, mUi, mContext, mHandlerCaller, mUserId, mLock,
+                activityToken, windowToken, appCallbackToken, hasCallback, flags,
+                mInfo.getServiceInfo().getComponentName(), packageName);
         mSessions.put(activityToken, newSession);
 
         /*
@@ -400,6 +381,10 @@
         session.updateLocked(autofillId, virtualBounds, value, flags);
     }
 
+    void removeSessionLocked(IBinder activityToken) {
+        mSessions.remove(activityToken);
+    }
+
     private void handleSessionSave(IBinder activityToken) {
         synchronized (mLock) {
             final Session session = mSessions.get(activityToken);
@@ -423,6 +408,25 @@
         mSessions.clear();
     }
 
+    void disableSelf() {
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            final String autoFillService = Settings.Secure.getStringForUser(
+                    mContext.getContentResolver(), Settings.Secure.AUTOFILL_SERVICE, mUserId);
+            if (mInfo.getServiceInfo().getComponentName().equals(
+                    ComponentName.unflattenFromString(autoFillService))) {
+                Settings.Secure.putStringForUser(mContext.getContentResolver(),
+                        Settings.Secure.AUTOFILL_SERVICE, null, mUserId);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    CharSequence getServiceLabel() {
+        return mInfo.getServiceInfo().loadLabel(mContext.getPackageManager());
+    }
+
     void dumpLocked(String prefix, PrintWriter pw) {
         final String prefix2 = prefix + "  ";
 
@@ -495,804 +499,4 @@
                 + ", component=" + (mInfo != null
                 ? mInfo.getServiceInfo().getComponentName() : null) + "]";
     }
-
-    /**
-     * State for a given view with a AutofillId.
-     *
-     * <p>This class holds state about a view and calls its listener when the fill UI is ready to
-     * be displayed for the view.
-     */
-    static final class ViewState {
-        interface Listener {
-            /**
-             * Called when the fill UI is ready to be shown for this view.
-             */
-            void onFillReady(FillResponse fillResponse, AutofillId focusedId,
-                    @Nullable AutofillValue value);
-        }
-
-        final AutofillId mId;
-        private final Listener mListener;
-        // TODO(b/33197203): would not need a reference to response and session if it was an inner
-        // class of Session...
-        private final Session mSession;
-        // TODO(b/33197203): encapsulate access so it's not called by UI
-        FillResponse mResponse;
-        Intent mAuthIntent;
-
-        private AutofillValue mAutofillValue;
-
-        // Bounds if a virtual view, null otherwise
-        private Rect mVirtualBounds;
-
-        private boolean mValueUpdated;
-
-        ViewState(Session session, AutofillId id, Listener listener) {
-            mSession = session;
-            mId = id;
-            mListener = listener;
-        }
-
-        /**
-         * Response should only be set once.
-         */
-        void setResponse(FillResponse response) {
-            mResponse = response;
-            maybeCallOnFillReady();
-        }
-
-        /**
-         * Used when a {@link FillResponse} requires authentication to be unlocked.
-         */
-        void setResponse(FillResponse response, Intent authIntent) {
-            mAuthIntent = authIntent;
-            setResponse(response);
-        }
-
-        CharSequence getServiceName() {
-            return mSession.getServiceName();
-        }
-
-        // TODO(b/33197203): need to refactor / rename / document this method to make it clear that
-        // it can change  the value and update the UI; similarly, should replace code that
-        // directly sets mAutoFilLValue to use encapsulation.
-        void update(@Nullable AutofillValue autofillValue, @Nullable Rect virtualBounds) {
-            if (autofillValue != null) {
-                mAutofillValue = autofillValue;
-            }
-            if (virtualBounds != null) {
-                mVirtualBounds = virtualBounds;
-            }
-
-            maybeCallOnFillReady();
-        }
-
-        /**
-         * Calls {@link
-         * Listener#onFillReady(FillResponse, AutofillId, AutofillValue)} if the
-         * fill UI is ready to be displayed (i.e. when response and bounds are set).
-         */
-        void maybeCallOnFillReady() {
-            if (mResponse != null && (mResponse.getAuthentication() != null
-                    || mResponse.getDatasets() != null)) {
-                mListener.onFillReady(mResponse, mId, mAutofillValue);
-            }
-        }
-
-        @Override
-        public String toString() {
-            return "ViewState: [id=" + mId + ", value=" + mAutofillValue + ", bounds=" + mVirtualBounds
-                    + ", updated = " + mValueUpdated + "]";
-        }
-
-        void dump(String prefix, PrintWriter pw) {
-            pw.print(prefix); pw.print("id:" ); pw.println(mId);
-            pw.print(prefix); pw.print("value:" ); pw.println(mAutofillValue);
-            pw.print(prefix); pw.print("updated:" ); pw.println(mValueUpdated);
-            pw.print(prefix); pw.print("virtualBounds:" ); pw.println(mVirtualBounds);
-            pw.print(prefix); pw.print("authIntent:" ); pw.println(mAuthIntent);
-        }
-    }
-
-    /**
-     * A session for a given activity.
-     *
-     * <p>This class manages the multiple {@link ViewState}s for each view it has, and keeps track
-     * of the current {@link ViewState} to display the appropriate UI.
-     *
-     * <p>Although the autofill requests and callbacks are stateless from the service's point of
-     * view, we need to keep state in the framework side for cases such as authentication. For
-     * example, when service return a {@link FillResponse} that contains all the fields needed
-     * to fill the activity but it requires authentication first, that response need to be held
-     * until the user authenticates or it times out.
-     */
-    // TODO(b/33197203): make sure sessions are removed (and tested by CTS):
-    // - On all authentication scenarios.
-    // - When user does not interact back after a while.
-    // - When service is unbound.
-    final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener,
-            AutoFillUI.AutoFillUiCallback {
-        private final IBinder mActivityToken;
-        private final IBinder mWindowToken;
-
-        /** Package name of the app that is auto-filled */
-        @NonNull private final String mPackageName;
-
-        @GuardedBy("mLock")
-        private final Map<AutofillId, ViewState> mViewStates = new ArrayMap<>();
-
-        @GuardedBy("mLock")
-        @Nullable
-        private ViewState mCurrentViewState;
-
-        private final IAutoFillManagerClient mClient;
-
-        @GuardedBy("mLock")
-        RemoteFillService mRemoteFillService;
-
-        // TODO(b/33197203): Get a response per view instead of per activity.
-        @GuardedBy("mLock")
-        private FillResponse mCurrentResponse;
-
-        /**
-         * Used to remember which {@link Dataset} filled the session.
-         */
-        // TODO(b/33197203): might need more than one once we support partitions
-        @GuardedBy("mLock")
-        private Dataset mAutoFilledDataset;
-
-        /**
-         * Assist structure sent by the app; it will be updated (sanitized, change values for save)
-         * before sent to {@link AutofillService}.
-         */
-        @GuardedBy("mLock")
-        private AssistStructure mStructure;
-
-        /**
-         * Whether the client has an {@link android.view.autofill.AutofillManager.AutofillCallback}.
-         */
-        private boolean mHasCallback;
-
-        /**
-         * Flags used to start the session.
-         */
-        private int mFlags;
-        private Session(@NonNull Context context, @NonNull IBinder activityToken,
-                @Nullable IBinder windowToken, @NonNull IBinder client, boolean hasCallback,
-                int flags, @NonNull String packageName) {
-            mRemoteFillService = new RemoteFillService(context,
-                    mInfo.getServiceInfo().getComponentName(), mUserId, this);
-            mActivityToken = activityToken;
-            mWindowToken = windowToken;
-            mHasCallback = hasCallback;
-            mFlags = flags;
-            mPackageName = packageName;
-
-            mClient = IAutoFillManagerClient.Stub.asInterface(client);
-            try {
-                client.linkToDeath(() -> {
-                    if (DEBUG) {
-                        Slog.d(TAG, "app binder died");
-                    }
-
-                    removeSelf();
-                }, 0);
-            } catch (RemoteException e) {
-                Slog.w(TAG, "linkToDeath() on mClient failed: " + e);
-            }
-
-            mMetricsLogger.action(MetricsProto.MetricsEvent.AUTOFILL_SESSION_STARTED, mPackageName);
-        }
-
-        // FillServiceCallbacks
-        @Override
-        public void onFillRequestSuccess(@Nullable FillResponse response,
-                @NonNull String servicePackageName) {
-            if (response == null) {
-                // Nothing to be done, but need to notify client.
-                notifyUnavailableToClient();
-                removeSelf();
-                return;
-            }
-
-            if ((response.getDatasets() == null || response.getDatasets().isEmpty())
-                            && response.getAuthentication() == null) {
-                // Response is "empty" from an UI point of view, need to notify client.
-                notifyUnavailableToClient();
-            }
-            synchronized (mLock) {
-                processResponseLocked(response);
-            }
-
-            LogMaker log = (new LogMaker(MetricsProto.MetricsEvent.AUTOFILL_REQUEST))
-                    .setType(MetricsProto.MetricsEvent.TYPE_SUCCESS)
-                    .setPackageName(mPackageName)
-                    .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS,
-                            response.getDatasets() == null ? 0 : response.getDatasets().size())
-                    .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_SERVICE,
-                            servicePackageName);
-            mMetricsLogger.write(log);
-        }
-
-        // FillServiceCallbacks
-        @Override
-        public void onFillRequestFailure(@Nullable CharSequence message,
-                @NonNull String servicePackageName) {
-            LogMaker log = (new LogMaker(MetricsProto.MetricsEvent.AUTOFILL_REQUEST))
-                    .setType(MetricsProto.MetricsEvent.TYPE_FAILURE)
-                    .setPackageName(mPackageName)
-                    .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_SERVICE,
-                            servicePackageName);
-            mMetricsLogger.write(log);
-
-            getUiForShowing().showError(message);
-            removeSelf();
-        }
-
-        // FillServiceCallbacks
-        @Override
-        public void onSaveRequestSuccess(@NonNull String servicePackageName) {
-            LogMaker log = (new LogMaker(
-                    MetricsProto.MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST))
-                    .setType(MetricsProto.MetricsEvent.TYPE_SUCCESS)
-                    .setPackageName(mPackageName)
-                    .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_SERVICE,
-                            servicePackageName);
-            mMetricsLogger.write(log);
-
-            // Nothing left to do...
-            removeSelf();
-        }
-
-        // FillServiceCallbacks
-        @Override
-        public void onSaveRequestFailure(@Nullable CharSequence message,
-                @NonNull String servicePackageName) {
-            LogMaker log = (new LogMaker(
-                    MetricsProto.MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST))
-                    .setType(MetricsProto.MetricsEvent.TYPE_FAILURE)
-                    .setPackageName(mPackageName)
-                    .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_SERVICE,
-                            servicePackageName);
-            mMetricsLogger.write(log);
-
-            getUiForShowing().showError(message);
-            removeSelf();
-        }
-
-        // FillServiceCallbacks
-        @Override
-        public void authenticate(IntentSender intent) {
-            final Intent fillInIntent;
-            synchronized (mLock) {
-                fillInIntent = createAuthFillInIntent(mStructure);
-            }
-            mHandlerCaller.getHandler().post(() -> startAuthentication(intent, fillInIntent));
-        }
-
-        // FillServiceCallbacks
-        @Override
-        public void onDisableSelf() {
-            final long identity = Binder.clearCallingIdentity();
-            try {
-                final String autoFillService = Settings.Secure.getStringForUser(
-                        mContext.getContentResolver(),
-                        Settings.Secure.AUTOFILL_SERVICE, mUserId);
-                if (mInfo.getServiceInfo().getComponentName().equals(
-                        ComponentName.unflattenFromString(autoFillService))) {
-                    Settings.Secure.putStringForUser(mContext.getContentResolver(),
-                            Settings.Secure.AUTOFILL_SERVICE, null, mUserId);
-                }
-            } finally {
-                Binder.restoreCallingIdentity(identity);
-            }
-            synchronized (mLock) {
-                removeSelfLocked();
-            }
-        }
-
-        // FillServiceCallbacks
-        @Override
-        public void onServiceDied(RemoteFillService service) {
-            // TODO(b/33197203): implement
-        }
-
-        // AutoFillUiCallback
-        @Override
-        public void fill(Dataset dataset) {
-            mHandlerCaller.getHandler().post(() -> autoFill(dataset));
-        }
-
-        // AutoFillUiCallback
-        @Override
-        public void save() {
-            mHandlerCaller.getHandler().obtainMessage(MSG_SERVICE_SAVE, mActivityToken)
-                    .sendToTarget();
-        }
-
-        // AutoFillUiCallback
-        @Override
-        public void cancelSave() {
-            mHandlerCaller.getHandler().post(() -> removeSelf());
-        }
-
-        // AutoFillUiCallback
-        @Override
-        public void requestShowFillUi(AutofillId id, int width, int height,
-                IAutofillWindowPresenter presenter) {
-            try {
-                mClient.requestShowFillUi(mWindowToken, id, width, height,
-                        mCurrentViewState.mVirtualBounds, presenter);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Error requesting to show fill UI", e);
-            }
-        }
-
-        // AutoFillUiCallback
-        @Override
-        public void requestHideFillUi(AutofillId id) {
-            try {
-                mClient.requestHideFillUi(mWindowToken, id);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Error requesting to hide fill UI", e);
-            }
-        }
-
-        public void setAuthenticationResultLocked(Bundle data) {
-            if (mCurrentResponse == null || data == null) {
-                removeSelf();
-            } else {
-                Parcelable result = data.getParcelable(
-                        AutofillManager.EXTRA_AUTHENTICATION_RESULT);
-                if (result instanceof FillResponse) {
-                    mMetricsLogger.action(MetricsProto.MetricsEvent.AUTOFILL_AUTHENTICATED,
-                            mPackageName);
-
-                    mCurrentResponse = (FillResponse) result;
-                    processResponseLocked(mCurrentResponse);
-                } else if (result instanceof Dataset) {
-                    Dataset dataset = (Dataset) result;
-                    final int index = mCurrentResponse.getDatasets().indexOf(mAutoFilledDataset);
-                    if (index >= 0) {
-                        mCurrentResponse.getDatasets().set(index, dataset);
-                        autoFill(dataset);
-                    }
-                }
-            }
-        }
-
-        public void setHasCallback(boolean hasIt) {
-            mHasCallback = hasIt;
-        }
-
-        /**
-         * Shows the save UI, when session can be saved.
-         *
-         * @return {@code true} if session is done, or {@code false} if it's pending user action.
-         */
-        public boolean showSaveLocked() {
-            if (mStructure == null) {
-                Slog.wtf(TAG, "showSaveLocked(): no mStructure");
-                return true;
-            }
-            if (mCurrentResponse == null) {
-                // Happens when the activity / session was finished before the service replied, or
-                // when the service cannot autofill it (and returned a null response).
-                if (DEBUG) {
-                    Slog.d(TAG, "showSaveLocked(): no mCurrentResponse");
-                }
-                return true;
-            }
-            final SaveInfo saveInfo = mCurrentResponse.getSaveInfo();
-            if (DEBUG) {
-                Slog.d(TAG, "showSaveLocked(): saveInfo=" + saveInfo);
-            }
-
-            /*
-             * The Save dialog is only shown if all conditions below are met:
-             *
-             * - saveInfo is not null
-             * - autofillValue of all required ids is not null
-             * - autofillValue of at least one id (required or optional) has changed.
-             */
-
-            if (saveInfo == null) {
-                return true;
-            }
-
-            final AutofillId[] requiredIds = saveInfo.getRequiredIds();
-            if (requiredIds == null || requiredIds.length == 0) {
-                Slog.w(TAG, "showSaveLocked(): no required ids on saveInfo");
-                return true;
-            }
-
-            boolean allRequiredAreNotEmpty = true;
-            boolean atLeastOneChanged = false;
-            for (int i = 0; i < requiredIds.length; i++) {
-                final AutofillId id = requiredIds[i];
-                final ViewState state = mViewStates.get(id);
-                if (state == null || state.mAutofillValue == null
-                         || state.mAutofillValue.isEmpty()) {
-                    final ViewNode node = findViewNodeByIdLocked(id);
-                    if (node == null) {
-                        Slog.w(TAG, "Service passed invalid id on SavableInfo: " + id);
-                        allRequiredAreNotEmpty = false;
-                        break;
-                    }
-                    final AutofillValue initialValue = node.getAutofillValue();
-                    if (initialValue == null || initialValue.isEmpty()) {
-                        if (DEBUG) {
-                            Slog.d(TAG, "finishSessionLocked(): empty initial value for " + id );
-                        }
-                        allRequiredAreNotEmpty = false;
-                        break;
-                    }
-                }
-                if (state.mValueUpdated) {
-                    final AutofillValue filledValue = findValue(mAutoFilledDataset, id);
-                    if (!state.mAutofillValue.equals(filledValue)) {
-                        if (DEBUG) {
-                            Slog.d(TAG, "finishSessionLocked(): found a change on " + id + ": "
-                                    + filledValue + " => " + state.mAutofillValue);
-                        }
-                        atLeastOneChanged = true;
-                    }
-                } else {
-                    if (state.mAutofillValue == null || state.mAutofillValue.isEmpty()) {
-                        if (DEBUG) {
-                            Slog.d(TAG, "finishSessionLocked(): empty value for " + id + ": "
-                                    + state.mAutofillValue);
-                        }
-                        allRequiredAreNotEmpty = false;
-                        break;
-
-                    }
-                }
-            }
-
-            if (allRequiredAreNotEmpty) {
-                if (!atLeastOneChanged && saveInfo.getOptionalIds() != null) {
-                    for (int i = 0; i < saveInfo.getOptionalIds().length; i++) {
-                        final AutofillId id = saveInfo.getOptionalIds()[i];
-                        final ViewState state = mViewStates.get(id);
-                        if (state != null && state.mAutofillValue != null && state.mValueUpdated) {
-                            final AutofillValue filledValue = findValue(mAutoFilledDataset, id);
-                            if (!state.mAutofillValue.equals(filledValue)) {
-                                if (DEBUG) {
-                                    Slog.d(TAG, "finishSessionLocked(): found a change on optional "
-                                            + id + ": " + filledValue + " => "
-                                            + state.mAutofillValue);
-                                }
-                                atLeastOneChanged = true;
-                                break;
-                            }
-                        }
-                    }
-                }
-                if (atLeastOneChanged) {
-                    getUiForShowing().showSaveUi(
-                            mInfo.getServiceInfo().loadLabel(mContext.getPackageManager()),
-                            saveInfo, mPackageName);
-                    return false;
-                }
-            }
-            // Nothing changed...
-            if (DEBUG) {
-                Slog.d(TAG, "showSaveLocked(): with no changes, comes no responsibilities."
-                        + "allRequiredAreNotNull=" + allRequiredAreNotEmpty
-                        + ", atLeastOneChanged=" + atLeastOneChanged);
-            }
-            return true;
-        }
-
-        /**
-         * Calls service when user requested save.
-         */
-        private void callSaveLocked() {
-            if (DEBUG) {
-                Slog.d(TAG, "callSaveLocked(): mViewStates=" + mViewStates);
-            }
-
-            final Bundle extras = this.mCurrentResponse.getExtras();
-
-            for (Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) {
-                final AutofillValue value = entry.getValue().mAutofillValue;
-                if (value == null) {
-                    if (VERBOSE) {
-                        Slog.v(TAG, "callSaveLocked(): skipping " + entry.getKey());
-                    }
-                    continue;
-                }
-                final AutofillId id = entry.getKey();
-                final ViewNode node = findViewNodeByIdLocked(id);
-                if (node == null) {
-                    Slog.w(TAG, "callSaveLocked(): did not find node with id " + id);
-                    continue;
-                }
-                if (VERBOSE) {
-                    Slog.v(TAG, "callSaveLocked(): updating " + id + " to " + value);
-                }
-
-                node.updateAutofillValue(value);
-            }
-
-            // Sanitize structure before it's sent to service.
-            mStructure.sanitizeForParceling(false);
-
-            if (VERBOSE) {
-                Slog.v(TAG, "Dumping " + mStructure + " before calling service.save()");
-                mStructure.dump();
-            }
-
-            mRemoteFillService.onSaveRequest(mStructure, extras);
-        }
-
-        void updateLocked(AutofillId id, Rect virtualBounds, AutofillValue value, int flags) {
-            if (mAutoFilledDataset != null && (flags & FLAG_VALUE_CHANGED) == 0) {
-                // TODO(b/33197203): ignoring because we don't support partitions yet
-                Slog.d(TAG, "updateLocked(): ignoring " + flags + " after app was autofilled");
-                return;
-            }
-
-            ViewState viewState = mViewStates.get(id);
-            if (viewState == null) {
-                viewState = new ViewState(this, id, this);
-                mViewStates.put(id, viewState);
-            }
-
-            if ((flags & FLAG_START_SESSION) != 0) {
-                // View is triggering autofill.
-                mCurrentViewState = viewState;
-                viewState.update(value, virtualBounds);
-                return;
-            }
-
-            if ((flags & FLAG_VALUE_CHANGED) != 0) {
-                if (value != null && !value.equals(viewState.mAutofillValue)) {
-                    viewState.mValueUpdated = true;
-
-                    // Must check if this update was caused by autofilling the view, in which
-                    // case we just update the value, but not the UI.
-                    if (mAutoFilledDataset != null) {
-                        final AutofillValue filledValue = findValue(mAutoFilledDataset, id);
-                        if (value.equals(filledValue)) {
-                            viewState.mAutofillValue = value;
-                            return;
-                        }
-                    }
-
-                    // Change value
-                    viewState.mAutofillValue = value;
-
-                    // Update the chooser UI
-                    if (value.isText()) {
-                        getUiForShowing().filterFillUi(value.getTextValue().toString());
-                    } else {
-                        getUiForShowing().filterFillUi(null);
-                    }
-                }
-
-                return;
-            }
-
-            if ((flags & FLAG_VIEW_ENTERED) != 0) {
-                // Remove the UI if the ViewState has changed.
-                if (mCurrentViewState != viewState) {
-                    mUi.hideFillUi(mCurrentViewState != null ? mCurrentViewState.mId : null);
-                    mCurrentViewState = viewState;
-                }
-
-                // If the ViewState is ready to be displayed, onReady() will be called.
-                viewState.update(value, virtualBounds);
-
-                // TODO(b/33197203): Remove when there is a response per activity.
-                if (mCurrentResponse != null) {
-                    viewState.setResponse(mCurrentResponse);
-                }
-
-                return;
-            }
-
-            if ((flags & FLAG_VIEW_EXITED) != 0) {
-                if (mCurrentViewState == viewState) {
-                    mUi.hideFillUi(viewState.mId);
-                    mCurrentViewState = null;
-                }
-                return;
-            }
-
-            Slog.w(TAG, "updateLocked(): unknown flags " + flags);
-        }
-
-        @Override
-        public void onFillReady(FillResponse response, AutofillId filledId,
-                @Nullable AutofillValue value) {
-            String filterText = null;
-            if (value != null && value.isText()) {
-                filterText = value.getTextValue().toString();
-            }
-
-            getUiForShowing().showFillUi(filledId, response, filterText, mPackageName);
-        }
-
-        private void notifyUnavailableToClient() {
-            if (mCurrentViewState == null) {
-                // TODO(b/33197203): temporary sanity check; should never happen
-                Slog.w(TAG, "notifyUnavailable(): mCurrentViewState is null");
-                return;
-            }
-            if (!mHasCallback) return;
-            try {
-                mClient.notifyNoFillUi(mWindowToken, mCurrentViewState.mId);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Error notifying client no fill UI: windowToken=" + mWindowToken
-                        + " id=" + mCurrentViewState.mId, e);
-            }
-        }
-
-        private void processResponseLocked(FillResponse response) {
-            if (DEBUG) {
-                Slog.d(TAG, "processResponseLocked(auth=" + response.getAuthentication()
-                    + "):" + response);
-            }
-
-            if (mCurrentViewState == null) {
-                // TODO(b/33197203): temporary sanity check; should never happen
-                Slog.w(TAG, "processResponseLocked(): mCurrentViewState is null");
-                return;
-            }
-
-            mCurrentResponse = response;
-
-            if (mCurrentResponse.getAuthentication() != null) {
-                // Handle authentication.
-                final Intent fillInIntent = createAuthFillInIntent(mStructure);
-                mCurrentViewState.setResponse(mCurrentResponse, fillInIntent);
-                return;
-            }
-
-            if ((mFlags & FLAG_MANUAL_REQUEST) != 0 && response.getDatasets() != null
-                    && response.getDatasets().size() == 1) {
-                Slog.d(TAG, "autofilling manual request directly");
-                autoFill(response.getDatasets().get(0));
-                return;
-            }
-
-            mCurrentViewState.setResponse(mCurrentResponse);
-        }
-
-        void autoFill(Dataset dataset) {
-            synchronized (mLock) {
-                mAutoFilledDataset = dataset;
-
-                // Autofill it directly...
-                if (dataset.getAuthentication() == null) {
-                    autoFillApp(dataset);
-                    return;
-                }
-
-                // ...or handle authentication.
-                Intent fillInIntent = createAuthFillInIntent(mStructure);
-                startAuthentication(dataset.getAuthentication(), fillInIntent);
-            }
-        }
-
-        CharSequence getServiceName() {
-            return AutofillManagerServiceImpl.this.getServiceName();
-        }
-
-        private Intent createAuthFillInIntent(AssistStructure structure) {
-            Intent fillInIntent = new Intent();
-            fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, structure);
-            return fillInIntent;
-        }
-
-        private void startAuthentication(IntentSender intent, Intent fillInIntent) {
-            try {
-                mClient.authenticate(intent, fillInIntent);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Error launching auth intent", e);
-            }
-        }
-
-        void dumpLocked(String prefix, PrintWriter pw) {
-            pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken);
-            pw.print(prefix); pw.print("mFlags: "); pw.println(mFlags);
-            pw.print(prefix); pw.print("mCurrentResponse: "); pw.println(mCurrentResponse);
-            pw.print(prefix); pw.print("mAutoFilledDataset: "); pw.println(mAutoFilledDataset);
-            pw.print(prefix); pw.print("mCurrentViewStates: "); pw.println(mCurrentViewState);
-            pw.print(prefix); pw.print("mViewStates: "); pw.println(mViewStates.size());
-            final String prefix2 = prefix + "  ";
-            for (Map.Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) {
-                pw.print(prefix); pw.print("State for id "); pw.println(entry.getKey());
-                entry.getValue().dump(prefix2, pw);
-            }
-            if (VERBOSE) {
-                pw.print(prefix); pw.print("mStructure: " );
-                // TODO(b/33197203): add method do dump AssistStructure on pw
-                if (mStructure != null) {
-                    pw.println("look at logcat" );
-                    mStructure.dump(); // dumps to logcat
-                } else {
-                    pw.println("null");
-                }
-            }
-            pw.print(prefix); pw.print("mHasCallback: "); pw.println(mHasCallback);
-            mRemoteFillService.dump(prefix, pw);
-        }
-
-        void autoFillApp(Dataset dataset) {
-            synchronized (mLock) {
-                try {
-                    if (DEBUG) {
-                        Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset);
-                    }
-                    mClient.autofill(mWindowToken, dataset.getFieldIds(), dataset.getFieldValues());
-                } catch (RemoteException e) {
-                    Slog.w(TAG, "Error autofilling activity: " + e);
-                }
-            }
-        }
-
-        private AutoFillUI getUiForShowing() {
-            synchronized (mLock) {
-                mUi.setCallback(this);
-                return mUi;
-            }
-        }
-
-        private ViewNode findViewNodeByIdLocked(AutofillId id) {
-            final int size = mStructure.getWindowNodeCount();
-            for (int i = 0; i < size; i++) {
-                final WindowNode window = mStructure.getWindowNodeAt(i);
-                final ViewNode root = window.getRootViewNode();
-                if (id.equals(root.getAutofillId())) {
-                    return root;
-                }
-                final ViewNode child = findViewNodeByIdLocked(root, id);
-                if (child != null) {
-                    return child;
-                }
-            }
-            return null;
-        }
-
-        private ViewNode findViewNodeByIdLocked(ViewNode parent, AutofillId id) {
-            final int childrenSize = parent.getChildCount();
-            if (childrenSize > 0) {
-                for (int i = 0; i < childrenSize; i++) {
-                    final ViewNode child = parent.getChildAt(i);
-                    if (id.equals(child.getAutofillId())) {
-                        return child;
-                    }
-                    final ViewNode grandChild = findViewNodeByIdLocked(child, id);
-                    if (grandChild != null && id.equals(grandChild.getAutofillId())) {
-                        return grandChild;
-                    }
-                }
-            }
-            return null;
-        }
-
-        private void destroyLocked() {
-            mRemoteFillService.destroy();
-            mUi.setCallback(null);
-            mMetricsLogger.action(MetricsProto.MetricsEvent.AUTOFILL_SESSION_FINISHED,
-                    mPackageName);
-        }
-
-        void removeSelf() {
-            synchronized (mLock) {
-                removeSelfLocked();
-            }
-        }
-
-        private void removeSelfLocked() {
-            if (VERBOSE) {
-                Slog.v(TAG, "removeSelfLocked()");
-            }
-            destroyLocked();
-            mSessions.remove(mActivityToken);
-        }
-    }
 }
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
new file mode 100644
index 0000000..1093e9e
--- /dev/null
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -0,0 +1,764 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.server.autofill;
+
+import static android.view.autofill.AutofillManager.FLAG_MANUAL_REQUEST;
+import static android.view.autofill.AutofillManager.FLAG_START_SESSION;
+import static android.view.autofill.AutofillManager.FLAG_VALUE_CHANGED;
+import static android.view.autofill.AutofillManager.FLAG_VIEW_ENTERED;
+import static android.view.autofill.AutofillManager.FLAG_VIEW_EXITED;
+
+import static com.android.server.autofill.Helper.DEBUG;
+import static com.android.server.autofill.Helper.VERBOSE;
+import static com.android.server.autofill.Helper.findValue;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.app.assist.AssistStructure.WindowNode;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.graphics.Rect;
+import android.metrics.LogMaker;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.service.autofill.AutofillService;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillResponse;
+import android.service.autofill.SaveInfo;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+import android.view.autofill.IAutoFillManagerClient;
+import android.view.autofill.IAutofillWindowPresenter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.os.HandlerCaller;
+import com.android.server.autofill.ui.AutoFillUI;
+
+import java.io.PrintWriter;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * A session for a given activity.
+ *
+ * <p>This class manages the multiple {@link ViewState}s for each view it has, and keeps track
+ * of the current {@link ViewState} to display the appropriate UI.
+ *
+ * <p>Although the autofill requests and callbacks are stateless from the service's point of
+ * view, we need to keep state in the framework side for cases such as authentication. For
+ * example, when service return a {@link FillResponse} that contains all the fields needed
+ * to fill the activity but it requires authentication first, that response need to be held
+ * until the user authenticates or it times out.
+ */
+// TODO(b/33197203): make sure sessions are removed (and tested by CTS):
+// - On all authentication scenarios.
+// - When user does not interact back after a while.
+// - When service is unbound.
+final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener,
+        AutoFillUI.AutoFillUiCallback {
+    private static final String TAG = "AutofillSession";
+
+    private final AutofillManagerServiceImpl mService;
+    private final IBinder mActivityToken;
+    private final IBinder mWindowToken;
+    private final HandlerCaller mHandlerCaller;
+    private final Object mLock;
+    private final AutoFillUI mUi;
+
+    private final MetricsLogger mMetricsLogger = new MetricsLogger();
+
+    /** Package name of the app that is auto-filled */
+    @NonNull private final String mPackageName;
+
+    @GuardedBy("mLock")
+    private final Map<AutofillId, ViewState> mViewStates = new ArrayMap<>();
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ViewState mCurrentViewState;
+
+    private final IAutoFillManagerClient mClient;
+
+    @GuardedBy("mLock")
+    RemoteFillService mRemoteFillService;
+
+    // TODO(b/33197203): Get a response per view instead of per activity.
+    @GuardedBy("mLock")
+    private FillResponse mCurrentResponse;
+
+    /**
+     * Used to remember which {@link Dataset} filled the session.
+     */
+    // TODO(b/33197203): might need more than one once we support partitions
+    @GuardedBy("mLock")
+    private Dataset mAutoFilledDataset;
+
+    /**
+     * Assist structure sent by the app; it will be updated (sanitized, change values for save)
+     * before sent to {@link AutofillService}.
+     */
+    @GuardedBy("mLock") AssistStructure mStructure;
+
+    /**
+     * Whether the client has an {@link android.view.autofill.AutofillManager.AutofillCallback}.
+     */
+    private boolean mHasCallback;
+
+    /**
+     * Flags used to start the session.
+     */
+    int mFlags;
+
+    Session(@NonNull AutofillManagerServiceImpl service, @NonNull AutoFillUI ui,
+            @NonNull Context context, @NonNull HandlerCaller handlerCaller, int userId,
+            @NonNull Object lock, @NonNull IBinder activityToken,
+            @Nullable IBinder windowToken, @NonNull IBinder client, boolean hasCallback,
+            int flags, @NonNull ComponentName componentName, @NonNull String packageName) {
+        mService = service;
+        mLock = lock;
+        mUi = ui;
+        mHandlerCaller = handlerCaller;
+        mRemoteFillService = new RemoteFillService(context, componentName, userId, this);
+        mActivityToken = activityToken;
+        mWindowToken = windowToken;
+        mHasCallback = hasCallback;
+        mPackageName = packageName;
+        mFlags = flags;
+
+        mClient = IAutoFillManagerClient.Stub.asInterface(client);
+        try {
+            client.linkToDeath(() -> {
+                if (DEBUG) {
+                    Slog.d(TAG, "app binder died");
+                }
+
+                removeSelf();
+            }, 0);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "linkToDeath() on mClient failed: " + e);
+        }
+
+        mMetricsLogger.action(MetricsEvent.AUTOFILL_SESSION_STARTED, mPackageName);
+    }
+
+    // FillServiceCallbacks
+    @Override
+    public void onFillRequestSuccess(@Nullable FillResponse response,
+            @NonNull String servicePackageName) {
+        if (response == null) {
+            // Nothing to be done, but need to notify client.
+            notifyUnavailableToClient();
+            removeSelf();
+            return;
+        }
+
+        if ((response.getDatasets() == null || response.getDatasets().isEmpty())
+                        && response.getAuthentication() == null) {
+            // Response is "empty" from an UI point of view, need to notify client.
+            notifyUnavailableToClient();
+        }
+        synchronized (mLock) {
+            processResponseLocked(response);
+        }
+
+        LogMaker log = (new LogMaker(MetricsEvent.AUTOFILL_REQUEST))
+                .setType(MetricsEvent.TYPE_SUCCESS)
+                .setPackageName(mPackageName)
+                .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS,
+                        response.getDatasets() == null ? 0 : response.getDatasets().size())
+                .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE,
+                        servicePackageName);
+        mMetricsLogger.write(log);
+    }
+
+    // FillServiceCallbacks
+    @Override
+    public void onFillRequestFailure(@Nullable CharSequence message,
+            @NonNull String servicePackageName) {
+        LogMaker log = (new LogMaker(MetricsEvent.AUTOFILL_REQUEST))
+                .setType(MetricsEvent.TYPE_FAILURE)
+                .setPackageName(mPackageName)
+                .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName);
+        mMetricsLogger.write(log);
+
+        getUiForShowing().showError(message);
+        removeSelf();
+    }
+
+    // FillServiceCallbacks
+    @Override
+    public void onSaveRequestSuccess(@NonNull String servicePackageName) {
+        LogMaker log = (new LogMaker(
+                MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST))
+                .setType(MetricsEvent.TYPE_SUCCESS)
+                .setPackageName(mPackageName)
+                .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName);
+        mMetricsLogger.write(log);
+
+        // Nothing left to do...
+        removeSelf();
+    }
+
+    // FillServiceCallbacks
+    @Override
+    public void onSaveRequestFailure(@Nullable CharSequence message,
+            @NonNull String servicePackageName) {
+        LogMaker log = (new LogMaker(
+                MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST))
+                .setType(MetricsEvent.TYPE_FAILURE)
+                .setPackageName(mPackageName)
+                .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName);
+        mMetricsLogger.write(log);
+
+        getUiForShowing().showError(message);
+        removeSelf();
+    }
+
+    // FillServiceCallbacks
+    @Override
+    public void authenticate(IntentSender intent) {
+        final Intent fillInIntent;
+        synchronized (mLock) {
+            fillInIntent = createAuthFillInIntent(mStructure);
+        }
+        mHandlerCaller.getHandler().post(() -> startAuthentication(intent, fillInIntent));
+    }
+
+    // FillServiceCallbacks
+    @Override
+    public void onDisableSelf() {
+        mService.disableSelf();
+        synchronized (mLock) {
+            removeSelfLocked();
+        }
+    }
+
+    // FillServiceCallbacks
+    @Override
+    public void onServiceDied(RemoteFillService service) {
+        // TODO(b/33197203): implement
+    }
+
+    // AutoFillUiCallback
+    @Override
+    public void fill(Dataset dataset) {
+        mHandlerCaller.getHandler().post(() -> autoFill(dataset));
+    }
+
+    // AutoFillUiCallback
+    @Override
+    public void save() {
+        mHandlerCaller.getHandler()
+                .obtainMessage(AutofillManagerServiceImpl.MSG_SERVICE_SAVE, mActivityToken)
+                .sendToTarget();
+    }
+
+    // AutoFillUiCallback
+    @Override
+    public void cancelSave() {
+        mHandlerCaller.getHandler().post(() -> removeSelf());
+    }
+
+    // AutoFillUiCallback
+    @Override
+    public void requestShowFillUi(AutofillId id, int width, int height,
+            IAutofillWindowPresenter presenter) {
+        try {
+            mClient.requestShowFillUi(mWindowToken, id, width, height,
+                    mCurrentViewState.mVirtualBounds, presenter);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error requesting to show fill UI", e);
+        }
+    }
+
+    // AutoFillUiCallback
+    @Override
+    public void requestHideFillUi(AutofillId id) {
+        try {
+            mClient.requestHideFillUi(mWindowToken, id);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error requesting to hide fill UI", e);
+        }
+    }
+
+    public void setAuthenticationResultLocked(Bundle data) {
+        if (mCurrentResponse == null || data == null) {
+            removeSelf();
+        } else {
+            Parcelable result = data.getParcelable(
+                    AutofillManager.EXTRA_AUTHENTICATION_RESULT);
+            if (result instanceof FillResponse) {
+                mMetricsLogger.action(MetricsEvent.AUTOFILL_AUTHENTICATED, mPackageName);
+
+                mCurrentResponse = (FillResponse) result;
+                processResponseLocked(mCurrentResponse);
+            } else if (result instanceof Dataset) {
+                Dataset dataset = (Dataset) result;
+                final int index = mCurrentResponse.getDatasets().indexOf(mAutoFilledDataset);
+                if (index >= 0) {
+                    mCurrentResponse.getDatasets().set(index, dataset);
+                    autoFill(dataset);
+                }
+            }
+        }
+    }
+
+    public void setHasCallback(boolean hasIt) {
+        mHasCallback = hasIt;
+    }
+
+    /**
+     * Shows the save UI, when session can be saved.
+     *
+     * @return {@code true} if session is done, or {@code false} if it's pending user action.
+     */
+    public boolean showSaveLocked() {
+        if (mStructure == null) {
+            Slog.wtf(TAG, "showSaveLocked(): no mStructure");
+            return true;
+        }
+        if (mCurrentResponse == null) {
+            // Happens when the activity / session was finished before the service replied, or
+            // when the service cannot autofill it (and returned a null response).
+            if (DEBUG) {
+                Slog.d(TAG, "showSaveLocked(): no mCurrentResponse");
+            }
+            return true;
+        }
+        final SaveInfo saveInfo = mCurrentResponse.getSaveInfo();
+        if (DEBUG) {
+            Slog.d(TAG, "showSaveLocked(): saveInfo=" + saveInfo);
+        }
+
+        /*
+         * The Save dialog is only shown if all conditions below are met:
+         *
+         * - saveInfo is not null
+         * - autofillValue of all required ids is not null
+         * - autofillValue of at least one id (required or optional) has changed.
+         */
+
+        if (saveInfo == null) {
+            return true;
+        }
+
+        final AutofillId[] requiredIds = saveInfo.getRequiredIds();
+        if (requiredIds == null || requiredIds.length == 0) {
+            Slog.w(TAG, "showSaveLocked(): no required ids on saveInfo");
+            return true;
+        }
+
+        boolean allRequiredAreNotEmpty = true;
+        boolean atLeastOneChanged = false;
+        for (int i = 0; i < requiredIds.length; i++) {
+            final AutofillId id = requiredIds[i];
+            final ViewState state = mViewStates.get(id);
+            if (state == null || state.mAutofillValue == null
+                     || state.mAutofillValue.isEmpty()) {
+                final ViewNode node = findViewNodeByIdLocked(id);
+                if (node == null) {
+                    Slog.w(TAG, "Service passed invalid id on SavableInfo: " + id);
+                    allRequiredAreNotEmpty = false;
+                    break;
+                }
+                final AutofillValue initialValue = node.getAutofillValue();
+                if (initialValue == null || initialValue.isEmpty()) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "finishSessionLocked(): empty initial value for " + id );
+                    }
+                    allRequiredAreNotEmpty = false;
+                    break;
+                }
+            }
+            if (state.mValueUpdated) {
+                final AutofillValue filledValue = findValue(mAutoFilledDataset, id);
+                if (!state.mAutofillValue.equals(filledValue)) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "finishSessionLocked(): found a change on " + id + ": "
+                                + filledValue + " => " + state.mAutofillValue);
+                    }
+                    atLeastOneChanged = true;
+                }
+            } else {
+                if (state.mAutofillValue == null || state.mAutofillValue.isEmpty()) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "finishSessionLocked(): empty value for " + id + ": "
+                                + state.mAutofillValue);
+                    }
+                    allRequiredAreNotEmpty = false;
+                    break;
+
+                }
+            }
+        }
+
+        if (allRequiredAreNotEmpty) {
+            if (!atLeastOneChanged && saveInfo.getOptionalIds() != null) {
+                for (int i = 0; i < saveInfo.getOptionalIds().length; i++) {
+                    final AutofillId id = saveInfo.getOptionalIds()[i];
+                    final ViewState state = mViewStates.get(id);
+                    if (state != null && state.mAutofillValue != null && state.mValueUpdated) {
+                        final AutofillValue filledValue = findValue(mAutoFilledDataset, id);
+                        if (!state.mAutofillValue.equals(filledValue)) {
+                            if (DEBUG) {
+                                Slog.d(TAG, "finishSessionLocked(): found a change on optional "
+                                        + id + ": " + filledValue + " => "
+                                        + state.mAutofillValue);
+                            }
+                            atLeastOneChanged = true;
+                            break;
+                        }
+                    }
+                }
+            }
+            if (atLeastOneChanged) {
+                getUiForShowing().showSaveUi(mService.getServiceLabel(), saveInfo, mPackageName);
+                return false;
+            }
+        }
+        // Nothing changed...
+        if (DEBUG) {
+            Slog.d(TAG, "showSaveLocked(): with no changes, comes no responsibilities."
+                    + "allRequiredAreNotNull=" + allRequiredAreNotEmpty
+                    + ", atLeastOneChanged=" + atLeastOneChanged);
+        }
+        return true;
+    }
+
+    /**
+     * Calls service when user requested save.
+     */
+    void callSaveLocked() {
+        if (DEBUG) {
+            Slog.d(TAG, "callSaveLocked(): mViewStates=" + mViewStates);
+        }
+
+        final Bundle extras = this.mCurrentResponse.getExtras();
+
+        for (Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) {
+            final AutofillValue value = entry.getValue().mAutofillValue;
+            if (value == null) {
+                if (VERBOSE) {
+                    Slog.v(TAG, "callSaveLocked(): skipping " + entry.getKey());
+                }
+                continue;
+            }
+            final AutofillId id = entry.getKey();
+            final ViewNode node = findViewNodeByIdLocked(id);
+            if (node == null) {
+                Slog.w(TAG, "callSaveLocked(): did not find node with id " + id);
+                continue;
+            }
+            if (VERBOSE) {
+                Slog.v(TAG, "callSaveLocked(): updating " + id + " to " + value);
+            }
+
+            node.updateAutofillValue(value);
+        }
+
+        // Sanitize structure before it's sent to service.
+        mStructure.sanitizeForParceling(false);
+
+        if (VERBOSE) {
+            Slog.v(TAG, "Dumping " + mStructure + " before calling service.save()");
+            mStructure.dump();
+        }
+
+        mRemoteFillService.onSaveRequest(mStructure, extras);
+    }
+
+    void updateLocked(AutofillId id, Rect virtualBounds, AutofillValue value, int flags) {
+        if (mAutoFilledDataset != null && (flags & FLAG_VALUE_CHANGED) == 0) {
+            // TODO(b/33197203): ignoring because we don't support partitions yet
+            Slog.d(TAG, "updateLocked(): ignoring " + flags + " after app was autofilled");
+            return;
+        }
+
+        ViewState viewState = mViewStates.get(id);
+        if (viewState == null) {
+            viewState = new ViewState(this, id, this);
+            mViewStates.put(id, viewState);
+        }
+
+        if ((flags & FLAG_START_SESSION) != 0) {
+            // View is triggering autofill.
+            mCurrentViewState = viewState;
+            viewState.update(value, virtualBounds);
+            return;
+        }
+
+        if ((flags & FLAG_VALUE_CHANGED) != 0) {
+            if (value != null && !value.equals(viewState.mAutofillValue)) {
+                viewState.mValueUpdated = true;
+
+                // Must check if this update was caused by autofilling the view, in which
+                // case we just update the value, but not the UI.
+                if (mAutoFilledDataset != null) {
+                    final AutofillValue filledValue = findValue(mAutoFilledDataset, id);
+                    if (value.equals(filledValue)) {
+                        viewState.mAutofillValue = value;
+                        return;
+                    }
+                }
+
+                // Change value
+                viewState.mAutofillValue = value;
+
+                // Update the chooser UI
+                if (value.isText()) {
+                    getUiForShowing().filterFillUi(value.getTextValue().toString());
+                } else {
+                    getUiForShowing().filterFillUi(null);
+                }
+            }
+
+            return;
+        }
+
+        if ((flags & FLAG_VIEW_ENTERED) != 0) {
+            // Remove the UI if the ViewState has changed.
+            if (mCurrentViewState != viewState) {
+                mUi.hideFillUi(mCurrentViewState != null ? mCurrentViewState.mId : null);
+                mCurrentViewState = viewState;
+            }
+
+            // If the ViewState is ready to be displayed, onReady() will be called.
+            viewState.update(value, virtualBounds);
+
+            // TODO(b/33197203): Remove when there is a response per activity.
+            if (mCurrentResponse != null) {
+                viewState.setResponse(mCurrentResponse);
+            }
+
+            return;
+        }
+
+        if ((flags & FLAG_VIEW_EXITED) != 0) {
+            if (mCurrentViewState == viewState) {
+                mUi.hideFillUi(viewState.mId);
+                mCurrentViewState = null;
+            }
+            return;
+        }
+
+        Slog.w(TAG, "updateLocked(): unknown flags " + flags);
+    }
+
+    @Override
+    public void onFillReady(FillResponse response, AutofillId filledId,
+            @Nullable AutofillValue value) {
+        String filterText = null;
+        if (value != null && value.isText()) {
+            filterText = value.getTextValue().toString();
+        }
+
+        getUiForShowing().showFillUi(filledId, response, filterText, mPackageName);
+    }
+
+    private void notifyUnavailableToClient() {
+        if (mCurrentViewState == null) {
+            // TODO(b/33197203): temporary sanity check; should never happen
+            Slog.w(TAG, "notifyUnavailable(): mCurrentViewState is null");
+            return;
+        }
+        if (!mHasCallback) return;
+        try {
+            mClient.notifyNoFillUi(mWindowToken, mCurrentViewState.mId);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error notifying client no fill UI: windowToken=" + mWindowToken
+                    + " id=" + mCurrentViewState.mId, e);
+        }
+    }
+
+    private void processResponseLocked(FillResponse response) {
+        if (DEBUG) {
+            Slog.d(TAG, "processResponseLocked(auth=" + response.getAuthentication()
+                + "):" + response);
+        }
+
+        if (mCurrentViewState == null) {
+            // TODO(b/33197203): temporary sanity check; should never happen
+            Slog.w(TAG, "processResponseLocked(): mCurrentViewState is null");
+            return;
+        }
+
+        mCurrentResponse = response;
+
+        if (mCurrentResponse.getAuthentication() != null) {
+            // Handle authentication.
+            final Intent fillInIntent = createAuthFillInIntent(mStructure);
+            mCurrentViewState.setResponse(mCurrentResponse, fillInIntent);
+            return;
+        }
+
+        if ((mFlags & FLAG_MANUAL_REQUEST) != 0 && response.getDatasets() != null
+                && response.getDatasets().size() == 1) {
+            Slog.d(TAG, "autofilling manual request directly");
+            autoFill(response.getDatasets().get(0));
+            return;
+        }
+
+        mCurrentViewState.setResponse(mCurrentResponse);
+    }
+
+    void autoFill(Dataset dataset) {
+        synchronized (mLock) {
+            mAutoFilledDataset = dataset;
+
+            // Autofill it directly...
+            if (dataset.getAuthentication() == null) {
+                autoFillApp(dataset);
+                return;
+            }
+
+            // ...or handle authentication.
+            Intent fillInIntent = createAuthFillInIntent(mStructure);
+            startAuthentication(dataset.getAuthentication(), fillInIntent);
+        }
+    }
+
+    CharSequence getServiceName() {
+        return mService.getServiceName();
+    }
+
+    private Intent createAuthFillInIntent(AssistStructure structure) {
+        Intent fillInIntent = new Intent();
+        fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, structure);
+        return fillInIntent;
+    }
+
+    private void startAuthentication(IntentSender intent, Intent fillInIntent) {
+        try {
+            mClient.authenticate(intent, fillInIntent);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error launching auth intent", e);
+        }
+    }
+
+    void dumpLocked(String prefix, PrintWriter pw) {
+        pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken);
+        pw.print(prefix); pw.print("mFlags: "); pw.println(mFlags);
+        pw.print(prefix); pw.print("mCurrentResponse: "); pw.println(mCurrentResponse);
+        pw.print(prefix); pw.print("mAutoFilledDataset: "); pw.println(mAutoFilledDataset);
+        pw.print(prefix); pw.print("mCurrentViewStates: "); pw.println(mCurrentViewState);
+        pw.print(prefix); pw.print("mViewStates: "); pw.println(mViewStates.size());
+        final String prefix2 = prefix + "  ";
+        for (Map.Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) {
+            pw.print(prefix); pw.print("State for id "); pw.println(entry.getKey());
+            entry.getValue().dump(prefix2, pw);
+        }
+        if (VERBOSE) {
+            pw.print(prefix); pw.print("mStructure: " );
+            // TODO(b/33197203): add method do dump AssistStructure on pw
+            if (mStructure != null) {
+                pw.println("look at logcat" );
+                mStructure.dump(); // dumps to logcat
+            } else {
+                pw.println("null");
+            }
+        }
+        pw.print(prefix); pw.print("mHasCallback: "); pw.println(mHasCallback);
+        mRemoteFillService.dump(prefix, pw);
+    }
+
+    void autoFillApp(Dataset dataset) {
+        synchronized (mLock) {
+            try {
+                if (DEBUG) {
+                    Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset);
+                }
+                mClient.autofill(mWindowToken, dataset.getFieldIds(), dataset.getFieldValues());
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Error autofilling activity: " + e);
+            }
+        }
+    }
+
+    private AutoFillUI getUiForShowing() {
+        synchronized (mLock) {
+            mUi.setCallback(this);
+            return mUi;
+        }
+    }
+
+    private ViewNode findViewNodeByIdLocked(AutofillId id) {
+        final int size = mStructure.getWindowNodeCount();
+        for (int i = 0; i < size; i++) {
+            final WindowNode window = mStructure.getWindowNodeAt(i);
+            final ViewNode root = window.getRootViewNode();
+            if (id.equals(root.getAutofillId())) {
+                return root;
+            }
+            final ViewNode child = findViewNodeByIdLocked(root, id);
+            if (child != null) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    private ViewNode findViewNodeByIdLocked(ViewNode parent, AutofillId id) {
+        final int childrenSize = parent.getChildCount();
+        if (childrenSize > 0) {
+            for (int i = 0; i < childrenSize; i++) {
+                final ViewNode child = parent.getChildAt(i);
+                if (id.equals(child.getAutofillId())) {
+                    return child;
+                }
+                final ViewNode grandChild = findViewNodeByIdLocked(child, id);
+                if (grandChild != null && id.equals(grandChild.getAutofillId())) {
+                    return grandChild;
+                }
+            }
+        }
+        return null;
+    }
+
+    void destroyLocked() {
+        mRemoteFillService.destroy();
+        mUi.setCallback(null);
+        mMetricsLogger.action(MetricsEvent.AUTOFILL_SESSION_FINISHED, mPackageName);
+    }
+
+    void removeSelf() {
+        synchronized (mLock) {
+            removeSelfLocked();
+        }
+    }
+
+    void removeSelfLocked() {
+        if (VERBOSE) {
+            Slog.v(TAG, "removeSelfLocked()");
+        }
+        destroyLocked();
+        mService.removeSessionLocked(mActivityToken);
+    }
+}
\ No newline at end of file
diff --git a/services/autofill/java/com/android/server/autofill/ViewState.java b/services/autofill/java/com/android/server/autofill/ViewState.java
new file mode 100644
index 0000000..d31dcfd
--- /dev/null
+++ b/services/autofill/java/com/android/server/autofill/ViewState.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.autofill;
+
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.service.autofill.FillResponse;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+
+import java.io.PrintWriter;
+
+/**
+ * State for a given view with a AutofillId.
+ *
+ * <p>This class holds state about a view and calls its listener when the fill UI is ready to
+ * be displayed for the view.
+ */
+final class ViewState {
+    interface Listener {
+        /**
+         * Called when the fill UI is ready to be shown for this view.
+         */
+        void onFillReady(FillResponse fillResponse, AutofillId focusedId,
+                @Nullable AutofillValue value);
+    }
+
+    final AutofillId mId;
+    private final Listener mListener;
+    // TODO(b/33197203): would not need a reference to response and session if it was an inner
+    // class of Session...
+    private final Session mSession;
+    private FillResponse mResponse;
+    private Intent mAuthIntent;
+
+    // TODO(b/33197203): encapsulate access so it's not called by UI
+    AutofillValue mAutofillValue;
+
+    // TODO(b/33197203): encapsulate access so it's not called by UI
+    // Bounds if a virtual view, null otherwise
+    Rect mVirtualBounds;
+
+    boolean mValueUpdated;
+
+    ViewState(Session session, AutofillId id, Listener listener) {
+        mSession = session;
+        mId = id;
+        mListener = listener;
+    }
+
+    /**
+     * Response should only be set once.
+     */
+    void setResponse(FillResponse response) {
+        mResponse = response;
+        maybeCallOnFillReady();
+    }
+
+    /**
+     * Used when a {@link FillResponse} requires authentication to be unlocked.
+     */
+    void setResponse(FillResponse response, Intent authIntent) {
+        mAuthIntent = authIntent;
+        setResponse(response);
+    }
+
+    CharSequence getServiceName() {
+        return mSession.getServiceName();
+    }
+
+    // TODO(b/33197203): need to refactor / rename / document this method to make it clear that
+    // it can change  the value and update the UI; similarly, should replace code that
+    // directly sets mAutoFilLValue to use encapsulation.
+    void update(@Nullable AutofillValue autofillValue, @Nullable Rect virtualBounds) {
+        if (autofillValue != null) {
+            mAutofillValue = autofillValue;
+        }
+        if (virtualBounds != null) {
+            mVirtualBounds = virtualBounds;
+        }
+
+        maybeCallOnFillReady();
+    }
+
+    /**
+     * Calls {@link
+     * Listener#onFillReady(FillResponse, AutofillId, AutofillValue)} if the
+     * fill UI is ready to be displayed (i.e. when response and bounds are set).
+     */
+    void maybeCallOnFillReady() {
+        if (mResponse != null && (mResponse.getAuthentication() != null
+                || mResponse.getDatasets() != null)) {
+            mListener.onFillReady(mResponse, mId, mAutofillValue);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "ViewState: [id=" + mId + ", value=" + mAutofillValue + ", bounds=" + mVirtualBounds
+                + ", updated = " + mValueUpdated + "]";
+    }
+
+    void dump(String prefix, PrintWriter pw) {
+        pw.print(prefix); pw.print("id:" ); pw.println(mId);
+        pw.print(prefix); pw.print("value:" ); pw.println(mAutofillValue);
+        pw.print(prefix); pw.print("updated:" ); pw.println(mValueUpdated);
+        pw.print(prefix); pw.print("virtualBounds:" ); pw.println(mVirtualBounds);
+        pw.print(prefix); pw.print("authIntent:" ); pw.println(mAuthIntent);
+    }
+}
\ No newline at end of file