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

咕咚移動技術團隊發表於2018-09-03

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

一.監聽安卓手機通知欄推送資訊

最近在需求中需要實現監聽安卓手機通知欄資訊的功能,比如實時獲取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 ,所以導致監聽效果無效。這一現象目前我在僅有的手機上並沒有出現,但是一旦遇到推薦的解決辦法:利用 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是否為空,為空則試圖使用反射獲取訊息內容。

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

關於安卓手機上拒接來電的功能,官方並未給出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/…

相關文章