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
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,5 +103,10 @@ public interface ToastPlugin extends Plugin {
|
||||
default Animator getOutAnimation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on orientation changes.
|
||||
*/
|
||||
default void onOrientationChange(int orientation) { }
|
||||
}
|
||||
}
|
||||
|
||||
21
packages/SystemUI/res/drawable/toast_background.xml
Normal file
21
packages/SystemUI/res/drawable/toast_background.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#FFFFFFFF" />
|
||||
<corners android:radius="@dimen/toast_bg_radius" />
|
||||
</shape>
|
||||
51
packages/SystemUI/res/layout/text_toast.xml
Normal file
51
packages/SystemUI/res/layout/text_toast.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxWidth="@dimen/toast_width"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/toast_background"
|
||||
android:backgroundTint="?android:attr/colorBackground"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<!-- Icon should be 24x24, make slightly larger to allow for shadowing, adjust via padding -->
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:alpha="@dimen/toast_icon_alpha"
|
||||
android:padding="11.5dp"
|
||||
android:layout_width="@dimen/toast_icon_size"
|
||||
android:layout_height="@dimen/toast_icon_size"/>
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="22dp"
|
||||
android:textSize="@dimen/toast_text_size"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:fontFamily="@*android:string/config_headlineFontFamily"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
||||
@@ -52,4 +52,6 @@
|
||||
<!-- (footer_height -48dp)/2 -->
|
||||
<dimen name="controls_management_footer_top_margin">4dp</dimen>
|
||||
<dimen name="controls_management_favorites_top_margin">8dp</dimen>
|
||||
|
||||
<dimen name="toast_y_offset">24dp</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -1346,4 +1346,11 @@
|
||||
<dimen name="rounded_slider_icon_size">24dp</dimen>
|
||||
<!-- rounded_slider_icon_size / 2 -->
|
||||
<dimen name="rounded_slider_icon_inset">12dp</dimen>
|
||||
|
||||
<dimen name="toast_width">296dp</dimen>
|
||||
<item name="toast_icon_alpha" format="float" type="dimen">1</item>
|
||||
<dimen name="toast_text_size">14sp</dimen>
|
||||
<dimen name="toast_y_offset">48dp</dimen>
|
||||
<dimen name="toast_icon_size">48dp</dimen>
|
||||
<dimen name="toast_bg_radius">28dp</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -36,4 +36,6 @@
|
||||
|
||||
<!-- People Tile flag -->
|
||||
<bool name="flag_conversations">false</bool>
|
||||
|
||||
<bool name="flag_toast_style">false</bool>
|
||||
</resources>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ToastPlugin>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<View> 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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
* <p>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
|
||||
|
||||
Reference in New Issue
Block a user