热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Android插件框架-通知实现篇

相信大家如果在项目中使用过插件框架或是对插件框架有一些了解,会知道插件框架有一个很无奈的问题,就是无法让插件自主发送通知,特别是那些自定义RemoteViews的通知.就连

相信大家如果在项目中使用过插件框架或是对插件框架有一些了解,会知道插件框架有一个很无奈的问题,就是无法让插件自主发送通知,特别是那些自定义RemoteViews的通知.

就连大名鼎鼎的DroidPlugin也在实现中只让插件发送那些使用系统自带布局的通知,可查看具体的实现代码, –蹭个关键字 哈哈

为什么通知不好实现

1. small icon

发送通知需要一个的drawable id,系统在接收到发送通知请求后,会根据包名和resource id检查该icon是否存在,不存在就crash. 
而插件的包名是没有存在系统已安装应用列表内的,所以直接crash

有人会说,我hook后使用宿主的包名不就行了,这就又有一个问题,宿主里面不一定存在这个id的drawable,
即使存在,也不太容易保证是你想要的那个id,毕竟id的生成工作是ide来协助完成的

当然我知道有项目真的在自己来控制生成resource id,但这个不是我们今天讨论的话题,就不过于深入了

2. 通知的显示由系统负责

这部分主要包含两个意思:

  • 显示的进程是系统进程
  • View的构造是由系统来负责

也带来了以下两个问题:

  • 无法通过插件框架核心的hook技术来拦截系统实现,在低版本的rom中,甚至在通知显示时,宿主程序都没有启动
  • 插件自定义布局系统找不到,也就无从构造

3. 插件通知的点击事件一般直接指向插件,系统在处理这些事件时,因为找不到插件也会引发crash

主要问题是因为点击事件触发后是由系统来处理的,系统处理时,插件甚至宿主都不一定启动,就算启动了,根据插件提供的信息系统也找不到相应的应用的

解决该问题的核心思路

1. 截图

提取插件即将要发送的通知的RemoteViews,转化成View,然后根据通知类型(是否包含bigContentView)截图成一个图片,然后宿主直接发送纯图片通知

2. 事件封包转发,统一处理

对插件通知的点击事件进行二次封包,当用户点击首先接收到消息的是插件框架(宿主),然后插件框架再解包,找出原始的真正意图,再通过插件框架将其跳转指定页面/启动service/发送通知等等

3. 使用占位的方式解决事件点击问题

宿主在发送插件通知的时候(实际是一张图片),如果该通知布局内包含按钮(开始/暂停,跳转之类的),那在该图片的下方or上方指定位置显示一个透明的按钮,参照思路2进行点击事件封装,然后供用户点击

具体的解决步骤

1.Hook系统的NotificationManager

实际是通过getService提取其INotificationManager,然后hook INotificationManager的以下几个方法:

  • enqueueNotification
  • enqueueNotificationWithTag
  • cancelNotification
  • cancelNotificationWithTag
  • cancelAllNotifications

当然,开始的时候按需hook就成,后期再去完善

2.将插件的RemoteViews转成View

核心方法是RemoteViews的apply方法,但需要关注一个细节:

在使用系统布局进行显示通知的时候,布局中会涉及到large icon和small icon.

然而在apply的时候,如果指定的是resource id,还是会根据包名和resource id去构造图片,依然会遇见找不到的问题,这样你构造出来的view,是缺少large icon和small icon的,在一些rom中会显示一个很扎眼的灰色块

所以,在转成view之前,你需要将RemoteViews里面包含large icon/small icon转成bitmap

如果large icon本身已经是bitmap的无需再转化,但如果是android M引进来的Icon对象,则依然需要将其转成bitmap,因为Icon最终在构造view时,依然是通过包名+resource id进行查找的

3.将View转成Bitmap

View转成Bitmap需要注意以下几个问题:

  • view的高度需要指定,默认情况下普通通知的高度是64dp,包含bigContentView的是255dp,按照插件发送的通知类型指定高度
  • 将view转化成bitmap需要有一个前提是view已经被layout的过,不然截出来的bitmap是黑色的
  • 经过测试发现,在将RemoteViews转成View的过程中,部分机型会将插件没有设置背景的layout设置成黑色,于是在截图的时候发现bitmap部分区域是黑色的,这个时候,需要将View中黑色layout替换成透明色

这部分因为和业务无关,相对无耦合,直接贴出代码吧

 public static Bitmap printView(View v, int width, int height) {
v.layout(0, 0, width, height);
int measuredWidth = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
int measuredHeight = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
v.measure(measuredWidth, measuredHeight);
v.layout(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());
int viewWidth = v.getMeasuredWidth();
int viewHeight = v.getMeasuredHeight();
if (viewWidth > 0 && viewHeight > 0) {
opration(v);
Bitmap bitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
v.draw(canvas);
return bitmap;
}
return null;
}

private static void opration(View view) {
try {
Drawable background = view.getBackground();
if (background instanceof ColorDrawable) {
ColorDrawable colorDrawable = (ColorDrawable) background;
if (colorDrawable.getColor() == Color.BLACK) {
colorDrawable.setColor(Color.TRANSPARENT);
}
}
if (view instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) view;
int childCount = vg.getChildCount();
for (int i = 0; i opration(vg.getChildAt(i));
}
}
} catch (Exception e) {
LogUtils.e(e);
}
}

4.找出RemoteViews中的所有点击事件

用户是通过setOnClickPendingIntent来设置点击事件的,而实现的原理是生成一个内部类SetOnClickPendingIntent实例,然后插入到一个mActions数组中.所以具体的实现方法如下:

  • 通过反射构造SetOnClickPendingIntent的Class
