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