NotificationListenerService的那些事兒

博文出處:NotificationListenerService的那些事兒,歡迎大家關(guān)注我的博客,謝謝!

最近在公司時接到一個需求:需要實時監(jiān)聽設(shè)備的通知欄消息,并可以捕獲到通知的內(nèi)容,然后進(jìn)行對應(yīng)的操作。剛看到這個需求的時候,腦子里第一反應(yīng)就是使用 AccessibilityService 。 AccessibilityService 支持的事件監(jiān)聽類型中有 TYPE_NOTIFICATION_STATE_CHANGED ,該事件類型就是用來監(jiān)聽通知欄消息狀態(tài)改變的,眾多的搶紅包插件利用的就是這個原理。

之后在 Github 上看到了 qianghongbao 這個搶紅包的項目,發(fā)現(xiàn)代碼里面有一個 QHBNotificationService 繼承了 NotificationListenerService ,這個 NotificationListenerService 極大地引起了我的興趣。查了一下資料,發(fā)現(xiàn) NotificationListenerService 是在 Android 4.3 (API 18)時被加入的,作用就是用來監(jiān)聽通知欄消息。并且官方建議在 Android 4.3 及以上使用 NotificationListenerService 來監(jiān)聽通知欄消息,以此取代 AccessibilityService 。

Notification Listener

NotificationListenerService 的使用范圍也挺廣的,比如我們熟知的搶紅包,智能手表同步通知,通知欄去廣告工具等,都是利用它來完成的。所以,我也想趕時髦地好好利用這把“利器”。最后方案也就出來了:在 Android 4.3 以下(API < 18)使用 AccessibilityService 來讀取新通知,在 Android 4.3 及以上(API >= 18)使用 NotificationListenerService 來滿足需求。

這也正是本篇博客誕生的“起源”。

NotificationListenerService

在這里,我們就做一個小需求:實時檢測微信的新通知,如果該通知是微信紅包的話,就進(jìn)入微信聊天頁面。

準(zhǔn)備好了嗎,我們開始吧!

首先創(chuàng)建一個 WeChatNotificationListenerService 繼承 NotificationListenerService 。然后在 AndroidManifest.xml 中進(jìn)行聲明相關(guān)權(quán)限和 <intent-filter>

<service android:name="com.yuqirong.listenwechatnotification.WeChatNotificationListenerService"
          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>

然后一般會重寫下面這三個方法:

  • onNotificationPosted(StatusBarNotification sbn) :當(dāng)有新通知到來時會回調(diào);
  • onNotificationRemoved(StatusBarNotification sbn) :當(dāng)有通知移除時會回調(diào);
  • onListenerConnected() :當(dāng) NotificationListenerService 是可用的并且和通知管理器連接成功時回調(diào)。

onNotificationPosted(StatusBarNotification sbn)

下面我們來看看 NotificationListenerService 中的重點: onNotificationPosted(StatusBarNotification sbn) 方法。

@Override
public void onNotificationPosted(StatusBarNotification sbn) {
    // 如果該通知的包名不是微信,那么 pass 掉
    if (!"com.tencent.mm".equals(sbn.getPackageName())) {
        return;
    }
    Notification notification = sbn.getNotification();
    if (notification == null) {
        return;
    }
    PendingIntent pendingIntent = null;
    // 當(dāng) API > 18 時,使用 extras 獲取通知的詳細(xì)信息
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        Bundle extras = notification.extras;
        if (extras != null) {
            // 獲取通知標(biāo)題
            String title = extras.getString(Notification.EXTRA_TITLE, "");
            // 獲取通知內(nèi)容
            String content = extras.getString(Notification.EXTRA_TEXT, "");
            if (!TextUtils.isEmpty(content) && content.contains("[微信紅包]")) {
                pendingIntent = notification.contentIntent;
            }
        }
    } else {
        // 當(dāng) API = 18 時,利用反射獲取內(nèi)容字段
        List<String> textList = getText(notification);
        if (textList != null && textList.size() > 0) {
            for (String text : textList) {
                if (!TextUtils.isEmpty(text) && text.contains("[微信紅包]")) {
                    pendingIntent = notification.contentIntent;
                    break;
                }
            }
        }
    }
    // 發(fā)送 pendingIntent 以此打開微信
    try {
        if (pendingIntent != null) {
            pendingIntent.send();
        }
    } catch (PendingIntent.CanceledException e) {
        e.printStackTrace();
    }
}

