【Android】当关闭通知消息权限后无法显示系统Toast的解决方案
2016-11-15 17:31
615 查看
前言
不知道大家是否遇到了当你们的App在5.0以上系统中被用户关闭消息通知后(其实用户本身只是想关闭Notification的,猜测),系统的Toast也神奇的无法显示。当然这个问题并不复杂,有很多种解决方案,我们逐一探讨一下,然后来看看到底哪种方式会好一点。
问题分析
直接跟踪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; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
解决思路
这边就来说说我这边的几种解决方案,就是大致我能想到的,哈哈。自己仿照系统的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); } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
嗯,这样写应该是没问题的,然后为啥没有效果呢??好吧,其实写了这么多,就是给自己挖坑,很明显,这个东西在现在的5.0以上机器中有一个悬浮窗权限,而且系统默认是关闭该权限的,只有用户手动打开才能显示,而且代码中也要添加如下一条权限。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />1
1
那么问题又回来了,用户一般不会打开,这不是又白搞么。
通过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(); } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
通过直接去当前页面最外层content布局来添加View。
说说这种方式吧,其实刚开始我也是没有想到的,因为一般很少回去直接拿Activity最外层的content布局去创建一个View并且显示在上面的。(ViewGroup) ((Activity) context).findViewById(android.R.id.content);1
1
其实我们是可以直接通过findViewById去直接拿到最外层布局的哦,当然context记得一定是Activity。
然后通过以下代码就可以直接把布局显示在当前content布局之上。
ViewGroup container = (ViewGroup) ((Activity) context).findViewById(android.R.id.content); View v = ((Activity) context).getLayoutInflater().inflate(R.layout.etoast,container);1
2
1
2
这种方式是不是有点奇怪,好吧,我也是这么想的,不过感觉还是非常的实在的,也不复杂,东西也不多,直接上代码。
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 EToast(Context context) { mContext = context; container = (ViewGroup) ((Activity) context) .findViewById(android.R.id.content); v = ((Activity) context).getLayoutInflater().inflate( R.layout.etoast, container); 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{ //这边主要是当切换Activity后我们应该更新当前持有的context,不然无法显示的 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) { //隐藏布局,没有remove主要是为了防止一个页面创建多次布局 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); } } //这个方法主要是为了解决用户在重启页面后单例还会持有上一个context, //并且上面的mContext.getClass().getName()其实是一样的 //所以使用上还需在你们的BaseActivity的onDestroy()方法中调用该方法 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)); } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
简单说下吧,代码应该是很简单的,然后简单封装了和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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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>1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9
优化
上面的几种方式我大致也都走了一遍,其实我觉得都没啥区别,看你喜欢用哪种吧。我其实是采用了第四种,因为第一种的话我是不喜欢队列的,比如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; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
据说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 = -1; 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{ if (checkNotification == -1){ 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 && context instanceof Activity){ checkNotification = isNotificationEnabled(context) ? 0 : 1; } if (checkNotification == 1) { 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; } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
然后直接把你项目的import Android.widget.Toast 全局替换成import 你Toast的包名 即可。
总结
最后呢,提前祝大家周末愉快,这周疯狂的七天工作日过了明天就要结束了,开黑开黑!!哈哈哈~ps:如果发现我上面有什么问题或者有好的解决方案的话欢迎和我留言讨论,比比在此谢过啦~
————————————————————
后期优化 Time:2016年11月14日13:46:01
由于isNotificationEnabled(…)方法存在问题,可能导致少量4.2.2机型报错,修改如下:
/** * 用来判断是否开启通知权限 * */ 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; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
感谢读者:月影的殇 的指出
相关文章推荐
- 【Android】当关闭通知消息权限后无法显示系统Toast的解决方案
- 当关闭通知消息权限后无法显示系统Toast的解决方案
- 当关闭通知消息权限后无法显示系统Toast的解决方案
- 关闭通知消息权限Toast无法显示的解决方案【android】
- 【Android】当关闭通知权限后无法显示Toast的解决方案V2.0
- 【Android】当关闭通知权限后无法显示Toast的解决方案V2.0
- 解决Android关闭通知消息权限无法弹出Toast的问题
- Android中当用户关闭通知消息权限后Toast显示不出的问题
- Android应用获取通知栏权限是否开启--以及跳转到系统设置界面--解决方案
- Android自定义Toast 解决关闭通知 Toast无法弹出
- Android应用获取通知栏权限是否开启--以及跳转到系统设置界面--解决方案
- android 显示通知关闭之后,Toast不显示的状态检测
- Android应用获取通知栏权限是否开启--以及跳转到系统设置界面--解决方案
- android 某些三星手机上不显示通知Notification和消息Toast
- Android应用获取通知栏权限是否开启--以及跳转到系统设置界面--解决方案
- android:检查系统是否开启消息通知权限,app应用点击开启通知时跳转到app详情页面开启权限
- 由关闭通知导致Toast无法正常显示到Java中的反射
- android Toast显示消息的几种方法
- Mac OS X下Android系统M2、华为部分手机无法连接问题之解决方案