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    }
167
168    /**
169     * Return the duration.
170     * @see #setDuration
171     */
172    @Duration
173    public int getDuration() {
174        return mDuration;
175    }
176
177    /**
178     * Set the margins of the view.
179     *
180     * @param horizontalMargin The horizontal margin, in percentage of the
181     *        container width, between the container's edges and the
182     *        notification
183     * @param verticalMargin The vertical margin, in percentage of the
184     *        container height, between the container's edges and the
185     *        notification
186     */
187    public void setMargin(float horizontalMargin, float verticalMargin) {
188        mTN.mHorizontalMargin = horizontalMargin;
189        mTN.mVerticalMargin = verticalMargin;
190    }
191
192    /**
193     * Return the horizontal margin.
194     */
195    public float getHorizontalMargin() {
196        return mTN.mHorizontalMargin;
197    }
198
199    /**
200     * Return the vertical margin.
201     */
202    public float getVerticalMargin() {
203        return mTN.mVerticalMargin;
204    }
205
206    /**
207     * Set the location at which the notification should appear on the screen.
208     * @see android.view.Gravity
209     * @see #getGravity
210     */
211    public void setGravity(int gravity, int xOffset, int yOffset) {
212        mTN.mGravity = gravity;
213        mTN.mX = xOffset;
214        mTN.mY = yOffset;
215    }
216
217     /**
218     * Get the location at which the notification should appear on the screen.
219     * @see android.view.Gravity
220     * @see #getGravity
221     */
222    public int getGravity() {
223        return mTN.mGravity;
224    }
225
226    /**
227     * Return the X offset in pixels to apply to the gravity's location.
228     */
229    public int getXOffset() {
230        return mTN.mX;
231    }
232
233    /**
234     * Return the Y offset in pixels to apply to the gravity's location.
235     */
236    public int getYOffset() {
237        return mTN.mY;
238    }
239
240    /**
241     * Gets the LayoutParams for the Toast window.
242     * @hide
243     */
244    public WindowManager.LayoutParams getWindowParams() {
245        return mTN.mParams;
246    }
247
248    /**
249     * Make a standard toast that just contains a text view.
250     *
251     * @param context  The context to use.  Usually your {@link android.app.Application}
252     *                 or {@link android.app.Activity} object.
253     * @param text     The text to show.  Can be formatted text.
254     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
255     *                 {@link #LENGTH_LONG}
256     *
257     */
258    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
259        Toast result = new Toast(context);
260
261        LayoutInflater inflate = (LayoutInflater)
262                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
263        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
264        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
265        tv.setText(text);
266
267        result.mNextView = v;
268        result.mDuration = duration;
269
270        return result;
271    }
272
273    /**
274     * Make a standard toast that just contains a text view with the text from a resource.
275     *
276     * @param context  The context to use.  Usually your {@link android.app.Application}
277     *                 or {@link android.app.Activity} object.
278     * @param resId    The resource id of the string resource to use.  Can be formatted text.
279     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
280     *                 {@link #LENGTH_LONG}
281     *
282     * @throws Resources.NotFoundException if the resource can't be found.
283     */
284    public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
285                                throws Resources.NotFoundException {
286        return makeText(context, context.getResources().getText(resId), duration);
287    }
288
289    /**
290     * Update the text in a Toast that was previously created using one of the makeText() methods.
291     * @param resId The new text for the Toast.
292     */
293    public void setText(@StringRes int resId) {
294        setText(mContext.getText(resId));
295    }
296
297    /**
298     * Update the text in a Toast that was previously created using one of the makeText() methods.
299     * @param s The new text for the Toast.
300     */
301    public void setText(CharSequence s) {
302        if (mNextView == null) {
303            throw new RuntimeException("This Toast was not created with Toast.makeText()");
304        }
305        TextView tv = (TextView) mNextView.findViewById(com.android.internal.R.id.message);
306        if (tv == null) {
307            throw new RuntimeException("This Toast was not created with Toast.makeText()");
308        }
309        tv.setText(s);
310    }
311
312    // =======================================================================================
313    // All the gunk below is the interaction with the Notification Service, which handles
314    // the proper ordering of these system-wide.
315    // =======================================================================================
316
317    private static INotificationManager sService;
318
319    static private INotificationManager getService() {
320        if (sService != null) {
321            return sService;
322        }
323        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
324        return sService;
325    }
326
327    private static class TN extends ITransientNotification.Stub {
328        final Runnable mShow = new Runnable() {
329            @Override
330            public void run() {
331                handleShow();
332            }
333        };
334
335        final Runnable mHide = new Runnable() {
336            @Override
337            public void run() {
338                handleHide();
339                // Don't do this in handleHide() because it is also invoked by handleShow()
340                mNextView = null;
341            }
342        };
343
344        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
345        final Handler mHandler = new Handler();
346
347        int mGravity;
348        int mX, mY;
349        float mHorizontalMargin;
350        float mVerticalMargin;
351
352
353        View mView;
354        View mNextView;
355
356        WindowManager mWM;
357
358        TN() {
359            // XXX This should be changed to use a Dialog, with a Theme.Toast
360            // defined that sets up the layout params appropriately.
361            final WindowManager.LayoutParams params = mParams;
362            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
363            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
364            params.format = PixelFormat.TRANSLUCENT;
365            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
366            params.type = WindowManager.LayoutParams.TYPE_TOAST;
367            params.setTitle("Toast");
368            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
369                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
370                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
371        }
372
373        /**
374         * schedule handleShow into the right thread
375         */
376        @Override
377        public void show() {
378            if (localLOGV) Log.v(TAG, "SHOW: " + this);
379            mHandler.post(mShow);
380        }
381
382        /**
383         * schedule handleHide into the right thread
384         */
385        @Override
386        public void hide() {
387            if (localLOGV) Log.v(TAG, "HIDE: " + this);
388            mHandler.post(mHide);
389        }
390
391        public void handleShow() {
392            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
393                    + " mNextView=" + mNextView);
394            if (mView != mNextView) {
395                // remove the old view if necessary
396                handleHide();
397                mView = mNextView;
398                Context context = mView.getContext().getApplicationContext();
399                String packageName = mView.getContext().getOpPackageName();
400                if (context == null) {
401                    context = mView.getContext();
402                }
403                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
404                // We can resolve the Gravity here by using the Locale for getting
405                // the layout direction
406                final Configuration config = mView.getContext().getResources().getConfiguration();
407                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
408                mParams.gravity = gravity;
409                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
410                    mParams.horizontalWeight = 1.0f;
411                }
412                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
413                    mParams.verticalWeight = 1.0f;
414                }
415                mParams.x = mX;
416                mParams.y = mY;
417                mParams.verticalMargin = mVerticalMargin;
418                mParams.horizontalMargin = mHorizontalMargin;
419                mParams.packageName = packageName;
420                if (mView.getParent() != null) {
421                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
422                    mWM.removeView(mView);
423                }
424                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
425                mWM.addView(mView, mParams);
426                trySendAccessibilityEvent();
427            }
428        }
429
430        private void trySendAccessibilityEvent() {
431            AccessibilityManager accessibilityManager =
432                    AccessibilityManager.getInstance(mView.getContext());
433            if (!accessibilityManager.isEnabled()) {
434                return;
435            }
436            // treat toasts as notifications since they are used to
437            // announce a transient piece of information to the user
438            AccessibilityEvent event = AccessibilityEvent.obtain(
439                    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
440            event.setClassName(getClass().getName());
441            event.setPackageName(mView.getContext().getPackageName());
442            mView.dispatchPopulateAccessibilityEvent(event);
443            accessibilityManager.sendAccessibilityEvent(event);
444        }
445
446        public void handleHide() {
447            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
448            if (mView != null) {
449                // note: checking parent() just to make sure the view has
450                // been added...  i have seen cases where we get here when
451                // the view isn't yet added, so let's try not to crash.
452                if (mView.getParent() != null) {
453                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
454                    mWM.removeView(mView);
455                }
456
457                mView = null;
458            }
459        }
460    }
461}
462