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