标签:att har _id 映射 comm 兼容性问题 rup 版本号 center
近期为了在部门内做一次小型的技术分享。深入了解了一下Notification的实现原理。以及android的权限机制。在此做个记录。文章可能比較长,没耐心的话就直接看题纲吧。
先看一下以下两张图
图一:
看到这图可能大家不太明确,这和我们的notification有什么关系,我来简介一下背景。这是发生在15年NBA季后赛期间,火箭队对阵小牛队,火箭队以3:1率先,仅仅要再赢一场就能淘汰对手。这时候火箭队的官方首席运营官发了这条官方推特。
翻译一下就是 “一把枪指着小牛的队标,哼哼,仅仅须要闭上你们额眼睛,立即就要结束了”。这条推特当时引起了非常多人的转发和评论,而且推送给了全部关注相关比赛的球迷以及媒体。
我们试想一下,你是一个小牛的球迷,输了比赛以后本来心情就非常差。这时候手机一震,收到这条通知栏推送。你是不是会有一种强烈的被蔑视感觉。当天推特上就掀起了一阵网络争议。不仅小牛球迷,其它中立球迷也表示这条推特讽刺意味十足,已经有侮辱对手的嫌疑了。当然了,通知栏表示我不背这个锅,谁来背?第二天,这位首席运营官就被火箭官方开除了,并宣称此推特仅代表前运营官个人意见与火箭队无不论什么关系。
图二:
说完别人,再来说说我们自己吧。3月5日那天,群里都在讨论这条推送。本意是我们的编辑打算推一个分手相关的歌单,可是文案考虑不周全,让人误解。
导致非常多用户感到莫名其妙,我们试想一下。你准备与你近期交往的对象一起吃个晚餐,出门前收到这条祝我们分手的通知。你是不是感到非常不爽呢。是的,在微博上随手一搜就发现有非常多用户是这样的不爽的感觉了。当然了我拿这个对照并没有说要炒这位编辑的鱿鱼噢。
好像偏题非常远了,说这么多事实上就是想说明一件事,应用程序的通知是非常重要的一环,处理的不好非常可能给用户带来不好的印象。轻则吐槽,重则直接卸载。
好了好了。言归正传,我先列一下题纲吧
一、Notification的使用
二、Notification跨进程通信的源代码分析
三、优雅地设计通知(7.0)
四、通知权限问题
五、安卓的权限机制(6.0)
六、总结
一、Notification的使用
眼下咱们酷狗里的通知使用主要有下面三种场景
1.消息中心的通知
2.下载歌曲的通知
3.通过PlaybackService启动的通知
以下简单分析一下这三种场景的通知是怎样实现的。
第一种是使用系统布局生成的普通通知样式
NotificationManagerCompat manager = NotificationManagerCompat.from(this); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setContentTitle() [1] .setLargeIcon() [2] .setContentText() [3] .setNumber() [4] .setSmallIcon() [5] .setWhen() [6] .setContentIntent(pendingIntent); manager.notify(tag, id, builder.build());
另外一种是使用自己定义的布局生成的通知样式
NotificationManagerCompat manager = NotificationManagerCompat.from(this); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setWhen() .setSmallIcon() .setLargeIcon() .setContentIntent(pendingIntent); RemoteViews remoteView = new RemoteViews(getPackageName, R.layout.custom); remoteView.setTextViewText(R.id.tv_title, “通知标题”); remoteView.setImageViewResource(R.id.iv_icon, R.drawable.icon); remoteView.setOnClickPendingIntent(R.id.iv_icon, pendingIntent); builder.setContent(remoteView); manager.notify(tag, id, builder.build()); RemoteViews不支持自己定义View等复杂View
自己定义通知呢有一点须要注意就是。这个自己定义的布局里的TextView字体的大小和颜色须要合理地配置,不然非常easy在不同系统中和其它app的通知展示方式不一样,导致用户通知栏由于这个而显得不美观,甚至非常突兀。那么,官方也是有给我们提供这种解决方式:
Android 5.0之前可用: android:style/TextAppearance.StatusBar.EventContent.Title // 通知标题样式 android:style/TextAppearance.StatusBar.EventContent // 通知内容样式 Android 5.0及更高版本号: android:style/TextAppearance.Material.Notification.Title // 通知标题样式 android:style/TextAppearance.Material.Notification // 通知内容样式
这种方式,能够来看看我们代码中怎样实现的。
public SystemNotification getSystemText() { mSystemNotification = new SystemNotification(); try { NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setContentTitle("SLNOTIFICATION_TITLE") .setContentText("SLNOTIFICATION_TEXT") .setSmallIcon(R.drawable.comm_ic_notification) .build(); LinearLayout group = new LinearLayout(this); RemoteViews tempView = builder.getNotification().contentView; ViewGroup event = (ViewGroup) tempView.apply(this, group); recurseGroup(event); group.removeAllViews(); } catch (Exception e) { mSystemNotification.titleColor = Color.BLACK; mSystemNotification.titleSize = 32; mSystemNotification.contentColor = Color.BLACK; mSystemNotification.contentSize = 24; } return mSystemNotification; } private boolean recurseGroup(ViewGroup gp) { for (int i = 0; i < gp.getChildCount(); i++) { View v = gp.getChildAt(i); if (v instanceof TextView) { final TextView text = (TextView) v; final String szText = text.getText().toString(); if ("SLNOTIFICATION_TITLE".equals(szText)) { mSystemNotification.titleColor = text.getTextColors().getDefaultColor(); mSystemNotification.titleSize = text.getTextSize(); // return true; } if ("SLNOTIFICATION_TEXT".equals(szText)) { mSystemNotification.contentColor = text.getTextColors().getDefaultColor(); mSystemNotification.contentSize = text.getTextSize(); // return true; } } // if (v instanceof ImageView) { // final ImageView image = (ImageView) v; // if (image.getBackground().getConstantState().equals(getResources().getDrawable(R.drawable.comm_ic_notification))) { // mSystemNotification.iconWidth = image.getWidth(); // mSystemNotification.iconHeight = image.getHeight(); // } // } if (v instanceof ViewGroup) {// 假设是ViewGroup 遍历搜索 recurseGroup((ViewGroup) gp.getChildAt(i)); } } return false; }
至于详细怎样实现的发送通知。我们待会再继续分析。
而第三种通知比較特殊。是用service.startForground(notification)的方式生成的通知。
我们酷狗启动的时候就会在通知栏生成一个能够控制播放的通知,这个通知就是playbackdservice在启动的时候生成的。
Notification notification = new Notification(); notification.icon = R.drawable.icon; notification.flags = mFlag; notification.contentView = mContentView; notification.contentIntent = pendingIntent mService.startForeground(id, notification);
二、Notification跨进程通信的源代码分析
这样就非常好地解释了remoteViews是怎样跨进程通信的。
这里我们要再跟进一下RemoteViews的源代码,来验证这段流程。
详细变化图里已经表现的非常清晰了。
仅仅有当我们的通知本身就非常特殊,不须要尾随系统的其它通知样式来展示时,才比較适合自己定义布局,眼下酷狗里的下载通知就有这种问题。
说到界面布局我想起来刚開始做通知的时候,遇到的一个小问题,这里也讲一下。为什么左上角的smallIcon看不到,是一团灰色呢。
事实上是从sdk21開始,Google要求。全部应用程序的通知栏图标,应该仅仅使用alpha图层来进行绘制,而不应该包含RGB图层。通俗地说,就是我们的通知栏图标不要带颜色就能够了。
原来如此,怪不得我之前在酷狗里看见这种代码感到非常好奇却不知道原因。
if (SystemUtils.getSdkInt() >= 21) { setSmallIcon(com.kugou.common.R.drawable.stat_notify_musicplayer_for5); } else { setSmallIcon(com.kugou.common.R.drawable.stat_notify_musicplayer); }
1、进行中的通知
2、监听清除事件的通知
3、不同优先级的通知
4、系统悬浮窗和锁屏的通知
5、不同Style样式的通知
6、Group通知
7、能够直接回复的通知
尤其是最后两点是7.0安卓系统独有的。
https://material.io/guidelines/patterns/notifications.html#notifications-behavior
这个站点是谷歌推出的设计平台有关通知这一块的设计吧。
这一部分我们能够来看看我写的demo吧。
四、通知权限问题
说到通知栏就不得不提通知栏权限问题。之前我们的歌单推送功能。产品找到我说曝光量比点击量大非常多,从技术上是什么原因导致用户收到以后并不去点击呢。
由于之前的逻辑是仅仅要运行了notify()方法就觉得通知曝光了,这里设计是有问题的,由于非常可能用户已经把我们程序的通知给禁止掉了。
那我们怎么知道自己的应用程序通知权限被禁止了呢?假设被禁止了又该怎么办呢?
带着这两个问题。我開始查阅资料了。
1、API24開始系统就提供了现成的方法来获取通知权限
NotificationManagerCompat.from(this).areNotificationEnable();
2、另一种方式就是通过反射获取
/** * 通过反射获取通知的开关状态 * @param context * @return */ public 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; }这样的方式呢实质就是通过AppOpsManager和AppOpsService去获取位于/data/system/文件夹下的文件Appops.xml里的数据。
这一块的流程我们待会再细致描写叙述一下。
好的,如今我们已经知道用户的权限了,假设确实被用户禁止了,我们有下面三个处理方式
1、最友好的方式当然是给用户一个弹窗,对我们为什么须要通知作一下阐述。然后引导用户去打开权限。
2、最不友好的方式就是通过AppOpsManager.setMode()方法去改动用户的权限
3、通过自己设计一个悬浮窗来替代系统的通知
第2种方式呢。在实践的时候发现运行就会抛出异常,以下是异常信息
SecurityException java.lang.SecurityException: uid 10835 does not have android.permission.UPDATE_APP_OPS_STATS.
非系统应用都没有改动权限的权限。怎样知道是不是系统应用呢。就是这个uid了。
看来谷歌已经把这条路给堵上了。
第3种方式应该是眼下比較普遍的做法了。产品希望一定要展示。那就仅仅能这么绕弯子了。
这里直接看项目里的OverlayUtils吧。
只是悬浮窗又涉及到还有一个悬浮窗的权限,须要用户打开才行,这么看来还是倾向于第一种让用户自己来决定吧。
这里记录一下,我測试了两款手机
华为:打开了通知,不管有没有打开悬浮窗权限,都能弹出悬浮窗
关闭了通知,须要打开悬浮窗权限才干弹出悬浮窗
小米:悬浮窗仅仅受悬浮窗权限控制,和是否打开通知没有关系
说完通知栏我们再看看,为什么我把通知栏权限禁止后。程序的Toast提示也都显示不了了。
查阅源代码后发现 Toast里也用到了NotificationManagerService。
在Toast运行show()方法后,走到enqueueToast的时候有这么一段代码
if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) { if (!isSystemToast) { Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request."); return; } }原来如此。这里也用到了检測通知权限的方法noteNotificationOp()。
Toast也被notification影响了,但是我们的程序里Toast无处不在。由于通知权限导致toast弹不出影响挺大的。那我们找找看替代方案吧,事实上和通知类似。前面几种就不说了。
事实上也是用WindowManager 悬浮窗。仅仅只是先通过系统toast拿到布局来用,这样显示效果就和系统toast一样了。
五、安卓的权限机制(6.0)
这里说到安卓的权限,我就想讲讲我还在实习的时候遇到的一个相关问题。balabalabala
当targetSdkVersion值为23下面(也就是android 6.0)的时候,权限是在程序安装的时候便询问了用户。并配置好。
可是当targetSdkVersion值为23或23以上的时候,权限是当使用的时候才会询问用户,假设代码不变的情况下,直接使用使用危急权限。程序会直接崩溃
java.lang.SecurityException: Permission Denial
眼下酷狗为21,临时还不会出现这个问题
官方已经提供了一套流程来配合app与用户之间的权限交互。
那targetSdkVersion是否该提升?官方说当用户设备与targerSdkVersion一致的时候,程序执行效率会提高,由于会少处理非常多兼容性问题,有待考证。
我们来看看6.0下的权限流程
左图是标准流程,右图是用户操作了不再提示
鉴权(检測权限)这一步来说一说。
这个之前提到过的AppOpsManager
Setting UI通过AppOpsManager与AppOpsService交互。给用户提供入口管理各个app的操作。
AppOpsService详细处理用户的各项设置,用户的设置项存储在 /data/system/appops.xml文件里。
AppOpsService也会被注入到各个相关的系统服务中,进行权限操作的检验。
各个权限操作对应的系统服务(比方定位相关的Location Service,Audio相关的Audio Service等)中注入AppOpsService的推断。
假设用户做了对应的设置,那么这些系统服务就要做出对应的处理。
(比方。LocationManagerSerivce的定位相关接口在实现时。会有推断调用该接口的app是否被用户设置成禁止该操作,假设有该设置,就不会继续进行定位。)
那这个appops.xml文件长啥样呢。我们来看看
op标签中
n标识权限的opCode,
t表示时间戳。
m标识权限值mode,有三种
1.MODE_ALLOWED = 0;
2.MODE_IGNORED = 1;
3.MODE_ERRORED = 2;
假设没有m值。则为默认值,每种权限都有一种相应默认值。在AppOpsManager.sOpDefaultMode数组中,这是一个int数组,下标代表opCode,内容代表默认权限值。其它属性能够參考writeState方法一一相应
六、总结
一、通知的选择
1.不依赖系统版本号都要显示相同UI的能够使用自己定义通知(比如酷狗播放通知)
2.须要与安卓系统版本号UI保持一致的使用系统通知(比如酷狗消息通知)
3.当你须要保护你的Service不被系统优先Kill掉,能够用service.startForeground(notification)
二、做通知栏拓展的时候尽可能考虑7.0的通知栏特性(由于这些都是官方针对人性化用户体验设计的)
三、当须要跨进程使用View的时候能够考虑RemoteViews
四、当通知权限受阻,考虑使用替代方案(悬浮窗等)
五、建立完好的权限询问机制(针对targetSdkVersion,提高效率且提升用户体验)
标签:att har _id 映射 comm 兼容性问题 rup 版本号 center
原文地址:http://www.cnblogs.com/tlnshuju/p/7367292.html