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