background

Toast is a common technology on the Android platform. From the perspective of users, Toast is the most basic prompt control for interaction between users and App. From a developer’s perspective, Toast is one of the most commonly used debugging tools during development. In addition, the Toast syntax is very simple, requiring only one line of code. Toast is widely used in Android development due to its simplicity and ease of use.

However, Toast is provided at the system level, does not rely on the front page, and there is a risk of abuse. In order to avoid these risks, Google has continuously optimized and restricted Android versions in the course of iterations. These limitations inevitably affected normal business logic, and during the iteration we encountered the following problems:

  1. When the [display notification] switch of an App is turned off in the setting, Toast will no longer pop up, which greatly affects the user experience.
  2. Toast occurs under Android 7.1.2(API25)BadTokenExceptionThe App crashes due to an exception.
  3. The customTYPE_TOASTType Window, occurring in Android 7.1.1, 7.1.2token null is not validThe App crashes due to an exception.

Struggle with Toast

In the business of Meituan platform, Toast is used as a prompt control for the interaction of the main process, such as prompting after placing an order, commenting or sharing. If Toast is restricted, it can lead to misunderstandings among users. In order to solve the problem that normal business Toast was limited by the system, we launched a series of struggles with Toast.

Struggle 1: Toast does not pop up

For example, a user complained that Meituan App did not give any hint after sharing moments, and did not know whether the sharing was successful. The specific reason is that the [display notification] switch of Meituan App is turned off in the Settings, so the notification permission cannot be obtained, which greatly affects the user experience. However, in the following systems of Android 4.4(API19), we cannot judge the status of the switch, that is, whether the notification permission is enabled, so we cannot sense whether Toast is popped up. To solve this problem, we need to start from the source code of Toast, and the final source code summary steps are as follows:

  1. inToast#show()In the source code, the Toast display is not controlled by AIDL, but is obtained using the INotificationManagerNotificationManagerService(NMS)This remote service.
  2. callservice.enqueueToast(pkg, tn, mDuration)The display of the current Toast is added to the notification queue and a TN object is passed, which is the display state that the NMS uses to send back the Toast.
  3. In the tn callback method, useWindowManagerAdd the constructed Toast to the current window. Note that the window’s type isTYPE_TOAST.

Toast does not pop up cause analysis

So why does disabling notification rights cause Toast to stop popping up? According to the above analysis, the Toast display is controlled by the NMS service, which verifies some permissions and tokens. Once the notification permissions are closed, Toast will not pop up.

Feasibility study

If the verification of NMS service can be bypassed, our demands can be met. The method of bypassing is to implement our own MToast according to Toast source code and replace NMS with our own ToastManager, as shown below:

Once the solution is decided, all you need to do is replace the code. As a platform App, Meituan App uses Toast a lot. Manual replacement is bound to leave something missing. In order to solve this problem with less manpower, we adopt the following scheme.

The solution

AspectJ is an AOP programming tool in Java. The basic principle is to modify the aspect of the code at the time of code compilation, insert our pre-written logic or directly replace the implementation of the current method. What meituan does is borrow AspectJ to intercept and replace Toast’s call implementation from the source.

The key codes are as follows:

@Aspect
public class ToastAspect {
  @Pointcut("call(* android.widget.Toast+.show(..) )")
  public void toastShow(a) {}@Around("toastShow()")
  public void toastShow(ProceedingJoinPoint point) {
     Toast toast = (Toast) point.getTarget();
     Context context = (Context) ReflectUtils.getValue(toast, "mContext");
     if (Build.VERSION.SDK_INT >= 19 && NotificationManagerCompat.from(context).areNotificationsEnabled()) {
         point.proceed(point.getArgs());
     } else{ floatToastShow(toast, context); }}private static void floatToastShow(Toast toast, Context context) {...newMToast(context) .setDuration(mDuration) .setView(mNextView) .setGravity(mGravity, mX, mY) .setMargin(mHorizontalMargin, mVerticalMargin) .show(); }}Copy the code

Where MToast is a Window of type TYPE_TOAST, the business code can continue to pop Toast without making any changes even if the notification permission is disabled. The bottom layer has been replaced with its own MToast by non-perceptive ones, achieving the goal with minimal cost.

