From 50737c90ae00d665e7585fc7896ad55684a79ec7 Mon Sep 17 00:00:00 2001 From: Beverly Date: Tue, 26 Jan 2021 14:16:22 -0500 Subject: [PATCH] Update Toast visuals + motion Puts Toast style/animation changes behind a FeatureFlag Toasts can be added via Toast APIs OR legacy apps can add the window directly. Because toast windows can be added directly by legacy apps, DisplayPolicy.java requires that toast windows are removed after TOAST_WINDOW_TIMEOUT. This also means Toast windows added via the Toast APIs are subject to this timeout. Therefore, this CL also increases the timeout by 600ms to account for SystemUI's in/out toast animations. Bug: 169587378 Test: atest ToastUITest Test: manual, rotate device while toast is showing Change-Id: Ib17655c253f4ec4f7018cc5f7939b88a50ea5014 --- core/java/android/widget/ToastPresenter.java | 44 +++-- .../android/systemui/plugins/ToastPlugin.java | 5 + .../res/drawable/toast_background.xml | 21 +++ packages/SystemUI/res/layout/text_toast.xml | 51 ++++++ packages/SystemUI/res/values-land/dimens.xml | 2 + packages/SystemUI/res/values/dimens.xml | 7 + packages/SystemUI/res/values/flags.xml | 2 + .../systemui/statusbar/FeatureFlags.java | 4 + .../android/systemui/toast/SystemUIToast.java | 136 +++++++++++++--- .../systemui/toast/ToastDefaultAnimation.kt | 105 ++++++++++++ .../android/systemui/toast/ToastFactory.java | 21 ++- .../com/android/systemui/toast/ToastLogger.kt | 9 ++ .../com/android/systemui/toast/ToastUI.java | 150 +++++++++++++----- .../android/systemui/toast/ToastUITest.java | 18 +-- .../NotificationManagerService.java | 23 ++- .../server/policy/PhoneWindowManager.java | 17 +- 16 files changed, 524 insertions(+), 91 deletions(-) create mode 100644 packages/SystemUI/res/drawable/toast_background.xml create mode 100644 packages/SystemUI/res/layout/text_toast.xml create mode 100644 packages/SystemUI/src/com/android/systemui/toast/ToastDefaultAnimation.kt diff --git a/core/java/android/widget/ToastPresenter.java b/core/java/android/widget/ToastPresenter.java index b484dfacbf6cb..2904a8c889a27 100644 --- a/core/java/android/widget/ToastPresenter.java +++ b/core/java/android/widget/ToastPresenter.java @@ -171,6 +171,22 @@ public class ToastPresenter { } } + /** + * Update the LayoutParameters of the currently showing toast view. This is used for layout + * updates based on orientation changes. + */ + public void updateLayoutParams(int xOffset, int yOffset, float horizontalMargin, + float verticalMargin, int gravity) { + checkState(mView != null, "Toast must be showing to update its layout parameters."); + Configuration config = mResources.getConfiguration(); + mParams.gravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection()); + mParams.x = xOffset; + mParams.y = yOffset; + mParams.horizontalMargin = horizontalMargin; + mParams.verticalMargin = verticalMargin; + addToastView(); + } + /** * Sets {@link WindowManager.LayoutParams#SYSTEM_FLAG_SHOW_FOR_ALL_USERS} flag if {@code * packageName} is a cross-user package. @@ -221,18 +237,7 @@ public class ToastPresenter { adjustLayoutParams(mParams, windowToken, duration, gravity, xOffset, yOffset, horizontalMargin, verticalMargin, removeWindowAnimations); - 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; - } + addToastView(); trySendAccessibilityEvent(mView, mPackageName); if (callback != null) { try { @@ -288,4 +293,19 @@ public class ToastPresenter { view.dispatchPopulateAccessibilityEvent(event); mAccessibilityManager.sendAccessibilityEvent(event); } + + private void addToastView() { + 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; + } + } } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ToastPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ToastPlugin.java index 0831e0ef77957..da079cf04403b 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ToastPlugin.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ToastPlugin.java @@ -103,5 +103,10 @@ public interface ToastPlugin extends Plugin { default Animator getOutAnimation() { return null; } + + /** + * Called on orientation changes. + */ + default void onOrientationChange(int orientation) { } } } diff --git a/packages/SystemUI/res/drawable/toast_background.xml b/packages/SystemUI/res/drawable/toast_background.xml new file mode 100644 index 0000000000000..5c45e8346e3c1 --- /dev/null +++ b/packages/SystemUI/res/drawable/toast_background.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/packages/SystemUI/res/layout/text_toast.xml b/packages/SystemUI/res/layout/text_toast.xml new file mode 100644 index 0000000000000..de4e062805fec --- /dev/null +++ b/packages/SystemUI/res/layout/text_toast.xml @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 51d7b8eff5fc3..24c7655e5ae4e 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -52,4 +52,6 @@ 4dp 8dp + + 24dp diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index afa98b56a13cc..da86ac9d326fe 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1346,4 +1346,11 @@ 24dp 12dp + + 296dp + 1 + 14sp + 48dp + 48dp + 28dp diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml index 01e54ff605823..02ea323f0465b 100644 --- a/packages/SystemUI/res/values/flags.xml +++ b/packages/SystemUI/res/values/flags.xml @@ -36,4 +36,6 @@ false + + false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java index e7b60c3a0d67a..504b0d9457158 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java @@ -70,4 +70,8 @@ public class FeatureFlags { public boolean isPeopleTileEnabled() { return mFlagReader.isEnabled(R.bool.flag_conversations); } + + public boolean isToastStyleEnabled() { + return mFlagReader.isEnabled(R.bool.flag_toast_style); + } } diff --git a/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java b/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java index e9fcf1aa95986..365cd2a5d20b3 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java +++ b/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java @@ -20,10 +20,21 @@ import android.animation.Animator; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.util.Log; +import android.view.LayoutInflater; import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; import android.widget.ToastPresenter; import com.android.internal.R; +import com.android.launcher3.icons.IconFactory; import com.android.systemui.plugins.ToastPlugin; /** @@ -35,23 +46,43 @@ public class SystemUIToast implements ToastPlugin.Toast { final CharSequence mText; final ToastPlugin.Toast mPluginToast; - final int mDefaultGravity; - final int mDefaultY; + private final String mPackageName; + private final int mUserId; + private final LayoutInflater mLayoutInflater; + private final boolean mToastStyleEnabled; + final int mDefaultX = 0; final int mDefaultHorizontalMargin = 0; final int mDefaultVerticalMargin = 0; - SystemUIToast(Context context, CharSequence text) { - this(context, text, null); + private int mDefaultY; + private int mDefaultGravity; + + @NonNull private final View mToastView; + @Nullable private final Animator mInAnimator; + @Nullable private final Animator mOutAnimator; + + SystemUIToast(LayoutInflater layoutInflater, Context context, CharSequence text, + String packageName, int userId, boolean toastStyleEnabled, int orientation) { + this(layoutInflater, context, text, null, packageName, userId, + toastStyleEnabled, orientation); } - SystemUIToast(Context context, CharSequence text, ToastPlugin.Toast pluginToast) { + SystemUIToast(LayoutInflater layoutInflater, Context context, CharSequence text, + ToastPlugin.Toast pluginToast, String packageName, int userId, + boolean toastStyleEnabled, int orientation) { + mToastStyleEnabled = toastStyleEnabled; + mLayoutInflater = layoutInflater; mContext = context; mText = text; mPluginToast = pluginToast; + mPackageName = packageName; + mUserId = userId; + mToastView = inflateToastView(); + mInAnimator = createInAnimator(); + mOutAnimator = createOutAnimator(); - mDefaultGravity = context.getResources().getInteger(R.integer.config_toastDefaultGravity); - mDefaultY = context.getResources().getDimensionPixelSize(R.dimen.toast_y_offset); + onOrientationChange(orientation); } @Override @@ -102,28 +133,19 @@ public class SystemUIToast implements ToastPlugin.Toast { @Override @NonNull public View getView() { - if (isPluginToast() && mPluginToast.getView() != null) { - return mPluginToast.getView(); - } - return ToastPresenter.getTextToastView(mContext, mText); + return mToastView; } @Override @Nullable public Animator getInAnimation() { - if (isPluginToast() && mPluginToast.getInAnimation() != null) { - return mPluginToast.getInAnimation(); - } - return null; + return mInAnimator; } @Override @Nullable public Animator getOutAnimation() { - if (isPluginToast() && mPluginToast.getOutAnimation() != null) { - return mPluginToast.getOutAnimation(); - } - return null; + return mOutAnimator; } /** @@ -136,4 +158,80 @@ public class SystemUIToast implements ToastPlugin.Toast { private boolean isPluginToast() { return mPluginToast != null; } + + private View inflateToastView() { + if (isPluginToast() && mPluginToast.getView() != null) { + return mPluginToast.getView(); + } + + View toastView; + if (mToastStyleEnabled) { + toastView = mLayoutInflater.inflate( + com.android.systemui.R.layout.text_toast, null); + ((TextView) toastView.findViewById(com.android.systemui.R.id.text)).setText(mText); + + ((ImageView) toastView.findViewById(com.android.systemui.R.id.icon)) + .setImageDrawable(getBadgedIcon(mContext, mPackageName, mUserId)); + } else { + toastView = ToastPresenter.getTextToastView(mContext, mText); + } + + return toastView; + } + + /** + * Called on orientation changes to update parameters associated with the toast placement. + */ + public void onOrientationChange(int orientation) { + if (mPluginToast != null) { + mPluginToast.onOrientationChange(orientation); + } + + mDefaultY = mContext.getResources().getDimensionPixelSize( + mToastStyleEnabled + ? com.android.systemui.R.dimen.toast_y_offset + : R.dimen.toast_y_offset); + mDefaultGravity = + mContext.getResources().getInteger(R.integer.config_toastDefaultGravity); + } + + private Animator createInAnimator() { + if (isPluginToast() && mPluginToast.getInAnimation() != null) { + return mPluginToast.getInAnimation(); + } + + return mToastStyleEnabled + ? ToastDefaultAnimation.Companion.toastIn(getView()) + : null; + } + + private Animator createOutAnimator() { + if (isPluginToast() && mPluginToast.getOutAnimation() != null) { + return mPluginToast.getOutAnimation(); + } + return mToastStyleEnabled + ? ToastDefaultAnimation.Companion.toastOut(getView()) + : null; + } + + /** + * Get badged app icon if necessary, similar as used in the Settings UI. + * @return The icon to use + */ + public static Drawable getBadgedIcon(@NonNull Context context, String packageName, + int userId) { + final PackageManager packageManager = context.getPackageManager(); + try { + final ApplicationInfo appInfo = packageManager.getApplicationInfoAsUser( + packageName, PackageManager.GET_META_DATA, userId); + UserHandle user = UserHandle.getUserHandleForUid(appInfo.uid); + IconFactory iconFactory = IconFactory.obtain(context); + Bitmap iconBmp = iconFactory.createBadgedIconBitmap( + appInfo.loadUnbadgedIcon(packageManager), user, false).icon; + return new BitmapDrawable(context.getResources(), iconBmp); + } catch (PackageManager.NameNotFoundException e) { + Log.e("SystemUIToast", "could not load icon for package=" + packageName + " e=" + e); + return null; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastDefaultAnimation.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastDefaultAnimation.kt new file mode 100644 index 0000000000000..603d69057e5c5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastDefaultAnimation.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2021 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.systemui.toast + +import android.animation.ObjectAnimator +import android.view.View +import android.view.animation.LinearInterpolator +import android.view.animation.PathInterpolator +import android.animation.AnimatorSet + +class ToastDefaultAnimation { + /** + * sum of the in and out animation durations cannot exceed + * [com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_ANIM_BUFFER] to prevent the toast + * window from being removed before animations are completed + */ + companion object { + // total duration shouldn't exceed NotificationManagerService's delay for "in" animation + fun toastIn(view: View): AnimatorSet? { + val icon: View? = view.findViewById(com.android.systemui.R.id.icon) + val text: View? = view.findViewById(com.android.systemui.R.id.text) + if (icon == null || text == null) { + return null + } + val linearInterp = LinearInterpolator() + val scaleInterp = PathInterpolator(0f, 0f, 0f, 1f) + val sX = ObjectAnimator.ofFloat(view, "scaleX", 0.9f, 1f).apply { + interpolator = scaleInterp + duration = 333 + } + val sY = ObjectAnimator.ofFloat(view, "scaleY", 0.9f, 1f).apply { + interpolator = scaleInterp + duration = 333 + } + val vA = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f).apply { + interpolator = linearInterp + duration = 66 + } + text.alpha = 0f // Set now otherwise won't apply until start delay + val tA = ObjectAnimator.ofFloat(text, "alpha", 0f, 1f).apply { + interpolator = linearInterp + duration = 283 + startDelay = 50 + } + icon.alpha = 0f // Set now otherwise won't apply until start delay + val iA = ObjectAnimator.ofFloat(icon, "alpha", 0f, 1f).apply { + interpolator = linearInterp + duration = 283 + startDelay = 50 + } + return AnimatorSet().apply { + playTogether(sX, sY, vA, tA, iA) + } + } + + fun toastOut(view: View): AnimatorSet? { + // total duration shouldn't exceed NotificationManagerService's delay for "out" anim + val icon: View? = view.findViewById(com.android.systemui.R.id.icon) + val text: View? = view.findViewById(com.android.systemui.R.id.text) + if (icon == null || text == null) { + return null + } + val linearInterp = LinearInterpolator() + val scaleInterp = PathInterpolator(0.3f, 0f, 1f, 1f) + val sX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.9f).apply { + interpolator = scaleInterp + duration = 250 + } + val sY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.9f).apply { + interpolator = scaleInterp + duration = 250 + } + val vA = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f).apply { + interpolator = linearInterp + duration = 100 + startDelay = 150 + } + val tA = ObjectAnimator.ofFloat(text, "alpha", 1f, 0f).apply { + interpolator = linearInterp + duration = 166 + } + val iA = ObjectAnimator.ofFloat(icon, "alpha", 1f, 0f).apply { + interpolator = linearInterp + duration = 166 + } + return AnimatorSet().apply { + playTogether(sX, sY, vA, tA, iA) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java b/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java index d8cb61c6b3497..8b782d4b79231 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java @@ -17,6 +17,7 @@ package com.android.systemui.toast; import android.content.Context; +import android.view.LayoutInflater; import androidx.annotation.NonNull; @@ -26,6 +27,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.ToastPlugin; import com.android.systemui.shared.plugins.PluginManager; +import com.android.systemui.statusbar.FeatureFlags; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -40,10 +42,18 @@ import javax.inject.Inject; public class ToastFactory implements Dumpable { // only one ToastPlugin can be connected at a time. private ToastPlugin mPlugin; + private final LayoutInflater mLayoutInflater; + private final boolean mToastStyleEnabled; @Inject - public ToastFactory(PluginManager pluginManager, DumpManager dumpManager) { + public ToastFactory( + LayoutInflater layoutInflater, + PluginManager pluginManager, + DumpManager dumpManager, + FeatureFlags featureFlags) { + mLayoutInflater = layoutInflater; dumpManager.registerDumpable("ToastFactory", this); + mToastStyleEnabled = featureFlags.isToastStyleEnabled(); pluginManager.addPluginListener( new PluginListener() { @Override @@ -64,11 +74,13 @@ public class ToastFactory implements Dumpable { * Create a toast to be shown by ToastUI. */ public SystemUIToast createToast(Context context, CharSequence text, String packageName, - int userId) { + int userId, int orientation) { if (isPluginAvailable()) { - return new SystemUIToast(context, text, mPlugin.createToast(text, packageName, userId)); + return new SystemUIToast(mLayoutInflater, context, text, mPlugin.createToast(text, + packageName, userId), packageName, userId, mToastStyleEnabled, orientation); } - return new SystemUIToast(context, text); + return new SystemUIToast(mLayoutInflater, context, text, packageName, userId, + mToastStyleEnabled, orientation); } private boolean isPluginAvailable() { @@ -79,5 +91,6 @@ public class ToastFactory implements Dumpable { public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { pw.println("ToastFactory:"); pw.println(" mAttachedPlugin=" + mPlugin); + pw.println(" mToastStyleEnabled=" + mToastStyleEnabled); } } diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt index 78173cf62a93d..51541bd3032eb 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt @@ -49,6 +49,15 @@ class ToastLogger @Inject constructor( }) } + fun logOrientationChange(text: String, isPortrait: Boolean) { + log(DEBUG, { + str1 = text + bool1 = isPortrait + }, { + "Orientation change for toast. msg=\'$str1\' isPortrait=$bool1" + }) + } + private inline fun log( logLevel: LogLevel, initializer: LogMessage.() -> Unit, diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java index 409d1361223c1..92ea1d0e5fbdd 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java @@ -16,27 +16,28 @@ package com.android.systemui.toast; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + import android.animation.Animator; import android.annotation.MainThread; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.INotificationManager; import android.app.ITransientNotificationCallback; import android.content.Context; +import android.content.res.Configuration; import android.os.IBinder; import android.os.ServiceManager; import android.os.UserHandle; import android.util.Log; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IAccessibilityManager; -import android.widget.Toast; import android.widget.ToastPresenter; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.SystemUI; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.util.concurrency.DelayableExecutor; import java.util.Objects; @@ -58,18 +59,19 @@ public class ToastUI extends SystemUI implements CommandQueue.Callbacks { private final IAccessibilityManager mIAccessibilityManager; private final AccessibilityManager mAccessibilityManager; private final ToastFactory mToastFactory; - private final DelayableExecutor mMainExecutor; private final ToastLogger mToastLogger; private SystemUIToast mToast; @Nullable private ToastPresenter mPresenter; @Nullable private ITransientNotificationCallback mCallback; + private ToastOutAnimatorListener mToastOutAnimatorListener; + + private int mOrientation = ORIENTATION_PORTRAIT; @Inject public ToastUI( Context context, CommandQueue commandQueue, ToastFactory toastFactory, - @Main DelayableExecutor mainExecutor, ToastLogger toastLogger) { this(context, commandQueue, INotificationManager.Stub.asInterface( @@ -77,21 +79,19 @@ public class ToastUI extends SystemUI implements CommandQueue.Callbacks { IAccessibilityManager.Stub.asInterface( ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)), toastFactory, - mainExecutor, toastLogger); } @VisibleForTesting ToastUI(Context context, CommandQueue commandQueue, INotificationManager notificationManager, @Nullable IAccessibilityManager accessibilityManager, - ToastFactory toastFactory, DelayableExecutor mainExecutor, ToastLogger toastLogger + ToastFactory toastFactory, ToastLogger toastLogger ) { super(context); mCommandQueue = commandQueue; mNotificationManager = notificationManager; mIAccessibilityManager = accessibilityManager; mToastFactory = toastFactory; - mMainExecutor = mainExecutor; mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mToastLogger = toastLogger; } @@ -105,36 +105,38 @@ public class ToastUI extends SystemUI implements CommandQueue.Callbacks { @MainThread public void showToast(int uid, String packageName, IBinder token, CharSequence text, IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) { - if (mPresenter != null) { - hideCurrentToast(); - } - UserHandle userHandle = UserHandle.getUserHandleForUid(uid); - Context context = mContext.createContextAsUser(userHandle, 0); - mToast = mToastFactory.createToast(context, text, packageName, userHandle.getIdentifier()); + Runnable showToastRunnable = () -> { + UserHandle userHandle = UserHandle.getUserHandleForUid(uid); + Context context = mContext.createContextAsUser(userHandle, 0); + mToast = mToastFactory.createToast(mContext /* sysuiContext */, text, packageName, + userHandle.getIdentifier(), mOrientation); - if (mToast.hasCustomAnimation()) { if (mToast.getInAnimation() != null) { mToast.getInAnimation().start(); } - final Animator hideAnimator = mToast.getOutAnimation(); - if (hideAnimator != null) { - final long durationMillis = duration == Toast.LENGTH_LONG - ? TOAST_LONG_TIME : TOAST_SHORT_TIME; - final long updatedDuration = mAccessibilityManager.getRecommendedTimeoutMillis( - (int) durationMillis, AccessibilityManager.FLAG_CONTENT_TEXT); - mMainExecutor.executeDelayed(() -> hideAnimator.start(), - updatedDuration - hideAnimator.getTotalDuration()); - } + + mCallback = callback; + mPresenter = new ToastPresenter(context, mIAccessibilityManager, + mNotificationManager, packageName); + // Set as trusted overlay so touches can pass through toasts + mPresenter.getLayoutParams().setTrustedOverlay(); + mToastLogger.logOnShowToast(uid, packageName, text.toString(), token.toString()); + mPresenter.show(mToast.getView(), token, windowToken, duration, mToast.getGravity(), + mToast.getXOffset(), mToast.getYOffset(), mToast.getHorizontalMargin(), + mToast.getVerticalMargin(), mCallback, mToast.hasCustomAnimation()); + }; + + if (mToastOutAnimatorListener != null) { + // if we're currently animating out a toast, show new toast after prev toast is hidden + mToastOutAnimatorListener.setShowNextToastRunnable(showToastRunnable); + } else if (mPresenter != null) { + // if there's a toast already showing that we haven't tried hiding yet, hide it and + // then show the next toast after its hidden animation is done + hideCurrentToast(showToastRunnable); + } else { + // else, show this next toast immediately + showToastRunnable.run(); } - mCallback = callback; - mPresenter = new ToastPresenter(context, mIAccessibilityManager, mNotificationManager, - packageName); - // Set as trusted overlay so touches can pass through toasts - mPresenter.getLayoutParams().setTrustedOverlay(); - mToastLogger.logOnShowToast(uid, packageName, text.toString(), token.toString()); - mPresenter.show(mToast.getView(), token, windowToken, duration, mToast.getGravity(), - mToast.getXOffset(), mToast.getYOffset(), mToast.getHorizontalMargin(), - mToast.getVerticalMargin(), mCallback, mToast.hasCustomAnimation()); } @Override @@ -146,12 +148,88 @@ public class ToastUI extends SystemUI implements CommandQueue.Callbacks { return; } mToastLogger.logOnHideToast(packageName, token.toString()); - hideCurrentToast(); + hideCurrentToast(null); } @MainThread - private void hideCurrentToast() { - mPresenter.hide(mCallback); + private void hideCurrentToast(Runnable runnable) { + if (mToast.getOutAnimation() != null) { + Animator animator = mToast.getOutAnimation(); + mToastOutAnimatorListener = new ToastOutAnimatorListener(mPresenter, mCallback, + runnable); + animator.addListener(mToastOutAnimatorListener); + animator.start(); + } else { + mPresenter.hide(mCallback); + if (runnable != null) { + runnable.run(); + } + } + mToast = null; mPresenter = null; + mCallback = null; + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + if (newConfig.orientation != mOrientation) { + mOrientation = newConfig.orientation; + if (mToast != null) { + mToastLogger.logOrientationChange(mToast.mText.toString(), + mOrientation == ORIENTATION_PORTRAIT); + mToast.onOrientationChange(mOrientation); + mPresenter.updateLayoutParams( + mToast.getXOffset(), + mToast.getYOffset(), + mToast.getHorizontalMargin(), + mToast.getVerticalMargin(), + mToast.getGravity()); + } + } + } + + /** + * Once the out animation for a toast is finished, start showing the next toast. + */ + class ToastOutAnimatorListener implements Animator.AnimatorListener { + final ToastPresenter mPrevPresenter; + final ITransientNotificationCallback mPrevCallback; + @Nullable Runnable mShowNextToastRunnable; + + ToastOutAnimatorListener( + @NonNull ToastPresenter presenter, + @NonNull ITransientNotificationCallback callback, + @Nullable Runnable runnable) { + mPrevPresenter = presenter; + mPrevCallback = callback; + mShowNextToastRunnable = runnable; + } + + void setShowNextToastRunnable(Runnable runnable) { + mShowNextToastRunnable = runnable; + } + + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + mPrevPresenter.hide(mPrevCallback); + if (mShowNextToastRunnable != null) { + mShowNextToastRunnable.run(); + } + mToastOutAnimatorListener = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + onAnimationEnd(animation); + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } } } 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 c743fd07c492f..365c62cddbdf6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java @@ -57,8 +57,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; +import com.android.systemui.statusbar.FeatureFlags; import org.junit.Before; import org.junit.Test; @@ -88,7 +87,6 @@ public class ToastUITest extends SysuiTestCase { private static final String TEXT = "Hello World"; private static final int MESSAGE_RES_ID = R.id.message; - private FakeExecutor mFakeDelayableExecutor = new FakeExecutor(new FakeSystemClock()); private Context mContextSpy; private ToastUI mToastUI; @Mock private LayoutInflater mLayoutInflater; @@ -99,6 +97,7 @@ public class ToastUITest extends SysuiTestCase { @Mock private PluginManager mPluginManager; @Mock private DumpManager mDumpManager; @Mock private ToastLogger mToastLogger; + @Mock private FeatureFlags mFeatureFlags; @Mock private ITransientNotificationCallback mCallback; @Captor private ArgumentCaptor mViewCaptor; @@ -107,12 +106,9 @@ public class ToastUITest extends SysuiTestCase { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - - // This is because inflate will result in WindowManager (WM) calls, which will fail since we - // are mocking it, so we mock LayoutInflater with the view obtained before mocking WM. - View view = ToastPresenter.getTextToastView(mContext, TEXT); - when(mLayoutInflater.inflate(eq(TEXT_TOAST_LAYOUT), any())).thenReturn(view); - mContext.addMockSystemService(LayoutInflater.class, mLayoutInflater); + when(mLayoutInflater.inflate(eq(TEXT_TOAST_LAYOUT), any())).thenReturn( + ToastPresenter.getTextToastView(mContext, TEXT)); + when(mFeatureFlags.isToastStyleEnabled()).thenReturn(false); mContext.addMockSystemService(WindowManager.class, mWindowManager); mContextSpy = spy(mContext); @@ -120,8 +116,8 @@ public class ToastUITest extends SysuiTestCase { doReturn(mContextSpy).when(mContextSpy).createContextAsUser(any(), anyInt()); mToastUI = new ToastUI(mContextSpy, mCommandQueue, mNotificationManager, - mAccessibilityManager, new ToastFactory(mPluginManager, mDumpManager), - mFakeDelayableExecutor, mToastLogger); + mAccessibilityManager, new ToastFactory(mLayoutInflater, mPluginManager, + mDumpManager, mFeatureFlags), mToastLogger); } @Test diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 5757d680919e3..eb241d98ca5c2 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -110,6 +110,8 @@ import static com.android.internal.util.Preconditions.checkArgument; import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER; +import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_ANIM_BUFFER; +import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_TIMEOUT; import static com.android.server.utils.PriorityDump.PRIORITY_ARG; import static com.android.server.utils.PriorityDump.PRIORITY_ARG_CRITICAL; import static com.android.server.utils.PriorityDump.PRIORITY_ARG_NORMAL; @@ -284,7 +286,6 @@ import com.android.server.notification.toast.CustomToastRecord; import com.android.server.notification.toast.TextToastRecord; import com.android.server.notification.toast.ToastRecord; import com.android.server.pm.PackageManagerService; -import com.android.server.policy.PhoneWindowManager; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.uri.UriGrantsManagerInternal; import com.android.server.utils.quota.MultiRateLimiter; @@ -358,7 +359,7 @@ public class NotificationManagerService extends SystemService { private static final int MESSAGE_RECONSIDER_RANKING = 1000; private static final int MESSAGE_RANKING_SORT = 1001; - static final int LONG_DELAY = PhoneWindowManager.TOAST_WINDOW_TIMEOUT; + static final int LONG_DELAY = TOAST_WINDOW_TIMEOUT - TOAST_WINDOW_ANIM_BUFFER; // 3.5 seconds static final int SHORT_DELAY = 2000; // 2 seconds // 1 second past the ANR timeout. @@ -3054,7 +3055,7 @@ public class NotificationManagerService extends SystemService { // If the callback fails, this will remove it from the list, so don't // assume that it's valid after this. if (index == 0) { - showNextToastLocked(); + showNextToastLocked(false); } } finally { Binder.restoreCallingIdentity(callingId); @@ -7416,7 +7417,7 @@ public class NotificationManagerService extends SystemService { } @GuardedBy("mToastQueue") - void showNextToastLocked() { + void showNextToastLocked(boolean lastToastWasTextRecord) { if (mIsCurrentToastShown) { return; // Don't show the same toast twice. } @@ -7430,7 +7431,7 @@ public class NotificationManagerService extends SystemService { mToastRateLimiter.isWithinQuota(userId, record.pkg, TOAST_QUOTA_TAG); if (tryShowToast(record, rateLimitingEnabled, isWithinQuota)) { - scheduleDurationReachedLocked(record); + scheduleDurationReachedLocked(record, lastToastWasTextRecord); mIsCurrentToastShown = true; if (rateLimitingEnabled) { mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG); @@ -7501,7 +7502,7 @@ public class NotificationManagerService extends SystemService { // Show the next one. If the callback fails, this will remove // it from the list, so don't assume that the list hasn't changed // after this point. - showNextToastLocked(); + showNextToastLocked(lastToast instanceof TextToastRecord); } } @@ -7515,7 +7516,7 @@ public class NotificationManagerService extends SystemService { } @GuardedBy("mToastQueue") - private void scheduleDurationReachedLocked(ToastRecord r) + private void scheduleDurationReachedLocked(ToastRecord r, boolean lastToastWasTextRecord) { mHandler.removeCallbacksAndMessages(r); Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r); @@ -7525,6 +7526,14 @@ public class NotificationManagerService extends SystemService { // preference. delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay, AccessibilityManager.FLAG_CONTENT_TEXT); + + if (lastToastWasTextRecord) { + delay += 250; // delay to account for previous toast's "out" animation + } + if (r instanceof TextToastRecord) { + delay += 333; // delay to account for this toast's "in" animation + } + mHandler.sendMessageDelayed(m, delay); } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index bc819617332de..6a441f1830d1d 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -346,8 +346,21 @@ public class PhoneWindowManager implements WindowManagerPolicy { /** Amount of time (in milliseconds) to wait for windows drawn before powering on. */ static final int WAITING_FOR_DRAWN_TIMEOUT = 1000; - /** Amount of time (in milliseconds) a toast window can be shown. */ - public static final int TOAST_WINDOW_TIMEOUT = 3500; // 3.5 seconds + /** + * Extra time for additional SystemUI animations. + *

Since legacy apps can add Toast windows directly instead of using Toast APIs, + * {@link DisplayPolicy} ensures that the window manager removes toast windows after + * TOAST_WINDOW_TIMEOUT. We increase this timeout by TOAST_WINDOW_ANIM_BUFFER to account for + * SystemUI's in/out toast animations, so that the toast text is still shown for a minimum + * of 3.5 seconds and the animations are finished before window manager removes the window. + */ + public static final int TOAST_WINDOW_ANIM_BUFFER = 600; + + /** + * Amount of time (in milliseconds) a toast window can be shown before it's automatically + * removed by window manager. + */ + public static final int TOAST_WINDOW_TIMEOUT = 3500 + TOAST_WINDOW_ANIM_BUFFER; /** * Lock protecting internal state. Must not call out into window