Refactor ToastPresenter to perform show()/hide()

In order to support multi-user, we need to create a new context based on
the user id and retrieve the services from it
(http://b/151414297#comment9). This meant retrieving the services in
ToastUI.showToast() instead of on its constructor, which would make the
code diverge from Toast$TN.handleShow(). In order to avoid that, now
seemed a good time to refactor ToastPresenter to perform show() and
hide().

This means ToastPresenter will now be instantiated in every request for
a new toast in ToastUI, but fortunately with the refactor we were able
to get rid of ToastEntry (which was also beign instantiated in every
request).

Also found out a bug with this where window tokens were being used to
locate the toasts instead of the (non-window) tokens. This is a bit
confusing because the method NM.finishToken(package, token) receives a
non-window token to locate the ToastRecord and then finish its window
token. This didn't have any side-effects because NM itself finishes the
tokens after a time-out. Added a test for this.

Bug: 152973950
Test: atest android.widget.cts29.ToastTest android.widget.cts.ToastTest
      ToastWindowTest ToastUITest NotificationManagerServiceTest
      LegacyToastTest
Change-Id: I13cf18890ca22022adb7576c8ecf3285a9b82299
diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java
index 4f14539..08b3293 100644
--- a/core/java/android/widget/Toast.java
+++ b/core/java/android/widget/Toast.java
@@ -117,7 +117,6 @@
     private final Binder mToken;
     private final Context mContext;
     private final Handler mHandler;
-    private final ToastPresenter mPresenter;
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
     final TN mTN;
     @UnsupportedAppUsage
@@ -165,8 +164,8 @@
         looper = getLooper(looper);
         mHandler = new Handler(looper);
         mCallbacks = new ArrayList<>();
-        mPresenter = new ToastPresenter(context, AccessibilityManager.getInstance(context));
-        mTN = new TN(mPresenter, context.getPackageName(), mToken, mCallbacks, looper);
+        mTN = new TN(context, context.getPackageName(), mToken,
+                mCallbacks, looper);
         mTN.mY = context.getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.toast_y_offset);
         mTN.mGravity = context.getResources().getInteger(
@@ -496,7 +495,7 @@
             return result;
         } else {
             Toast result = new Toast(context, looper);
-            View v = result.mPresenter.getTextToastView(text);
+            View v = ToastPresenter.getTextToastView(context, text);
             result.mNextView = v;
             result.mDuration = duration;
 
@@ -565,13 +564,14 @@
         if (sService != null) {
             return sService;
         }
-        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
+        sService = INotificationManager.Stub.asInterface(
+                ServiceManager.getService(Context.NOTIFICATION_SERVICE));
         return sService;
     }
 
     private static class TN extends ITransientNotification.Stub {
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
-        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
+        private final WindowManager.LayoutParams mParams;
 
         private static final int SHOW = 0;
         private static final int HIDE = 1;
@@ -608,9 +608,13 @@
          * The parameter {@code callbacks} is not copied and is accessed with itself as its own
          * lock.
          */
-        TN(ToastPresenter presenter, String packageName, Binder token, List<Callback> callbacks,
+        TN(Context context, String packageName, Binder token, List<Callback> callbacks,
                 @Nullable Looper looper) {
-            mPresenter = presenter;
+            WindowManager windowManager = context.getSystemService(WindowManager.class);
+            AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(context);
+            mPresenter = new ToastPresenter(context, windowManager, accessibilityManager,
+                    getService(), packageName);
+            mParams = mPresenter.getLayoutParams();
             mPackageName = packageName;
             mToken = token;
             mCallbacks = callbacks;
@@ -645,8 +649,6 @@
                     }
                 }
             };
