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