/** * Show the view or text notification for a short period of time. This time * could be user-definable. This is the default. * @see #setDuration */ publicstaticfinalint LENGTH_SHORT = 0;
/** * Show the view or text notification for a long period of time. This time * could be user-definable. * @see #setDuration */ publicstaticfinalint LENGTH_LONG = 1;
/** * ... * ... . Therefore, we do hide * such windows to prevent them from overlaying other apps. * * @hide */ publiclong hideTimeoutMilliseconds = -1;
Prevent apps to overlay other apps via toast windows
It was possible for apps to put toast type windows that overlay other apps which toast winodws aren't removed after a timeout.
Now for apps targeting SDK greater than N MR1 to add a toast window one needs to have a special token. The token is added by the notificatoion manager service only for the lifetime of the shown toast and is then removed including all windows associated with this token. This prevents apps to add arbitrary toast windows.
Since legacy apps may rely on the ability to directly add toasts we mitigate by allowing these apps to still add such windows for unlimited duration if this app is the currently focused one, i.e. the user interacts with it then it can overlay itself, otherwise we make sure these toast windows are removed after a timeout like a toast would be.
We don't allow more that one toast window per UID being added at a time which prevents 1) legacy apps to put the same toast after a timeout to go around our new policy of hiding toasts after a while; 2) modern apps to reuse the passed token to add more than one window; Note that the notification manager shows toasts one at a time.
/** * Set how long to show the view for. * @see #LENGTH_SHORT * @see #LENGTH_LONG */ publicvoidsetDuration(@Durationint duration){ mDuration = duration; mTN.mDuration = duration; }
...
/** * Make a standard toast that just contains a text view. * * @param context The context to use. Usually your {@link android.app.Application} * or {@link android.app.Activity} object. * @param text The text to show. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} * */ publicstatic Toast makeText(Context context, CharSequence text, @Durationint duration){ return makeText(context, null, text, duration); }
/** * Denotes that the annotated element of integer type, represents * a logical type and that its value should be one of the explicitly * named constants. If the {@link #flag()} attribute is set to true, * multiple constants can be combined. * ... */
不信邪的我们可以快速在一个 demo Android 工程里写一句这样的代码试试:
1
Toast.makeText(this, "Hello", 2);
Android Studio 首先就不会同意,警告你 Must be one of: Toast.LENGTH_SHORT, Toast.LENGTH_LONG,但实际这段代码是可以通过编译的,因为 Duration 注解的 Retention 为 RetentionPolicy.SOURCE,我的理解是该注解主要能用于 IDE 的智能提示警告,编译期就被丢掉了。
new Thread(new Runnable() { @Override publicvoidrun(){ Toast.makeText(MainActivity.this, "Call toast on non-UI thread", Toast.LENGTH_SHORT) .show(); } }).start();
啊哦~很遗憾程序直接挂掉了。
1 2 3 4 5 6 7 8
11-07 13:35:33.980 2020-2035/org.mazhuang.androiduidemos E/AndroidRuntime: FATAL EXCEPTION: Thread-77 java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() at android.widget.Toast$TN.<init>(Toast.java:390) at android.widget.Toast.<init>(Toast.java:114) at android.widget.Toast.makeText(Toast.java:277) at android.widget.Toast.makeText(Toast.java:267) at org.mazhuang.androiduidemos.MainActivity$1.run(MainActivity.java:27) at java.lang.Thread.run(Thread.java:856)
// looper = null TN(String packageName, @Nullable Looper looper) { ... if (looper == null) { // Use Looper.myLooper() if looper is not specified. looper = Looper.myLooper(); if (looper == null) { thrownew RuntimeException( "Can't toast on a thread that has not called Looper.prepare()"); } } ... }
至此,我们已经追踪到了我们的崩溃的 RuntimeException,即要避免进入抛出异常的逻辑,要么调用的时候传递一个 Looper 进来(无法直接实现,能传递 Looper 参数的构造方法与 makeText 方法是 hide 的),要么 Looper.myLooper() 返回不为 null,提示信息 Can't create handler inside thread that has not called Looper.prepare() 里给出了方法,那我们在 toast 前面加一句 Looper.prepare() 试试?这次不崩溃了,但依然不弹出 Toast,毕竟,这个线程在调用完 show() 方法后就直接结束了,没有调用 Looper.loop(),至于为什么调用 Toast 的线程结束与否会对 Toast 的显示隐藏等起影响,在本文的后面的章节里会进行分析。
/** * Make a standard toast to display using the specified looper. * If looper is null, Looper.myLooper() is used. * @hide */ publicstatic Toast makeText(@NonNull Context context, @Nullable Looper looper, @NonNull CharSequence text, @Durationint duration){ Toast result = new Toast(context, looper);
LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null); TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message); tv.setText(text);
TN(String packageName, @Nullable Looper looper) { // XXX This should be changed to use a Dialog, with a Theme.Toast // defined that sets up the layout params appropriately. final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = com.android.internal.R.style.Animation_Toast; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mPackageName = packageName;
if (looper == null) { // Use Looper.myLooper() if looper is not specified. looper = Looper.myLooper(); if (looper == null) { thrownew RuntimeException( "Can't toast on a thread that has not called Looper.prepare()"); } } mHandler = new Handler(looper, null) { ... }; }
设置了 LayoutParams 的初始值,在后面 show 的时候会用到,设置了包名和 Looper、Handler。
/** * Show the view for the specified duration. */ publicvoidshow(){ if (mNextView == null) { thrownew RuntimeException("setView must have been called"); }
@Override publicvoidenqueueToast(String pkg, ITransientNotification callback, int duration) { ...
synchronized (mToastQueue) { ... try { ToastRecord record; int index = indexOfToastLocked(pkg, callback); // If it's already in the queue, we update it in place, we don't // move it to the end of the queue. if (index >= 0) { record = mToastQueue.get(index); record.update(duration); } else { // Limit the number of toasts that any given package except the android // package can enqueue. Prevents DOS attacks and deals with leaks. if (!isSystemToast) { int count = 0; finalint N = mToastQueue.size(); for (int i=0; i<N; i++) { final ToastRecord r = mToastQueue.get(i); if (r.pkg.equals(pkg)) { count++; if (count >= MAX_PACKAGE_NOTIFICATIONS) { Slog.e(TAG, "Package has already posted " + count + " toasts. Not showing more. Package=" + pkg); return; } } } }
Binder token = new Binder(); mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY); record = new ToastRecord(callingPid, pkg, callback, duration, token); mToastQueue.add(record); index = mToastQueue.size() - 1; keepProcessAliveIfNeededLocked(callingPid); } // If it's at index 0, it's the current toast. It doesn't matter if it's // new or just been updated. Call back and tell it to show itself. // If the callback fails, this will remove it from the list, so don't // assume that it's valid after this. if (index == 0) { showNextToastLocked(); } } finally { Binder.restoreCallingIdentity(callingId); } } }
// Limit the number of toasts that any given package except the android // package can enqueue. Prevents DOS attacks and deals with leaks. if (!isSystemToast) { int count = 0; finalint N = mToastQueue.size(); for (int i=0; i<N; i++) { final ToastRecord r = mToastQueue.get(i); if (r.pkg.equals(pkg)) { count++; if (count >= MAX_PACKAGE_NOTIFICATIONS) { Slog.e(TAG, "Package has already posted " + count + " toasts. Not showing more. Package=" + pkg); return; } } } }
keepProcessAliveIfNeededLocked(record.pid); if (mToastQueue.size() > 0) { // Show the next one. If the callback fails, this will remove // it from the list, so don't assume that the list hasn't changed // after this point. showNextToastLocked(); // 继续显示队列里的下一个 Toast } }
/** * schedule handleShow into the right thread */ @Override publicvoidshow(IBinder windowToken){ if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.obtainMessage(SHOW, windowToken).sendToTarget(); }
/** * schedule handleHide into the right thread */ @Override publicvoidhide(){ if (localLOGV) Log.v(TAG, "HIDE: " + this); mHandler.obtainMessage(HIDE).sendToTarget(); }