如何无缝监听安卓手机通知栏推送信息以及拒接来电
作者:咕咚移动技术团队-乔瑟琳
一.监听安卓手机通知栏推送信息
最近在需求中需要实现监听安卓手机通知栏信息的功能,比如实时获取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 {
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);
}
}
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 地址 :github.com/CodoonDemo/…
—————END—————
我是南尘,只做比心的公众号,欢迎关注我。
推荐阅读:
[外文翻译]Kotlin 在 Android 开发中的 16 个建议
欢迎关注南尘的公众号:nanchen
做不完的开源,写不完的矫情,只做比心的公众号,如果你喜欢,你可以选择分享给大家。如果你有好的文章,欢迎投稿,让我们一起来分享。