
バージョン4.3以降、
NotificationListenerServiceを使用してシステム内のすべての通知を追跡する機能がAndroid OSに追加されました。 残念ながら、以前のバージョンのOSとの後方互換性はありません。 古いバージョンのオペレーティングシステムを搭載したデバイスでこのような機能が必要な場合はどうすればよいですか?
この記事では、Android OSバージョン4.0-4.2で通知を追跡するための松葉杖とハッキングのセットを見つけることができます。 すべてのデバイスで結果が100%実行できるわけではないため、特定の場合に通知を削除するように松葉杖を追加する必要があります。
この問題に関するインターネット上の情報を検索すると、
AccessibilityServiceを使用して
TYPE_NOTIFICATION_STATE_CHANGEDイベントを追跡する必要があるという結論に至ります。 テストでは、このイベントは通知がステータスバーに追加されたときにのみ発生し、通知が削除されたときには発生しないことが示されました。 受け取った通知に関する追加データの読み取りと削除の追跡は、この問題を解決する上で最大の松葉杖です。
追加情報で着信通知を追跡する
そのため、通知が到着し、イベント
TYPE_NOTIFICATION_STATE_CHANGEDが受信されました。
AccessibilityEvent.getPackageName()メソッドを使用して、通知を送信したアプリケーションのパッケージ名を見つけることができます。 通知自体は
AccessibilityRecord.getParcelableData()メソッドを使用して抽出できます;出力では、
Notificationタイプのオブジェクトを取得します。 しかし、残念ながら、抽出された通知に含まれる利用可能なデータのセットは非常に少ないです。 通知の削除をさらに追跡するには、少なくともテキストの見出しを取得する必要があります。 これを行うには、反射や他の松葉杖を使用する必要があります。
コードpublic CharSequence getNotificationTitle(Notification notification, String packageName) { CharSequence title = null; title = getExpandedTitle(notification); if (title == null) { Bundle extras = NotificationCompat.getExtras(notification); if (extras != null) { Timber.d("getNotificationTitle: has extras: %1$s", extras.toString()); title = extras.getCharSequence("android.title"); Timber.d("getNotificationTitle: notification has no title, trying to get from bundle. found: %1$s", title); } } if (title == null) {
上記のコードでは、
通知タイプのオブジェクトに関連するすべての文字列値とビューIDが取得されます。 このために、反射と
Parcelableオブジェクトからの
読み取りが使用されます。 ただし、どのビューIDに通知タイトルがあるかはわかりません。 これを判断するには、次のコードを使用します。
コード public static final String NOTIFICATION_TITLE_DATA = "1"; public static final String BIG_NOTIFICATION_TITLE_DATA = "8"; public static final String INBOX_NOTIFICATION_TITLE_DATA = "9"; public int mNotificationTitleId = 0; public int mBigNotificationTitleId = 0; public int mInboxNotificationTitleId = 0; private void detectNotificationIds() { Timber.d("detectNotificationIds"); NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mContext) .setContentTitle(NOTIFICATION_TITLE_DATA); Notification n = mBuilder.build(); LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); ViewGroup localView;
上記のコードのロジックは、ヘッダーの一意のテキスト値を使用してテスト通知を作成することです。
LayoutInflaterを使用してこの通知のビューが作成され、以前に指定されたテキストを持つ子TextViewが再帰検索によって検索されます。 見つかったオブジェクトのID。すべての着信通知のヘッダーの一意の識別子になります。
ヘッダーが抽出された後、さらに確認するために、アクティブな通知のリストにいくつかのパッケージ、タイトルを保存します。
コード ConcurrentLinkedQueue<NotificationData> mAvailableNotifications = new ConcurrentLinkedQueue<>(); @Override public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) { switch (accessibilityEvent.getEventType()) { case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: Timber.d("onAccessibilityEvent: notification state changed"); if (accessibilityEvent.getParcelableData() != null && accessibilityEvent.getParcelableData() instanceof Notification) { Notification n = (Notification) accessibilityEvent.getParcelableData(); String packageName = accessibilityEvent.getPackageName().toString(); Timber.d("onAccessibilityEvent: notification posted package: %1$s; notification: %2$s", packageName, n); mAvailableNotifications.add(new NotificationData(mNotificationParser.getNotificationTitle(n, packageName), packageName));
最初の部分は対処されたようです。 このアプローチは、Androidのさまざまなバージョンで多少安定しています。 通知の削除を追跡しようとする2番目の部分に移りましょう。
削除通知の追跡
通知がいつ削除されたかを調べる標準的な方法は不可能なので、質問に答える必要があります。どのような場合に削除できますか? 次のオプションが思い浮かびます:
- ユーザースワイプ通知
- ユーザーが通知をクリックしてアプリケーションを開くと、消えました。
- ユーザーが[すべての通知をクリア]ボタンをクリックしました。
- アプリケーション自体が通知を削除しました。
私はすぐに最後のポイントで何もできなかったことを認めざるを得ませんが、そのような行動はあまり頻繁ではないので、あまり人気がないという希望があります。
各シナリオを個別に検討してみましょう。
ユーザースワイプ通知
ユーザーが通知をホイップしたときに発生するイベントを追跡したところ、ステータス行に属する
windowIdを持つパッケージ名
「android.system.ui」に対してタイプ
TYPE_WINDOW_CONTENT_CHANGEDのイベントが生成されていることが
わかりました。 残念ながら、アプリケーションを切り替えるウィンドウにもパッケージ名
「android.system.ui」がありますが、
windowIdは異なり
ます 。 WindowIdは定数ではなく、デバイスの再起動後またはAndroidの異なるバージョンで変更される場合があります。
イベントがステータス行から正確に来たことを計算する方法は? 私はこの問題についてかなり困惑しなければなりませんでした。 結局、このために特定の松葉杖を実装する必要がありました。 ユーザーが通知を削除したときに、ステータス行を展開することを提案しました。 特定のアクセシビリティの説明を含むすべての通知をクリアするためのボタンが必要です。 幸いなことに、定数はAndroidの異なるバージョンで同じ名前を持っています。 ここで、このボタンの存在についてビュー階層を分析する必要があり、ステータスバーに属するwindowIdを検出できます。 おそらく、行商人の1人がこれを行うためのより信頼性の高い方法を知っているので、知識を共有していただければ幸いです。
イベントがステータス行に属しているかどうかを確認します。
コード private void findClearAllButton() { Timber.d("findClearAllButton: called"); Resources res; try { res = mPackageManager.getResourcesForApplication(SYSTEMUI_PACKAGE_NAME); int i = res.getIdentifier("accessibility_clear_all", "string", "com.android.systemui"); if (i != 0) { mClearButtonName = res.getString(i); } } catch (Exception exp) { Timber.e(exp, null); } } public boolean isStatusBarWindowEvent(AccessibilityEvent accessibilityEvent) { boolean result = false; if (!SYSTEMUI_PACKAGE_NAME.equals(accessibilityEvent.getPackageName())) { Timber.v("isStatusBarWindowEvent: not system ui package"); } else if (mStatusBarWindowId != -1) {
次に、通知が削除されているか、まだ存在するかを判断する必要があります。 100%の信頼性を持たない方法を使用します。ステータス行からすべての行を抽出し、以前に保存された通知ヘッダーと一致するものを探します。 タイトルが欠落している場合、通知は削除されたと思われます。 目的の
windowIdでイベントが到着しますが、
AccessibilityNodeInfoが空であることが発生します(ユーザーが利用可能な最後の通知をホイップすると発生します)。 この場合、すべての通知が削除されたと思われます。
コード private void updateNotifications(AccessibilityEvent accessibilityEvent) { AccessibilityNodeInfo node = accessibilityEvent.getSource(); node = mStatusBarWindowUtils.getRootNode(node); boolean removed = false; Set<String> titles = node == null ? Collections.emptySet() : recursiveGetStrings(node); for (Iterator<NotificationData> iter = mAvailableNotifications.iterator(); iter.hasNext(); ) { NotificationData data = iter.next(); if (!titles.contains(data.title.toString())) {
イベント処理コード case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
ユーザーが通知をクリックしてアプリケーションを開くと、消えました
最初のケースのように、パッケージ名
「android.system.ui」の
TYPE_WINDOW_CONTENT_CHANGEDイベントがこの動作によって生成される場合は理想的であり、このケースを個別に考慮する必要はありません。 しかし、テストでは、必要なイベントが生成されることは示されましたが、常にではありません。Androidのバージョン、ステータスバーを閉じる速度に依存します。 私のアプリケーションでは、ユーザーに通知の欠落を通知するのを停止する必要がありました。 安全にプレイし、ユーザーが通知を逃したアプリケーションを開いたため、以前に保存された通知は彼にとって重要ではなく、自分自身を思い出さないかもしれないと考えることができると判断されました。
アプリケーションが開くと、イベント
TYPE_WINDOW_STATE_CHANGEDが生成されます。そこからpackageNameを見つけて、追跡されたすべての通知を削除できます。
コード private void removeNotificationsFor(String packageName) { boolean removed = false; Timber.d("removeNotificationsFor: %1$s", packageName); for (Iterator<NotificationData> iter = mAvailableNotifications.iterator(); iter.hasNext(); ) { NotificationData data = iter.next(); if (TextUtils.equals(packageName, data.packageName)) { iter.remove(); removed = true; } } if (removed) { Timber.d("removeNotificationsFor: removed for %1$s", packageName); onNotificationRemoved(); } }
イベント処理コード case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
ユーザーがすべての通知をクリアをクリックした
ここでは、前の場合と同様に、
TYPE_WINDOW_CONTENT_CHANGEDイベントが常に生成されるとは限りません。 ユーザーがボタンをクリックしたため、以前に受信した通知は重要ではなくなり、通知を停止することを想定しなければなりませんでした。
ステータス行で
TYPE_VIEW_CLICKEDイベントを追跡する必要があり、「すべてクリア」ボタンに属している場合は、すべての通知の追跡を停止します。
コード public boolean isClearNotificationsButtonEvent(AccessibilityEvent accessibilityEvent) { return TextUtils.equals(accessibilityEvent.getClassName(), android.widget.ImageView.class.getName()) && TextUtils.equals(accessibilityEvent.getContentDescription(), mClearButtonName); }
イベント処理コード case AccessibilityEvent.TYPE_VIEW_CLICKED:
バージョン4.0までのAndroidには何がありますか?
残念ながら、通知の削除を追跡する有効な方法を見つけることができませんでした。
AccessibilityServiceでViewHierarchyを操作する機能は、APIバージョン14以降でのみ追加されました。ViewHierarchyステータスバーに直接アクセスする方法を誰かが知っている場合は、この問題を解決できます。
PS
この記事で説明されているトピックに誰かが興味を持っていることを願っています。 通知の削除を追跡した結果を改善する方法についてのご意見をお待ちしております。
ここからほとんどの情報を取得しました
https://github.com/minhdangoz/notifications-widget (いくつかの場所で終了しなければなりませんでした)
Readyプロジェクト
https://github.com/httpdispatch/MissedNotificationsReminder-通知を逃したことを思い出させるアプリケーション。 v14ビルドバリアントを選択することを忘れないでください。 v18はNotificationListenerServiceを介して機能します