作者: 咕咚移动技术团队 - 乔瑟琳
一. 监听安卓手机通知栏推送信息
最近在需求中需要实现监听安卓手机通知栏信息的功能, 比如实时获取 qq, 微信, 短信消息. 一开始评估是件挺简单的事儿, 实现 NotificationListenerService, 直接上代码. 实现步骤如下:
1. 添加 <intent-filter>:
- <service android:name="com.example.yuanting.msgpushandcall.service.NotifyService"
- android:label="@string/app_name"
- android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
- <intent-filter>
- <action android:name="android.service.notification.NotificationListenerService" />
- </intent-filter>
- </service>
复制代码
2. 打开通知监听设置
- try {
- Intent intent;
- if (android.os.Build.VERSION.SDK_INT>= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
- intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
- } else {
- intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
- }
- startActivity(intent);
- } catch (Exception e) {
- e.printStackTrace();
- }
复制代码
3. 然后重写以下这三个方法:
onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) : 当有新通知到来时会回调;
onNotificationRemoved(StatusBarNotification sbn) : 当有通知移除时会回调;
onListenerConnected() : 当 NotificationListenerService 是可用的并且和通知管理器连接成功时回调.
而我们要获取通知栏的信息则需要在
onNotificationPosted
方法内获取 , 之前在网上查了一些文章有的通过判断 API 是否大于 18 来采取不同的办法, 大致是 = 18 则利用反射获取 Notification 的内容,>18 则通过
Notification.extras
来获取通知内容, 而经测试在部分安卓手机上即使 API>18
Notification.extras
是等于 null 的. 因此不能通过此方法获取通知栏信息
4. 过滤包名
默认开启了 NotificationListenerService 将收到系统所有开启了推送开关的应用的推送消息, 如果想要收到指定应用消息, 则需过滤该应用的包名:
- String packageName = sbn.getPackageName();
- if (!packageName.contains(ComeMessage.MMS) && !packageName.contains(ComeMessage.QQ) && !packageName.contains(ComeMessage.WX)) {
- return;
- }
复制代码
短信, QQ, 微信对应的包名则为:
- public static final String QQ="com.tencent.mobileqq";
- public static final String WX="com.tencent.mm";
- public static final String MMS="com.android.mms";
复制代码
5. 获取通知消息
- String content = null;
- if (sbn.getNotification().tickerText != null) {
- content = sbn.getNotification().tickerText.toString();
- }
复制代码
在 onNotificationPosted 方法内通过上面的方法即可获取部分手机的通知栏消息, 但是但是重点来了, 在部分手机上, 比如华为荣耀某系列 sbn.getNotification().tickerText == null, 经调试发现仅在 StatusBarNotification 对象内部的一个 view 的成员变量上有推送消息内容, 因此不得不用上了反射去获取 view 上的内容
- private Map<String, Object> getNotiInfo(Notification notification) {
- int key = 0;
- if (notification == null)
- return null;
- RemoteViews views = notification.contentView;
- if (views == null)
- return null;
- Class secretClass = views.getClass();
- try {
- Map<String, Object> text = new HashMap<>();
- Field outerFields[] = secretClass.getDeclaredFields();
- for (int i = 0; i <outerFields.length; i++) {
- if (!outerFields[i].getName().equals("mActions"))
- continue;
- outerFields[i].setAccessible(true);
- ArrayList<Object> actions = (ArrayList<Object>) outerFields[i].get(views);
- for (Object action : actions) {
- Field innerFields[] = action.getClass().getDeclaredFields();
- Object value = null;
- Integer type = null;
- for (Field field : innerFields) {
- field.setAccessible(true);
- if (field.getName().equals("value")) {
- value = field.get(action);
- } else if (field.getName().equals("type")) {
- type = field.getInt(action);
- }
- }
- // 经验所得 type 等于 9 10 为短信 title 和内容, 不排除其他厂商拿不到的情况
- if (type != null && (type == 9 || type == 10)) {
- if (key == 0) {
- text.put("title", value != null ? value.toString() : "");
- } else if (key == 1) {
- text.put("text", value != null ? value.toString() : "");
- } else {
- text.put(Integer.toString(key), value != null ? value.toString() : null);
- }
- key++;
- }
- }
- key = 0;
- }
- return text;
- } catch (Exception e) {
- e.printStackTrace();
- }
- return null;
- }
复制代码
那么经过以上方法: 先获取 sbn.getNotification().tickerText, 如果为空, 则尝试使用反射获取 view 上的内容, 目前测试了主流机型, 暂无任何兼容性问题.
6. 解决杀掉进程再次启动不触发监听问题
因为 NotificationListenerService 被杀后再次启动时, 并没有去 bindService , 所以导致监听效果无效. 这一现象目前我在仅有的手机上并没有出现, 但是一旦遇到推荐的解决办法: 利用 NotificationListenerService 先 disable 再 enable , 重新触发系统的 rebind 操作. 代码如下:
- private void toggleNotificationListenerService() {
- PackageManager pm = getPackageManager();
- pm.setComponentEnabledSetting(new ComponentName(this, com.fanwei.alipaynotification.ui.AlipayNotificationListenerService.class),
- PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
- pm.setComponentEnabledSetting(new ComponentName(this, com.fanwei.alipaynotification.ui.AlipayNotificationListenerService.class),
- PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
- }
复制代码
整个消息推送的流程如上, 重点即在解决不通手机上获取消息的兼容性问题, 不能简单的通过 api 版本去区分获取哪个对象, 实践得出的结论是通过判断 tiketText 是否为空, 为空则试图使用反射获取消息内容.
二. 实现安卓手机上拒接来电的功能
关于安卓手机上拒接来电的功能, 官方并未给出 api, 搜索了许多资料, 花样百出, 有使用模拟 mediaButton 按键, 有使用反射拿系统的 endCall 方法的, 但经测试在目前主流的机型上都存在问题. 特总结了如下的方法, 亲测有效:
1. 判断是否有电话权限
- if(ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){
- ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.CALL_PHONE},1000);
- }
复制代码
这一点十分重要, 这是动态申请电话相关的权限, 值得注意的是不管你的 targetSdk 是否高于安卓 6.0, 都需要动态的申请此权限, 否则, 我们在后面通过反射获取相应的 API, 部分手机也会 crash, 提示你没有 readPhoneState 等权限, 虽然这与官方定义的不一致, 但国内安卓手机关于权限这块儿确实是各不相同.
2. 监听来电状态
- public class PhoneCallListener extends PhoneStateListener {
- @Override
- public void onCallStateChanged(int state, String incomingNumber) {
- switch (state) {
- case TelephonyManager.CALL_STATE_OFFHOOK: // 电话通话的状态
- break;
- case TelephonyManager.CALL_STATE_RINGING: // 电话响铃的状态
- PhoneCallUtil.endPhone(MainActivity.this);
- break;
- }
- super.onCallStateChanged(state, incomingNumber);
- }
- }
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- telephonyManager = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
- callListener = new PhoneCallListener();
- telephonyManager.listen(callListener, PhoneStateListener.LISTEN_CALL_STATE);
- }
复制代码
这块儿是对电话状态的监听, 一开始并无可注意的 tip, 但在自测期间发现了些奇怪的现象, 比如你直接 telephonyManager.listen(new PhoneCallListener(), PhoneStateListener.LISTEN_CALL_STATE); 直接 new 一个对象传入 listene 方法, 在某些手机这个电话监听会在某些操作后失效. 解决的办法则是该将 PhoneCallListener 的对象申明成成员变量, 让外面的的对象所持有, 这样在跨进程通信时这个回调不被回收.
3. 新建 aidl 文件, 并通过反射获取挂断电话 API
按照系统 iTelephony.aidl 文件的路径, 新建一个相同文件, 其接口内方法只需要写 endCall(), 注意路径必须要完全相同:
- package com.android.internal.telephony;
- interface ITelephony {
- boolean endCall();
- }
复制代码
java 方法:
- public static void endPhone(Context context) {
- TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
- Method method = null;
- try {
- method = TelephonyManager.class.getDeclaredMethod("getITelephony");
- method.setAccessible(true);
- ITelephony telephony = (ITelephony) method.invoke(telephonyManager);
- telephony.endCall();
- } catch (NoSuchMethodException e) {
- e.printStackTrace();
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- } catch (RemoteException e) {
- e.printStackTrace();
- } catch (InvocationTargetException e) {
- e.printStackTrace();
- }
- }
复制代码
经过以上三步, 能够实现挂断电话的功能, 但是经过多种机型的测试, 在 vivo 手机上, 还是因为权限的问题不能生效, vivo 手机上报出的错误如下:
- AndroidRuntime: FATAL EXCEPTION: main
- Process: com.example.yuanting.msgpushandcall, PID: 6170
- java.lang.SecurityException: MODIFY_PHONE_STATE permission required.
- at android.os.Parcel.readException(Parcel.java:1684)
- at android.os.Parcel.readException(Parcel.java:1637)
- at com.android.internal.telephony.ITelephony$Stub$Proxy.endCall(ITelephony.java:1848)
- at com.example.yuanting.msgpushandcall.utils.PhoneCallUtil.endPhone(PhoneCallUtil.java:25)
- at com.example.yuanting.msgpushandcall.MainActivity$PhoneCallListener.onCallStateChanged(MainActivity.java:80)
- at android.telephony.PhoneStateListener$1.handleMessage(PhoneStateListener.java:298)
- at android.os.Handler.dispatchMessage(Handler.java:102)
- at android.os.Looper.loop(Looper.java:154)
- at android.app.ActivityThread.main(ActivityThread.java:6211)
- at java.lang.reflect.Method.invoke(Native Method)
复制代码
神奇的安卓手机, 在源码内, 查到了仅仅是挂断电话是不需要修改手机电话权限的, 接听电话才需要 MODIFY_PHONE_STATE, 但是部分手机还是报没有权限, 这就是安卓吧~~, 因此目前该方法并没有兼容 vivo 手机.
三. 总结
以上是近期对消息通知, 来电拒接的一些总结, 关于来电的拒接功能, 部分手机还存在兼容性问题, 后续有新的思路会持续更新. 在文章中有不足之处或错误指出望予以指出, 不胜感激.
git 地址 : https://github.com/yuanting2016/MsgPushAndCall
来源: https://juejin.im/post/5b8cec04518825273f07d0c3