如何無縫監聽安卓手機通知欄推送信息以及拒接來電

做者:咕咚移動技術團隊-喬瑟琳java

一.監聽安卓手機通知欄推送信息

最近在需求中須要實現監聽安卓手機通知欄信息的功能,好比實時獲取qq、微信、短信消息。一開始評估是件挺簡單的事兒,實現 NotificationListenerService,直接上代碼。實現步驟以下:
android

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將收到系統全部開啓了推送開關的應用的推送消息,若是想要收到指定應用消息,則需過濾該應用的包名:git

String packageName = sbn.getPackageName();
        if (!packageName.contains(ComeMessage.MMS) && !packageName.contains(ComeMessage.QQ) && !packageName.contains(ComeMessage.WX)) {
            return;
        }
複製代碼

短信、QQ、微信對應的包名則爲:github

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上的內容
api

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上的內容,目前測試了主流機型,暫無任何兼容性問題。bash

6.解決殺掉進程再次啓動不觸發監聽問題

由於 NotificationListenerService 被殺後再次啓動時,並無去 bindService ,因此致使監聽效果無效。這一現象目前我在僅有的手機上並無出現,可是一旦遇到推薦的解決辦法:利用 NotificationListenerServicedisableenable ,從新觸發系統的 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是否爲空,爲空則試圖使用反射獲取消息內容。app

二.實現安卓手機上拒接來電的功能

關於安卓手機上拒接來電的功能,官方並未給出api,搜索了許多資料,花樣百出,有使用模擬mediaButton按鍵、有使用反射拿系統的endCall方法的,但經測試在目前主流的機型上都存在問題。特總結了以下的方法,親測有效:
ide

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等權限,雖然這與官方定義的不一致,但國內安卓手機關於權限這塊兒確實是各不相同。
oop

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/…

相關文章
相關標籤/搜索