关于 Toast 的两个问题分析和解决方案


提示:本文基于 Android API 25

一、WindowManager.BadTokenException

Toast 在 Android API 25 上有几率会抛出这个异常

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_toast_window_manager_bad_token_exception)

        Toast.makeText(this, "测试 Toast Exception", Toast.LENGTH_SHORT).apply {
            fixWindowManagerBadTokenExceptionIn(this)
        }.show()
		
		// 这里 sleep 2 秒
        TimeUnit.SECONDS.sleep(2)

        LogUtils.i(TAG, "休眠后打印")
    }

以上的代码在 Android API 25 上的设备上必现这个异常

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@cdadaf1 is not valid; is your activity running?
    at android.view.ViewRootImpl.setView(ViewRootImpl.java:990)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:533)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:95)
    at android.widget.Toast$TN.handleShow(Toast.java:504)
    at android.widget.Toast$TN$2.handleMessage(Toast.java:387)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at com.study.ToastWindowManagerBadTokenExceptionActivity$SafelyHandlerWrapper.dispatchMessage(ToastWindowManagerBadTokenExceptionActivity.kt:64)
    at android.os.Looper.loop(Looper.java:159)
    at android.app.ActivityThread.main(ActivityThread.java:6385)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1096)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:883)

异常描述 android.view.WindowManager$BadTokenException: Unable to add window – token android.os.BinderProxy@cdadaf1 is not valid; is your activity running? 下面就来分析一下原因

    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        Toast result = new Toast(context);

        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);
        
        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }

makeText 就是创建一个 Toast 对象并且 inflate 一个 View

    public Toast(Context context) {
        mContext = context;
        mTN = new TN();
        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);
    }

在 Toast 的构造方法里创建 TN 对象

        TN() {
            // 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;
        }

TN 的构造方法里只是创建了一个 WindowManager.LayoutParams 对象 type 是 WindowManager.LayoutParams.TYPE_TOAST 再回头看下 show 方法

    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

继续跟着看 getService 方法

    private static INotificationManager sService;

    static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }

这里是 AIDL 的实现具体的实现是 com.android.server.notification.NotificationManagerService#mService 调用它的 enqueueToast 方法

public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            // 省略部分代码,这部分第二个问题再分析

            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    int index = indexOfToastLocked(pkg, callback);
                    if (index >= 0) {
                        // 更新
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } else {
                        if (!isSystemToast) {
                            int count = 0;
                            final int N = mToastQueue.size();
                            for (int i=0; i<N; i++) {
                                 final ToastRecord r = mToastQueue.get(i);
                                 if (r.pkg.equals(pkg)) {
                                     count++;
                                     // 非系统应用 Toast 列表最大数 50
                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                         Slog.e(TAG, "Package has already posted " + count
                                                + " toasts. Not showing more. Package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }

                        Binder token = new Binder();
                        // 创建 token 并存储到一个 HashMap 中
                        mWindowManagerInternal.addWindowToken(token,
                                WindowManager.LayoutParams.TYPE_TOAST);
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveIfNeededLocked(callingPid);
                    }
                    
                    // 显示下一个 Toast 
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

上面 mWindowManagerInternal 的实现是 com.android.server.wm.WindowManagerService.LocalService

    public void addWindowToken(IBinder token, int type) {
        WindowManagerService.this.addWindowToken(token, type);
    }

    public void addWindowToken(IBinder token, int type) {
        if (!checkCallingPermission(android.Manifest.permission.MANAGE_APP_TOKENS,
                "addWindowToken()")) {
            throw new SecurityException("Requires MANAGE_APP_TOKENS permission");
        }

        synchronized(mWindowMap) {
            WindowToken wtoken = mTokenMap.get(token);
            if (wtoken != null) {
                Slog.w(TAG_WM, "Attempted to add existing input method token: " + token);
                return;
            }
            // 创建 WindowToken 对象
            wtoken = new WindowToken(this, token, type, true);
            // 加入到 HashMap 中
            mTokenMap.put(token, wtoken);
            if (type == TYPE_WALLPAPER) {
                mWallpaperControllerLocked.addWallpaperToken(wtoken);
            }
        }
    }

再看一下显示下一个 Toast 的方法 showNextToastLocked

    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
            	// 这里的 callback 是传过来的 TN 对象
                record.callback.show(record.token);
                // 发送延时消息移除 Toast 
                scheduleTimeoutLocked(record);
                return;
            } catch (RemoteException e) {
				// 省略部分代码
            }
        }
    }

先看一下 TN 的 show 方法

        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(0, windowToken).sendToTarget();
        }
        
        final Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                IBinder token = (IBinder) msg.obj;
                handleShow(token);
            }
        };

        public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                // We can resolve the Gravity here by using the Locale for getting
                // the layout direction
                final Configuration config = mView.getContext().getResources().getConfiguration();
                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
                mParams.gravity = gravity;
                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
                    mParams.horizontalWeight = 1.0f;
                }
                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
                    mParams.verticalWeight = 1.0f;
                }
                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;
                mParams.token = windowToken;
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
                // 通过 WindowManager addView 这里的 mWM 是 android.view.WindowManagerImpl
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            }
        }