-
-            presenter.startLayoutParams(mParams, packageName);
         }
 
         private List<Callback> getCallbacks() {
@@ -691,31 +693,9 @@
                 // remove the old view if necessary
                 handleHide();
                 mView = mNextView;
-                Context context = mView.getContext().getApplicationContext();
-                if (context == null) {
-                    context = mView.getContext();
-                }
-                mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
-                mPresenter.adjustLayoutParams(mParams, windowToken, mDuration, mGravity, mX, mY,
-                        mHorizontalMargin, mVerticalMargin);
-                if (mView.getParent() != null) {
-                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
-                    mWM.removeView(mView);
-                }
-                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
-                // Since the notification manager service cancels the token right
-                // after it notifies us to cancel the toast there is an inherent
-                // race and we may attempt to add a window after the token has been
-                // invalidated. Let us hedge against that.
-                try {
-                    mWM.addView(mView, mParams);
-                    mPresenter.trySendAccessibilityEvent(mView, mPackageName);
-                    for (Callback callback : getCallbacks()) {
-                        callback.onToastShown();
-                    }
-                } catch (WindowManager.BadTokenException e) {
-                    /* ignore */
-                }
+                mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
+                        mHorizontalMargin, mVerticalMargin,
+                        new CallbackBinder(getCallbacks(), mHandler));
             }
         }
 
@@ -723,25 +703,9 @@
         public void handleHide() {
             if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
             if (mView != null) {
-                // note: checking parent() just to make sure the view has
-                // been added...  i have seen cases where we get here when
-                // the view isn't yet added, so let's try not to crash.
-                if (mView.getParent() != null) {
-                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
-                    mWM.removeViewImmediate(mView);
-                }
-
-
-                // Now that we've removed the view it's safe for the server to release
-                // the resources.
-                try {
-                    getService().finishToken(mPackageName, mToken);
-                } catch (RemoteException e) {
-                }
-
-                for (Callback callback : getCallbacks()) {
-                    callback.onToastHidden();
-                }
+                checkState(mView == mPresenter.getView(),
+                        "Trying to hide toast view different than the last one displayed");
+                mPresenter.hide(new CallbackBinder(getCallbacks(), mHandler));
                 mView = null;
             }
         }
diff --git a/core/java/android/widget/ToastPresenter.java b/core/java/android/widget/ToastPresenter.java
index 0447b6b..e9d4aa6 100644
--- a/core/java/android/widget/ToastPresenter.java
+++ b/core/java/android/widget/ToastPresenter.java
@@ -16,11 +16,18 @@
 
 package android.widget;
 
