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 android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.StringRes;
23 import android.app.INotificationManager;
24 import android.app.ITransientNotification;
25 import android.compat.annotation.UnsupportedAppUsage;
26 import android.content.Context;
27 import android.content.res.Configuration;
28 import android.content.res.Resources;
29 import android.graphics.PixelFormat;
30 import android.os.Build;
31 import android.os.Handler;
32 import android.os.IBinder;
33 import android.os.Looper;
34 import android.os.Message;
35 import android.os.RemoteException;
36 import android.os.ServiceManager;
37 import android.util.Log;
38 import android.view.Gravity;
39 import android.view.LayoutInflater;
40 import android.view.View;
41 import android.view.WindowManager;
42 import android.view.accessibility.AccessibilityEvent;
43 import android.view.accessibility.AccessibilityManager;
44 
45 import java.lang.annotation.Retention;
46 import java.lang.annotation.RetentionPolicy;
47 
48 /**
49  * A toast is a view containing a quick little message for the user.  The toast class
50  * helps you create and show those.
51  * {@more}
52  *
53  * <p>
54  * When the view is shown to the user, appears as a floating view over the
55  * application.  It will never receive focus.  The user will probably be in the
56  * middle of typing something else.  The idea is to be as unobtrusive as
57  * possible, while still showing the user the information you want them to see.
58  * Two examples are the volume control, and the brief message saying that your
59  * settings have been saved.
60  * <p>
61  * The easiest way to use this class is to call one of the static methods that constructs
62  * everything you need and returns a new Toast object.
63  *
64  * <div class="special reference">
65  * <h3>Developer Guides</h3>
66  * <p>For information about creating Toast notifications, read the
67  * <a href="{@docRoot}guide/topics/ui/notifiers/toasts.html">Toast Notifications</a> developer
68  * guide.</p>
69  * </div>
70  */
71 public class Toast {
72     static final String TAG = "Toast";
73     static final boolean localLOGV = false;
74 
75     /** @hide */
76     @IntDef(prefix = { "LENGTH_" }, value = {
77             LENGTH_SHORT,
78             LENGTH_LONG
79     })
80     @Retention(RetentionPolicy.SOURCE)
81     public @interface Duration {}
82 
83     /**
84      * Show the view or text notification for a short period of time.  This time
85      * could be user-definable.  This is the default.
86      * @see #setDuration
87      */
88     public static final int LENGTH_SHORT = 0;
89 
90     /**
91      * Show the view or text notification for a long period of time.  This time
92      * could be user-definable.
93      * @see #setDuration
94      */
95     public static final int LENGTH_LONG = 1;
96 
97     final Context mContext;
98     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
99     final TN mTN;
100     @UnsupportedAppUsage
101     int mDuration;
102     View mNextView;
103 
104     /**
105      * Construct an empty Toast object.  You must call {@link #setView} before you
106      * can call {@link #show}.
107      *
108      * @param context  The context to use.  Usually your {@link android.app.Application}
109      *                 or {@link android.app.Activity} object.
110      */
Toast(Context context)111     public Toast(Context context) {
112         this(context, null);
113     }
114 
115     /**
116      * Constructs an empty Toast object.  If looper is null, Looper.myLooper() is used.
117      * @hide
118      */
Toast(@onNull Context context, @Nullable Looper looper)119     public Toast(@NonNull Context context, @Nullable Looper looper) {
120         mContext = context;
121         mTN = new TN(context.getPackageName(), looper);
122         mTN.mY = context.getResources().getDimensionPixelSize(
123                 com.android.internal.R.dimen.toast_y_offset);
124         mTN.mGravity = context.getResources().getInteger(
125                 com.android.internal.R.integer.config_toastDefaultGravity);
126     }
127 
128     /**
129      * Show the view for the specified duration.
130      */
show()131     public void show() {
132         if (mNextView == null) {
133             throw new RuntimeException("setView must have been called");
134         }
135 
136         INotificationManager service = getService();
137         String pkg = mContext.getOpPackageName();
138         TN tn = mTN;
139         tn.mNextView = mNextView;
140         final int displayId = mContext.getDisplayId();
141 
142         try {
143             service.enqueueToast(pkg, tn, mDuration, displayId);
144         } catch (RemoteException e) {
145             // Empty
146         }
147     }
148 
149     /**
150      * Close the view if it's showing, or don't show it if it isn't showing yet.
151      * You do not normally have to call this.  Normally view will disappear on its own
152      * after the appropriate duration.
153      */
cancel()154     public void cancel() {
155         mTN.cancel();
156     }
157 
158     /**
159      * Set the view to show.
160      * @see #getView
161      */
setView(View view)162     public void setView(View view) {
163         mNextView = view;
164     }
165 
166     /**
167      * Return the view.
168      * @see #setView
169      */
getView()170     public View getView() {
171         return mNextView;
172     }
173 
174     /**
175      * Set how long to show the view for.
176      * @see #LENGTH_SHORT
177      * @see #LENGTH_LONG
178      */
setDuration(@uration int duration)179     public void setDuration(@Duration int duration) {
180         mDuration = duration;
181         mTN.mDuration = duration;
182     }
183 
184     /**
185      * Return the duration.
186      * @see #setDuration
187      */
188     @Duration
getDuration()189     public int getDuration() {
190         return mDuration;
191     }
192 
193     /**
194      * Set the margins of the view.
195      *
196      * @param horizontalMargin The horizontal margin, in percentage of the
197      *        container width, between the container's edges and the
198      *        notification
199      * @param verticalMargin The vertical margin, in percentage of the
200      *        container height, between the container's edges and the
201      *        notification
202      */
setMargin(float horizontalMargin, float verticalMargin)203     public void setMargin(float horizontalMargin, float verticalMargin) {
204         mTN.mHorizontalMargin = horizontalMargin;
205         mTN.mVerticalMargin = verticalMargin;
206     }
207 
208     /**
209      * Return the horizontal margin.
210      */
getHorizontalMargin()211     public float getHorizontalMargin() {
212         return mTN.mHorizontalMargin;
213     }
214 
215     /**
216      * Return the vertical margin.
217      */
getVerticalMargin()218     public float getVerticalMargin() {
219         return mTN.mVerticalMargin;
220     }
221 
222     /**
223      * Set the location at which the notification should appear on the screen.
224      * @see android.view.Gravity
225      * @see #getGravity
226      */
setGravity(int gravity, int xOffset, int yOffset)227     public void setGravity(int gravity, int xOffset, int yOffset) {
228         mTN.mGravity = gravity;
229         mTN.mX = xOffset;
230         mTN.mY = yOffset;
231     }
232 
233      /**
234      * Get the location at which the notification should appear on the screen.
235      * @see android.view.Gravity
236      * @see #getGravity
237      */
getGravity()238     public int getGravity() {
239         return mTN.mGravity;
240     }
241 
242     /**
243      * Return the X offset in pixels to apply to the gravity's location.
244      */
getXOffset()245     public int getXOffset() {
246         return mTN.mX;
247     }
248 
249     /**
250      * Return the Y offset in pixels to apply to the gravity's location.
251      */
getYOffset()252     public int getYOffset() {
253         return mTN.mY;
254     }
255 
256     /**
257      * Gets the LayoutParams for the Toast window.
258      * @hide
259      */
260     @UnsupportedAppUsage
getWindowParams()261     public WindowManager.LayoutParams getWindowParams() {
262         return mTN.mParams;
263     }
264 
265     /**
266      * Make a standard toast that just contains a text view.
267      *
268      * @param context  The context to use.  Usually your {@link android.app.Application}
269      *                 or {@link android.app.Activity} object.
270      * @param text     The text to show.  Can be formatted text.
271      * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
272      *                 {@link #LENGTH_LONG}
273      *
274      */
makeText(Context context, CharSequence text, @Duration int duration)275     public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
276         return makeText(context, null, text, duration);
277     }
278 
279     /**
280      * Make a standard toast to display using the specified looper.
281      * If looper is null, Looper.myLooper() is used.
282      * @hide
283      */
makeText(@onNull Context context, @Nullable Looper looper, @NonNull CharSequence text, @Duration int duration)284     public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
285             @NonNull CharSequence text, @Duration int duration) {
286         Toast result = new Toast(context, looper);
287 
288         LayoutInflater inflate = (LayoutInflater)
289                 context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
290         View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
291         TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
292         tv.setText(text);
293 
294         result.mNextView = v;
295         result.mDuration = duration;
296 
297         return result;
298     }
299 
300     /**
301      * Make a standard toast that just contains a text view with the text from a resource.
302      *
303      * @param context  The context to use.  Usually your {@link android.app.Application}
304      *                 or {@link android.app.Activity} object.
305      * @param resId    The resource id of the string resource to use.  Can be formatted text.
306      * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
307      *                 {@link #LENGTH_LONG}
308      *
309      * @throws Resources.NotFoundException if the resource can't be found.
310      */
makeText(Context context, @StringRes int resId, @Duration int duration)311     public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
312                                 throws Resources.NotFoundException {
313         return makeText(context, context.getResources().getText(resId), duration);
314     }
315 
316     /**
317      * Update the text in a Toast that was previously created using one of the makeText() methods.
318      * @param resId The new text for the Toast.
319      */
setText(@tringRes int resId)320     public void setText(@StringRes int resId) {
321         setText(mContext.getText(resId));
322     }
323 
324     /**
325      * Update the text in a Toast that was previously created using one of the makeText() methods.
326      * @param s The new text for the Toast.
327      */
setText(CharSequence s)328     public void setText(CharSequence s) {
329         if (mNextView == null) {
330             throw new RuntimeException("This Toast was not created with Toast.makeText()");
331         }
332         TextView tv = mNextView.findViewById(com.android.internal.R.id.message);
333         if (tv == null) {
334             throw new RuntimeException("This Toast was not created with Toast.makeText()");
335         }
336         tv.setText(s);
337     }
338 
339     // =======================================================================================
340     // All the gunk below is the interaction with the Notification Service, which handles
341     // the proper ordering of these system-wide.
342     // =======================================================================================
343 
344     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
345     private static INotificationManager sService;
346 
347     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
getService()348     static private INotificationManager getService() {
349         if (sService != null) {
350             return sService;
351         }
352         sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
353         return sService;
354     }
355 
356     private static class TN extends ITransientNotification.Stub {
357         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
358         private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
359 
360         private static final int SHOW = 0;
361         private static final int HIDE = 1;
362         private static final int CANCEL = 2;
363         final Handler mHandler;
364 
365         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
366         int mGravity;
367         int mX;
368         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
369         int mY;
370         float mHorizontalMargin;
371         float mVerticalMargin;
372 
373 
374         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
375         View mView;
376         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
377         View mNextView;
378         int mDuration;
379 
380         WindowManager mWM;
381 
382         String mPackageName;
383 
384         static final long SHORT_DURATION_TIMEOUT = 4000;
385         static final long LONG_DURATION_TIMEOUT = 7000;
386 
TN(String packageName, @Nullable Looper looper)387         TN(String packageName, @Nullable Looper looper) {
388             // XXX This should be changed to use a Dialog, with a Theme.Toast
389             // defined that sets up the layout params appropriately.
390             final WindowManager.LayoutParams params = mParams;
391             params.height = WindowManager.LayoutParams.WRAP_CONTENT;
392             params.width = WindowManager.LayoutParams.WRAP_CONTENT;
393             params.format = PixelFormat.TRANSLUCENT;
394             params.windowAnimations = com.android.internal.R.style.Animation_Toast;
395             params.type = WindowManager.LayoutParams.TYPE_TOAST;
396             params.setTitle("Toast");
397             params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
398                     | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
399                     | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
400 
401             mPackageName = packageName;
402 
403             if (looper == null) {
404                 // Use Looper.myLooper() if looper is not specified.
405                 looper = Looper.myLooper();
406                 if (looper == null) {
407                     throw new RuntimeException(
408                             "Can't toast on a thread that has not called Looper.prepare()");
409                 }
410             }
411             mHandler = new Handler(looper, null) {
412                 @Override
413                 public void handleMessage(Message msg) {
414                     switch (msg.what) {
415                         case SHOW: {
416                             IBinder token = (IBinder) msg.obj;
417                             handleShow(token);
418                             break;
419                         }
420                         case HIDE: {
421                             handleHide();
422                             // Don't do this in handleHide() because it is also invoked by
423                             // handleShow()
424                             mNextView = null;
425                             break;
426                         }
427                         case CANCEL: {
428                             handleHide();
429                             // Don't do this in handleHide() because it is also invoked by
430                             // handleShow()
431                             mNextView = null;
432                             try {
433                                 getService().cancelToast(mPackageName, TN.this);
434                             } catch (RemoteException e) {
435                             }
436                             break;
437                         }
438                     }
439                 }
440             };
441         }
442 
443         /**
444          * schedule handleShow into the right thread
445          */
446         @Override
447         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
show(IBinder windowToken)448         public void show(IBinder windowToken) {
449             if (localLOGV) Log.v(TAG, "SHOW: " + this);
450             mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
451         }
452 
453         /**
454          * schedule handleHide into the right thread
455          */
456         @Override
hide()457         public void hide() {
458             if (localLOGV) Log.v(TAG, "HIDE: " + this);
459             mHandler.obtainMessage(HIDE).sendToTarget();
460         }
461 
cancel()462         public void cancel() {
463             if (localLOGV) Log.v(TAG, "CANCEL: " + this);
464             mHandler.obtainMessage(CANCEL).sendToTarget();
465         }
466 
handleShow(IBinder windowToken)467         public void handleShow(IBinder windowToken) {
468             if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
469                     + " mNextView=" + mNextView);
470             // If a cancel/hide is pending - no need to show - at this point
471             // the window token is already invalid and no need to do any work.
472             if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
473                 return;
474             }
475             if (mView != mNextView) {
476                 // remove the old view if necessary
477                 handleHide();
478                 mView = mNextView;
479                 Context context = mView.getContext().getApplicationContext();
480                 String packageName = mView.getContext().getOpPackageName();
481                 if (context == null) {
482                     context = mView.getContext();
483                 }
484                 mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
485                 // We can resolve the Gravity here by using the Locale for getting
486                 // the layout direction
487                 final Configuration config = mView.getContext().getResources().getConfiguration();
488                 final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
489                 mParams.gravity = gravity;
490                 if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
491                     mParams.horizontalWeight = 1.0f;
492                 }
493                 if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
494                     mParams.verticalWeight = 1.0f;
495                 }
496                 mParams.x = mX;
497                 mParams.y = mY;
498                 mParams.verticalMargin = mVerticalMargin;
499                 mParams.horizontalMargin = mHorizontalMargin;
500                 mParams.packageName = packageName;
501                 mParams.hideTimeoutMilliseconds = mDuration ==
502                     Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
503                 mParams.token = windowToken;
504                 if (mView.getParent() != null) {
505                     if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
506                     mWM.removeView(mView);
507                 }
508                 if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
509                 // Since the notification manager service cancels the token right
510                 // after it notifies us to cancel the toast there is an inherent
511                 // race and we may attempt to add a window after the token has been
512                 // invalidated. Let us hedge against that.
513                 try {
514                     mWM.addView(mView, mParams);
515                     trySendAccessibilityEvent();
516                 } catch (WindowManager.BadTokenException e) {
517                     /* ignore */
518                 }
519             }
520         }
521 
trySendAccessibilityEvent()522         private void trySendAccessibilityEvent() {
523             AccessibilityManager accessibilityManager =
524                     AccessibilityManager.getInstance(mView.getContext());
525             if (!accessibilityManager.isEnabled()) {
526                 return;
527             }
528             // treat toasts as notifications since they are used to
529             // announce a transient piece of information to the user
530             AccessibilityEvent event = AccessibilityEvent.obtain(
531                     AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
532             event.setClassName(getClass().getName());
533             event.setPackageName(mView.getContext().getPackageName());
534             mView.dispatchPopulateAccessibilityEvent(event);
535             accessibilityManager.sendAccessibilityEvent(event);
536         }
537 
538         @UnsupportedAppUsage
handleHide()539         public void handleHide() {
540             if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
541             if (mView != null) {
542                 // note: checking parent() just to make sure the view has
543                 // been added...  i have seen cases where we get here when
544                 // the view isn't yet added, so let's try not to crash.
545                 if (mView.getParent() != null) {
546                     if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
547                     mWM.removeViewImmediate(mView);
548                 }
549 
550 
551                 // Now that we've removed the view it's safe for the server to release
552                 // the resources.
553                 try {
554                     getService().finishToken(mPackageName, this);
555                 } catch (RemoteException e) {
556                 }
557 
558                 mView = null;
559             }
560         }
561     }
562 }
563