preface

As we all know, the Toast display time has two choices, the long display is 3.5 seconds, and the end display is 2 seconds. So if you want to do a long display, how do you do it? There is a legacy app that is implemented by opening a thread and calling the show method repeatedly. It has no problem in these years until the system version is updated to Android9.0. The implementation is as follows:

mToast = new Toast(context); mToast.setDuration(Toast.LENGTH_LONG); mToast.setView(layout); . mToast.show(); // Call the show method repeatedly in the thread to achieve the purpose of long displayCopy the code

On Android9.0, Toast flashed up and disappeared, not for as long as expected. Why is that?

An overview of the

Here we first have a general understanding of Toast display process.

Toast to use

When you use Toast, the simple way is as follows:

Toast.makeText(mContext, "hello world", duration).show();
Copy the code

This will display a toast. There is also a custom view:

mToast = new Toast(context);
mToast.setDuration(Toast.LENGTH_LONG);
mToast.setView(layout);
mToast.show(); 
Copy the code

The principle is the same: first new Toast, then set the display duration, set the view to be displayed in Toast (text is also a view), and then show it.

Principle of Toast

Toast to realize

Let’s look at the Toast implementation:

//frameworks/base/core/java/android/widget/Toast.java
public Toast(@NonNull Context context, @Nullable Looper looper) {
    mContext = context;
    mTN = new TN(context.getPackageName(), looper);
    mTN.mY = context.getResources().getDimensionPixelSize(
            com.android.internal.R.dimen.toast_y_offset);
    mTN.mGravity = context.getResources().getInteger(
            com.android.internal.R.integer.config_toastDefaultGravity);
}
Copy the code

The Toast constructor is very simple, mainly the mTN member, where all subsequent operations on Toast are performed. Next, set the Toast display duration and display contents:

public void setView(View view) {
    mNextView = view;
}

public void setDuration(@Duration int duration) {
    mDuration = duration;
    mTN.mDuration = duration;
}
Copy the code

Toast display

public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } INotificationManager service = getService(); / / this is a notification service String PKG. = mContext getOpPackageName (); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }Copy the code

The show method is simple and ends up calling the enqueueToast method of the notification service:

frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java public void enqueueToast(String pkg, ITransientNotification callback, int duration) { ... final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg)); . synchronized (mToastQueue) { int callingPid = Binder.getCallingPid(); long callingId = Binder.clearCallingIdentity(); try { ToastRecord record; int index; // All packages aside from the android package can enqueue one toast at a time if (! isSystemToast) { index = indexOfToastPackageLocked(pkg); } else { index = indexOfToastLocked(pkg, callback); } // If the package already has a toast, we update its toast // in the queue, we don't move it to the end of the queue. if (index >= 0) { record = mToastQueue.get(index); record.update(duration); try { record.callback.hide(); } catch (RemoteException e) { } record.update(callback); } else { 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); }}}Copy the code

Toast management is centralized management through ToastRecord type list, NotificationManagerService will each Toast encapsulation for ToastRecord object, and add to the mToastQueue, MToastQueue is of type ArrayList. In enqueueToast, it first checks whether the application is a system application. If so, indexOfToastLocked checks whether any toAsts that meet the conditions exist:

int indexOfToastLocked(String pkg, ITransientNotification callback)
{
    IBinder cbak = callback.asBinder();
    ArrayList<ToastRecord> list = mToastQueue;
    int len = list.size();
    for (int i=0; i<len; i++) {
        ToastRecord r = list.get(i);
        if (r.pkg.equals(pkg) && r.callback.asBinder().equals(cbak)) {
            return i;
        }
    }
    return -1;
}
Copy the code

Judgment is based on the package name and the callback, the callback is said to TN, this is a type of Binder, inherited from ITransientNotification. The Stub. If the conditions are met, the corresponding index is returned; otherwise, -1 is returned. If the ToastRecord object is added to the mToastQueue and the index is 0, the ToastRecord object will be added to the mToastQueue.

record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
Copy the code

Then the following branch will follow:

if (index == 0) { showNextToastLocked(); } void showNextToastLocked() {ToastRecord record = mtoastqueue.get (0); while (record ! = null) { if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback); try { record.callback.show(record.token); / / call TN class show method scheduleDurationReachedLocked (record); // When time is up, hide Toast return; } catch (RemoteException e) { ... }}}Copy the code

As mentioned above, TN provides show, hide, cancel and other methods externally. In these methods, internal handlers are used to process:

//frameworks/base/core/java/android/widget/Toast.java public void show(IBinder windowToken) { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.obtainMessage(SHOW, windowToken).sendToTarget(); } case SHOW: {IBinder token = (IBinder) msg.obj; handleShow(token); break; } public void handleShow(IBinder windowToken) { ... if (mView ! = mNextView) { // remove the old view if necessary handleHide(); mView = mNextView; . mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); . try { mWM.addView(mView, mParams); / / to the WMS for the next step of operation, it turns out, our view trySendAccessibilityEvent (); } catch (WindowManager.BadTokenException e) { /* ignore */ } } }Copy the code

Calling the show method ends up calling the HandlesHow method, where the VIEW is displayed using the WMS service.

Toast to hide

That’s it. When do you hide and disappear? In scheduleDurationReachedLocked approach:

//frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
private void scheduleDurationReachedLocked(ToastRecord r)
{
    mHandler.removeCallbacksAndMessages(r);
    Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
    long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
    mHandler.sendMessageDelayed(m, delay);
}
Copy the code

Here, a handler is also used for processing. The delay time depends on the Toast display time we set earlier. The long duration is 3.5 seconds, and the short duration is 2 seconds.

MESSAGE_DURATION_REACHED Message handling is as follows:

case MESSAGE_DURATION_REACHED: handleDurationReached((ToastRecord)msg.obj); break; private void handleDurationReached(ToastRecord record) { if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback); synchronized (mToastQueue) { int index = indexOfToastLocked(record.pkg, record.callback); if (index >= 0) { cancelToastLocked(index); } } } void cancelToastLocked(int index) { ToastRecord record = mToastQueue.get(index); try { record.callback.hide(); // Hide the Toast} catch (RemoteException e) {... } ToastRecord lastToast = mToastQueue.remove(index); // Remove Toast from the list... 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(); }}Copy the code

This method calls TN’s hide method to hide the Toast and then removes it from the list. Take a look at the hidden process:

case HIDE: { handleHide(); // Don't do this in handleHide() because it is also invoked by // handleShow() mNextView = null; // Break the view; } public void handleHide() { if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView); if (mView ! = null) { ... mWM.removeViewImmediate(mView); . mView = null; }}Copy the code

The hidden process, as simple as it is, is to remove the view from the window and Null the mNextView and mView.

So much for showing and hiding Toast. Here’s why multiple shows cause Toast to disappear.

The disappearance of the Toast

If the mToastQueue is not empty and the toastqueue exists, it will branch into the following branches:

if (! isSystemToast) { index = indexOfToastPackageLocked(pkg); } else { index = indexOfToastLocked(pkg, callback); } // If the package already has a toast, we update its toast // in the queue, we don't move it to the end of the queue. if (index >= 0) { record = mToastQueue.get(index); record.update(duration); try { record.callback.hide(); // Hide} catch (RemoteException e) {} Record.update (callback); }}Copy the code

The hide process is already clear, freeing resources and setting mNextView and mView to Null. ShowNextToastLocked () is then called to display the second Toast, and finally to TN’s handleShow method:

public void handleShow(IBinder windowToken) {
    // ...
    if (mView != mNextView) {
        // ...
        mView = mNextView;
        // ...
        mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        // ...
        mWM.addView(mView, mParams);
        // ...
    }
}
Copy the code

Since all toasts correspond to a TN object, mView and mNextView are null, mwm.addView () will not be executed, and Toast will not be displayed.

The solution

What if I want to display a Toast all the time in Android9.0? Use local toasts, not global toasts.

One thing that’s a little strange is that Android10.0 rolls this mechanism back when I look at the code. On Android10.0, Toast can always be displayed:

If (index >= 0) {record = mtoastqueue.get (index); record.update(duration); }Copy the code

conclusion

Android9.0 is the only version of Android that does this. It simply disables the app from displaying Toast for long periods of time. This was removed in version 10.0, did you find it inappropriate?

Wechat official account

I also wrote an article in the wechat public number, update more timely, interested in the following two-dimensional code can scan, or wechat search [Android system combat development], pay attention to surprise oh!