從上面的代碼可知,對于分析 Notification 的內(nèi)容分為了兩種:

  • 當(dāng) API > 18 時,利用 Notification.extras 來獲取通知內(nèi)容。extras 是在 API 19 時被加入的;
  • 當(dāng) API = 18 時,利用反射獲取 Notification 中的內(nèi)容。具體的代碼在下方。
public List<String> getText(Notification notification) {
    if (null == notification) {
        return null;
    }
    RemoteViews views = notification.bigContentView;
    if (views == null) {
        views = notification.contentView;
    }
    if (views == null) {
        return null;
    }
    // Use reflection to examine the m_actions member of the given RemoteViews object.
    // It's not pretty, but it works.
    List<String> text = new ArrayList<>();
    try {
        Field field = views.getClass().getDeclaredField("mActions");
        field.setAccessible(true);
        @SuppressWarnings("unchecked")
        ArrayList<Parcelable> actions = (ArrayList<Parcelable>) field.get(views);
        // Find the setText() and setTime() reflection actions
        for (Parcelable p : actions) {
            Parcel parcel = Parcel.obtain();
            p.writeToParcel(parcel, 0);
            parcel.setDataPosition(0);
            // The tag tells which type of action it is (2 is ReflectionAction, from the source)
            int tag = parcel.readInt();
            if (tag != 2) continue;
            // View ID
            parcel.readInt();
            String methodName = parcel.readString();
            if (null == methodName) {
                continue;
            } else if (methodName.equals("setText")) {
                // Parameter type (10 = Character Sequence)
                parcel.readInt();
                // Store the actual string
                String t = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel).toString().trim();
                text.add(t);
            }
            parcel.recycle();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return text;
}

憑著 onNotificationPosted(StatusBarNotification sbn) 方法就已經(jīng)可以完成監(jiān)聽微信通知并打開的動作了。下面我們來看一下其他關(guān)于 NotificationListenerService 的二三事。

取消通知

有了監(jiān)聽,NotificationListenerService 自然提供了可以取消通知的方法。取消通知的方法有:

  • cancelNotification(String key) :是 API >= 21 才可以使用的。利用 StatusBarNotificationgetKey() 方法來獲取 key 并取消通知。
  • cancelNotification(String pkg, String tag, int id) :在 API < 21 時可以使用,在 API >= 21 時使用此方法來取消通知將無效,被廢棄。

最后,取消通知的方法:

public void cancelNotification(StatusBarNotification sbn) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        cancelNotification(sbn.getKey());
    } else {
        cancelNotification(sbn.getPackageName(), sbn.getTag(), sbn.getId());
    }
}

檢測通知監(jiān)聽服務(wù)是否被授權(quán)

public boolean isNotificationListenerEnabled(Context context) {
    Set<String> packageNames = NotificationManagerCompat.getEnabledListenerPackages(this);
    if (packageNames.contains(context.getPackageName())) {
        return true;
    }
    return false;
}

打開通知監(jiān)聽設(shè)置頁面

public void openNotificationListenSettings() {
    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();
    }
}

被殺后再次啟動時,監(jiān)聽不生效的問題

這個問題來源于知乎問題: NotificationListenerService不能監(jiān)聽到通知,研究了一天不知道是什么原因?

從問題的回答中可以了解到,是因為 NotificationListenerService 被殺后再次啟動時,并沒有去 bindService ,所以導(dǎo)致監(jiān)聽效果無效。

最后,在回答中還給出了解決方案:利用 NotificationListenerService 先 disable 再 enable ,重新觸發(fā)系統(tǒng)的 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);
}

