Unify toast presentation code

Extract window layout construction and view inflation from Toast and
ToastUI into common @hide class ToastPresenter. This is desirable by
itself but also in preparation for making system toasts visible to all
users.

Bug: 149408635
Test: atest ToastUITest android.widget.cts.ToastTest
      android.widget.cts29.ToastTest android.server.wm.ToastTest

Change-Id: Ifdd7ee37687da2e4c7abed61569cf342f95b2611
diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java
index 9e4ebfe..436d683 100644
--- a/core/java/android/widget/Toast.java
+++ b/core/java/android/widget/Toast.java
@@ -31,9 +31,7 @@
 import android.compat.annotation.EnabledAfter;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
-import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.graphics.PixelFormat;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Handler;
@@ -43,11 +41,8 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.util.Log;
-import android.view.Gravity;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.WindowManager;
-import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 
 import com.android.internal.annotations.GuardedBy;
@@ -122,6 +117,7 @@
     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
@@ -172,7 +168,8 @@
         looper = getLooper(looper);
         mHandler = new Handler(looper);
         mCallbacks = new ArrayList<>();
-        mTN = new TN(context.getPackageName(), mToken, mCallbacks, looper);
+        mPresenter = new ToastPresenter(context, AccessibilityManager.getInstance(context));
+        mTN = new TN(mPresenter, context.getPackageName(), mToken, mCallbacks, looper);
         mTN.mY = context.getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.toast_y_offset);
         mTN.mGravity = context.getResources().getInteger(
@@ -504,13 +501,7 @@
             return result;
         } else {
             Toast result = new Toast(context, looper);
-
-            LayoutInflater inflate = (LayoutInflater)
-                    context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-            View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
-            TextView tv = (TextView) v.findViewById(com.android.internal.R.id.message);
-            tv.setText(text);
-
+            View v = result.mPresenter.getTextToastView(text);
             result.mNextView = v;
             result.mDuration = duration;
 
@@ -611,34 +602,20 @@
 
         final String mPackageName;
         final Binder mToken;
+        private final ToastPresenter mPresenter;
 
         @GuardedBy("mCallbacks")
         private final List<Callback> mCallbacks;
 
-        static final long SHORT_DURATION_TIMEOUT = 4000;
-        static final long LONG_DURATION_TIMEOUT = 7000;
-
         /**
          * Creates a {@link ITransientNotification} object.
          *
          * The parameter {@code callbacks} is not copied and is accessed with itself as its own
          * lock.
          */
-        TN(String packageName, Binder token, List<Callback> callbacks, @Nullable Looper looper) {
-            // XXX This should be changed to use a Dialog, with a Theme.Toast
-            // defined that sets up the layout params appropriately.
-            final WindowManager.LayoutParams params = mParams;
-            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
-            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
-            params.format = PixelFormat.TRANSLUCENT;
-            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
-            params.type = WindowManager.LayoutParams.TYPE_TOAST;
-            params.setFitInsetsIgnoringVisibility(true);
-            params.setTitle("Toast");
-            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
-                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
-
+        TN(ToastPresenter presenter, String packageName, Binder token, List<Callback> callbacks,
+                @Nullable Looper looper) {
+            mPresenter = presenter;
             mPackageName = packageName;
             mToken = token;
             mCallbacks = callbacks;
@@ -673,6 +650,8 @@
                     }
                 }
             };
