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

一、发现问题

只是想关闭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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容