美文网首页安卓之美
Android用户关闭APP通知导致Toast不显示的解决方案

Android用户关闭APP通知导致Toast不显示的解决方案

作者: 183207efd207 | 来源:发表于2016-12-19 11:02 被阅读372次
    一、发现问题

    只是想关闭Notification, 但Toast意外躺枪不显示了,我的第一想法这不科学啊,所以去看看源码WTF?

    二、定位问题:

    源码中可发现

    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
        }
    }
    
    public void cancel() {
        mTN.hide();
    
        try {
            getService().cancelToast(mContext.getPackageName(), mTN);
        } catch (RemoteException e) {
            // Empty
        }
    }
    

    可以看到先是获取一个服务INotificationManager service = getService();,显示时调用其service.enqueueToast(pkg, tn, mDuration);

    而这个INotificationManager在用户关闭消息通知权限的同时被禁用了,所以我们的Toast无法显示。

    三、解决方案

    经过一番看源码和在某一篇关于Toast源码分析的博文中了解到

    Toast的显示路径:

    1. 通过new Toast(Context context)或者makeText(...)方法实例化Toast对象
    2. 调用show()方法之后,实例会加入到一个TN变量(AIDL)的服务队列中,而这个队列由系统维护
    3. TN控制Toast的显示和消息

    解决思路就有了:
    既然系统不允许我们调用Toast,那么我们就自立门户——自己写一个Toast出来。
    我们自己参照Toast的源码,重写一份,最后show的时候,不进入TN维护的队列,我们自己用Handler+Queue来维护Toast的消息队列。

    public class CustomToast implements IToast {
    
       private static Handler mHandler = new Handler();
    
       /**
        * 维护toast的队列
        */
       private static BlockingQueue<CustomToast> mQueue = new LinkedBlockingQueue<CustomToast>();
    
       /**
        * 原子操作:判断当前是否在读取{**@linkplain **#mQueue 队列}来显示toast
        */
       protected static AtomicInteger mAtomicInteger = new AtomicInteger(0);
    
       private WindowManager mWindowManager;
    
       private long mDurationMillis;
    
       private View mView;
    
       private WindowManager.LayoutParams mParams;
    
       private Context mContext;
    
       public static IToast makeText(Context context, String text, long duration) {
          return new CustomToast(context).setText(text).setDuration(duration)
                .setGravity(Gravity.BOTTOM, 0, DisplayUtil.dip2px(context, 64));
       }
    
       public CustomToast(Context context) {
    
          mContext = context;
          mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
          mParams = new WindowManager.LayoutParams();
          mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
          mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
          mParams.format = PixelFormat.TRANSLUCENT;
          mParams.windowAnimations = android.R.style.Animation_Toast;
          mParams.type = WindowManager.LayoutParams.TYPE_TOAST;
          mParams.setTitle("Toast");
          mParams.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                          WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
          // 默认CustomToast在下方居中
          mParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
       }
    
       /**
        * Set the location at which the notification should appear on the screen.
        *
        * **@param **gravity
        * **@param **xOffset
        * **@param **yOffset
        */
       @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
       @Override
       public IToast setGravity(int gravity, int xOffset, int yOffset) {
    
          // We can resolve the Gravity here by using the Locale for getting
          // the layout direction
          final int finalGravity;
          if (Build.VERSION.SDK_INT >= 14) {
             final Configuration config = mView.getContext().getResources().getConfiguration();
             finalGravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection());
          } else {
             finalGravity = gravity;
          }
          mParams.gravity = finalGravity;
          if ((finalGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
             mParams.horizontalWeight = 1.0f;
          }
          if ((finalGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
             mParams.verticalWeight = 1.0f;
          }
          mParams.y = yOffset;
          mParams.x = xOffset;
          return this;
       }
    
       @Override
       public IToast setDuration(long durationMillis) {
          if (durationMillis < 0) {
             mDurationMillis = 0;
          }
          if (durationMillis == Toast.LENGTH_SHORT) {
             mDurationMillis = 2000;
          } else if (durationMillis == Toast.LENGTH_LONG) {
             mDurationMillis = 3500;
          } else {
             mDurationMillis = durationMillis;
          }
          return this;
       }
    
       /**
        * 不能和{**@link **#setText(String)}一起使用,要么{**@link **#setView(View)} 要么{**@link **#setView(View)}
        *
        * **@param **view 传入view
        *
        * **@return **自身对象
        */
       @Override
       public IToast setView(View view) {
          mView = view;
          return this;
       }
    
       @Override
       public IToast setMargin(float horizontalMargin, float verticalMargin) {
          mParams.horizontalMargin = horizontalMargin;
          mParams.verticalMargin = verticalMargin;
          return this;
       }
    
       /**
        * 不能和{**@link **#setView(View)}一起使用,要么{**@link **#setView(View)} 要么{**@link **#setView(View)}
        *
        * **@param **text 字符串
        *
        * **@return **自身对象
        */
       @Override
       public IToast setText(String text) {
    
          // 模拟Toast的布局文件 com.android.internal.R.layout.transient_notification
          // 虽然可以手动用java写,但是不同厂商系统,这个布局的设置好像是不同的,因此我们自己获取原生Toast的view进行配置
    
          View view = Toast.makeText(mContext, text, Toast.LENGTH_SHORT).getView();
          if (view != null) {
             TextView tv = (TextView) view.findViewById(android.R.id.message);
             tv.setText(text);
             setView(view);
          }
    
          return this;
       }
    
       @Override
       public void show() {
          // 1. 将本次需要显示的toast加入到队列中
          mQueue.offer(this);
    
          // 2. 如果队列还没有激活,就激活队列,依次展示队列中的toast
          if (0 == mAtomicInteger.get()) {
             mAtomicInteger.incrementAndGet();
             mHandler.post(mActivite);
          }
       }
    
       @Override
       public void cancel() {
          // 1. 如果队列已经处于非激活状态或者队列没有toast了,就表示队列没有toast正在展示了,直接return
          if (0 == mAtomicInteger.get() && mQueue.isEmpty()) {
             return;
          }
    
          // 2. 当前显示的toast是否为本次要取消的toast,如果是的话
          // 2.1 先移除之前的队列逻辑
          // 2.2 立即暂停当前显示的toast
          // 2.3 重新激活队列
          if (this.equals(mQueue.peek())) {
             mHandler.removeCallbacks(mActivite);
             mHandler.post(mHide);
             mHandler.post(mActivite);
          }
    
          //TODO 如果一个Toast在队列中的等候展示,当调用了这个toast的取消时,考虑是否应该从对队列中移除,看产品需求吧
       }
    
       private void handleShow() {
          if (mView != null) {
             if (mView.getParent() != null) {
                mWindowManager.removeView(mView);
             }
             mWindowManager.addView(mView, mParams);
          }
       }
    
       private void handleHide() {
          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) {
                mWindowManager.removeView(mView);
                // 同时从队列中移除这个toast
                mQueue.poll();
             }
             mView = null;
          }
       }
    
       private static void activeQueue() {
          CustomToast miuiToast = mQueue.peek();
          if (miuiToast == null) {
             // 如果不能从队列中获取到toast的话,那么就表示已经暂时完所有的toast了
             // 这个时候需要标记队列状态为:非激活读取中
             mAtomicInteger.decrementAndGet();
          } else {
    
             // 如果还能从队列中获取到toast的话,那么就表示还有toast没有展示
             // 1. 展示队首的toast
             // 2. 设置一定时间后主动采取toast消失措施
             // 3. 设置展示完毕之后再次执行本逻辑,以展示下一个toast
             mHandler.post(miuiToast.mShow);
             mHandler.postDelayed(miuiToast.mHide, miuiToast.mDurationMillis);
             mHandler.postDelayed(mActivite, miuiToast.mDurationMillis);
          }
       }
    
       private final Runnable mShow = new Runnable() {
          @Override
          public void run() {
             handleShow();
          }
       };
    
       private final Runnable mHide = new Runnable() {
          @Override
          public void run() {
             handleHide();
          }
       };
    
       private final static Runnable mActivite = new Runnable() {
          @Override
          public void run() {
             activeQueue();
          }
       };
    
    }
    
    四、使用方法

    问题解决后,想到这是一个通用性的问题,可以搞一个库出来共享,所以就打成了aar上传到我们的maven私服,便于复用。
    compile 'xsl.common:toaster:1.0.1'
    Toaster实现了自定义的IToast接口,IToast的接口方法基本和原来的Toast相差无几, 所以从系统的Toast转到我们自定义的Toaster的成本极低,其实就是改个名字而已,其他用法完全一致。

    //System Toast
    Toast.makeText(MainActivity.this, "show System Toast", Toast.LENGTH_SHORT).show();
    //Custom Toast
    Toaster.makeText(this, "show Custom Toast", Toast.LENGTH_SHORT).show();
    
    五、发散思维

    还有什么别的解决思路?

    自己仿照系统的Toast然后用自己的消息队列来维护,让其不受NotificationManagerService影响。(本文采用)
    通过WindowManager自己来写一个通知。
    通过Dialog、PopupWindow来编写一个自定义通知。
    通过直接去当前页面最外层content布局来添加View。

    相关文章

      网友评论

      • krmao:这个不需要动态申请 SYSTEM_ALERT_WINDOW 权限吧?
        krmao:仔细研读了代码, 真大神也!
        而且Android N以上也无需动态申请权限, 可能是 TypeToast 的问题, 不知道是不是所有机型包括小米都是适配的.

        附上 Kotlin 版本, 增加立即取消功能 cancelAll
        https://github.com/krmao/template/blob/master/apps/app-template/android/arsenal/libraries/library-base/src/main/java/com/smart/library/util/CXToast.kt
      • Itachi001:私服地址呢?
        183207efd207:既然是私服,当然不能暴露出来了

      本文标题:Android用户关闭APP通知导致Toast不显示的解决方案

      本文链接:https://www.haomeiwen.com/subject/koftvttx.html