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