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