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