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.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      */
    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      */
    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      */
    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      */
    149     public void cancel() {
    150         mTN.cancel();
    151     }
    152 
    153     /**
    154      * Set the view to show.
    155      * @see #getView
    156      */
    157     public void setView(View view) {
    158         mNextView = view;
    159     }
    160 
    161     /**
    162      * Return the view.
    163      * @see #setView
    164      */
    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      */
    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
    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      */
    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      */
    206     public float getHorizontalMargin() {
    207         return mTN.mHorizontalMargin;
    208     }
    209 
    210     /**
    211      * Return the vertical margin.
    212      */
    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      */
    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      */
    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      */
    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      */
    247     public int getYOffset() {
    248         return mTN.mY;
    249     }
    250 
    251     /**
    252      * Gets the LayoutParams for the Toast window.
    253      * @hide
    254      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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
    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
    442         public void hide() {
    443             if (localLOGV) Log.v(TAG, "HIDE: " + this);
    444             mHandler.obtainMessage(HIDE).sendToTarget();
    445         }
    446 
    447         public void cancel() {
    448             if (localLOGV) Log.v(TAG, "CANCEL: " + this);
    449             mHandler.obtainMessage(CANCEL).sendToTarget();
    450         }
    451 
    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 
    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 
    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