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