看下 WindowManagerImpl 的 addView 方法

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        // 省略部分代码    

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
        	// 省略部分代码

			// 创建 ViewRootImpl 对象 ViewRootImpl 不是 View 但却是顶级 View 的 parent
			// 所有 View 刷新的时候都是一层层通过 getParent 找到 ViewRootImpl 触发刷新
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            // 调用 ViewRootImpl 的 setView
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }
    public ViewRootImpl(Context context, Display display) {
        mContext = context;
        // 这里创建了 mWindowSession 具体实现是 com.android.server.wm.Session
        mWindowSession = WindowManagerGlobal.getWindowSession();
        // 省略部分代码
    }

接着看 ViewRootImpl 的 setView 方法

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;

                // 省略部分代码    
                
                try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                } catch (RemoteException e) {
                    mAdded = false;
                    mView = null;
                    mAttachInfo.mRootView = null;
                    mInputChannel = null;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    throw new RuntimeException("Adding window failed", e);
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }

                // 省略部分代码

                if (res < WindowManagerGlobal.ADD_OKAY) {
                    mAttachInfo.mRootView = null;
                    mAdded = false;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    switch (res) {
                        // 根据上面抛出来的异常信息肯定是触发了这个 case 
                        case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                        case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not valid; is your activity running?");
                        case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not for an application");
                        case WindowManagerGlobal.ADD_APP_EXITING:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- app for token " + attrs.token
                                    + " is exiting");
                        case WindowManagerGlobal.ADD_DUPLICATE_ADD:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- window " + mWindow
                                    + " has already been added");
                        case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
                            // Silently ignore -- we would have just removed it
                            // right away, anyway.
                            return;
                        case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
                            throw new WindowManager.BadTokenException("Unable to add window "
                                    + mWindow + " -- another window of type "
                                    + mWindowAttributes.type + " already exists");
                        case WindowManagerGlobal.ADD_PERMISSION_DENIED:
                            throw new WindowManager.BadTokenException("Unable to add window "
                                    + mWindow + " -- permission denied for window type "
                                    + mWindowAttributes.type);
                        case WindowManagerGlobal.ADD_INVALID_DISPLAY:
                            throw new WindowManager.InvalidDisplayException("Unable to add window "
                                    + mWindow + " -- the specified display can not be found");
                        case WindowManagerGlobal.ADD_INVALID_TYPE:
                            throw new WindowManager.InvalidDisplayException("Unable to add window "
                                    + mWindow + " -- the specified window type "
                                    + mWindowAttributes.type + " is not valid");
                    }
                    throw new RuntimeException(
                            "Unable to add window -- unknown error code " + res);
                }

                // 省略部分代码
            }
        }
    }

再接着看

    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            Rect outOutsets, InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }

