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