OSDN Git Service

Hides the Save UI while handling a pending intent from CustomDescription.
authorFelipe Leme <felipeal@google.com>
Thu, 3 Aug 2017 21:27:57 +0000 (14:27 -0700)
committerFelipe Leme <felipeal@google.com>
Tue, 29 Aug 2017 01:04:15 +0000 (18:04 -0700)
When the AutofillService sets a PendingIntent to launch an activity when
clicking a chield view (for example, to lauch a web page displayign the terms
and conditions of saving something), the system must hide the Save UI and
restore it after the new activity is dismissed.

That sounds simple in the surface, but it requires a huge refactoring behind
the scenes, such as injecting a token in the activity intent and using that
token to hide / cancel the UI during some activity lifecycle events.

Test: lotta of brand-new shinning tests on CtsAutoFillServiceTestCases
Test: cts-tradefed run commandAndExit cts-dev -m CtsAutoFillServiceTestCases

Change-Id: Ie8ec7aeb2c63cab68467046c1a9dcf85dbcc24ec
Fixes: 64309238

core/java/android/app/Activity.java
core/java/android/service/autofill/CustomDescription.java
core/java/android/view/autofill/AutofillManager.java
core/java/android/view/autofill/IAutoFillManager.aidl
core/java/android/view/autofill/IAutoFillManagerClient.aidl
services/autofill/java/com/android/server/autofill/AutofillManagerService.java
services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
services/autofill/java/com/android/server/autofill/Session.java
services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
services/autofill/java/com/android/server/autofill/ui/PendingUi.java [new file with mode: 0644]
services/autofill/java/com/android/server/autofill/ui/SaveUi.java

index 72d5ede..785a8f7 100644 (file)
@@ -1863,8 +1863,18 @@ public class Activity extends ContextThemeWrapper
         getApplication().dispatchActivityStopped(this);
         mTranslucentCallback = null;
         mCalled = true;
-        if (isFinishing() && mAutoFillResetNeeded) {
-            getAutofillManager().commit();
+
+        if (isFinishing()) {
+            if (mAutoFillResetNeeded) {
+                getAutofillManager().commit();
+            } else if (mIntent != null
+                    && mIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) {
+                // Activity was launched when user tapped a link in the Autofill Save UI - since
+                // user launched another activity, the Save UI should not be restored when this
+                // activity is finished.
+                getAutofillManager().onPendingSaveUi(AutofillManager.PENDING_UI_OPERATION_CANCEL,
+                        mIntent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN));
+            }
         }
     }
 
@@ -5500,6 +5510,13 @@ public class Activity extends ContextThemeWrapper
         } else {
             mParent.finishFromChild(this);
         }