該方法使用前提是 NotificationListenerService 已經(jīng)被用戶授予了權(quán)限,否則無效。另外,在自己的小米手機(jī)上實測,重新完成 rebind 操作需要等待 10 多秒(我的手機(jī)測試過大概在 13 秒左右)。幸運(yùn)的是,官方也已經(jīng)發(fā)現(xiàn)了這個問題,在 API 24 中提供了 requestRebind(ComponentName componentName) 方法來支持重新綁定。

AccessibilityService

講完了 NotificationListenerService 之后,按照前面說的那樣,在 API < 18 的時候使用 AccessibilityService 。

同樣,創(chuàng)建一個 WeChatAccessibilityService ,并且在 AndroidManifest.xml 中進(jìn)行聲明:

<service
    android:name="com.yuqirong.listenwechatnotification.WeChatAccessibilityService"
    android:label="@string/app_name"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
    </intent-filter>
    <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/accessible_service_config" />
</service>

聲明之后,還要對 WeChatAccessibilityService 進(jìn)行配置。需要在 res 目錄下新建一個 xml 文件夾,在里面新建一個 accessible_service_config.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeNotificationStateChanged"
    android:accessibilityFeedbackType="feedbackAllMask"
    android:accessibilityFlags="flagIncludeNotImportantViews"
    android:canRetrieveWindowContent="true"
    android:description="@string/app_name"
    android:notificationTimeout="100"
    android:packageNames="com.tencent.mm" />

最后就是代碼了:

public class WeChatAccessibilityService extends AccessibilityService {

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (Build.VERSION.SDK_INT < 18) {
            Notification notification = (Notification) event.getParcelableData();
            List<String> textList = getText(notification);
            if (textList != null && textList.size() > 0) {
                for (String text : textList) {
                    if (!TextUtils.isEmpty(text) &&
                            text.contains("[微信紅包]")) {
                        final PendingIntent pendingIntent = notification.contentIntent;
                        try {
                            if (pendingIntent != null) {
                                pendingIntent.send();
                            }
                        } catch (PendingIntent.CanceledException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                }
            }
        }
    }

    @Override
    public void onInterrupt() {

    }
    
}

看了一圈 WeChatAccessibilityService 的代碼,發(fā)現(xiàn)和 WeChatNotificationListenerService 在 API < 18 時處理的邏輯是一樣的,getText(notification) 方法就是上面那個,在這里就不復(fù)制粘貼了,基本沒什么好講的了。

有了 WeChatAccessibilityService 之后,在 API < 18 的情況下也能監(jiān)聽通知啦。\(ο)/

我們終于實現(xiàn)了當(dāng)初許下的那個需求了。 cry ...

總結(jié)

除了監(jiān)聽通知之外,AccessibilityService 還可以進(jìn)行模擬點擊、檢測界面變化等功能。具體的可以在 GitHub 上搜索搶紅包有關(guān)的 Repo 進(jìn)行深入學(xué)習(xí)。

NotificationListenerService 的監(jiān)聽通知功能更加強(qiáng)大,也更加專業(yè)。在一些設(shè)備上,如果 NotificationListenerService 被授予了權(quán)限,那么可以做到該監(jiān)聽進(jìn)程不死的效果,也算是另類的進(jìn)程?;睢?/p>

今天就到這兒了,拜拜!!

源碼下載:ListenWeChatNotification.rar

References

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,366評論 25 708
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,835評論 4 61
  • 咱們每個人住的房子可能是大的,也可能是小一點的,不管房子大小總會有一個小天地屬于我們,放置著我們的好朋友:...
    lily綠茶麗麗閱讀 1,403評論 0 1
  • 敬篤 舞動的云,像是在談?wù)撉锾?,抹去的晚霞,使天空漸漸陷入黑夜。一只晚歸的麻雀,此刻,才懂得離巢的懊惱。 樓宇間,...
    山谷小道士閱讀 414評論 1 4
  • Nancy Burson 1982 I really was, in a certain way, more in...
    思踐于人閱讀 393評論 0 0

友情鏈接更多精彩內(nèi)容