+
+            presenter.startLayoutParams(mParams);
         }
 
         private List<Callback> getCallbacks() {
@@ -718,30 +697,12 @@
                 handleHide();
                 mView = mNextView;
                 Context context = mView.getContext().getApplicationContext();
-                String packageName = mView.getContext().getOpPackageName();
                 if (context == null) {
                     context = mView.getContext();
                 }
-                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
-                // We can resolve the Gravity here by using the Locale for getting
-                // the layout direction
-                final Configuration config = mView.getContext().getResources().getConfiguration();
-                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
-                mParams.gravity = gravity;
-                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
-                    mParams.horizontalWeight = 1.0f;
-                }
-                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
-                    mParams.verticalWeight = 1.0f;
-                }
-                mParams.x = mX;
-                mParams.y = mY;
-                mParams.verticalMargin = mVerticalMargin;
-                mParams.horizontalMargin = mHorizontalMargin;
-                mParams.packageName = packageName;
-                mParams.hideTimeoutMilliseconds = mDuration ==
-                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
-                mParams.token = windowToken;
+                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);
@@ -753,7 +714,7 @@
                 // invalidated. Let us hedge against that.
                 try {
                     mWM.addView(mView, mParams);
-                    trySendAccessibilityEvent();
+                    mPresenter.trySendAccessibilityEvent(mView, mPackageName);
                     for (Callback callback : getCallbacks()) {
                         callback.onToastShown();
                     }
@@ -763,22 +724,6 @@
             }
         }
 
