1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import static com.android.internal.util.Preconditions.checkNotNull;
20 import static com.android.internal.util.Preconditions.checkState;
21 
22 import android.annotation.IntDef;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.StringRes;
26 import android.app.INotificationManager;
27 import android.app.ITransientNotification;
28 import android.app.ITransientNotificationCallback;
29 import android.compat.Compatibility;
30 import android.compat.annotation.ChangeId;
31 import android.compat.annotation.EnabledAfter;
32 import android.compat.annotation.UnsupportedAppUsage;
33 import android.content.Context;
34 import android.content.res.Resources;
35 import android.graphics.drawable.Drawable;
36 import android.os.Binder;
37 import android.os.Build;
38 import android.os.Handler;
39 import android.os.IBinder;
40 import android.os.Looper;
41 import android.os.Message;
42 import android.os.RemoteException;
43 import android.os.ServiceManager;
44 import android.util.Log;
45 import android.view.View;
46 import android.view.WindowManager;
47 import android.view.accessibility.IAccessibilityManager;
48 import android.widget.flags.Flags;
49 
50 import com.android.internal.annotations.GuardedBy;
51 import com.android.internal.annotations.VisibleForTesting;
52 
53 import java.lang.annotation.Retention;
54 import java.lang.annotation.RetentionPolicy;
55 import java.lang.ref.WeakReference;
56 import java.util.ArrayList;
57 import java.util.List;
58 
59 /**
60  * A toast is a view containing a quick little message for the user.  The toast class
61  * helps you create and show those.
62  * {@more}
63  *
64  * <p>
65  * When the view is shown to the user, appears as a floating view over the
66  * application.  It will never receive focus.  The user will probably be in the
67  * middle of typing something else.  The idea is to be as unobtrusive as
68  * possible, while still showing the user the information you want them to see.
69  * Two examples are the volume control, and the brief message saying that your
70  * settings have been saved.
71  * <p>
72  * The easiest way to use this class is to call one of the static methods that constructs
73  * everything you need and returns a new Toast object.
74  * <p>
75  * Note that
76  * <a href="{@docRoot}reference/com/google/android/material/snackbar/Snackbar">Snackbars</a> are
77  * preferred for brief messages while the app is in the foreground.
78  * <p>
79  * Note that toasts being sent from the background are rate limited, so avoid sending such toasts
80  * in quick succession.
81  * <p>
82  * Starting with Android 12 (API level 31), apps targeting Android 12 or newer will have
83  * their toasts limited to two lines.
84  *
85  * <div class="special reference">
86  * <h3>Developer Guides</h3>
87  * <p>For information about creating Toast notifications, read the
88  * <a href="{@docRoot}guide/topics/ui/notifiers/toasts.html">Toast Notifications</a> developer
89  * guide.</p>
90  * </div>
91  */
92 public class Toast {
93     static final String TAG = "Toast";
94     static final boolean localLOGV = false;
95 
96     /** @hide */
97     @IntDef(prefix = { "LENGTH_" }, value = {
98             LENGTH_SHORT,
99             LENGTH_LONG
100     })
101     @Retention(RetentionPolicy.SOURCE)
102     public @interface Duration {}
103 
104     /**
105      * Show the view or text notification for a short period of time.  This time
106      * could be user-definable.  This is the default.
107      * @see #setDuration
108      */
109     public static final int LENGTH_SHORT = 0;
110 
111     /**
112      * Show the view or text notification for a long period of time.  This time
113      * could be user-definable.
114      * @see #setDuration
115      */
116     public static final int LENGTH_LONG = 1;
117 
118     /**
119      * Text toasts will be rendered by SystemUI instead of in-app, so apps can't circumvent
120      * background custom toast restrictions.
121      */
122     @ChangeId
123     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
124     private static final long CHANGE_TEXT_TOASTS_IN_THE_SYSTEM = 147798919L;
125 
126 
127     private final Binder mToken;
128     private final Context mContext;
129     private final Handler mHandler;
130     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
131     final TN mTN;
132     @UnsupportedAppUsage
133     int mDuration;
134 
135     /**
136      * This is also passed to {@link TN} object, where it's also accessed with itself as its own
137      * lock.
138      */
139     @GuardedBy("mCallbacks")
140     private final List<Callback> mCallbacks;
141 
142     /**
143      * View to be displayed, in case this is a custom toast (e.g. not created with {@link
144      * #makeText(Context, int, int)} or its variants).
145      */
146     @Nullable
147     private View mNextView;
148 
149     /**
150      * Text to be shown, in case this is NOT a custom toast (e.g. created with {@link
151      * #makeText(Context, int, int)} or its variants).
152      */
153     @Nullable
154     private CharSequence mText;
155 
156     /**
157      * Construct an empty Toast object.  You must call {@link #setView} before you
158      * can call {@link #show}.
159      *
160      * @param context  The context to use.  Usually your {@link android.app.Activity} object.
161      */
Toast(Context context)162     public Toast(Context context) {
163         this(context, null);
164     }
165 
166     /**
167      * Constructs an empty Toast object.  If looper is null, Looper.myLooper() is used.
168      * @hide
169      */
Toast(@onNull Context context, @Nullable Looper looper)170     public Toast(@NonNull Context context, @Nullable Looper looper) {
171         mContext = context;
172         mToken = new Binder();
173         looper = getLooper(looper);
174         mHandler = new Handler(looper);
175         mCallbacks = new ArrayList<>();
176         mTN = new TN(context, context.getPackageName(), mToken,
177                 mCallbacks, looper);
178         mTN.mY = context.getResources().getDimensionPixelSize(
179                 com.android.internal.R.dimen.toast_y_offset);
180         mTN.mGravity = context.getResources().getInteger(
181                 com.android.internal.R.integer.config_toastDefaultGravity);
182     }
183 
getLooper(@ullable Looper looper)184     private Looper getLooper(@Nullable Looper looper) {
185         if (looper != null) {
186             return looper;
187         }
188         return checkNotNull(Looper.myLooper(),
189                 "Can't toast on a thread that has not called Looper.prepare()");
190     }
191 
192     /**
193      * Show the view for the specified duration.
194      *
195      * <p>Note that toasts being sent from the background are rate limited, so avoid sending such
196      * toasts in quick succession.
197      */
show()198     public void show() {
199         if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
200             checkState(mNextView != null || mText != null, "You must either set a text or a view");
201         } else {
202             if (mNextView == null) {
203                 throw new RuntimeException("setView must have been called");
204             }
205         }
206 
207         INotificationManager service = getService();
208         String pkg = mContext.getOpPackageName();
209         TN tn = mTN;
210         if (Flags.toastNoWeakref()) {
211             tn.mNextView = mNextView;
212         } else {
213             tn.mNextViewWeakRef = new WeakReference<>(mNextView);
214         }
215         final boolean isUiContext = mContext.isUiContext();
216         final int displayId = mContext.getDisplayId();
217 
218         boolean wasEnqueued = false;
219         try {
220             if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
221                 if (mNextView != null) {
222                     // It's a custom toast
223                     wasEnqueued = service.enqueueToast(pkg, mToken, tn, mDuration, isUiContext,
224                             displayId);
225                 } else {
226                     // It's a text toast
227                     ITransientNotificationCallback callback =
228                             new CallbackBinder(mCallbacks, mHandler);
229                     wasEnqueued = service.enqueueTextToast(pkg, mToken, mText, mDuration,
230                             isUiContext, displayId, callback);
231                 }
232             } else {
233                 wasEnqueued = service.enqueueToast(pkg, mToken, tn, mDuration, isUiContext,
234                         displayId);
235             }
236         } catch (RemoteException e) {
237             // Empty
238         } finally {
239             if (Flags.toastNoWeakref()) {
240                 if (!wasEnqueued) {
241                     tn.mNextViewWeakRef = null;
242                     tn.mNextView = null;
243                 }
244             }
245         }
246     }
247 
248     /**
249      * Close the view if it's showing, or don't show it if it isn't showing yet.
250      * You do not normally have to call this.  Normally view will disappear on its own
251      * after the appropriate duration.
252      */
cancel()253     public void cancel() {
254         if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)
255                 && mNextView == null) {
256             try {
257                 getService().cancelToast(mContext.getOpPackageName(), mToken);
258             } catch (RemoteException e) {
259                 // Empty
260             }
261         } else {
262             mTN.cancel();
263         }
264     }
265 
266     /**
267      * Set the view to show.
268      *
269      * @see #getView
270      * @deprecated Custom toast views are deprecated. Apps can create a standard text toast with the
271      *      {@link #makeText(Context, CharSequence, int)} method, or use a
272      *      <a href="{@docRoot}reference/com/google/android/material/snackbar/Snackbar">Snackbar</a>
273      *      when in the foreground. Starting from Android {@link Build.VERSION_CODES#R}, apps
274      *      targeting API level {@link Build.VERSION_CODES#R} or higher that are in the background
275      *      will not have custom toast views displayed.
276      */
277     @Deprecated
setView(View view)278     public void setView(View view) {
279         mNextView = view;
280     }
281 
282     /**
283      * Return the view.
284      *
285      * <p>Toasts constructed with {@link #Toast(Context)} that haven't called {@link #setView(View)}
286      * with a non-{@code null} view will return {@code null} here.
287      *
288      * <p>Starting from Android {@link Build.VERSION_CODES#R}, in apps targeting API level {@link
289      * Build.VERSION_CODES#R} or higher, toasts constructed with {@link #makeText(Context,
290      * CharSequence, int)} or its variants will also return {@code null} here unless they had called
291      * {@link #setView(View)} with a non-{@code null} view. If you want to be notified when the
292      * toast is shown or hidden, use {@link #addCallback(Callback)}.
293      *
294      * @see #setView
295      * @deprecated Custom toast views are deprecated. Apps can create a standard text toast with the
296      *      {@link #makeText(Context, CharSequence, int)} method, or use a
297      *      <a href="{@docRoot}reference/com/google/android/material/snackbar/Snackbar">Snackbar</a>
298      *      when in the foreground. Starting from Android {@link Build.VERSION_CODES#R}, apps
299      *      targeting API level {@link Build.VERSION_CODES#R} or higher that are in the background
300      *      will not have custom toast views displayed.
301      */
302     @Deprecated
getView()303     @Nullable public View getView() {
304         return mNextView;
305     }
306 
307     /**
308      * Set how long to show the view for.
309      * @see #LENGTH_SHORT
310      * @see #LENGTH_LONG
311      */
setDuration(@uration int duration)312     public void setDuration(@Duration int duration) {
313         mDuration = duration;
314         mTN.mDuration = duration;
315     }
316 
317     /**
318      * Return the duration.
319      * @see #setDuration
320      */
321     @Duration
getDuration()322     public int getDuration() {
323         return mDuration;
324     }
325 
326     /**
327      * Set the margins of the view.
328      *
329      * <p><strong>Warning:</strong> Starting from Android {@link Build.VERSION_CODES#R}, for apps
330      * targeting API level {@link Build.VERSION_CODES#R} or higher, this method is a no-op when
331      * called on text toasts.
332      *
333      * @param horizontalMargin The horizontal margin, in percentage of the
334      *        container width, between the container's edges and the
335      *        notification
336      * @param verticalMargin The vertical margin, in percentage of the
337      *        container height, between the container's edges and the
338      *        notification
339      */
setMargin(float horizontalMargin, float verticalMargin)340     public void setMargin(float horizontalMargin, float verticalMargin) {
341         if (isSystemRenderedTextToast()) {
342             Log.e(TAG, "setMargin() shouldn't be called on text toasts, the values won't be used");
343         }
344         mTN.mHorizontalMargin = horizontalMargin;
345         mTN.mVerticalMargin = verticalMargin;
346     }
347 
348     /**
349      * Return the horizontal margin.
350      *
351      * <p><strong>Warning:</strong> Starting from Android {@link Build.VERSION_CODES#R}, for apps
352      * targeting API level {@link Build.VERSION_CODES#R} or higher, this method shouldn't be called
353      * on text toasts as its return value may not reflect actual value since text toasts are not
354      * rendered by the app anymore.
355      */
getHorizontalMargin()356     public float getHorizontalMargin() {
357         if (isSystemRenderedTextToast()) {
358             Log.e(TAG, "getHorizontalMargin() shouldn't be called on text toasts, the result may "
359                     + "not reflect actual values.");
360         }
361         return mTN.mHorizontalMargin;
362     }
363 
364     /**
365      * Return the vertical margin.
366      *
367      * <p><strong>Warning:</strong> Starting from Android {@link Build.VERSION_CODES#R}, for apps
368      * targeting API level {@link Build.VERSION_CODES#R} or higher, this method shouldn't be called
369      * on text toasts as its return value may not reflect actual value since text toasts are not
370      * rendered by the app anymore.
371      */
getVerticalMargin()372     public float getVerticalMargin() {
373         if (isSystemRenderedTextToast()) {
374             Log.e(TAG, "getVerticalMargin() shouldn't be called on text toasts, the result may not"
375                     + " reflect actual values.");
376         }
377         return mTN.mVerticalMargin;
378     }
379 
380     /**
381      * Set the location at which the notification should appear on the screen.
382      *
383      * <p><strong>Warning:</strong> Starting from Android {@link Build.VERSION_CODES#R}, for apps
384      * targeting API level {@link Build.VERSION_CODES#R} or higher, this method is a no-op when
385      * called on text toasts.
386      *
387      * @see android.view.Gravity
388      * @see #getGravity
389      */
setGravity(int gravity, int xOffset, int yOffset)390     public void setGravity(int gravity, int xOffset, int yOffset) {
391         if (isSystemRenderedTextToast()) {
392             Log.e(TAG, "setGravity() shouldn't be called on text toasts, the values won't be used");
393         }
394         mTN.mGravity = gravity;
395         mTN.mX = xOffset;
396         mTN.mY = yOffset;
397     }
398 
399      /**
400      * Get the location at which the notification should appear on the screen.
401      *
402      * <p><strong>Warning:</strong> Starting from Android {@link Build.VERSION_CODES#R}, for apps
403      * targeting API level {@link Build.VERSION_CODES#R} or higher, this method shouldn't be called
404      * on text toasts as its return value may not reflect actual value since text toasts are not
405      * rendered by the app anymore.
406      *
407      * @see android.view.Gravity
408      * @see #getGravity
409      */
getGravity()410     public int getGravity() {
411         if (isSystemRenderedTextToast()) {
412             Log.e(TAG, "getGravity() shouldn't be called on text toasts, the result may not reflect"
413                     + " actual values.");
414         }
415         return mTN.mGravity;
416     }
417 
418     /**
419      * Return the X offset in pixels to apply to the gravity's location.
420      *
421      * <p><strong>Warning:</strong> Starting from Android {@link Build.VERSION_CODES#R}, for apps
422      * targeting API level {@link Build.VERSION_CODES#R} or higher, this method shouldn't be called
423      * on text toasts as its return value may not reflect actual value since text toasts are not
424      * rendered by the app anymore.
425      */
getXOffset()426     public int getXOffset() {
427         if (isSystemRenderedTextToast()) {
428             Log.e(TAG, "getXOffset() shouldn't be called on text toasts, the result may not reflect"
429                     + " actual values.");
430         }
431         return mTN.mX;
432     }
433 
434     /**
435      * Return the Y offset in pixels to apply to the gravity's location.
436      *
437      * <p><strong>Warning:</strong> Starting from Android {@link Build.VERSION_CODES#R}, for apps
438      * targeting API level {@link Build.VERSION_CODES#R} or higher, this method shouldn't be called
439      * on text toasts as its return value may not reflect actual value since text toasts are not
440      * rendered by the app anymore.
441      */
getYOffset()442     public int getYOffset() {
443         if (isSystemRenderedTextToast()) {
444             Log.e(TAG, "getYOffset() shouldn't be called on text toasts, the result may not reflect"
445                     + " actual values.");
446         }
447         return mTN.mY;
448     }
449 
isSystemRenderedTextToast()450     private boolean isSystemRenderedTextToast() {
451         return Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM) && mNextView == null;
452     }
453 
454     /**
455      * Adds a callback to be notified when the toast is shown or hidden.
456      *
457      * Note that if the toast is blocked for some reason you won't get a call back.
458      *
459      * @see #removeCallback(Callback)
460      */
addCallback(@onNull Callback callback)461     public void addCallback(@NonNull Callback callback) {
462         checkNotNull(callback);
463         synchronized (mCallbacks) {
464             mCallbacks.add(callback);
465         }
466     }
467 
468     /**
469      * Removes a callback previously added with {@link #addCallback(Callback)}.
470      */
removeCallback(@onNull Callback callback)471     public void removeCallback(@NonNull Callback callback) {
472         synchronized (mCallbacks) {
473             mCallbacks.remove(callback);
474         }
475     }
476 
477     /**
478      * Gets the LayoutParams for the Toast window.
479      * @hide
480      */
481     @UnsupportedAppUsage
getWindowParams()482     @Nullable public WindowManager.LayoutParams getWindowParams() {
483         if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
484             if (mNextView != null) {
485                 // Custom toasts
486                 return mTN.mParams;
487             } else {
488                 // Text toasts
489                 return null;
490             }
491         } else {
492             // Text and custom toasts are app-rendered
493             return mTN.mParams;
494         }
495     }
496 
497     /**
498      * Make a standard toast that just contains text.
499      *
500      * @param context  The context to use.  Usually your {@link android.app.Activity} object.
501      * @param text     The text to show.  Can be formatted text.
502      * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
503      *                 {@link #LENGTH_LONG}
504      *
505      */
makeText(Context context, CharSequence text, @Duration int duration)506     public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
507         return makeText(context, null, text, duration);
508     }
509 
510     /**
511      * Make a standard toast to display using the specified looper.
512      * If looper is null, Looper.myLooper() is used.
513      *
514      * @hide
515      */
makeText(@onNull Context context, @Nullable Looper looper, @NonNull CharSequence text, @Duration int duration)516     public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
517             @NonNull CharSequence text, @Duration int duration) {
518         Toast result = new Toast(context, looper);
519 
520         if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
521             result.mText = text;
522         } else {
523             result.mNextView = ToastPresenter.getTextToastView(context, text);
524         }
525 
526         result.mDuration = duration;
527         return result;
528     }
529 
530     /**
531      * Make a standard toast with an icon to display using the specified looper.
532      * If looper is null, Looper.myLooper() is used.
533      *
534      * The toast will be a custom view that's rendered by the app (instead of by SystemUI).
535      * In Android version R and above, non-system apps can only render the toast
536      * when it's in the foreground.
537      *
538      * @hide
539      */
makeCustomToastWithIcon(@onNull Context context, @Nullable Looper looper, @NonNull CharSequence text, @Duration int duration, @NonNull Drawable icon)540     public static Toast makeCustomToastWithIcon(@NonNull Context context, @Nullable Looper looper,
541             @NonNull CharSequence text, @Duration int duration, @NonNull Drawable icon) {
542         if (icon == null) {
543             throw new IllegalArgumentException("Drawable icon should not be null "
544                     + "for makeCustomToastWithIcon");
545         }
546 
547         Toast result = new Toast(context, looper);
548         result.mNextView = ToastPresenter.getTextToastViewWithIcon(context, text, icon);
549         result.mDuration = duration;
550         return result;
551     }
552 
553     /**
554      * Make a standard toast that just contains text from a resource.
555      *
556      * @param context  The context to use.  Usually your {@link android.app.Activity} object.
557      * @param resId    The resource id of the string resource to use.  Can be formatted text.
558      * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
559      *                 {@link #LENGTH_LONG}
560      *
561      * @throws Resources.NotFoundException if the resource can't be found.
562      */
makeText(Context context, @StringRes int resId, @Duration int duration)563     public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
564                                 throws Resources.NotFoundException {
565         return makeText(context, context.getResources().getText(resId), duration);
566     }
567 
568     /**
569      * Update the text in a Toast that was previously created using one of the makeText() methods.
570      * @param resId The new text for the Toast.
571      */
setText(@tringRes int resId)572     public void setText(@StringRes int resId) {
573         setText(mContext.getText(resId));
574     }
575 
576     /**
577      * Update the text in a Toast that was previously created using one of the makeText() methods.
578      * @param s The new text for the Toast.
579      */
setText(CharSequence s)580     public void setText(CharSequence s) {
581         if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
582             if (mNextView != null) {
583                 throw new IllegalStateException(
584                         "Text provided for custom toast, remove previous setView() calls if you "
585                                 + "want a text toast instead.");
586             }
587             mText = s;
588         } else {
589             if (mNextView == null) {
590                 throw new RuntimeException("This Toast was not created with Toast.makeText()");
591             }
592             TextView tv = mNextView.findViewById(com.android.internal.R.id.message);
593             if (tv == null) {
594                 throw new RuntimeException("This Toast was not created with Toast.makeText()");
595             }
596             tv.setText(s);
597         }
598     }
599 
600     /**
601      * Get the Toast.TN ITransientNotification object
602      * @return TN
603      * @hide
604      */
605     @VisibleForTesting
getTn()606     public TN getTn() {
607         return mTN;
608     }
609 
610     // =======================================================================================
611     // All the gunk below is the interaction with the Notification Service, which handles
612     // the proper ordering of these system-wide.
613     // =======================================================================================
614 
615     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
616     private static INotificationManager sService;
617 
618     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
getService()619     static private INotificationManager getService() {
620         if (sService != null) {
621             return sService;
622         }
623         sService = INotificationManager.Stub.asInterface(
624                 ServiceManager.getService(Context.NOTIFICATION_SERVICE));
625         return sService;
626     }
627 
628     /**
629      * @hide
630      */
631     @VisibleForTesting
632     public static class TN extends ITransientNotification.Stub {
633         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
634         private final WindowManager.LayoutParams mParams;
635 
636         private static final int SHOW = 0;
637         private static final int HIDE = 1;
638         private static final int CANCEL = 2;
639         final Handler mHandler;
640 
641         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
642         int mGravity;
643         int mX;
644         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
645         int mY;
646         float mHorizontalMargin;
647         float mVerticalMargin;
648 
649 
650         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
651         View mView;
652         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
653         WeakReference<View> mNextViewWeakRef;
654         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
655         View mNextView;
656         int mDuration;
657 
658         WindowManager mWM;
659 
660         final String mPackageName;
661         final Binder mToken;
662         private final ToastPresenter mPresenter;
663 
664         @GuardedBy("mCallbacks")
665         private final WeakReference<List<Callback>> mCallbacks;
666 
667         /**
668          * Creates a {@link ITransientNotification} object.
669          *
670          * The parameter {@code callbacks} is not copied and is accessed with itself as its own
671          * lock.
672          */
TN(Context context, String packageName, Binder token, List<Callback> callbacks, @Nullable Looper looper)673         TN(Context context, String packageName, Binder token, List<Callback> callbacks,
674                 @Nullable Looper looper) {
675             IAccessibilityManager accessibilityManager = IAccessibilityManager.Stub.asInterface(
676                     ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
677             mPresenter = new ToastPresenter(context, accessibilityManager, getService(),
678                     packageName);
679             mParams = mPresenter.getLayoutParams();
680             mPackageName = packageName;
681             mToken = token;
682             mCallbacks = new WeakReference<>(callbacks);
683 
684             mHandler = new Handler(looper, null) {
685                 @Override
686                 public void handleMessage(Message msg) {
687                     switch (msg.what) {
688                         case SHOW: {
689                             IBinder token = (IBinder) msg.obj;
690                             handleShow(token);
691                             break;
692                         }
693                         case HIDE: {
694                             handleHide();
695                             // Don't do this in handleHide() because it is also invoked by
696                             // handleShow()
697                             if (Flags.toastNoWeakref()) {
698                                 mNextView = null;
699                             } else {
700                                 mNextViewWeakRef = null;
701                             }
702                             break;
703                         }
704                         case CANCEL: {
705                             handleHide();
706                             // Don't do this in handleHide() because it is also invoked by
707                             // handleShow()
708                             if (Flags.toastNoWeakref()) {
709                                 mNextView = null;
710                             } else {
711                                 mNextViewWeakRef = null;
712                             }
713                             try {
714                                 getService().cancelToast(mPackageName, mToken);
715                             } catch (RemoteException e) {
716                             }
717                             break;
718                         }
719                     }
720                 }
721             };
722         }
723 
getCallbacks()724         private List<Callback> getCallbacks() {
725             synchronized (mCallbacks) {
726                 if (mCallbacks.get() != null) {
727                     return new ArrayList<>(mCallbacks.get());
728                 } else {
729                     return new ArrayList<>();
730                 }
731             }
732         }
733 
734         /**
735          * schedule handleShow into the right thread
736          */
737         @Override
738         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
show(IBinder windowToken)739         public void show(IBinder windowToken) {
740             if (localLOGV) Log.v(TAG, "SHOW: " + this);
741             mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
742         }
743 
744         /**
745          * schedule handleHide into the right thread
746          */
747         @Override
hide()748         public void hide() {
749             if (localLOGV) Log.v(TAG, "HIDE: " + this);
750             mHandler.obtainMessage(HIDE).sendToTarget();
751         }
752 
cancel()753         public void cancel() {
754             if (localLOGV) Log.v(TAG, "CANCEL: " + this);
755             mHandler.obtainMessage(CANCEL).sendToTarget();
756         }
757 
handleShow(IBinder windowToken)758         public void handleShow(IBinder windowToken) {
759             if (Flags.toastNoWeakref()) {
760                 if (localLOGV) {
761                     Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
762                             + " mNextView=" + mNextView);
763                 }
764             } else {
765                 if (localLOGV) {
766                     Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
767                             + " mNextView=" + mNextViewWeakRef);
768                 }
769             }
770             // If a cancel/hide is pending - no need to show - at this point
771             // the window token is already invalid and no need to do any work.
772             if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
773                 return;
774             }
775             if (Flags.toastNoWeakref()) {
776                 if (mNextView != null && mView != mNextView) {
777                     // remove the old view if necessary
778                     handleHide();
779                     mView = mNextView;
780                     if (mView != null) {
781                         mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
782                                 mHorizontalMargin, mVerticalMargin,
783                                 new CallbackBinder(getCallbacks(), mHandler));
784                     }
785                 }
786             } else {
787                 if (mNextViewWeakRef != null && mView != mNextViewWeakRef.get()) {
788                     // remove the old view if necessary
789                     handleHide();
790                     mView = mNextViewWeakRef.get();
791                     if (mView != null) {
792                         mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
793                                 mHorizontalMargin, mVerticalMargin,
794                                 new CallbackBinder(getCallbacks(), mHandler));
795                     }
796                 }
797             }
798         }
799 
800         @UnsupportedAppUsage
handleHide()801         public void handleHide() {
802             if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
803             if (mView != null) {
804                 checkState(mView == mPresenter.getView(),
805                         "Trying to hide toast view different than the last one displayed");
806                 mPresenter.hide(new CallbackBinder(getCallbacks(), mHandler));
807                 mView = null;
808             }
809         }
810 
811         /**
812          * Get the next view to show for enqueued toasts
813          * Custom toast views are deprecated.
814          * @see #setView(View)
815          *
816          * @return next view
817          * @hide
818          */
819         @VisibleForTesting
getNextView()820         public View getNextView() {
821             if (Flags.toastNoWeakref()) {
822                 return mNextView;
823             } else {
824                 return (mNextViewWeakRef != null) ? mNextViewWeakRef.get() : null;
825             }
826         }
827     }
828 
829     /**
830      * Callback object to be called when the toast is shown or hidden.
831      *
832      * @see #makeText(Context, CharSequence, int)
833      * @see #addCallback(Callback)
834      */
835     public abstract static class Callback {
836         /**
837          * Called when the toast is displayed on the screen.
838          */
onToastShown()839         public void onToastShown() {}
840 
841         /**
842          * Called when the toast is hidden.
843          */
onToastHidden()844         public void onToastHidden() {}
845     }
846 
847     private static class CallbackBinder extends ITransientNotificationCallback.Stub {
848         private final Handler mHandler;
849 
850         @GuardedBy("mCallbacks")
851         private final List<Callback> mCallbacks;
852 
853         /**
854          * Creates a {@link ITransientNotificationCallback} object.
855          *
856          * The parameter {@code callbacks} is not copied and is accessed with itself as its own
857          * lock.
858          */
CallbackBinder(List<Callback> callbacks, Handler handler)859         private CallbackBinder(List<Callback> callbacks, Handler handler) {
860             mCallbacks = callbacks;
861             mHandler = handler;
862         }
863 
864         @Override
onToastShown()865         public void onToastShown() {
866             mHandler.post(() -> {
867                 for (Callback callback : getCallbacks()) {
868                     callback.onToastShown();
869                 }
870             });
871         }
872 
873         @Override
onToastHidden()874         public void onToastHidden() {
875             mHandler.post(() -> {
876                 for (Callback callback : getCallbacks()) {
877                     callback.onToastHidden();
878                 }
879             });
880         }
881 
getCallbacks()882         private List<Callback> getCallbacks() {
883             synchronized (mCallbacks) {
884                 return new ArrayList<>(mCallbacks);
885             }
886         }
887     }
888 }
889