【Android】当关闭通知消息权限后无法显示系统Toast的解决方案

前言

不知道大家是否遇到了当你们的App在5.0以上系统中被用户关闭消息通知后(其实用户本身只是想关闭Notification的,猜测),系统的Toast也神奇的无法显示。当然这个问题并不复杂,有很多种解决方案,我们逐一探讨一下,然后来看看到底哪种方式会好一点。

干货

项目地址:Blincheng/Toast
1.在project的build文件中添加如下:

allprojects {
    repositories {
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}

2.然后在app的build文件依赖即可

compile 'com.github.Blincheng:Toast:v1.0'

3.使用和系统的Toast完全一致,唯一要注意的就是该Toast的路径是import com.yiguo.toast.Toast;

Toast.makeText(MainActivity.this,"我是一个屏蔽通知我也是可以显示的Toast",Toast.LENGTH_SHORT).show();

问题分析

直接跟踪Toast的源码,其实我们可以发现,果真Toast其实是通过NotificationManagerService 维护一个toast队列,然后通知给Toast中的客户端 TN 调用 WindowManager 添加view。那么当用户关闭通知权限后自然也无法显示Toast了。

/**
     * Show the view for the specified duration.
     */
    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
        }
    }
    ....
    static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }

解决思路

这边就来说说我这边的几种解决方案,就是大致我能想到的,哈哈。

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

仿照系统Toast自己来维护Toast消息队列

这部分我就不写了,大家有兴趣可以看下解决小米MIUI系统上后台应用没法弹Toast的问题 这篇博文,东西写的很详细,内容也很细,大家可以看看。

通过WindowManager自己来写一个通知

说起WindowManager,其实我对这个东西的第一印象就是强大,悬浮窗什么的其实都是通过WindowManager来实现的,那么我们来看看怎么实现,我就直接上代码了

package com.yiguo.utils;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;

/**
 * Created by bl on 2016/10/11.
 */

public class Toast {
    private Context mContext;
    private WindowManager wm;
    private int mDuration;
    private View mNextView;
    public static final int LENGTH_SHORT = 1500;
    public static final int LENGTH_LONG = 3000;

    public Toast(Context context) {
        mContext = context.getApplicationContext();
        wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    }

    public static Toast makeText(Context context, CharSequence text,
                                 int duration) {
        Toast result = new Toast(context);
        View view = android.widget.Toast.makeText(context, text, android.widget.Toast.LENGTH_SHORT).getView();
        if (view != null){
            TextView tv = (TextView) view.findViewById(android.R.id.message);
            tv.setText(text);
        }
        result.mNextView = view;
        result.mDuration = duration;
        return result;
    }

    public static Toast makeText(Context context, int resId, int duration)
            throws Resources.NotFoundException {
        return makeText(context, context.getResources().getText(resId),duration);
    }

    public void show() {
        if (mNextView != null) {
            WindowManager.LayoutParams params = new WindowManager.LayoutParams();
            params.gravity = Gravity.CENTER | Gravity.CENTER_HORIZONTAL;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                    | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
            params.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = android.R.style.Animation_Toast;
            params.y = dip2px(mContext, 64);
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            wm.addView(mNextView, params);
            new Handler().postDelayed(new Runnable() {

                @Override
                public void run() {
                    if (mNextView != null) {
                        wm.removeView(mNextView);
                        mNextView = null;
                        wm = null;
                    }
                }
            }, mDuration);
        }
    }

    /**
     * dip与px的转换
     *
     * @参数   @param context
     * @参数   @param dipValue
     * @返回值 int
     *
     */
    private int dip2px(Context context, float dipValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }
}

嗯,这样写应该是没问题的,然后为啥没有效果呢??好吧,其实写了这么多,就是给自己挖坑,很明显,这个东西在现在的5.0以上机器中有一个悬浮窗权限,而且系统默认是关闭该权限的,只有用户手动打开才能显示,而且代码中也要添加如下一条权限。

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

