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