-        private void trySendAccessibilityEvent() {
-            AccessibilityManager accessibilityManager =
-                    AccessibilityManager.getInstance(mView.getContext());
-            if (!accessibilityManager.isEnabled()) {
-                return;
-            }
-            // treat toasts as notifications since they are used to
-            // announce a transient piece of information to the user
-            AccessibilityEvent event = AccessibilityEvent.obtain(
-                    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
-            event.setClassName(getClass().getName());
-            event.setPackageName(mView.getContext().getPackageName());
-            mView.dispatchPopulateAccessibilityEvent(event);
-            accessibilityManager.sendAccessibilityEvent(event);
-        }
-
         @UnsupportedAppUsage
         public void handleHide() {
             if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
diff --git a/core/java/android/widget/ToastPresenter.java b/core/java/android/widget/ToastPresenter.java
new file mode 100644
index 0000000..654ab50
--- /dev/null
+++ b/core/java/android/widget/ToastPresenter.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 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 android.widget;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.os.IBinder;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.internal.R;
+
+/**
+ * Class responsible for toast presentation inside app's process and in system UI.
+ *
+ * @hide
+ */
+public class ToastPresenter {
+    private static final long SHORT_DURATION_TIMEOUT = 4000;
+    private static final long LONG_DURATION_TIMEOUT = 7000;
+
+    private final Context mContext;
+    private final AccessibilityManager mAccessibilityManager;
+
+    public ToastPresenter(Context context, AccessibilityManager accessibilityManager) {
+        mContext = context;
+        mAccessibilityManager = accessibilityManager;
+    }
+
+    /**
+     * Initializes {@code params} with default values for toasts.
+     */
+    public void startLayoutParams(WindowManager.LayoutParams params) {
+        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.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+    }
+
+    /**
+     * 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,
+            int duration, int gravity, int xOffset, int yOffset, float horizontalMargin,
+            float verticalMargin) {
+        Configuration config = mContext.getResources().getConfiguration();
+        int absGravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection());
+        params.gravity = absGravity;
+        if ((absGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
+            params.horizontalWeight = 1.0f;
+        }
+        if ((absGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
+            params.verticalWeight = 1.0f;
+        }
+        params.x = xOffset;
+        params.y = yOffset;
+        params.horizontalMargin = horizontalMargin;
+        params.verticalMargin = verticalMargin;
+        params.packageName = mContext.getPackageName();
+        params.hideTimeoutMilliseconds =
+                (duration == Toast.LENGTH_LONG) ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
+        params.token = windowToken;
+    }
+
+    /**
+     * Returns the default text toast view for message {@code text}.
+     */
+    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;
+    }
+
+    /**
+     * Sends {@link AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED} event if accessibility is
+     * enabled.
+     */
+    public void trySendAccessibilityEvent(View view, String packageName) {
+        if (!mAccessibilityManager.isEnabled()) {
+            return;
+        }
+        AccessibilityEvent event = AccessibilityEvent.obtain(
+                AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
+        event.setClassName(Toast.class.getName());
+        event.setPackageName(packageName);
+        view.dispatchPopulateAccessibilityEvent(event);
+        mAccessibilityManager.sendAccessibilityEvent(event);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
index edab4a7..34a2520 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
@@ -21,21 +21,16 @@
 import android.app.INotificationManager;
 import android.app.ITransientNotificationCallback;
 import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.PixelFormat;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.util.Log;
-import android.view.Gravity;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup.LayoutParams;
 import android.view.WindowManager;
-import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
-import android.widget.TextView;
 import android.widget.Toast;
+import android.widget.ToastPresenter;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
@@ -64,6 +59,7 @@
     private final WindowManager mWindowManager;
     private final INotificationManager mNotificationManager;
     private final AccessibilityManager mAccessibilityManager;
+    private final ToastPresenter mPresenter;
     private ToastEntry mCurrentToast;
 
     @Inject
@@ -83,6 +79,7 @@
         mWindowManager = windowManager;
         mNotificationManager = notificationManager;
         mAccessibilityManager = accessibilityManager;
+        mPresenter = new ToastPresenter(context, accessibilityManager);
     }
 
     @Override
@@ -97,7 +94,7 @@
         if (mCurrentToast != null) {
             hideCurrentToast();
         }
-        View view = getView(text);
+        View view = mPresenter.getTextToastView(text);
         LayoutParams params = getLayoutParams(windowToken, duration);
         mCurrentToast = new ToastEntry(packageName, token, view, windowToken, callback);
         try {
@@ -106,7 +103,7 @@
             Log.w(TAG, "Error while attempting to show toast from " + packageName, e);
             return;
         }
-        trySendAccessibilityEvent(view, packageName);
+        mPresenter.trySendAccessibilityEvent(view, packageName);
         if (callback != null) {
             try {
                 callback.onToastShown();
@@ -148,56 +145,13 @@
         mCurrentToast = null;
     }
 
-    private void trySendAccessibilityEvent(View view, String packageName) {
-        if (!mAccessibilityManager.isEnabled()) {
-            return;
-        }
-        AccessibilityEvent event = AccessibilityEvent.obtain(
-                AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
-        event.setClassName(Toast.class.getName());
-        event.setPackageName(packageName);
-        view.dispatchPopulateAccessibilityEvent(event);
-        mAccessibilityManager.sendAccessibilityEvent(event);
-    }
-
-    private View getView(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;
-    }
-
     private LayoutParams getLayoutParams(IBinder windowToken, int duration) {
         WindowManager.LayoutParams params = new WindowManager.LayoutParams();
-        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
-        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
-        params.format = PixelFormat.TRANSLUCENT;
-        params.windowAnimations = com.android.internal.R.style.Animation_Toast;
-        params.type = WindowManager.LayoutParams.TYPE_TOAST;
-        params.setTitle("Toast");
-        params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
-                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
-        Configuration config = mContext.getResources().getConfiguration();
-        int specificGravity = mContext.getResources().getInteger(
+        mPresenter.startLayoutParams(params);
+        int gravity = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_toastDefaultGravity);
-        int gravity = Gravity.getAbsoluteGravity(specificGravity, config.getLayoutDirection());
-        params.gravity = gravity;
-        if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
-            params.horizontalWeight = 1.0f;
-        }
-        if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
-            params.verticalWeight = 1.0f;
-        }
-        params.x = 0;
-        params.y = mContext.getResources().getDimensionPixelSize(R.dimen.toast_y_offset);
-        params.verticalMargin = 0;
-        params.horizontalMargin = 0;
-        params.packageName = mContext.getPackageName();
-        params.hideTimeoutMilliseconds =
-                (duration == Toast.LENGTH_LONG) ? DURATION_LONG : DURATION_SHORT;
-        params.token = windowToken;
+        int yOffset = mContext.getResources().getDimensionPixelSize(R.dimen.toast_y_offset);
+        mPresenter.adjustLayoutParams(params, windowToken, duration, gravity, 0, yOffset, 0, 0);
         return params;
     }