作者:咕咚移動技術團隊-喬瑟琳
一.監聽安卓手機通知欄推送資訊
最近在需求中需要實現監聽安卓手機通知欄資訊的功能,比如實時獲取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 地址 :github.com/CodoonDemo/…