diff options
| author | Bernardo Rufino <brufino@google.com> | 2020-04-01 16:46:45 +0100 |
|---|---|---|
| committer | Bernardo Rufino <brufino@google.com> | 2020-04-03 12:57:08 +0000 |
| commit | dd4f23812774226a29b27d01e183f6a24fbed2d3 (patch) | |
| tree | f54961b8a7a5bad301e6e4d2edf18ac8e622211a /core/java | |
| parent | f9617e33f94f1459d0b24dba46c443ebcea96542 (diff) | |
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
Diffstat (limited to 'core/java')
| -rw-r--r-- | core/java/android/widget/Toast.java | 72 | ||||
| -rw-r--r-- | core/java/android/widget/ToastPresenter.java | 140 |
2 files changed, 144 insertions, 68 deletions
diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java index 4f14539dd976..08b32930971a 100644 --- a/core/java/android/widget/Toast.java +++ b/core/java/android/widget/Toast.java @@ -117,7 +117,6 @@ public class Toast { 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 @@ public class Toast { 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 @@ public class Toast { 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 @@ public class Toast { 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 @@ public class Toast { * 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 @@ public class Toast { } } }; - - presenter.startLayoutParams(mParams, packageName); } private List<Callback> getCallbacks() { @@ -691,31 +693,9 @@ public class Toast { // 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 class Toast { 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 0447b6bb9f11..e9d4aa668891 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 @@ import com.android.internal.util.ArrayUtils; * @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; + } + + /** + * Returns the {@link View} being shown at the moment or {@code null} if no toast is being + * displayed. + */ + @Nullable + public View getView() { + return mView; } /** - * Initializes {@code params} with default values for toasts. + * Returns the {@link IBinder} token used to display the toast or {@code null} if there is no + * toast being shown at the moment. */ - public void startLayoutParams(WindowManager.LayoutParams params, String packageName) { + @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 @@ public class ToastPresenter { * 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 @@ public class ToastPresenter { } /** - * 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; } /** |