+import static com.android.internal.util.Preconditions.checkState;
+
+import android.annotation.Nullable;
+import android.app.INotificationManager;
+import android.app.ITransientNotificationCallback;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.PixelFormat;
 import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -37,41 +44,94 @@
  * @hide
  */
 public class ToastPresenter {
+    private static final String TAG = "ToastPresenter";
+    private static final String WINDOW_TITLE = "Toast";
     private static final long SHORT_DURATION_TIMEOUT = 4000;
     private static final long LONG_DURATION_TIMEOUT = 7000;
 
+    /**
+     * Returns the default text toast view for message {@code text}.
+     */
+    public static View getTextToastView(Context context, CharSequence text) {
+        View view = LayoutInflater.from(context).inflate(
+                R.layout.transient_notification, null);
+        TextView textView = view.findViewById(com.android.internal.R.id.message);
+        textView.setText(text);
+        return view;
+    }
+
     private final Context mContext;
     private final Resources mResources;
+    private final WindowManager mWindowManager;
     private final AccessibilityManager mAccessibilityManager;
+    private final INotificationManager mNotificationManager;
+    private final String mPackageName;
+    private final WindowManager.LayoutParams mParams;
+    @Nullable private View mView;
+    @Nullable private IBinder mToken;
 
-    public ToastPresenter(Context context, AccessibilityManager accessibilityManager) {
+    public ToastPresenter(Context context, WindowManager windowManager,
+            AccessibilityManager accessibilityManager,
+            INotificationManager notificationManager, String packageName) {
         mContext = context;
         mResources = context.getResources();
+        mWindowManager = windowManager;
         mAccessibilityManager = accessibilityManager;
+        mNotificationManager = notificationManager;
+        mPackageName = packageName;
+        mParams = createLayoutParams();
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    public WindowManager.LayoutParams getLayoutParams() {
+        return mParams;
     }
 
     /**
-     * Initializes {@code params} with default values for toasts.
+     * Returns the {@link View} being shown at the moment or {@code null} if no toast is being
+     * displayed.
      */
-    public void startLayoutParams(WindowManager.LayoutParams params, String packageName) {
+    @Nullable
+    public View getView() {
+        return mView;
+    }
+
+    /**
+     * Returns the {@link IBinder} token used to display the toast or {@code null} if there is no
+     * toast being shown at the moment.
+     */
+    @Nullable
+    public IBinder getToken() {
+        return mToken;
+    }
+
+    /**
+     * Creates {@link WindowManager.LayoutParams} with default values for toasts.
+     */
+    private WindowManager.LayoutParams createLayoutParams() {
+        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
         params.height = WindowManager.LayoutParams.WRAP_CONTENT;
         params.width = WindowManager.LayoutParams.WRAP_CONTENT;
         params.format = PixelFormat.TRANSLUCENT;
         params.windowAnimations = R.style.Animation_Toast;
         params.type = WindowManager.LayoutParams.TYPE_TOAST;
         params.setFitInsetsIgnoringVisibility(true);
-        params.setTitle("Toast");
+        params.setTitle(WINDOW_TITLE);
         params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
-        setShowForAllUsersIfApplicable(params, packageName);
+        setShowForAllUsersIfApplicable(params, mPackageName);
+        return params;
     }
 
     /**
      * Customizes {@code params} according to other parameters, ready to be passed to {@link
      * WindowManager#addView(View, ViewGroup.LayoutParams)}.
      */
-    public void adjustLayoutParams(WindowManager.LayoutParams params, IBinder windowToken,
+    private void adjustLayoutParams(WindowManager.LayoutParams params, IBinder windowToken,
             int duration, int gravity, int xOffset, int yOffset, float horizontalMargin,
             float verticalMargin) {
         Configuration config = mResources.getConfiguration();
@@ -97,7 +157,7 @@
      * Sets {@link WindowManager.LayoutParams#SYSTEM_FLAG_SHOW_FOR_ALL_USERS} flag if {@code
      * packageName} is a cross-user package.
      *
-     * Implementation note:
+     * <p>Implementation note:
      *     This code is safe to be executed in SystemUI and the app's process:
      *         <li>SystemUI: It's running on a trusted domain so apps can't tamper with it. SystemUI
      *             has the permission INTERNAL_SYSTEM_WINDOW needed by the flag, so SystemUI can add
@@ -120,14 +180,66 @@
     }
 
     /**
-     * Returns the default text toast view for message {@code text}.
+     * Shows the toast in {@code view} with the parameters passed and callback {@code callback}.
      */
-    public View getTextToastView(CharSequence text) {
-        View view = LayoutInflater.from(mContext).inflate(
-                R.layout.transient_notification, null);
-        TextView textView = view.findViewById(com.android.internal.R.id.message);
-        textView.setText(text);
-        return view;
+    public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
+            int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
+            @Nullable ITransientNotificationCallback callback) {
+        checkState(mView == null, "Only one toast at a time is allowed, call hide() first.");
+        mView = view;
+        mToken = token;
+
+        adjustLayoutParams(mParams, windowToken, duration, gravity, xOffset, yOffset,
+                horizontalMargin, verticalMargin);
+        if (mView.getParent() != null) {
+            mWindowManager.removeView(mView);
+        }
+        try {
+            mWindowManager.addView(mView, mParams);
+        } catch (WindowManager.BadTokenException e) {
+            // Since the notification manager service cancels the token right after it notifies us
+            // to cancel the toast there is an inherent race and we may attempt to add a window
+            // after the token has been invalidated. Let us hedge against that.
+            Log.w(TAG, "Error while attempting to show toast from " + mPackageName, e);
+            return;
+        }
+        trySendAccessibilityEvent(mView, mPackageName);
+        if (callback != null) {
+            try {
+                callback.onToastShown();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
+            }
+        }
+    }
+
+    /**
+     * Hides toast that was shown using {@link #show(View, IBinder, IBinder, int,
+     * int, int, int, float, float, ITransientNotificationCallback)}.
+     *
+     * <p>This method has to be called on the same thread on which {@link #show(View, IBinder,
+     * IBinder, int, int, int, int, float, float, ITransientNotificationCallback)} was called.
+     */
+    public void hide(@Nullable ITransientNotificationCallback callback) {
+        checkState(mView != null, "No toast to hide.");
+
+        if (mView.getParent() != null) {
+            mWindowManager.removeViewImmediate(mView);
+        }
+        try {
+            mNotificationManager.finishToken(mPackageName, mToken);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error finishing toast window token from package " + mPackageName, e);
+        }
+        if (callback != null) {
+            try {
+                callback.onToastHidden();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastHide()", e);
+            }
+        }
+        mView = null;
+        mToken = null;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
index 0242e834..9ccb9bf 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
@@ -21,15 +21,13 @@
 import android.app.INotificationManager;
 import android.app.ITransientNotificationCallback;
 import android.content.Context;
+import android.content.res.Resources;
 import android.os.IBinder;
-import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.util.Log;
 import android.view.View;
-import android.view.ViewGroup.LayoutParams;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
-import android.widget.Toast;
 import android.widget.ToastPresenter;
 
 import com.android.internal.R;
@@ -49,18 +47,14 @@
 public class ToastUI extends SystemUI implements CommandQueue.Callbacks {
     private static final String TAG = "ToastUI";
 
-    /**
-     * Values taken from {@link Toast}.
-     */
-    private static final long DURATION_SHORT = 4000;
-    private static final long DURATION_LONG = 7000;
-
     private final CommandQueue mCommandQueue;
     private final WindowManager mWindowManager;
     private final INotificationManager mNotificationManager;
     private final AccessibilityManager mAccessibilityManager;
-    private final ToastPresenter mPresenter;
-    private ToastEntry mCurrentToast;
+    private final int mGravity;
+    private final int mY;
+    @Nullable private ToastPresenter mPresenter;
+    @Nullable private ITransientNotificationCallback mCallback;
 
     @Inject
     public ToastUI(Context context, CommandQueue commandQueue) {
@@ -79,7 +73,9 @@
         mWindowManager = windowManager;
         mNotificationManager = notificationManager;
         mAccessibilityManager = accessibilityManager;
-        mPresenter = new ToastPresenter(context, accessibilityManager);
+        Resources resources = mContext.getResources();
+        mGravity = resources.getInteger(R.integer.config_toastDefaultGravity);
+        mY = resources.getDimensionPixelSize(R.dimen.toast_y_offset);
     }
 
     @Override
@@ -91,33 +87,21 @@
     @MainThread
     public void showToast(String packageName, IBinder token, CharSequence text,
             IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) {
-        if (mCurrentToast != null) {
+        if (mPresenter != null) {
             hideCurrentToast();
         }
-        View view = mPresenter.getTextToastView(text);
-        LayoutParams params = getLayoutParams(packageName, windowToken, duration);
-        mCurrentToast = new ToastEntry(packageName, token, view, windowToken, callback);
-        try {
-            mWindowManager.addView(view, params);
-        } catch (WindowManager.BadTokenException e) {
-            Log.w(TAG, "Error while attempting to show toast from " + packageName, e);
-            return;
-        }
-        mPresenter.trySendAccessibilityEvent(view, packageName);
-        if (callback != null) {
-            try {
-                callback.onToastShown();
-            } catch (RemoteException e) {
-                Log.w(TAG, "Error calling back " + packageName + " to notify onToastShow()", e);
-            }
-        }
+        View view = ToastPresenter.getTextToastView(mContext, text);
+        mCallback = callback;
+        mPresenter = new ToastPresenter(mContext, mWindowManager, mAccessibilityManager,
+                mNotificationManager, packageName);
+        mPresenter.show(view, token, windowToken, duration, mGravity, 0, mY, 0, 0, mCallback);
     }
 
     @Override
     @MainThread
     public void hideToast(String packageName, IBinder token) {
-        if (mCurrentToast == null || !Objects.equals(mCurrentToast.packageName, packageName)
-                || !Objects.equals(mCurrentToast.token, token)) {
+        if (mPresenter == null || !Objects.equals(mPresenter.getPackageName(), packageName)
+                || !Objects.equals(mPresenter.getToken(), token)) {
             Log.w(TAG, "Attempt to hide non-current toast from package " + packageName);
             return;
         }
@@ -126,51 +110,7 @@
 
     @MainThread
     private void hideCurrentToast() {
-        if (mCurrentToast.view.getParent() != null) {
-            mWindowManager.removeViewImmediate(mCurrentToast.view);
-        }
-        String packageName = mCurrentToast.packageName;
-        try {
-            mNotificationManager.finishToken(packageName, mCurrentToast.windowToken);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Error finishing toast window token from package " + packageName, e);
-        }
-        if (mCurrentToast.callback != null) {
-            try {
-                mCurrentToast.callback.onToastHidden();
-            } catch (RemoteException e) {
-                Log.w(TAG, "Error calling back " + packageName + " to notify onToastHide()", e);
-            }
-        }
-        mCurrentToast = null;
-    }
-
-    private LayoutParams getLayoutParams(String packageName, IBinder windowToken, int duration) {
-        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
-        mPresenter.startLayoutParams(params, packageName);
-        int gravity = mContext.getResources().getInteger(
-                com.android.internal.R.integer.config_toastDefaultGravity);
-        int yOffset = mContext.getResources().getDimensionPixelSize(R.dimen.toast_y_offset);
-        mPresenter.adjustLayoutParams(params, windowToken, duration, gravity, 0, yOffset, 0, 0);
-        return params;
-    }
-
-    private static class ToastEntry {
-        public final String packageName;
-        public final IBinder token;
-        public final View view;
-        public final IBinder windowToken;
-
-        @Nullable
-        public final ITransientNotificationCallback callback;
-
-        private ToastEntry(String packageName, IBinder token, View view, IBinder windowToken,
-                @Nullable ITransientNotificationCallback callback) {
-            this.packageName = packageName;
-            this.token = token;
-            this.view = view;
-            this.windowToken = windowToken;
-            this.callback = callback;
-        }
+        mPresenter.hide(mCallback);
+        mPresenter = null;
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java b/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java
index bc3a5b1..65fbe79 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java
@@ -176,7 +176,7 @@
 
         mToastUI.hideToast(PACKAGE_NAME_1, TOKEN_1);
 
-        verify(mNotificationManager).finishToken(PACKAGE_NAME_1, WINDOW_TOKEN_1);
+        verify(mNotificationManager).finishToken(PACKAGE_NAME_1, TOKEN_1);
     }
 
     @Test
@@ -218,7 +218,7 @@
         mToastUI.showToast(PACKAGE_NAME_2, TOKEN_2, TEXT, WINDOW_TOKEN_2, Toast.LENGTH_LONG, null);
 
         verify(mWindowManager).removeViewImmediate(view);
-        verify(mNotificationManager).finishToken(PACKAGE_NAME_1, WINDOW_TOKEN_1);
+        verify(mNotificationManager).finishToken(PACKAGE_NAME_1, TOKEN_1);
         verify(mCallback).onToastHidden();
     }