Home | History | Annotate | Download | only in widget
      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.annotation.UnsupportedAppUsage;
     24 import android.app.INotificationManager;
     25 import android.app.ITransientNotification;
     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      */
    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      */
    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      */
    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      */
    154     public void cancel() {
    155         mTN.cancel();
    156     }
    157 
    158     /**
    159      * Set the view to show.
    160      * @see #getView
    161      */
    162     public void setView(View view) {
    163         mNextView = view;
    164     }
    165 
    166     /**
    167      * Return the view.
    168      * @see #setView
    169      */
    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      */
    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
    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      */
    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      */
    211     public float getHorizontalMargin() {
    212         return mTN.mHorizontalMargin;
    213     }
    214 
    215     /**
    216      * Return the vertical margin.
    217      */
    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      */
    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      */
    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      */
    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      */
    252     public int getYOffset() {
    253         return mTN.mY;
    254     }
    255 
    256     /**
    257      * Gets the LayoutParams for the Toast window.
    258      * @hide
    259      */
    260     @UnsupportedAppUsage
    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      */
    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      */
    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      */
    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      */
    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      */
    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)
    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 
    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)
    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
    457         public void hide() {
    458             if (localLOGV) Log.v(TAG, "HIDE: " + this);
    459             mHandler.obtainMessage(HIDE).sendToTarget();
    460         }
    461 
    462         public void cancel() {
    463             if (localLOGV) Log.v(TAG, "CANCEL: " + this);
    464             mHandler.obtainMessage(CANCEL).sendToTarget();
    465         }
    466 
    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 
    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
    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