Java tutorial
/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.widget; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringRes; import android.annotation.UnsupportedAppUsage; import android.app.INotificationManager; import android.app.ITransientNotification; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.PixelFormat; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * A toast is a view containing a quick little message for the user. The toast class * helps you create and show those. * {@more} * * <p> * When the view is shown to the user, appears as a floating view over the * application. It will never receive focus. The user will probably be in the * middle of typing something else. The idea is to be as unobtrusive as * possible, while still showing the user the information you want them to see. * Two examples are the volume control, and the brief message saying that your * settings have been saved. * <p> * The easiest way to use this class is to call one of the static methods that constructs * everything you need and returns a new Toast object. * * <div class="special reference"> * <h3>Developer Guides</h3> * <p>For information about creating Toast notifications, read the * <a href="{@docRoot}guide/topics/ui/notifiers/toasts.html">Toast Notifications</a> developer * guide.</p> * </div> */ public class Toast { static final String TAG = "Toast"; static final boolean localLOGV = false; /** @hide */ @IntDef(prefix = { "LENGTH_" }, value = { LENGTH_SHORT, LENGTH_LONG }) @Retention(RetentionPolicy.SOURCE) public @interface Duration { } /** * Show the view or text notification for a short period of time. This time * could be user-definable. This is the default. * @see #setDuration */ public static final int LENGTH_SHORT = 0; /** * Show the view or text notification for a long period of time. This time * could be user-definable. * @see #setDuration */ public static final int LENGTH_LONG = 1; final Context mContext; @UnsupportedAppUsage final TN mTN; @UnsupportedAppUsage int mDuration; View mNextView; /** * Construct an empty Toast object. You must call {@link #setView} before you * can call {@link #show}. * * @param context The context to use. Usually your {@link android.app.Application} * or {@link android.app.Activity} object. */ public Toast(Context context) { this(context, null); } /** * Constructs an empty Toast object. If looper is null, Looper.myLooper() is used. * @hide */ public Toast(@NonNull Context context, @Nullable Looper looper) { mContext = context; mTN = new TN(context.getPackageName(), looper); mTN.mY = context.getResources().getDimensionPixelSize(com.android.internal.R.dimen.toast_y_offset); mTN.mGravity = context.getResources().getInteger(com.android.internal.R.integer.config_toastDefaultGravity); } /** * Show the view for the specified duration. */ public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } } /** * Close the view if it's showing, or don't show it if it isn't showing yet. * You do not normally have to call this. Normally view will disappear on its own * after the appropriate duration. */ public void cancel() { mTN.cancel(); } /** * Set the view to show. * @see #getView */ public void setView(View view) { mNextView = view; } /** * Return the view. * @see #setView */ public View getView() { return mNextView; } /** * Set how long to show the view for. * @see #LENGTH_SHORT * @see #LENGTH_LONG */ public void setDuration(@Duration int duration) { mDuration = duration; mTN.mDuration = duration; } /** * Return the duration. * @see #setDuration */ @Duration public int getDuration() { return mDuration; } /** * Set the margins of the view. * * @param horizontalMargin The horizontal margin, in percentage of the * container width, between the container's edges and the * notification * @param verticalMargin The vertical margin, in percentage of the * container height, between the container's edges and the * notification */ public void setMargin(float horizontalMargin, float verticalMargin) { mTN.mHorizontalMargin = horizontalMargin; mTN.mVerticalMargin = verticalMargin; } /** * Return the horizontal margin. */ public float getHorizontalMargin() { return mTN.mHorizontalMargin; } /** * Return the vertical margin. */ public float getVerticalMargin() { return mTN.mVerticalMargin; } /** * Set the location at which the notification should appear on the screen. * @see android.view.Gravity * @see #getGravity */ public void setGravity(int gravity, int xOffset, int yOffset) { mTN.mGravity = gravity; mTN.mX = xOffset; mTN.mY = yOffset; } /** * Get the location at which the notification should appear on the screen. * @see android.view.Gravity * @see #getGravity */ public int getGravity() { return mTN.mGravity; } /** * Return the X offset in pixels to apply to the gravity's location. */ public int getXOffset() { return mTN.mX; } /** * Return the Y offset in pixels to apply to the gravity's location. */ public int getYOffset() { return mTN.mY; } /** * Gets the LayoutParams for the Toast window. * @hide */ @UnsupportedAppUsage public WindowManager.LayoutParams getWindowParams() { return mTN.mParams; } /** * Make a standard toast that just contains a text view. * * @param context The context to use. Usually your {@link android.app.Application} * or {@link android.app.Activity} object. * @param text The text to show. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} * */ public static Toast makeText(Context context, CharSequence text, @Duration int duration) { return makeText(context, null, text, duration); } /** * Make a standard toast to display using the specified looper. * If looper is null, Looper.myLooper() is used. * @hide */ public static Toast makeText(@NonNull Context context, @Nullable Looper looper, @NonNull CharSequence text, @Duration int duration) { Toast result = new Toast(context, looper); LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null); TextView tv = (TextView) v.findViewById(com.android.internal.R.id.message); tv.setText(text); result.mNextView = v; result.mDuration = duration; return result; } /** * Make a standard toast that just contains a text view with the text from a resource. * * @param context The context to use. Usually your {@link android.app.Application} * or {@link android.app.Activity} object. * @param resId The resource id of the string resource to use. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} * * @throws Resources.NotFoundException if the resource can't be found. */ public static Toast makeText(Context context, @StringRes int resId, @Duration int duration) throws Resources.NotFoundException { return makeText(context, context.getResources().getText(resId), duration); } /** * Update the text in a Toast that was previously created using one of the makeText() methods. * @param resId The new text for the Toast. */ public void setText(@StringRes int resId) { setText(mContext.getText(resId)); } /** * Update the text in a Toast that was previously created using one of the makeText() methods. * @param s The new text for the Toast. */ public void setText(CharSequence s) { if (mNextView == null) { throw new RuntimeException("This Toast was not created with Toast.makeText()"); } TextView tv = mNextView.findViewById(com.android.internal.R.id.message); if (tv == null) { throw new RuntimeException("This Toast was not created with Toast.makeText()"); } tv.setText(s); } // ======================================================================================= // All the gunk below is the interaction with the Notification Service, which handles // the proper ordering of these system-wide. // ======================================================================================= @UnsupportedAppUsage private static INotificationManager sService; @UnsupportedAppUsage static private INotificationManager getService() { if (sService != null) { return sService; } sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); return sService; } private static class TN extends ITransientNotification.Stub { @UnsupportedAppUsage private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(); private static final int SHOW = 0; private static final int HIDE = 1; private static final int CANCEL = 2; final Handler mHandler; @UnsupportedAppUsage int mGravity; int mX; @UnsupportedAppUsage int mY; float mHorizontalMargin; float mVerticalMargin; @UnsupportedAppUsage View mView; @UnsupportedAppUsage View mNextView; int mDuration; WindowManager mWM; String mPackageName; static final long SHORT_DURATION_TIMEOUT = 4000; static final long LONG_DURATION_TIMEOUT = 7000; TN(String packageName, @Nullable Looper looper) { // XXX This should be changed to use a Dialog, with a Theme.Toast // defined that sets up the layout params appropriately. final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = com.android.internal.R.style.Animation_Toast; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; mPackageName = packageName; if (looper == null) { // Use Looper.myLooper() if looper is not specified. looper = Looper.myLooper(); if (looper == null) { throw new RuntimeException("Can't toast on a thread that has not called Looper.prepare()"); } } mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { case SHOW: { IBinder token = (IBinder) msg.obj; handleShow(token); break; } case HIDE: { handleHide(); // Don't do this in handleHide() because it is also invoked by // handleShow() mNextView = null; break; } case CANCEL: { handleHide(); // Don't do this in handleHide() because it is also invoked by // handleShow() mNextView = null; try { getService().cancelToast(mPackageName, TN.this); } catch (RemoteException e) { } break; } } } }; } /** * schedule handleShow into the right thread */ @Override @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) public void show(IBinder windowToken) { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.obtainMessage(SHOW, windowToken).sendToTarget(); } /** * schedule handleHide into the right thread */ @Override public void hide() { if (localLOGV) Log.v(TAG, "HIDE: " + this); mHandler.obtainMessage(HIDE).sendToTarget(); } public void cancel() { if (localLOGV) Log.v(TAG, "CANCEL: " + this); mHandler.obtainMessage(CANCEL).sendToTarget(); } public void handleShow(IBinder windowToken) { if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView + " mNextView=" + mNextView); // If a cancel/hide is pending - no need to show - at this point // the window token is already invalid and no need to do any work. if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) { return; } if (mView != mNextView) { // remove the old view if necessary handleHide(); mView = mNextView; Context context = mView.getContext().getApplicationContext(); String packageName = mView.getContext().getOpPackageName(); if (context == null) { context = mView.getContext(); } mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); // We can resolve the Gravity here by using the Locale for getting // the layout direction final Configuration config = mView.getContext().getResources().getConfiguration(); final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection()); mParams.gravity = gravity; if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { mParams.horizontalWeight = 1.0f; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { mParams.verticalWeight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; mParams.packageName = packageName; mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; mParams.token = windowToken; if (mView.getParent() != null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeView(mView); } if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this); // Since the notification manager service cancels the token right // after it notifies us to cancel the toast there is an inherent // race and we may attempt to add a window after the token has been // invalidated. Let us hedge against that. try { mWM.addView(mView, mParams); trySendAccessibilityEvent(); } catch (WindowManager.BadTokenException e) { /* ignore */ } } } private void trySendAccessibilityEvent() { AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(mView.getContext()); if (!accessibilityManager.isEnabled()) { return; } // treat toasts as notifications since they are used to // announce a transient piece of information to the user AccessibilityEvent event = AccessibilityEvent .obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); event.setClassName(getClass().getName()); event.setPackageName(mView.getContext().getPackageName()); mView.dispatchPopulateAccessibilityEvent(event); accessibilityManager.sendAccessibilityEvent(event); } @UnsupportedAppUsage public void handleHide() { if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView); if (mView != null) { // note: checking parent() just to make sure the view has // been added... i have seen cases where we get here when // the view isn't yet added, so let's try not to crash. if (mView.getParent() != null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeViewImmediate(mView); } // Now that we've removed the view it's safe for the server to release // the resources. try { getService().finishToken(mPackageName, this); } catch (RemoteException e) { } mView = null; } } } }