com.android.server.wm.WindowManagerService#addWindow

    public int addWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            InputChannel outInputChannel) {
        // 省略部分代码    
        synchronized(mWindowMap) {
            // 省略部分代码

            boolean addToken = false;
            WindowToken token = mTokenMap.get(attrs.token);
            AppWindowToken atoken = null;
            boolean addToastWindowRequiresToken = false;

            if (token == null) {
                if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                    Slog.w(TAG_WM, "Attempted to add application window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                if (type == TYPE_INPUT_METHOD) {
                    Slog.w(TAG_WM, "Attempted to add input method window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                if (type == TYPE_VOICE_INTERACTION) {
                    Slog.w(TAG_WM, "Attempted to add voice interaction window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                if (type == TYPE_WALLPAPER) {
                    Slog.w(TAG_WM, "Attempted to add wallpaper window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                if (type == TYPE_DREAM) {
                    Slog.w(TAG_WM, "Attempted to add Dream window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                if (type == TYPE_QS_DIALOG) {
                    Slog.w(TAG_WM, "Attempted to add QS dialog window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                if (type == TYPE_ACCESSIBILITY_OVERLAY) {
                    Slog.w(TAG_WM, "Attempted to add Accessibility overlay window with unknown token "
                            + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                if (type == TYPE_TOAST) {
                    // Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
                    if (doesAddToastWindowRequireToken(attrs.packageName, callingUid,
                            attachedWindow)) {
                        Slog.w(TAG_WM, "Attempted to add a toast window with unknown token "
                                + attrs.token + ".  Aborting.");
                        return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                }
                token = new WindowToken(this, attrs.token, -1, false);
                addToken = true;
            } 
            // 省略部分代码

        }

        // 省略部分代码   

        return res;
    }

这里应该是走到了 token 等于 null 的分支,上面已经说了会把 token 加入到 HashMap 中为什么这里为 null 呢,再返回看一下 scheduleTimeoutLocked 这个发送延时消息的方法

    private void scheduleTimeoutLocked(ToastRecord r)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
        // 这里根据传入的 duration 设置延迟时间
        // Toast.LENGTH_LONG 对应 LONG_DELAY 
        // Toast.LENGTH_SHORT 对应 SHORT_DELAY
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        mHandler.sendMessageDelayed(m, delay);
    }
    
// com.android.server.notification.NotificationManagerService
static final int LONG_DELAY = PhoneWindowManager.TOAST_WINDOW_TIMEOUT;
static final int SHORT_DELAY = 2000;

// com.android.server.policy.PhoneWindowManager
/** Amount of time (in milliseconds) a toast window can be shown. */
public static final int TOAST_WINDOW_TIMEOUT = 3500; // 3.5 seconds

所以可以知道 LENGTH_LONG 显示时长 3500 毫秒 LENGTH_SHORT 显示时长 2000 毫秒,所以上面休眠 2000 毫秒会触发这个延时消息

    private final class WorkerHandler extends Handler
    {
        @Override
        public void handleMessage(Message msg)
        {
            switch (msg.what)
            {
                case MESSAGE_TIMEOUT:
                    handleTimeout((ToastRecord)msg.obj);
                    break;
				// 省略部分代码	
            }
        }

    }

    private void handleTimeout(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();
        } catch (RemoteException e) {
			// 省略部分代码
        }

		// 移除当前的 toast
        ToastRecord lastToast = mToastQueue.remove(index);
        // 这里会移除 token 
        mWindowManagerInternal.removeWindowToken(lastToast.token, true);

        keepProcessAliveIfNeededLocked(record.pid);
        // 显示下一个 Toast 
        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();
        }
    }

    public void hide() {
        if (localLOGV) Log.v(TAG, "HIDE: " + this);
        mHandler.post(mHide);
    }

    final Runnable mHide = new Runnable() {
        @Override
        public void run() {
            handleHide();
            // Don't do this in handleHide() because it is also invoked by handleShow()
            mNextView = null;
        }
    };

    public void handleHide() {
        if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
        if (mView != null) {
            // note: checking parent() just to make sure the view has
            // been added...  i have seen cases where we get here when
            // the view isn't yet added, so let's try not to crash.
            if (mView.getParent() != null) {
                if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                // 移除 view
                mWM.removeViewImmediate(mView);
            }

            mView = null;
        }
    }

看下 removeWindowToken 的代码

    public void removeWindowToken(IBinder token) {
        if (!checkCallingPermission(android.Manifest.permission.MANAGE_APP_TOKENS,
                "removeWindowToken()")) {
            throw new SecurityException("Requires MANAGE_APP_TOKENS permission");
        }

        final long origId = Binder.clearCallingIdentity();
        synchronized(mWindowMap) {
            DisplayContent displayContent = null;
            // 移除 token
            WindowToken wtoken = mTokenMap.remove(token);
        }
        // 省略部分代码
    }

可以看到在这里移除了 token 所以再去获取所以返回了 null 总结一下就是当 UI 线程的某个消息阻塞导致了 TN 的 show 方法在 hide 方法之后执行所以抛出了这个异常。此异常只会发生在 Android API 25 因为之后 google 处理了这个问题
Android 源码差异
这里是 Android API 25 和 Android API 26 的差异可以看到是直接 catch 住了这个异常,这个异常不能在弹出 toast 的地方直接捕获因为异常是在消息循环里抛出的。修复的方式有两种,一种是通过反射替换掉 TN 的 mHandler 然后对 handleMessage 加 tay-catch 优点是比较简单,缺点是粒度太粗可能在其他场景触发的这个异常也会被捕获。代码如下:

    // 修复 Android API 25 上的 android.view.WindowManager$BadTokenException
    private fun fixWindowManagerBadTokenExceptionIn(toast: Toast) {
        if (Build.VERSION.SDK_INT != Build.VERSION_CODES.N_MR1) {
            return
        }

        try {
            val mTnField = Toast::class.java.getDeclaredField("mTN")
            mTnField.isAccessible = true
            val mTn = mTnField.get(toast)

            val mHandlerField = mTn.javaClass.getDeclaredField("mHandler")
            mHandlerField.isAccessible = true
            val mHandler = mHandlerField.get(mTn) as? Handler ?: return

            mHandlerField.set(mTn, SafelyHandlerWrapper(mHandler))
        } catch (e: Exception) {
            e.printStackTrace()
        }

另一种也是利用反射不过替换的是 view 里的 mContext 对象然后再通过 mContext 获取各种 service 的时候就会调用这个 context 的 getSystemService 中判断是 Context.WINDOW_SERVICE 就可以对原本返回的 WindowManager 做一层包装然后对这个 WindowManager 的 addView 方法做 try-catch 代码就不贴了 Github 地址

二、部分设备关闭通知权限后不能弹出 Toast

用户可能会关闭 App 的通知权限,用户的预期可能只是想屏蔽推送消息并不是要屏蔽 Toast 当各种需要弹出 Toast 没有弹出会比较奇怪用户体验太差。先分析下问题产生的原因,再回头看下

    private final IBinder mService = new INotificationManager.Stub() {

        @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            if (DBG) {
                Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
                        + " duration=" + duration);
            }

            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
                return ;
            }

            final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
            final boolean isPackageSuspended =
                    isPackageSuspendedForUser(pkg, Binder.getCallingUid());
			// 当三个条件都满足时流程就结束了
		    // noteNotificationOp 就是用来检测是否有通知权限的
            if (ENABLE_BLOCKED_TOASTS && (!noteNotificationOp(pkg, Binder.getCallingUid())
                    || isPackageSuspended)) {
                if (!isSystemToast) {
                    Slog.e(TAG, "Suppressing toast from package " + pkg
                            + (isPackageSuspended
                                    ? " due to package suspended by administrator."
                                    : " by user request."));
                    return;
                }
            }
        }
    }    

可以看到如果没有通知权限流程就结束了不会把 Toast 加入到列表中也不会显示解决方案有以下几种:
第一种是避开 NotificationManagerService 自己通过 WindowManager 处理 view 的显示和隐藏,缺点是当没有悬浮窗权限时只能在当前页面显示,具体的实现有 ToastUtils
第二种是通过 dialog 来实现把 Toast 的 view 设置给 dialog 的 contentView 具体的实现有 smart-show 这种方案缺点同样是只能显示在当前页面
第三种是通过 Snackbar 具体的分析有 Toast与Snackbar的那点事 因为没有代码所以看不到具体的效果,缺点是相对于 Toast 的使用麻烦太多
第四种是通过动态代理,再看一下将 Toast 加入列表的方法实现

    private final IBinder mService = new INotificationManager.Stub() {

        @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            if (DBG) {
                Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
                        + " duration=" + duration);
            }

            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
                return ;
            }

			// isSystemToast 的值取决于 isCallerSystem() 和 "android".equals(pkg)
			// 所以 pkg 的值如果是 "android" isSystemToast 就是 true 就可以通过校验
            final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
            final boolean isPackageSuspended =
                    isPackageSuspendedForUser(pkg, Binder.getCallingUid());
			// 当三个条件都满足时流程就结束了
		    // noteNotificationOp 就是用来检测是否有通知权限的
            if (ENABLE_BLOCKED_TOASTS && (!noteNotificationOp(pkg, Binder.getCallingUid())
                    || isPackageSuspended)) {
                // 这里 isSystemToast 如果返回 true 就可以通过校验    
                if (!isSystemToast) {
                    Slog.e(TAG, "Suppressing toast from package " + pkg
                            + (isPackageSuspended
                                    ? " due to package suspended by administrator."
                                    : " by user request."));
                    return;
                }
            }
        }
    }    

根据上面的分析只要把第一个参数 pkg 的值改为 “true” 就可以了,具体代码在这里 Android部分手机通知权限关闭无法打出Toast 这种方案缺点同样是在 Android API 29 将此 API 加入了受限制的灰名单中

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(
                ServiceManager.getService(Context.NOTIFICATION_SERVICE));
        return sService;
    }

可以看到这种方式只可以在 targetSdkVersion 小于 29 的项目中使用还是需要结合其他方案


第一种 [ToastUtils] 也已经处理了这种场景

参考与感谢

提示:上面列出的链接不再重复列出
WindowManager$BadTokenException(WindowManager源码分析)

WindowManager$BadTokenException-解决方案

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值