Struggle. 2:BadTokenException

Meituan App online often reported BadTokenExceptionCrash, and focused on Android 5.0-Android 7.1.2 models. The specific Crash stack is as follows:

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@6caa743 is not valid; is your activity running?
    at android.view.ViewRootImpl.setView(ViewRootImpl.java:607)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:341)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:106)
    at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3242)`BadTokenException`
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2544)
    at android.app.ActivityThread.access$900(ActivityThread.java:168)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1378)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:150)
    at android.app.ActivityThread.main(ActivityThread.java:5665)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:822)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:712)
Copy the code

BadTokenExceptionCause analysis,

As we know, on Android, the display of any view depends on a view Window, and Toast also needs a Window. As analyzed above, the type of this Window is TYPE_TOAST, which is a system Window. This window is eventually managed by the WindowManagerService(WMS) flag. But how can our normal application have permission to add system Windows? After reviewing the source code, the following steps are required:

  1. When a Toast is displayed, the NMS generates a token, and the NMS itself is a system-level service, so the token generated by it must have permission to add system Windows.
  2. The NMS passes the generated tokens back to our own application process through the ITransientNotification, or TN object.
  3. The application calls the handleShow method to add a window to WindowManager.
  4. WindowManager checks whether the token of the current window is valid, and if so, adds the window to display Toast; If not, the above exception is thrown and Crash occurs.

The detailed schematic diagram is as follows:

In the NMS source code of Android 7.1.1, the key code is as follows:

void showNextToastLocked(a) {
   ToastRecord record = mToastQueue.get(0);
   while(record ! =null) {
       try {
           // Call the show method of the TN object to display toast and pass back the token
           record.callback.show(record.token);
           // Timeout processing
           scheduleTimeoutLocked(record);
           return;
       } catch(RemoteException e) { ... }}}private void scheduleTimeoutLocked(ToastRecord r)
{
   mHandler.removeCallbacksAndMessages(r);
   Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
   long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
   // According to the toast display time, delay the trigger message, and finally call the following method
   mHandler.sendMessageDelayed(m, delay);
}

private void handleTimeout(ToastRecord record)
{
   synchronized (mToastQueue) {
       int index = indexOfToastLocked(record.pkg, record.callback);
       if (index >= 0) { cancelToastLocked(index); }}}void cancelToastLocked(int index) {
   ToastRecord record = mToastQueue.get(index);
   try {
       // Call the tn object's hide method to hide the toast
       record.callback.hide();
   } catch (RemoteException e) {
      ...
   }

   ToastRecord lastToast = mToastQueue.remove(index);
   // Remove the toastmaster of the current toast
   mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY); . }Copy the code

Validation problem

After showNextToastLocked() is called, if handleShow() cannot be called because the main thread is blocked, the timeout logic will be triggered and the token will be invalid. After the main thread is blocked, the token is invalid when the Toast show method is executed, and the BadTokenException exception is thrown, causing the Crash.

You can verify this exception with the following code:

Toast.makeText(this."Crash test", Toast.LENGTH_SHORT).show();
try {
   Thread.sleep(5000);
} catch (InterruptedException e) {
   e.printStackTrace();
}
Copy the code

The solution

So how do you resolve this exception? The first thought is to add a try-catch to Toast, but it doesn’t work because the exception is not thrown immediately in the current thread. Instead, it is added to the message queue, waiting for the message to actually execute. Google fixed this issue in the Android 8.0 code submission. Comparing the 8.0 source code with the previous version, we found that Google caught the exception in the message execution as we analyzed. What about crashes prior to 8.0? The Meituan platform uses a generic solution similar to proxy reflection, as shown below:

Basic Principles: We use our own ToastHandler to replace the Handler inside Toast. ToastHandler is used to catch exceptions. This is the same as the fix for Android 8.0, except that one is solved at the system level and the other is solved at the user level.

Struggle three:token null is not valid

In Android 7.1.1, 7.1.2, and Android 8.0 released in August last year, we have another exception that token NULL is not valid. This exception stack is as follows:

