查看原文
其他

如何无缝监听安卓手机通知栏推送信息以及拒接来电

乔瑟琳 nanchen 2019-06-27

作者:咕咚移动技术团队-乔瑟琳

一.监听安卓手机通知栏推送信息

最近在需求中需要实现监听安卓手机通知栏信息的功能,比如实时获取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的内容,&gt;18则通过Notification.extras来获取通知内容,而经测试在部分安卓手机上即使API&gt;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 地址 :github.com/CodoonDemo/…

—————END—————



我是南尘,只做比心的公众号,欢迎关注我。

推荐阅读:

Android 系统恐生变?

Better Kotlin

[外文翻译]Kotlin 在 Android 开发中的 16 个建议


欢迎关注南尘的公众号:nanchen
做不完的开源,写不完的矫情,只做比心的公众号,如果你喜欢,你可以选择分享给大家。如果你有好的文章,欢迎投稿,让我们一起来分享。
          长按上方二维码关注        做不完的开源,写不完的矫情        一起来看 nanchen 同学的成长笔记




    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存