+
+        // Activity was launched when user tapped a link in the Autofill Save UI - Save UI must
+        // be restored now.
+        if (mIntent != null && mIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) {
+            getAutofillManager().onPendingSaveUi(AutofillManager.PENDING_UI_OPERATION_RESTORE,
+                    mIntent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN));
+        }
     }
 
     /**
@@ -6234,6 +6251,11 @@ public class Activity extends ContextThemeWrapper
         }
 
         mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);
+
+        final AutofillManager afm = getAutofillManager();
+        if (afm != null) {
+            afm.dump(prefix, writer);
+        }
     }
 
     /**
index 4f06bd7..9a4cbc4 100644 (file)
@@ -19,6 +19,8 @@ package android.service.autofill;
 import static android.view.autofill.Helper.sDebug;
 
 import android.annotation.NonNull;
+import android.app.Activity;
+import android.app.PendingIntent;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Log;
@@ -130,6 +132,18 @@ public final class CustomDescription implements Parcelable {
         /**
          * Default constructor.
          *
+         * <p><b>Note:</b> If any child view of presentation triggers a
+         * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent) pending intent
+         * on click}, such {@link PendingIntent} must follow the restrictions below, otherwise
+         * it might not be triggered or the Save affordance might not be shown when its activity
+         * is finished:
+         * <ul>
+         *   <li>It cannot be created with the {@link PendingIntent#FLAG_IMMUTABLE} flag.
+         *   <li>It must be a PendingIntent for an {@link Activity}.
+         *   <li>The activity must call {@link Activity#finish()} when done.
+         *   <li>The activity should not launch other activities.
+         * </ul>
+         *
          * @param parentPresentation template presentation with (optional) children views.
          */
         public Builder(RemoteViews parentPresentation) {
index 29e5523..61cbce9 100644 (file)
@@ -30,12 +30,14 @@ import android.content.IntentSender;
 import android.graphics.Rect;
 import android.metrics.LogMaker;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.service.autofill.AutofillService;
 import android.service.autofill.FillEventHistory;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.DebugUtils;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.View;
@@ -44,6 +46,7 @@ import com.android.internal.annotations.GuardedBy;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto;
 
+import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
@@ -154,8 +157,15 @@ public final class AutofillManager {
     public static final String EXTRA_CLIENT_STATE =
             "android.view.autofill.extra.CLIENT_STATE";
 
-    static final String SESSION_ID_TAG = "android:sessionId";
-    static final String LAST_AUTOFILLED_DATA_TAG = "android:lastAutoFilledData";
+
+    /** @hide */
+    public static final String EXTRA_RESTORE_SESSION_TOKEN =
+            "android.view.autofill.extra.RESTORE_SESSION_TOKEN";
+
+    private static final String SESSION_ID_TAG = "android:sessionId";
+    private static final String STATE_TAG = "android:state";
+    private static final String LAST_AUTOFILLED_DATA_TAG = "android:lastAutoFilledData";
+
 
     /** @hide */ public static final int ACTION_START_SESSION = 1;
     /** @hide */ public static final int ACTION_VIEW_ENTERED =  2;
@@ -175,6 +185,44 @@ public final class AutofillManager {
     public static final int AUTHENTICATION_ID_DATASET_ID_UNDEFINED = 0xFFFF;
 
     /**
+     * Used on {@link #onPendingSaveUi(int, IBinder)} to cancel the pending UI.
+     *
+     * @hide
+     */
+    public static final int PENDING_UI_OPERATION_CANCEL = 1;
+
+    /**
+     * Used on {@link #onPendingSaveUi(int, IBinder)} to restore the pending UI.
+     *
+     * @hide
+     */
+    public static final int PENDING_UI_OPERATION_RESTORE = 2;
+
+    /**
+     * Initial state of the autofill context, set when there is no session (i.e., when
+     * {@link #mSessionId} is {@link #NO_SESSION}).
+     *
+     * @hide
+     */
+    public static final int STATE_UNKNOWN = 1;
+
+    /**
+     * State where the autofill context hasn't been {@link #commit() finished} nor
+     * {@link #cancel() canceled} yet.
+     *
+     * @hide
+     */
+    public static final int STATE_ACTIVE = 2;
+
+    /**
+     * State where the autofill context has been {@link #commit() finished} but the server still has
+     * a session because the Save UI hasn't been dismissed yet.
+     *
+     * @hide
+     */
+    public static final int STATE_SHOWING_SAVE_UI = 4;
+
+    /**
      * Makes an authentication id from a request id and a dataset id.
      *
      * @param requestId The request id.
@@ -233,6 +281,9 @@ public final class AutofillManager {
     private int mSessionId = NO_SESSION;
 
     @GuardedBy("mLock")
+    private int mState = STATE_UNKNOWN;
+
+    @GuardedBy("mLock")
     private boolean mEnabled;
 
     /** If a view changes to this mapping the autofill operation was successful */
@@ -344,12 +395,13 @@ public final class AutofillManager {
         synchronized (mLock) {
             mLastAutofilledData = savedInstanceState.getParcelable(LAST_AUTOFILLED_DATA_TAG);
 
-            if (mSessionId != NO_SESSION) {
+            if (isActiveLocked()) {
                 Log.w(TAG, "New session was started before onCreate()");
                 return;
             }
 
             mSessionId = savedInstanceState.getInt(SESSION_ID_TAG, NO_SESSION);
+            mState = savedInstanceState.getInt(STATE_TAG, STATE_UNKNOWN);
 
             if (mSessionId != NO_SESSION) {
                 ensureServiceClientAddedIfNeededLocked();
@@ -363,6 +415,7 @@ public final class AutofillManager {
                         if (!sessionWasRestored) {
                             Log.w(TAG, "Session " + mSessionId + " could not be restored");
                             mSessionId = NO_SESSION;
+                            mState = STATE_UNKNOWN;
                         } else {
                             if (sDebug) {
                                 Log.d(TAG, "session " + mSessionId + " was restored");
@@ -387,7 +440,7 @@ public final class AutofillManager {
      */
     public void onVisibleForAutofill() {
         synchronized (mLock) {
-            if (mEnabled && mSessionId != NO_SESSION && mTrackedViews != null) {
+            if (mEnabled && isActiveLocked() && mTrackedViews != null) {
                 mTrackedViews.onVisibleForAutofillLocked();
             }
         }
@@ -408,7 +461,9 @@ public final class AutofillManager {
             if (mSessionId != NO_SESSION) {
                 outState.putInt(SESSION_ID_TAG, mSessionId);
             }
-
+            if (mState != STATE_UNKNOWN) {
+                outState.putInt(STATE_TAG, mState);
+            }
             if (mLastAutofilledData != null) {
                 outState.putParcelable(LAST_AUTOFILLED_DATA_TAG, mLastAutofilledData);
             }
@@ -514,7 +569,7 @@ public final class AutofillManager {
                 final AutofillId id = getAutofillId(view);
                 final AutofillValue value = view.getAutofillValue();
 
-                if (mSessionId == NO_SESSION) {
+                if (!isActiveLocked()) {
                     // Starts new session.
                     startSessionLocked(id, null, value, flags);
                 } else {
@@ -541,7 +596,7 @@ public final class AutofillManager {
         synchronized (mLock) {
             ensureServiceClientAddedIfNeededLocked();
 
-            if (mEnabled && mSessionId != NO_SESSION) {
+            if (mEnabled && isActiveLocked()) {
                 final AutofillId id = getAutofillId(view);
 
                 // Update focus on existing session.
@@ -582,7 +637,7 @@ public final class AutofillManager {
     private void notifyViewVisibilityChangedInternal(@NonNull View view, int virtualId,
             boolean isVisible, boolean virtual) {
         synchronized (mLock) {
-            if (mEnabled && mSessionId != NO_SESSION) {
+            if (mEnabled && isActiveLocked()) {
                 final AutofillId id = virtual ? getAutofillId(view, virtualId)
                         : view.getAutofillId();
                 if (!isVisible && mFillableIds != null) {
@@ -636,7 +691,7 @@ public final class AutofillManager {
             } else {
                 final AutofillId id = getAutofillId(view, virtualId);
 
-                if (mSessionId == NO_SESSION) {
+                if (!isActiveLocked()) {
                     // Starts new session.
                     startSessionLocked(id, bounds, null, flags);
                 } else {
@@ -665,7 +720,7 @@ public final class AutofillManager {
         synchronized (mLock) {
             ensureServiceClientAddedIfNeededLocked();
 
-            if (mEnabled && mSessionId != NO_SESSION) {
+            if (mEnabled && isActiveLocked()) {
                 final AutofillId id = getAutofillId(view, virtualId);
 
                 // Update focus on existing session.
@@ -709,7 +764,7 @@ public final class AutofillManager {
                 }
             }
 
-            if (!mEnabled || mSessionId == NO_SESSION) {
+            if (!mEnabled || !isActiveLocked()) {
                 return;
             }
 
@@ -737,7 +792,7 @@ public final class AutofillManager {
             return;
         }
         synchronized (mLock) {
-            if (!mEnabled || mSessionId == NO_SESSION) {
+            if (!mEnabled || !isActiveLocked()) {
                 return;
             }
 
@@ -762,7 +817,7 @@ public final class AutofillManager {
             return;
         }
         synchronized (mLock) {
-            if (!mEnabled && mSessionId == NO_SESSION) {
+            if (!mEnabled && !isActiveLocked()) {
                 return;
             }
 
@@ -786,7 +841,7 @@ public final class AutofillManager {
             return;
         }
         synchronized (mLock) {
-            if (!mEnabled && mSessionId == NO_SESSION) {
+            if (!mEnabled && !isActiveLocked()) {
                 return;
             }
 
@@ -868,7 +923,7 @@ public final class AutofillManager {
         if (sDebug) Log.d(TAG, "onAuthenticationResult(): d=" + data);
 
         synchronized (mLock) {
-            if (mSessionId == NO_SESSION || data == null) {
+            if (!isActiveLocked() || data == null) {
                 return;
             }
             final Parcelable result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT);
@@ -895,13 +950,19 @@ public final class AutofillManager {
             @NonNull AutofillValue value, int flags) {
         if (sVerbose) {
             Log.v(TAG, "startSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value
-                    + ", flags=" + flags);
+                    + ", flags=" + flags + ", state=" + mState);
+        }
+        if (mState != STATE_UNKNOWN) {
+            if (sDebug) Log.d(TAG, "not starting session for " + id + " on state " + mState);
+            return;
         }
-
         try {
             mSessionId = mService.startSession(mContext.getActivityToken(),
                     mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(),
                     mCallback != null, flags, mContext.getOpPackageName());
+            if (mSessionId != NO_SESSION) {
+                mState = STATE_ACTIVE;
+            }
             final AutofillClient client = getClientLocked();
             if (client != null) {
                 client.autofillCallbackResetableStateAvailable();
@@ -912,7 +973,9 @@ public final class AutofillManager {
     }
 
     private void finishSessionLocked() {
-        if (sVerbose) Log.v(TAG, "finishSessionLocked()");
+        if (sVerbose) Log.v(TAG, "finishSessionLocked(): " + mState);
+
+        if (!isActiveLocked()) return;
 
         try {
             mService.finishSession(mSessionId, mContext.getUserId());
@@ -920,12 +983,13 @@ public final class AutofillManager {
             throw e.rethrowFromSystemServer();
         }
 
-        mTrackedViews = null;
-        mSessionId = NO_SESSION;
+        resetSessionLocked();
     }
 
     private void cancelSessionLocked() {
-        if (sVerbose) Log.v(TAG, "cancelSessionLocked()");
+        if (sVerbose) Log.v(TAG, "cancelSessionLocked(): " + mState);
+
+        if (!isActiveLocked()) return;
 
         try {
             mService.cancelSession(mSessionId, mContext.getUserId());
@@ -938,7 +1002,9 @@ public final class AutofillManager {
 
     private void resetSessionLocked() {
         mSessionId = NO_SESSION;
+        mState = STATE_UNKNOWN;
         mTrackedViews = null;
+        mFillableIds = null;
     }
 
     private void updateSessionLocked(AutofillId id, Rect bounds, AutofillValue value, int action,
@@ -947,7 +1013,6 @@ public final class AutofillManager {
             Log.v(TAG, "updateSessionLocked(): id=" + id + ", bounds=" + bounds
                     + ", value=" + value + ", action=" + action + ", flags=" + flags);
         }
-
         boolean restartIfNecessary = (flags & FLAG_MANUAL_REQUEST) != 0;
 
         try {
@@ -958,6 +1023,7 @@ public final class AutofillManager {
                 if (newId != mSessionId) {
                     if (sDebug) Log.d(TAG, "Session restarted: " + mSessionId + "=>" + newId);
                     mSessionId = newId;
+                    mState = (mSessionId == NO_SESSION) ? STATE_UNKNOWN : STATE_ACTIVE;
                     final AutofillClient client = getClientLocked();
                     if (client != null) {
                         client.autofillCallbackResetableStateAvailable();
@@ -1219,6 +1285,27 @@ public final class AutofillManager {
         }
     }
 
+    private void setSaveUiState(int sessionId, boolean shown) {
+        if (sDebug) Log.d(TAG, "setSaveUiState(" + sessionId + "): " + shown);
+        synchronized (mLock) {
+            if (mSessionId != NO_SESSION) {
+                // Race condition: app triggered a new session after the previous session was
+                // finished but before server called setSaveUiState() - need to cancel the new
+                // session to avoid further inconsistent behavior.
+                Log.w(TAG, "setSaveUiState(" + sessionId + ", " + shown
+                        + ") called on existing session " + mSessionId + "; cancelling it");
+                cancelSessionLocked();
+            }
+            if (shown) {
+                mSessionId = sessionId;
+                mState = STATE_SHOWING_SAVE_UI;
+            } else {
+                mSessionId = NO_SESSION;
+                mState = STATE_UNKNOWN;
+            }
+        }
+    }
+
     private void requestHideFillUi(AutofillId id) {
         final View anchor = findView(id);
         if (sVerbose) Log.v(TAG, "requestHideFillUi(" + id + "): anchor = " + anchor);
@@ -1329,6 +1416,46 @@ public final class AutofillManager {
         return mService != null;
     }
 
+    /** @hide */
+    public void onPendingSaveUi(int operation, IBinder token) {
+        if (sVerbose) Log.v(TAG, "onPendingSaveUi(" + operation + "): " + token);
+
+        synchronized (mLock) {
+            try {
+                mService.onPendingSaveUi(operation, token);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /** @hide */
+    public void dump(String outerPrefix, PrintWriter pw) {
+        pw.print(outerPrefix); pw.println("AutofillManager:");
+        final String pfx = outerPrefix + "  ";
+        pw.print(pfx); pw.print("sessionId: "); pw.println(mSessionId);
+        pw.print(pfx); pw.print("state: "); pw.println(
+                DebugUtils.flagsToString(AutofillManager.class, "STATE_", mState));
+        pw.print(pfx); pw.print("enabled: "); pw.println(mEnabled);
+        pw.print(pfx); pw.print("hasService: "); pw.println(mService != null);
+        pw.print(pfx); pw.print("hasCallback: "); pw.println(mCallback != null);
+        pw.print(pfx); pw.print("last autofilled data: "); pw.println(mLastAutofilledData);
+        pw.print(pfx); pw.print("tracked views: ");
+        if (mTrackedViews == null) {
+            pw.println("null");
+        } else {
+            final String pfx2 = pfx + "  ";
+            pw.println();
+            pw.print(pfx2); pw.print("visible:"); pw.println(mTrackedViews.mVisibleTrackedIds);
+            pw.print(pfx2); pw.print("invisible:"); pw.println(mTrackedViews.mInvisibleTrackedIds);
+        }
+        pw.print(pfx); pw.print("fillable ids: "); pw.println(mFillableIds);
+    }
+
+    private boolean isActiveLocked() {
+        return mState == STATE_ACTIVE;
+    }
+
     private void post(Runnable runnable) {
         final AutofillClient client = getClientLocked();
         if (client == null) {
@@ -1668,12 +1795,12 @@ public final class AutofillManager {
         }
 
         @Override
-        public void startIntentSender(IntentSender intentSender) {
+        public void startIntentSender(IntentSender intentSender, Intent intent) {
             final AutofillManager afm = mAfm.get();
             if (afm != null) {
                 afm.post(() -> {
                     try {
-                        afm.mContext.startIntentSender(intentSender, null, 0, 0, 0);
+                        afm.mContext.startIntentSender(intentSender, intent, 0, 0, 0);
                     } catch (IntentSender.SendIntentException e) {
                         Log.e(TAG, "startIntentSender() failed for intent:" + intentSender, e);
                     }
@@ -1691,5 +1818,13 @@ public final class AutofillManager {
                 );
             }
         }
+
+        @Override
+        public void setSaveUiState(int sessionId, boolean shown) {
+            final AutofillManager afm = mAfm.get();
+            if (afm != null) {
+                afm.post(() ->afm.setSaveUiState(sessionId, shown));
+            }
+        }
     }
 }
index 627afa7..6bd9bec 100644 (file)
@@ -49,4 +49,5 @@ interface IAutoFillManager {
     void disableOwnedAutofillServices(int userId);
     boolean isServiceSupported(int userId);
     boolean isServiceEnabled(int userId, String packageName);
+    void onPendingSaveUi(int operation, IBinder token);
 }
index d18b181..0eae858 100644 (file)
@@ -72,7 +72,12 @@ oneway interface IAutoFillManagerClient {
     void notifyNoFillUi(int sessionId, in AutofillId id);
 
     /**
-     * Starts the provided intent sender
+     * Starts the provided intent sender.
      */
-    void startIntentSender(in IntentSender intentSender);
+    void startIntentSender(in IntentSender intentSender, in Intent intent);
+
+   /**
+     * Sets the state of the Autofill Save UI for a given session.
+     */
+   void setSaveUiState(int sessionId, boolean shown);
 }
index 71f699c..ddc819d 100644 (file)
@@ -655,6 +655,21 @@ public final class AutofillManagerService extends SystemService {
         }
 
         @Override
+        public void onPendingSaveUi(int operation, IBinder token) {
+            Preconditions.checkNotNull(token, "token");
+            Preconditions.checkArgument(operation == AutofillManager.PENDING_UI_OPERATION_CANCEL
+                    || operation == AutofillManager.PENDING_UI_OPERATION_RESTORE,
+                    "invalid operation: %d", operation);
+            synchronized (mLock) {
+                final AutofillManagerServiceImpl service = peekServiceForUserLocked(
+                        UserHandle.getCallingUserId());
+                if (service != null) {
+                    service.onPendingSaveUi(operation, token);
+                }
+            }
+        }
+
+        @Override
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
 
index 751c054..20ccee2 100644 (file)
@@ -41,7 +41,6 @@ import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
 import android.service.autofill.AutofillService;
@@ -52,10 +51,12 @@ import android.service.autofill.FillResponse;
 import android.service.autofill.IAutoFillService;
 import android.text.TextUtils;
 import android.util.ArraySet;
+import android.util.DebugUtils;
 import android.util.LocalLog;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
 import android.view.autofill.AutofillValue;
 import android.view.autofill.IAutoFillManagerClient;
 
@@ -233,26 +234,6 @@ final class AutofillManagerServiceImpl {
         }
     }
 
-    /**
-     * Used by {@link AutofillManagerServiceShellCommand} to request save for the current top app.
-     */
-    void requestSaveForUserLocked(IBinder activityToken) {
-        if (!isEnabled()) {
-            return;
-        }
-
-        final int numSessions = mSessions.size();
-        for (int i = 0; i < numSessions; i++) {
-            final Session session = mSessions.valueAt(i);
-            if (session.getActivityTokenLocked().equals(activityToken)) {
-                session.callSaveLocked();
-                return;
-            }
-        }
-
-        Slog.w(TAG, "requestSaveForUserLocked(): no session for " + activityToken);
-    }
-
     boolean addClientLocked(IAutoFillManagerClient client) {
         if (mClients == null) {
             mClients = new RemoteCallbackList<>();
@@ -290,6 +271,7 @@ final class AutofillManagerServiceImpl {
         if (!isEnabled()) {
             return 0;
         }
+        if (sVerbose) Slog.v(TAG, "startSession(): token=" + activityToken + ", flags=" + flags);
 
         // Occasionally clean up abandoned sessions
         pruneAbandonedSessionsLocked();
@@ -461,6 +443,25 @@ final class AutofillManagerServiceImpl {
         }
     }
 
+    void onPendingSaveUi(int operation, @NonNull IBinder token) {
+        if (sVerbose) Slog.v(TAG, "onPendingSaveUi(" + operation + "): " + token);
+        synchronized (mLock) {
+            final int sessionCount = mSessions.size();
+            for (int i = sessionCount - 1; i >= 0; i--) {
+                final Session session = mSessions.valueAt(i);
+                if (session.isSaveUiPendingForToken(token)) {
+                    session.onPendingSaveUi(operation, token);
+                    return;
+                }
+            }
+        }
+        if (sDebug) {
+            Slog.d(TAG, "No pending Save UI for token " + token + " and operation "
+                    + DebugUtils.flagsToString(AutofillManager.class, "PENDING_UI_OPERATION_",
+                            operation));
+        }
+    }
+
     void destroyLocked() {
         if (sVerbose) Slog.v(TAG, "destroyLocked()");
 
@@ -622,8 +623,12 @@ final class AutofillManagerServiceImpl {
     }
 
     void destroySessionsLocked() {
+        if (mSessions.size() == 0) {
+            mUi.destroyAll(AutofillManager.NO_SESSION, null, null);
+            return;
+        }
         while (mSessions.size() > 0) {
-            mSessions.valueAt(0).removeSelfLocked();
+            mSessions.valueAt(0).forceRemoveSelfLocked();
         }
     }
 
index f8fb13a..95db603 100644 (file)
@@ -77,6 +77,7 @@ import com.android.internal.os.HandlerCaller;
 import com.android.internal.os.IResultReceiver;
 import com.android.internal.util.ArrayUtils;
 import com.android.server.autofill.ui.AutoFillUI;
+import com.android.server.autofill.ui.PendingUi;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -164,10 +165,16 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
     @GuardedBy("mLock")
     private boolean mDestroyed;
 
-    /** Whether the session is currently saving */
+    /** Whether the session is currently saving. */
     @GuardedBy("mLock")
     private boolean mIsSaving;
 
+    /**
+     * Helper used to handle state of Save UI when it must be hiding to show a custom description
+     * link and later recovered.
+     */
+    @GuardedBy("mLock")
+    private PendingUi mPendingSaveUi;
 
     /**
      * Receiver of assist data from the app's {@link Activity}.
@@ -701,7 +708,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
         mHandlerCaller.getHandler().post(() -> {
             try {
                 synchronized (mLock) {
-                    mClient.startIntentSender(intentSender);
+                    mClient.startIntentSender(intentSender, null);
                 }
             } catch (RemoteException e) {
                 Slog.e(TAG, "Error launching auth intent", e);
@@ -964,8 +971,17 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
 
                 if (sDebug) Slog.d(TAG, "Good news, everyone! All checks passed, show save UI!");
                 mService.setSaveShown(id);
+                final IAutoFillManagerClient client = getClient();
+                mPendingSaveUi = new PendingUi(mActivityToken);
                 getUiForShowing().showSaveUi(mService.getServiceLabel(), saveInfo,
-                        valueFinder, mPackageName, this);
+                        valueFinder, mPackageName, this, mPendingSaveUi, id, client);
+                if (client != null) {
+                    try {
+                        client.setSaveUiState(id, true);
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Error notifying client to set save UI state to shown: " + e);
+                    }
+                }
                 mIsSaving = true;
                 return false;
             }
@@ -1246,7 +1262,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
 
                 // Remove the UI if the ViewState has changed.
                 if (mCurrentViewId != viewState.id) {
-                    hideFillUiIfOwnedByMe();
+                    mUi.hideFillUi(this);
                     mCurrentViewId = viewState.id;
                 }
 
@@ -1256,7 +1272,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
             case ACTION_VIEW_EXITED:
                 if (mCurrentViewId == viewState.id) {
                     if (sVerbose) Slog.d(TAG, "Exiting view " + id);
-                    hideFillUiIfOwnedByMe();
+                    mUi.hideFillUi(this);
                     mCurrentViewId = null;
                 }
                 break;
@@ -1396,7 +1412,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
     private void processResponseLocked(@NonNull FillResponse newResponse, int flags) {
         // Make sure we are hiding the UI which will be shown
         // only if handling the current response requires it.
-        hideAllUiIfOwnedByMe();
+        mUi.hideAll(this);
 
         final int requestId = newResponse.getRequestId();
         if (sVerbose) {
@@ -1583,6 +1599,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
         pw.print(prefix); pw.print("mViewStates size: "); pw.println(mViewStates.size());
         pw.print(prefix); pw.print("mDestroyed: "); pw.println(mDestroyed);
         pw.print(prefix); pw.print("mIsSaving: "); pw.println(mIsSaving);
+        pw.print(prefix); pw.print("mPendingSaveUi: "); pw.println(mPendingSaveUi);
         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);
@@ -1644,7 +1661,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
                 }
                 if (!ids.isEmpty()) {
                     if (waitingDatasetAuth) {
-                        hideFillUiIfOwnedByMe();
+                        mUi.hideFillUi(this);
                     }
                     if (sDebug) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset);
 
@@ -1664,38 +1681,65 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
         }
     }
 
+    /**
+     * Cleans up this session.
+     *
+     * <p>Typically called in 2 scenarios:
+     *
+     * <ul>
+     *   <li>When the session naturally finishes (i.e., from {@link #removeSelfLocked()}.
+     *   <li>When the service hosting the session is finished (for example, because the user
+     *       disabled it).
+     * </ul>
+     */
     RemoteFillService destroyLocked() {
         if (mDestroyed) {
             return null;
         }
-        hideAllUiIfOwnedByMe();
+        mUi.destroyAll(id, getClient(), this);
         mUi.clearCallback(this);
         mDestroyed = true;
         mMetricsLogger.action(MetricsEvent.AUTOFILL_SESSION_FINISHED, mPackageName);
         return mRemoteFillService;
     }
 
-    private void hideAllUiIfOwnedByMe() {
-        mUi.hideAll(this);
-    }
+    /**
+     * Cleans up this session and remove it from the service always, even if it does have a pending
+     * Save UI.
+     */
+    void forceRemoveSelfLocked() {
+        if (sVerbose) Slog.v(TAG, "forceRemoveSelfLocked(): " + mPendingSaveUi);
 
-    private void hideFillUiIfOwnedByMe() {
-        mUi.hideFillUi(this);
+        mPendingSaveUi = null;
+        removeSelfLocked();
+        mUi.destroyAll(id, getClient(), this);
     }
 
+    /**
+     * Thread-safe version of {@link #removeSelfLocked()}.
+     */
     private void removeSelf() {
         synchronized (mLock) {
             removeSelfLocked();
         }
     }
 
+    /**
+     * Cleans up this session and remove it from the service, but but only if it does not have a
+     * pending Save UI.
+     */
     void removeSelfLocked() {
-        if (sVerbose) Slog.v(TAG, "removeSelfLocked()");
+        if (sVerbose) Slog.v(TAG, "removeSelfLocked(): " + mPendingSaveUi);
         if (mDestroyed) {
             Slog.w(TAG, "Call to Session#removeSelfLocked() rejected - session: "
                     + id + " destroyed");
             return;
         }
+        if (isSaveUiPending()) {
+            Slog.i(TAG, "removeSelfLocked() ignored, waiting for pending save ui");
+            return;
+        }
+
         final RemoteFillService remoteFillService = destroyLocked();
         mService.removeSessionLocked(id);
         if (remoteFillService != null) {
@@ -1703,6 +1747,25 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
         }
     }
 
+    void onPendingSaveUi(int operation, @NonNull IBinder token) {
+        getUiForShowing().onPendingSaveUi(operation, token);
+    }
+
+    /**
+     * Checks whether this session is hiding the Save UI to handle a custom description link for
+     * a specific {@code token} created by {@link PendingUi#PendingUi(IBinder)}.
+     */
+    boolean isSaveUiPendingForToken(@NonNull IBinder token) {
+        return isSaveUiPending() && token.equals(mPendingSaveUi.getToken());
+    }
+
+    /**
+     * Checks whether this session is hiding the Save UI to handle a custom description link.
+     */
+    private boolean isSaveUiPending() {
+        return mPendingSaveUi != null && mPendingSaveUi.getState() == PendingUi.STATE_PENDING;
+    }
+
     private int getLastResponseIndexLocked() {
         // The response ids are monotonically increasing so
         // we just find the largest id which is the last. We
index 67ee185..7febf83 100644 (file)
@@ -25,6 +25,8 @@ import android.content.IntentSender;
 import android.metrics.LogMaker;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
 import android.service.autofill.Dataset;
 import android.service.autofill.FillResponse;
 import android.service.autofill.SaveInfo;
@@ -33,6 +35,7 @@ import android.text.TextUtils;
 import android.util.Slog;
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillManager;
+import android.view.autofill.IAutoFillManagerClient;
 import android.view.autofill.IAutofillWindowPresenter;
 import android.widget.Toast;
 
@@ -143,7 +146,6 @@ public final class AutoFillUI {
             if (callback != mCallback) {
                 return;
             }
-            hideSaveUiUiThread(callback);
             if (mFillUi != null) {
                 mFillUi.setFilterText(filterText);
             }
@@ -245,7 +247,8 @@ public final class AutoFillUI {
      */
     public void showSaveUi(@NonNull CharSequence providerLabel, @NonNull SaveInfo info,
             @NonNull ValueFinder valueFinder, @NonNull String packageName,
-            @NonNull AutoFillUiCallback callback) {
+            @NonNull AutoFillUiCallback callback, @NonNull PendingUi pendingUi,
+            int sessionId, @Nullable IAutoFillManagerClient client) {
         if (sVerbose) Slog.v(TAG, "showSaveUi() for " + packageName + ": " + info);
         int numIds = 0;
         numIds += info.getRequiredIds() == null ? 0 : info.getRequiredIds().length;
@@ -260,21 +263,22 @@ public final class AutoFillUI {
                 return;
             }
             hideAllUiThread(callback);
-            mSaveUi = new SaveUi(mContext, providerLabel, info, valueFinder, mOverlayControl,
-                    new SaveUi.OnSaveListener() {
+            mSaveUi = new SaveUi(mContext, pendingUi, providerLabel, info, valueFinder,
+                    mOverlayControl, client, new SaveUi.OnSaveListener() {
                 @Override
                 public void onSave() {
                     log.setType(MetricsProto.MetricsEvent.TYPE_ACTION);
-                    hideSaveUiUiThread(callback);
+                    hideSaveUiUiThread(mCallback);
                     if (mCallback != null) {
                         mCallback.save();
                     }
+                    destroySaveUiUiThread(sessionId, client);
                 }
 
                 @Override
                 public void onCancel(IntentSender listener) {
                     log.setType(MetricsProto.MetricsEvent.TYPE_DISMISS);
-                    hideSaveUiUiThread(callback);
+                    hideSaveUiUiThread(mCallback);
                     if (listener != null) {
                         try {
                             listener.sendIntent(mContext, 0, null, null, null);
@@ -286,6 +290,7 @@ public final class AutoFillUI {
                     if (mCallback != null) {
                         mCallback.cancelSave();
                     }
+                    destroySaveUiUiThread(sessionId, client);
                 }
 
                 @Override
@@ -304,12 +309,33 @@ public final class AutoFillUI {
     }
 
     /**
+     * Executes an operation in the pending save UI, if any.
+     */
+    public void onPendingSaveUi(int operation, @NonNull IBinder token) {
+        mHandler.post(() -> {
+            if (mSaveUi != null) {
+                mSaveUi.onPendingUi(operation, token);
+            } else {
+                Slog.w(TAG, "onPendingSaveUi(" + operation + "): no save ui");
+            }
+        });
+    }
+
+    /**
      * Hides all UI affordances.
      */
     public void hideAll(@Nullable AutoFillUiCallback callback) {
         mHandler.post(() -> hideAllUiThread(callback));
     }
 
+    /**
+     * Destroy all UI affordances.
+     */
+    public void destroyAll(int sessionId, @Nullable IAutoFillManagerClient client,
+            @Nullable AutoFillUiCallback callback) {
+        mHandler.post(() -> destroyAllUiThread(sessionId, client, callback));
+    }
+
     public void dump(PrintWriter pw) {
         pw.println("Autofill UI");
         final String prefix = "  ";
@@ -343,12 +369,41 @@ public final class AutoFillUI {
                     + ", mCallback=" + mCallback);
         }
         if (mSaveUi != null && (callback == null || callback == mCallback)) {
-            mSaveUi.destroy();
-            mSaveUi = null;
+            mSaveUi.hide();
         }
     }
 
     @android.annotation.UiThread
+    private void destroySaveUiUiThread(int sessionId, @Nullable IAutoFillManagerClient client) {
+        if (mSaveUi == null) {
+            // Calling destroySaveUiUiThread() twice is normal - it usually happens when the
+            // first call is made after the SaveUI is hidden and the second when the session is
+            // finished.
+            if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): already destroyed");
+            return;
+        }
+
+        if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): id=" + sessionId);
+        mSaveUi.destroy();
+        mSaveUi = null;
+        if (client != null) {
+            try {
+                if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): notifying client");
+                client.setSaveUiState(sessionId, false);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Error notifying client to set save UI state to hidden: " + e);
+            }
+        }
+    }
+
+    @android.annotation.UiThread
+    private void destroyAllUiThread(int sessionId, @Nullable IAutoFillManagerClient client,
+            @Nullable AutoFillUiCallback callback) {
+        hideFillUiUiThread(callback);
+        destroySaveUiUiThread(sessionId, client);
+    }
+
+    @android.annotation.UiThread
     private void hideAllUiThread(@Nullable AutoFillUiCallback callback) {
         hideFillUiUiThread(callback);
         hideSaveUiUiThread(callback);
diff --git a/services/autofill/java/com/android/server/autofill/ui/PendingUi.java b/services/autofill/java/com/android/server/autofill/ui/PendingUi.java
new file mode 100644 (file)
index 0000000..87263ed
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * 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.ui;
+
+import android.annotation.NonNull;
+import android.os.IBinder;
+import android.util.DebugUtils;
+
+/**
+ * Helper class used to handle a pending Autofill affordance such as the Save UI.
+ *
+ * <p>This class is not thread safe.
+ */
+// NOTE: this class could be an interface implemented by Session, but that would make it harder
+// to move the Autofill UI logic to a different process.
+public final class PendingUi {
+
+    public static final int STATE_CREATED = 1;
+    public static final int STATE_PENDING = 2;
+    public static final int STATE_FINISHED = 4;
+
+    private final IBinder mToken;
+    private int mState;
+
+    /**
+     * Default constructor.
+     *
+     * @param token token used to identify this pending UI.
+     */
+    public PendingUi(@NonNull IBinder token) {
+        mToken = token;
+        mState = STATE_CREATED;
+    }
+
+    /**
+     * Gets the token used to identify this pending UI.
+     */
+    @NonNull
+    public IBinder getToken() {
+        return mToken;
+    }
+
+    /**
+     * Sets the current lifecycle state.
+     */
+    public void setState(int state) {
+        mState = state;
+    }
+
+    /**
+     * Gets the current lifecycle state.
+     */
+    public int getState() {
+        return mState;
+    }
+
+    /**
+     * Determines whether the given token matches the token used to identify this pending UI.
+     */
+    public boolean matches(IBinder token) {
+        return mToken.equals(token);
+    }
+
+    @Override
+    public String toString() {
+        return "PendingUi: [token=" + mToken + ", state="
+                + DebugUtils.flagsToString(PendingUi.class, "STATE_", mState) + "]";
+    }
+}
index 3727c6e..67c1b8c 100644 (file)
@@ -21,9 +21,14 @@ import static com.android.server.autofill.Helper.sVerbose;
 
 import android.annotation.NonNull;
 import android.app.Dialog;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
 import android.content.Context;
+import android.content.Intent;
 import android.content.IntentSender;
 import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
 import android.service.autofill.CustomDescription;
 import android.service.autofill.SaveInfo;
 import android.service.autofill.ValueFinder;
@@ -31,15 +36,17 @@ import android.text.Html;
 import android.util.ArraySet;
 import android.util.Slog;
 import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
 import android.view.Window;
 import android.view.WindowManager;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.IAutoFillManagerClient;
 import android.widget.RemoteViews;
 import android.widget.ScrollView;
 import android.widget.TextView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
 
 import com.android.internal.R;
 import com.android.server.UiThread;
@@ -109,12 +116,15 @@ final class SaveUi {
 
     private final CharSequence mTitle;
     private final CharSequence mSubTitle;
+    private final PendingUi mPendingUi;
 
     private boolean mDestroyed;
 
-    SaveUi(@NonNull Context context, @NonNull CharSequence providerLabel, @NonNull SaveInfo info,
+    SaveUi(@NonNull Context context, @NonNull PendingUi pendingUi,
+           @NonNull CharSequence providerLabel, @NonNull SaveInfo info,
            @NonNull ValueFinder valueFinder, @NonNull OverlayControl overlayControl,
-           @NonNull OnSaveListener listener) {
+           @NonNull IAutoFillManagerClient client, @NonNull OnSaveListener listener) {
+        mPendingUi= pendingUi;
         mListener = new OneTimeListener(listener);
         mOverlayControl = overlayControl;
 
@@ -171,8 +181,49 @@ final class SaveUi {
 
             final RemoteViews presentation = customDescription.getPresentation(valueFinder);
             if (presentation != null) {
+                final RemoteViews.OnClickHandler handler = new RemoteViews.OnClickHandler() {
+                    @Override
+                    public boolean onClickHandler(View view, PendingIntent pendingIntent,
+                            Intent intent) {
+                        // We need to hide the Save UI before launching the pending intent, and
+                        // restore back it once the activity is finished, and that's achieved by
+                        // adding a custom extra in the activity intent.
+                        if (pendingIntent != null) {
+                            if (intent == null) {
+                                Slog.w(TAG,
+                                        "remote view on custom description does not have intent");
+                                return false;
+                            }
+                            if (!pendingIntent.isActivity()) {
+                                Slog.w(TAG, "ignoring custom description pending intent that's not "
+                                        + "for an activity: " + pendingIntent);
+                                return false;
+                            }
+                            if (sVerbose) {
+                                Slog.v(TAG,
+                                        "Intercepting custom description intent: " + intent);
+                            }
+                            final IBinder token = mPendingUi.getToken();
+                            intent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token);
+                            try {
+                                client.startIntentSender(pendingIntent.getIntentSender(),
+                                        intent);
+                                mPendingUi.setState(PendingUi.STATE_PENDING);
+                                if (sDebug) {
+                                    Slog.d(TAG, "hiding UI until restored with token " + token);
+                                }
+                                hide();
+                            } catch (RemoteException e) {
+                                Slog.w(TAG, "error triggering pending intent: " + intent);
+                                return false;
+                            }
+                        }
+                        return true;
+                    }
+                };
+
                 try {
-                    final View customSubtitleView = presentation.apply(context, null);
+                    final View customSubtitleView = presentation.apply(context, null, handler);
                     subtitleContainer = view.findViewById(R.id.autofill_save_custom_subtitle);
                     subtitleContainer.addView(customSubtitleView);
                     subtitleContainer.setVisibility(View.VISIBLE);
@@ -202,7 +253,7 @@ final class SaveUi {
         } else {
             noButton.setText(R.string.autofill_save_no);
         }
-        View.OnClickListener cancelListener =
+        final View.OnClickListener cancelListener =
                 (v) -> mListener.onCancel(info.getNegativeActionListener());
         noButton.setOnClickListener(cancelListener);
 
@@ -212,6 +263,9 @@ final class SaveUi {
         mDialog = new Dialog(context, R.style.Theme_DeviceDefault_Light_Panel);
         mDialog.setContentView(view);
 
+        // Dialog can be dismissed when touched outside.
+        mDialog.setOnDismissListener((d) -> mListener.onCancel(info.getNegativeActionListener()));
+
         final Window window = mDialog.getWindow();
         window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
         window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
@@ -227,9 +281,50 @@ final class SaveUi {
         params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title);
         params.windowAnimations = R.style.AutofillSaveAnimation;
 
+        show();
+    }
+
+    /**
+     * Update the pending UI, if any.
+     *
+     * @param operation how to update it.
+     * @param token token associated with the pending UI - if it doesn't match the pending token,
+     * the operation will be ignored.
+     */
+    void onPendingUi(int operation, @NonNull IBinder token) {
+        if (!mPendingUi.matches(token)) {
+            Slog.w(TAG, "restore(" + operation + "): got token " + token + " instead of "
+                    + mPendingUi.getToken());
+            return;
+        }
+        switch (operation) {
+            case AutofillManager.PENDING_UI_OPERATION_RESTORE:
+                if (sDebug) Slog.d(TAG, "Restoring save dialog for " + token);
+                show();
+                break;
+            case AutofillManager.PENDING_UI_OPERATION_CANCEL:
+                if (sDebug) Slog.d(TAG, "Cancelling pending save dialog for " + token);
+                hide();
+                break;
+            default:
+                Slog.w(TAG, "restore(): invalid operation " + operation);
+        }
+        mPendingUi.setState(PendingUi.STATE_FINISHED);
+    }
+
+    private void show() {
         Slog.i(TAG, "Showing save dialog: " + mTitle);
         mDialog.show();
         mOverlayControl.hideOverlays();
+   }
+
+    void hide() {
+        if (sVerbose) Slog.v(TAG, "Hiding save dialog.");
+        try {
+            mDialog.hide();
+        } finally {
+            mOverlayControl.showOverlays();
+        }
     }
 
     void destroy() {
@@ -238,7 +333,6 @@ final class SaveUi {
             throwIfDestroyed();
             mListener.onDestroy();
             mHandler.removeCallbacksAndMessages(mListener);
-            if (sVerbose) Slog.v(TAG, "destroy(): dismissing dialog");
             mDialog.dismiss();
             mDestroyed = true;
         } finally {
@@ -260,6 +354,7 @@ final class SaveUi {
     void dump(PrintWriter pw, String prefix) {
         pw.print(prefix); pw.print("title: "); pw.println(mTitle);
         pw.print(prefix); pw.print("subtitle: "); pw.println(mSubTitle);
+        pw.print(prefix); pw.print("pendingUi: "); pw.println(mPendingUi);
 
         final View view = mDialog.getWindow().getDecorView();
         final int[] loc = view.getLocationOnScreen();