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
17package android.widget;
18
19import android.annotation.IntDef;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.annotation.StringRes;
23import android.app.INotificationManager;
24import android.app.ITransientNotification;
25import android.content.Context;
26import android.content.res.Configuration;
27import android.content.res.Resources;
28import android.graphics.PixelFormat;
29import android.os.Handler;
30import android.os.IBinder;
31import android.os.Looper;
32import android.os.Message;
33import android.os.RemoteException;
34import android.os.ServiceManager;
35import android.util.Log;
36import android.view.Gravity;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.WindowManager;
40import android.view.accessibility.AccessibilityEvent;
41import android.view.accessibility.AccessibilityManager;
42
43import java.lang.annotation.Retention;
44import 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 */
69public 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     */
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     */
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     */
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     */
146    public void cancel() {
147        mTN.cancel();
148    }
149
150    /**
151     * Set the view to show.
152     * @see #getView
153     */
154    public void setView(View view) {
155        mNextView = view;
156    }
157
158    /**
159     * Return the view.
160     * @see #setView
161     */
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     */
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
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     */
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     */
203    public float getHorizontalMargin() {
204        return mTN.mHorizontalMargin;
205    }
206
207    /**
208     * Return the vertical margin.
209     */
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     */
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     */
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     */
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     */
244    public int getYOffset() {
245        return mTN.mY;
246    }
247
248    /**
249     * Gets the LayoutParams for the Toast window.
250     * @hide
251     */
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     */
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     */
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     */
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     */
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     */
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
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
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
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
439        public void hide() {
440            if (localLOGV) Log.v(TAG, "HIDE: " + this);
441            mHandler.obtainMessage(HIDE).sendToTarget();
442        }
443
444        public void cancel() {
445            if (localLOGV) Log.v(TAG, "CANCEL: " + this);
446            mHandler.obtainMessage(CANCEL).sendToTarget();
447        }
448
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
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
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