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