Class OnClickHandlerClass= Class.forName("android.widget.RemoteViews$SetOnClickPendingIntent");
  • 继续通过反射找出mActions数组 – 方法略
  • 遍历mActions数组,找出所有的SetOnClickPendingIntent

5.从SetOnClickPendingIntent中找出响应该点击事件的view,找出其原始坐标

具体方法如下:

  • 反射找出viewId
  • 根据viewId,从构造出来的通知View中找出目标子View
  • 找出子View相对根View的坐标,具体的方法如下:
    public static float getDescendantCoordRelativeToSelf(View descendant, View rootView, int[] coord) {
float scale = 1.0f;
float[] pt = {coord[0], coord[1]};
//坐标值进行当前窗口的矩阵映射,比如View进行了旋转之类,它的坐标系会发生改变。map之后,会把点转换为改变之前的坐标。
descendant.getMatrix().mapPoints(pt);
//转换为直接父窗口的坐标
scale *= descendant.getScaleX();
pt[0] += descendant.getLeft();
pt[1] += descendant.getTop();
ViewParent viewParent = descendant.getParent();
//循环获得父窗口的父窗口,并且依次计算在每个父窗口中的坐标
while (viewParent instanceof View && viewParent != rootView) {
final View view = (View) viewParent;
view.getMatrix().mapPoints(pt);
scale *= view.getScaleX();//这个是计算X的缩放值。此处可以不管
//转换为相当于可视区左上角的坐标,scrollX,scollY是去掉滚动的影响
pt[0] += view.getLeft() - view.getScrollX();
pt[1] += view.getTop() - view.getScrollY();
viewParent = view.getParent();
}
coord[0] = Math.round(pt[0]);
coord[1] = Math.round(pt[1]);
return scale;
}

6.将原始的点击事件PendingIntent进行封包,统一处理插件通知的所有点击事件

再次解释一下为什么这么做的原因: 点击事件由用户触发,但真正执行是由系统来执行,所以如果使用插件通知原有的点击事件,一般是导向插件本身的,系统会因为找不到插件而出现crash亦或没有反应

具体的解决思路如下:

  • 提取插件通知所有的PendingInteng(包括contentPendingIntent以及通知View内部的子view的PendingIntent)
  • 在宿主中定义一个透明的ClickHandleActivity,将上一步提取的PendingIntent通过参数的方式设置进去,构造出一个启动ClickHandleActivity的点击事件
  • ClickHandleActivity接到事件后,提取原始的PendingInteng,然后再根据插件框架的原理进行后续操作(启动Activity/启动Service/发送Broadcast),同时记得要把这个ClickHandleActivity给finish掉

其中有一个需要注意的点是,我们在获取PendingIntent的时候,已经没办法知道这个事件到底是要去干什么,是要启动Activity还是service,亦或是发送广播的

我这边的解决思路是在插件发送广播时,hook IActivityManager的getIntentSender方法,然后将类型给保存下来,然后在封包插件通知点击事件的时候,再将类型给传过去
这个思路虽然已经验证可行,但总觉得有点low,如果有哪位找到更合适的方法,麻烦同步一下给我,非常感谢

7.使用子View的相对坐标以及封包后的点击事件,设置到新构造出来的RemoteViews中去

解决该问题的思路是:

  • 在新的RemoteViews的layout中提前预设多个透明子view
  • 根据之前获取的子view相对坐标,调整预设的透明子view的位置
  • 将封包后的点击事件设置到预设的透明子view中

这里面根据经验发现有一个坑需要填:在RemoteViews没有提供设置View margin的方法,只能设置padding
所以,没办法,我在预设透明子view的结构如下:

        android:id="@+id/notify_layout1"
android:layout_hljs-string">"match_parent"
android:layout_hljs-string">"match_parent"
android:visibility="gone">

android:id="@+id/notify_btn1"
android:layout_hljs-string">"wrap_content"
android:layout_hljs-string">"wrap_content" />


android:id="@+id/notify_layout2"
android:layout_hljs-string">"match_parent"
android:layout_hljs-string">"match_parent">

android:id="@+id/notify_btn2"
android:layout_hljs-string">"0dp"
android:layout_hljs-string">"0dp" />

...

实际响应事件的是notify_btn*,但需要通过notify_layout*来调整位置

8.将截图的bitmap设置到新的RemoteViews中

9.copy原插件Notification的属性,构造新的Notification对象,设置small icon为宿主的small icon,然后发送即可

到此,整个实现过程就结束了,对于绝大部分场景应该都能完美解决了,希望能帮到大家


推荐阅读
  • Spring学习(4):Spring管理对象之间的关联关系
    本文是关于Spring学习的第四篇文章,讲述了Spring框架中管理对象之间的关联关系。文章介绍了MessageService类和MessagePrinter类的实现,并解释了它们之间的关联关系。通过学习本文,读者可以了解Spring框架中对象之间的关联关系的概念和实现方式。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • This article discusses the efficiency of using char str[] and char *str and whether there is any reason to prefer one over the other. It explains the difference between the two and provides an example to illustrate their usage. ... [详细]
  • 本文实例讲述了Android编程实现读取工程中的txt文件功能。分享给大家供大家参考,具体如下:1.众所周知,Android的res文件夹 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
  • 配置IPv4静态路由实现企业网内不同网段用户互访
    本文介绍了通过配置IPv4静态路由实现企业网内不同网段用户互访的方法。首先需要配置接口的链路层协议参数和IP地址,使相邻节点网络层可达。然后按照静态路由组网图的操作步骤,配置静态路由。这样任意两台主机之间都能够互通。 ... [详细]
author-avatar
yoyokk99的秋天
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有