android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
   at android.view.ViewRootImpl.setView(ViewRootImpl.java:683)
   at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
   at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
Copy the code

token null is not validCause analysis,

This exception is not a Toast exception, but a result of some restrictions Google has placed on Windows Manage. Android has made some restrictions and changes to Windows Manager since version 7.1.1. In particular, Windows of type TYPE_TOAST must be added by passing a token for permission verification. Toast source in 7.1.1 and above also has the change, Toast WindowManager. LayoutParams added a token attribute parameters, has been the source of the property on the above analysis, it is initialized in the NMS, are used to add the check window types. When the notification permission is disabled, AspectJ will eventually call our encapsulated MToast, but the MToast does not go through the NMS, so it cannot obtain this property. In addition, even if we generate a token ourselves according to the NMS method, The token does not have the TYPE_TOAST permission added to it, so the exception cannot be avoided.

The key codes in the source code are as follows:

The method signature has a token of type IBinder created in the NMS
public void handleShow(IBinder windowToken) {...if(mView ! = mNextView) { ... mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; mParams.packageName = packageName; mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;// The token is added here
     mParams.token = windowToken;
     
     if(mView.getParent() ! =null) {
         if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeView(mView); }...try {
         // Version 8.0 catches this exception
         mWM.addView(mView, mParams);
         trySendAccessibilityEvent();
     } catch (WindowManager.BadTokenException e) {
         /* ignore */}}}Copy the code

The solution

After investigation, we found that Google’s restrictions on Windows Manager forced us to give up using TYPE_TOAST Windows instead of Toast, which also represented the end of our scheme of using Windows Manager.

Struggle to summarize

Our core goal is to continue to see notifications when users turn off the notification message switch, so we used WindowManager to add a custom Window to replace Toast. However, during the replacement process, we encountered some Toast Crash exceptions. In order to solve these crashes, We propose using a custom ToastHandler to catch exceptions and ensure that the app works properly. In the scheme promotion, in order to complete the replacement with less manpower and higher efficiency, we used AspectJ’s scheme. Finally, starting from Android 7.1.1, due to Google’s restrictions on Windows Manager, this method of replacing Toast with a custom Window is no longer feasible, so we started to look for other feasible solutions to replace Toast.

Possible alternatives to Toast

In order to continue to enable users to see notifications and shield the Crash caused by Toast even when the notification permission is disabled, we investigated, analyzed and tried the following schemes.

  1. In 7.1.1 and above, continue to use WindowManager mode, but need to change type to TYPE_PHONE and other floating window permissions.
  2. Use Dialog, DialogFragment, PopupWindow and other popover controls to implement a notification.
  3. Following the Snackbar implementation, find a parent layout to which you can add a layout, and add notifications using addView.

The common point of the above schemes is to bypass the check of notification permission. Even if the user disables notification permission, our self-defined notification can still pop up unaffected, but there are also obvious defects, as shown in the figure below:

After comparison, we also adopt Snackbar to replace Toast, because Snackbar is the official control recommended by Android after MaterialDesign is launched from 5.0 system, and is better than Toast in terms of interaction friendliness. For example: It supports gesture operation and CoordinatorLayout linkage, etc. Snackbar as a prompt control is also widely used in the market at present, while other schemes have obvious defects as follows:

First, use WindowManager to add floating Windows. Although this method keeps perfect consistency with the native Toast, it requires too many permissions and pits. The TYPE_PHONE permissions are much more sensitive than the TYPE_TOAST permissions, and on Android 8.0 you must use the TYPE_APPLICATION_OVERLAY type, and apply for two permissions that not only need to be declared in the manifest file, In addition, most mobile phones are closed by default, so we need to guide users to turn on the Toast. If users choose not to turn on the Toast, they still cannot pop up. Also need to adapt to many customized ROM domestic models. It is not desirable to bypass the notification permissions pit and jump into the hover window permissions pit.

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW"/>
Copy the code

Dialog, DialogFragment, and PopupWindow all rely heavily on activities. They cannot be created or displayed without an Activity as a context, and simple notifications are too heavy. In addition, in terms of UI presentation and API consistency, Toast has almost nothing to do with the cost of additional encapsulation.

