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