那么问题又回来了,用户一般不会打开,这不是又白搞么。

通过Dialog、PopupWindow来编写一个自定义通知。

这个方案貌似也是可行的,代码就不写了,提醒一点就是一般来说Dialog和PopupWindow显示时有一个隔板,用户是无法点击其余部分控件的,所以记得加上以上属性。

 public static void setPopupWindowTouchModal(PopupWindow popupWindow,
                                                boolean touchModal) {
        if (null == popupWindow) {
            return;
        }
        Method method;
        try {
            method = PopupWindow.class.getDeclaredMethod("setTouchModal",
                    boolean.class);
            method.setAccessible(true);
            method.invoke(popupWindow, touchModal);

        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

通过直接去当前页面最外层content布局来添加View。

说说这种方式吧,其实刚开始我也是没有想到的,因为一般很少回去直接拿Activity最外层的content布局去创建一个View并且显示在上面的。

(ViewGroup) ((Activity) context).findViewById(android.R.id.content);

其实我们是可以直接通过findViewById去直接拿到最外层布局的哦,当然context记得一定是Activity。
然后通过以下代码就可以直接把布局显示在当前content布局之上。

ViewGroup container = (ViewGroup) ((Activity) context).findViewById(android.R.id.content);
View v = ((Activity) context).getLayoutInflater().inflate(R.layout.etoast,container);

这种方式是不是有点奇怪,好吧,我也是这么想的,不过感觉还是非常的实在的,也不复杂,东西也不多,直接上代码。

public class EToast {
    public static final int LENGTH_SHORT = 0;
    public static final int LENGTH_LONG = 1;
    private static EToast result;
    private final int ANIMATION_DURATION = 600;
    private static TextView mTextView;
    private ViewGroup container;
    private View v;

    private int HIDE_DELAY = 2000;

    private LinearLayout mContainer;
    private AlphaAnimation mFadeOutAnimation;
    private AlphaAnimation mFadeInAnimation;

    private boolean isShow = false;
    private static Context mContext;
    private Handler mHandler = new Handler();
    private String TOAST_TAG = "EToast_Log";

    private EToast(Context context) {
        mContext = context;
        container = (ViewGroup) ((Activity) context)
                .findViewById(android.R.id.content);
        View viewWithTag = container.findViewWithTag(TOAST_TAG);
        if(viewWithTag == null){
            v = ((Activity) context).getLayoutInflater().inflate(
                    R.layout.etoast, container);
            v.setTag(TOAST_TAG);
        }else{
            v = viewWithTag;
        }
        mContainer = (LinearLayout) v.findViewById(R.id.mbContainer);
        mContainer.setVisibility(View.GONE);
        mTextView = (TextView) v.findViewById(R.id.mbMessage);
    }

    public static EToast makeText(Context context, String message, int HIDE_DELAY) {
        if(result == null){
            result = new EToast(context);
        }else{
            if(!mContext.getClass().getName().equals(context.getClass().getName())){
                result = new EToast(context);
            }
        }
        if(HIDE_DELAY == LENGTH_LONG){
            result.HIDE_DELAY = 2500;
        }else{
            result.HIDE_DELAY = 1500;
        }
        mTextView.setText(message);
        return result;
    };
    public static EToast makeText(Context context, int resId, int HIDE_DELAY) {
        String mes = "";
        try{
            mes = context.getResources().getString(resId);
        } catch (Resources.NotFoundException e) {
            e.printStackTrace();
        }
        return makeText(context,mes,HIDE_DELAY);
    }
    public void show() {
        if(isShow){
            return;
        }
        isShow = true;
        mFadeInAnimation = new AlphaAnimation(0.0f, 1.0f);
        mFadeOutAnimation = new AlphaAnimation(1.0f, 0.0f);
        mFadeOutAnimation.setDuration(ANIMATION_DURATION);
        mFadeOutAnimation
                .setAnimationListener(new Animation.AnimationListener() {
                    @Override
                    public void onAnimationStart(Animation animation) {
                        isShow = false;
                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        mContainer.setVisibility(View.GONE);
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {
                    }
                });
        mContainer.setVisibility(View.VISIBLE);

        mFadeInAnimation.setDuration(ANIMATION_DURATION);

        mContainer.startAnimation(mFadeInAnimation);
        mHandler.postDelayed(mHideRunnable, HIDE_DELAY);
    }

    private final Runnable mHideRunnable = new Runnable() {
        @Override
        public void run() {
            mContainer.startAnimation(mFadeOutAnimation);
        }
    };
    public void cancel(){
        if(isShow) {
            isShow = false;
            mContainer.setVisibility(View.GONE);
            mHandler.removeCallbacks(mHideRunnable);
        }
    }
    public static void reset(){
        result = null;
    }
    public void setText(CharSequence s){
        if(result == null) return;
        TextView mTextView = (TextView) v.findViewById(R.id.mbMessage);
        if(mTextView == null) throw new RuntimeException("This Toast was not created with Toast.makeText()");
        mTextView.setText(s);
    }
    public void setText(int resId) {
        setText(mContext.getText(resId));
    }
}

简单说下吧,代码应该是很简单的,然后简单封装了和Toast相同的几个方法。嗯,其实大家也应该能发现我这边的布局其实是一直都在的,只是直接GONE掉了。所以呢,还是有待优化的地方,当然可以去想想是不是可以直接remove()掉什么的。我这边也没有用队列,我觉得在一个Toast显示的期间如果再需要显示另一个Toast,直接把当前的文本改过来就好了,没有必要搞个队列的,而且系统Toast我最厌恶的就是这个了,用户如果不停的点击,那Toast一个接一个的显示,这个我觉得是不合理的。上面的布局文件我也贴一下吧。有一点大家还是要注意下,因为我在完善的过程中其实遇到了很多种情况的BUG,所以最终需要大家再BaseActivity中的onDestory()方法中去手动调用一下EToast.reset();具体可以看源码中的解释。
etoast.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:id="@+id/mbContainer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="50dp"
    android:paddingRight="50dp"
    android:layout_marginBottom="50dp"
    android:gravity="bottom|center">
    <LinearLayout
        android:id="@+id/toast_linear"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/shape_eroast_bg"
        android:gravity="bottom|center"
        android:padding="5dp"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/mbMessage"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:layout_margin="5dp"
            android:layout_gravity="center"
            android:textColor="#ffffffff"
            android:shadowColor="#BB000000"
            android:shadowRadius="2.75"/>
    </LinearLayout>
</LinearLayout>

shape_eroast_bg.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 实心 -->
    <solid
        android:color="@color/BlackTransparent" />
    <corners
        android:radius="45dp"
        />
</shape>

优化

上面的几种方式我大致也都走了一遍,其实我觉得都没啥区别,看你喜欢用哪种吧。我其实是采用了第四种,因为第一种的话我是不喜欢队列的,比如5个Toast排队还要一个一个等待显示,这样的体验我是不喜欢的。第二种就不推荐了,因为又涉及到了其他的权限。第三种我没试,实现应该是不难的,效果的话也是随你喜欢。最后我采用的是第四种,因为这种方式之前是没有用到过的,也尝试一下。

那么来说说优化,如果直接替换掉系统的Toast,那相当的暴力,肯定妥妥的。那么我们能不能智能的去判断一下呢,如果用户没有关闭通知权限,那么久跟随系统的Toast去吧,这样好让App采用系统风格,对吧。
方法是有的,如下:

/**
 * 用来判断是否开启通知权限
 * */
    private static boolean isNotificationEnabled(Context context){

        AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
        ApplicationInfo appInfo = context.getApplicationInfo();

        String pkg = context.getApplicationContext().getPackageName();

        int uid = appInfo.uid;

        Class appOpsClass = null; /* Context.APP_OPS_MANAGER */

        try {

            appOpsClass = Class.forName(AppOpsManager.class.getName());

            Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String.class);

            Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);
            int value = (int)opPostNotificationValue.get(Integer.class);
            return ((int)checkOpNoThrowMethod.invoke(mAppOps,value, uid, pkg) == AppOpsManager.MODE_ALLOWED);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }

据说android24 可以使用NotificationManagerCompat.areNotificationsEnabled()来判断,具体大家可以尝试。那么如何来替换老项目中的Toast呢?
我这边的话自定义Toast就是EToast了。为什么要E开头呢,因为公......你懂的。然后写一个Toast的工具类,如下:

/**
 * Created by blin on 2016/10/11.
 */

public class Toast {
    private static final String CHECK_OP_NO_THROW = "checkOpNoThrow";
    private static final String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";
    private static int checkNotification = 0;
    private Object mToast;
    public static final int LENGTH_SHORT = 0;
    public static final int LENGTH_LONG = 1;
    private Toast(Context context, String message, int duration) {
        try{
            checkNotification = isNotificationEnabled(context) ? 0 : 1;
            if (checkNotification == 1 && context instanceof Activity) {
                mToast = EToast.makeText(context, message, duration);
            } else {
                mToast = android.widget.Toast.makeText(context, message, duration);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    private Toast(Context context, int resId, int duration) {
        if (checkNotification == -1){
            checkNotification = isNotificationEnabled(context) ? 0 : 1;
        }

        if (checkNotification == 1 && context instanceof Activity) {
            mToast = EToast.makeText(context, resId, duration);
        } else {
            mToast = android.widget.Toast.makeText(context, resId, duration);
        }
    }

    public static Toast makeText(Context context, String message, int duration) {
        return new Toast(context,message,duration);
    }
    public static Toast makeText(Context context, int resId, int duration) {
        return new Toast(context,resId,duration);
    }

    public void show() {
        if(mToast instanceof EToast){
            ((EToast) mToast).show();
        }else if(mToast instanceof android.widget.Toast){
            ((android.widget.Toast) mToast).show();
        }
    }
    public void cancel(){
        if(mToast instanceof EToast){
            ((EToast) mToast).cancel();
        }else if(mToast instanceof android.widget.Toast){
            ((android.widget.Toast) mToast).cancel();
        }
    }
    public void setText(int resId){
        if(mToast instanceof EToast){
            ((EToast) mToast).setText(resId);
        }else if(mToast instanceof android.widget.Toast){
            ((android.widget.Toast) mToast).setText(resId);
        }
    }
    public void setText(CharSequence s){
        if(mToast instanceof EToast){
            ((EToast) mToast).setText(s);
        }else if(mToast instanceof android.widget.Toast){
            ((android.widget.Toast) mToast).setText(s);
        }
    }
    /**
     * 用来判断是否开启通知权限
     * */
    private static boolean isNotificationEnabled(Context context){
        if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT){
            return true;
        }
        AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
        ApplicationInfo appInfo = context.getApplicationInfo();

        String pkg = context.getApplicationContext().getPackageName();

        int uid = appInfo.uid;

        Class appOpsClass = null; /* Context.APP_OPS_MANAGER */

        try {

            appOpsClass = Class.forName(AppOpsManager.class.getName());

            Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String.class);

            Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);
            int value = (int)opPostNotificationValue.get(Integer.class);
            return ((int)checkOpNoThrowMethod.invoke(mAppOps,value, uid, pkg) == AppOpsManager.MODE_ALLOWED);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }
}

然后直接把你项目的import android.widget.Toast 全局替换成import 你Toast的包名 即可。

总结

最后呢,提前祝大家周末愉快,这周疯狂的七天工作日过了明天就要结束了,开黑开黑!!哈哈哈~

ps:如果发现我上面有什么问题或者有好的解决方案的话欢迎和我留言讨论,比比在此谢过啦~

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

推荐阅读更多精彩内容