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