Have a problem

We encountered the following two problems when replacing Toast with Snackbar:

  1. When a Snackbar pops up, it is obscured by Dialog, PopupWindow, etc.
  2. Snackbar cannot be displayed across pages, which is determined by the implementation principle of Snackbar.

The solution

First of all, in order to meet the scalability and flexibility of our own business, we refer to the source code of the system Snackbar to customize according to demand, such as diversified style extensions, in-and-out animation extensions, extensions that support custom layout, etc., with richer interfaces. On the one hand, it is to solve the above problems. On the other hand, it is also to develop and adapt quickly in the iterative process of business. Here are the basic class diagram dependencies:

Problem one solved

When Snackbar pops up, it is covered by Dialog, PopupWindow and other controls. The reason is that Snackbar relies on View. When passing the View of the Activity layout to Snackbar as the parent View of Snackbar display dependency, After the Dialog, PopupWindow, etc., the Snackbar will be blocked by the control. The correct approach is to pass the views that PopupWindow and Dialog depend on directly to the Snackbar. So our custom Snackbar not only supports passing this View, but also directly passing PopupWindow and Dialog instances. The SnackbarBuilder method in the figure above reflects this change.

Problem two solution

Complicating matters is that Snackbar does not support cross-page presentation, and we have a lot of code like this in the project:

Toast.makeText(this."Popup message", Toast.LENGTH_SHORT).show();
finish();
Copy the code

When Snackbar is replaced with Toast, the message will flash before the user can check it, because the Activity that Snackbar depends on is destroyed. To solve this problem, we have discussed three solutions:

Plan 1: Use startActivityForResult to replace all notifications displayed across pages, that is, use startActivityForResult on page A to jump to page B, and rewrite the logic of popping Toast on page B to pop Snackbar on page A.

The advantage of this solution is that the responsibility for what notification should be displayed after the page is finished and who should trigger the notification display lies with the caller. The downside is that the code changes a lot. So we abandoned it.

Scheme 2: Using Application. ActivityLifecycleCallbacks global to monitor the Activity of life cycle, when a page is closed, record the Snackbar needs to display the remaining time, after entering the next Activity, Let the Snackbar continue to be displayed.

This scheme: the advantage is that the code change momentum is small; The disadvantage is that during page switching, if the Snackbar is not displayed, there will be a flash. Although technically good and the code is very low in invasiveness, the flicker is unacceptable to the product, so this option is not considered.

Solution 3: Use local broadcast for cross-page display, which is the final solution adopted by Meituan. The specific principle is as follows

  1. Register A broadcast using the currently passed Context before page A jumps to page B.
  2. Before page B finishes, send the broadcast that A registered before the jump and return the message that needs to be displayed as an Intent.
  3. Get the instance of page A in the broadcast, use Snackbar to display the message returned from page B, and unRegister the current broadcast.

This is the automatic version of scheme 1. In order to achieve the automatic effect and minimize the intrusion of the original code, we designed an auxiliary class, namely SnackbarHelper in the figure above. The schematic diagram is as follows:

SnackbarHelper provides a unified entrance, low access cost, Just need to use original context. StartActivity (), the context, startActivityForResult (), the context. The finish () place to SnackBarHelper method can be of the same name below. In this way, the cross-page display of Snackbar is completed through the broadcast method, and the amount of code modification on the business side is only to change the call method, and the change is minimal.

conclusion

At present, this solution is widely used in Meituan business and can cover most scenarios. The presentation form of notifications is basically the same as Toast, which not only solves the dilemma that users cannot see notifications when notifications are banned, but also reduces the customer complaint rate.

Author’s brief introduction

Zi Yao, senior engineer of Meituan-Dianping, joined Meituan-Dianping in 2017, responsible for the research and development of platform search and platform homepage.

Teng Fei, senior engineer of Meituan-Dianping, joined Meituan-Dianping in 2015 and is in charge of platform basic business group, responsible for platform business iteration.

recruitment

If you are interested in our team, you can follow our column. The client technology team of meituan platform is recruiting technical experts for a long time. Interested students can send their resumes to fangjintao#meituan.com, detailed JD.