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