 Starting with version 4.3, Android OS has added the ability to track all notifications in the system using NotificationListenerService . Unfortunately, backward compatibility with previous versions of the OS is missing. What if such functionality is needed on devices with an older version of the operating system?
 Starting with version 4.3, Android OS has added the ability to track all notifications in the system using NotificationListenerService . Unfortunately, backward compatibility with previous versions of the OS is missing. What if such functionality is needed on devices with an older version of the operating system?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) { // if title was not found, use package name as title title = packageName; } Timber.d("getNotificationTitle: discovered title %1$s", title); return title; } private CharSequence getExpandedTitle(Notification n) { CharSequence title = null; RemoteViews view = n.contentView; // first get information from the original content view title = extractTitleFromView(view); // then try get information from the expanded view if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { view = getBigContentView(n); title = extractTitleFromView(view); } Timber.d("getExpandedTitle: discovered title %1$s", title); return title; } private CharSequence extractTitleFromView(RemoteViews view) { CharSequence title = null; HashMap<Integer, CharSequence> notificationStrings = getNotificationStringFromRemoteViews(view); if (notificationStrings.size() > 0) { // get title string if available if (notificationStrings.containsKey(mNotificationTitleId)) { title = notificationStrings.get(mNotificationTitleId); } else if (notificationStrings.containsKey(mBigNotificationTitleId)) { title = notificationStrings.get(mBigNotificationTitleId); } else if (notificationStrings.containsKey(mInboxNotificationTitleId)) { title = notificationStrings.get(mInboxNotificationTitleId); } } return title; } // use reflection to extract string from remoteviews object private HashMap<Integer, CharSequence> getNotificationStringFromRemoteViews(RemoteViews view) { HashMap<Integer, CharSequence> notificationText = new HashMap<>(); try { ArrayList<Parcelable> actions = null; Field fs = RemoteViews.class.getDeclaredField("mActions"); if (fs != null) { fs.setAccessible(true); //noinspection unchecked actions = (ArrayList<Parcelable>) fs.get(view); } if (actions != null) { // 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 int viewId = parcel.readInt(); String methodName = parcel.readString(); //noinspection ConstantConditions if (methodName == null) continue; // Save strings else if (methodName.equals("setText")) { // Parameter type (10 = Character Sequence) int i = parcel.readInt(); // Store the actual string try { CharSequence t = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); notificationText.put(viewId, t); } catch (Exception exp) { Timber.d("getNotificationStringFromRemoteViews: Can't get the text for setText with viewid:" + viewId + " parameter type:" + i + " reason:" + exp.getMessage()); } } parcel.recycle(); } } } catch (Exception exp) { Timber.e(exp, null); } return notificationText; }  /* * Data constants used to parse notification view ids */ 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"; /** * The id of the notification title view. Initialized in the {@link #detectNotificationIds()} method */ public int mNotificationTitleId = 0; /** * The id of the big notification title view. Initialized in the {@link #detectNotificationIds()} method */ public int mBigNotificationTitleId = 0; /** * The id of the inbox notification title view. Initialized in the {@link #detectNotificationIds()} method */ public int mInboxNotificationTitleId = 0; /** * Detect required view ids which are used to parse notification information */ 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; // detect id's from normal view localView = (ViewGroup) inflater.inflate(n.contentView.getLayoutId(), null); n.contentView.reapply(mContext, localView); recursiveDetectNotificationsIds(localView); // detect id's from expanded views if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { NotificationCompat.BigTextStyle bigtextstyle = new NotificationCompat.BigTextStyle(); mBuilder.setContentTitle(BIG_NOTIFICATION_TITLE_DATA); mBuilder.setStyle(bigtextstyle); n = mBuilder.build(); detectExpandedNotificationsIds(n); NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); mBuilder.setContentTitle(INBOX_NOTIFICATION_TITLE_DATA); mBuilder.setStyle(inboxStyle); n = mBuilder.build(); detectExpandedNotificationsIds(n); } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void detectExpandedNotificationsIds(Notification n) { LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); ViewGroup localView = (ViewGroup) inflater.inflate(n.bigContentView.getLayoutId(), null); n.bigContentView.reapply(mContext, localView); recursiveDetectNotificationsIds(localView); } private void recursiveDetectNotificationsIds(ViewGroup v) { for (int i = 0; i < v.getChildCount(); i++) { View child = v.getChildAt(i); if (child instanceof ViewGroup) recursiveDetectNotificationsIds((ViewGroup) child); else if (child instanceof TextView) { String text = ((TextView) child).getText().toString(); int id = child.getId(); switch (text) { case NOTIFICATION_TITLE_DATA: mNotificationTitleId = id; break; case BIG_NOTIFICATION_TITLE_DATA: mBigNotificationTitleId = id; break; case INBOX_NOTIFICATION_TITLE_DATA: mInboxNotificationTitleId = id; break; } } } }  /** * List to store currently active notifications data */ 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)); // fire event onNotificationPosted(); } break; ... } } /** * Simple notification information holder */ class NotificationData { CharSequence title; CharSequence packageName; public NotificationData(CharSequence title, CharSequence packageName) { this.title = title; this.packageName = packageName; } }  /** * Find "clear all notifications" button accessibility text used by the systemui application */ 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); } } /** * Check whether accessibility event belongs to the status bar window by checking event package * name and window id * * @param accessibilityEvent * @return */ 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) { // if status bar window id is already initialized result = accessibilityEvent.getWindowId() == mStatusBarWindowId; Timber.v("isStatusBarWindowEvent: comparing window ids %1$d %2$d, result %3$b", mStatusBarWindowId, accessibilityEvent.getWindowId(), result); } else { Timber.v("isStatusBarWindowEvent: status bar window id not initialized, starting detection"); AccessibilityNodeInfo node = accessibilityEvent.getSource(); node = getRootNode(node); if (hasClearButton(node)) { Timber.v("isStatusBarWindowEvent: the root node has clear text button in the view hierarchy. Remember window id for future use"); mStatusBarWindowId = accessibilityEvent.getWindowId(); result = isStatusBarWindowEvent(accessibilityEvent); } if (!result) { Timber.v("isStatusBarWindowEvent: can't initizlie status bar window id"); } } return result; } /** * Get the root node for the specified node if it is not null * * @param node * @return the root node for the specified node in the view hierarchy */ public AccessibilityNodeInfo getRootNode(AccessibilityNodeInfo node) { if (node != null) { // workaround for Android 4.0.3 to avoid NPE. Should to remember first call of the node.getParent() such // as second call may return null AccessibilityNodeInfo parent; while ((parent = node.getParent()) != null) { node = parent; } } return node; } /** * Check whether the node has clear notifications button in the view hierarchy * * @param node * @return */ private boolean hasClearButton(AccessibilityNodeInfo node) { boolean result = false; if (node == null) { return result; } Timber.d("hasClearButton: %1$s %2$d %3$s", node.getClassName(), node.getWindowId(), node.getContentDescription()); if (TextUtils.equals(mClearButtonName, node.getContentDescription())) { result = true; } else { for (int i = 0; i < node.getChildCount(); i++) { if (hasClearButton(node.getChild(i))) { result = true; break; } } } return result; }  /** * Update the available notification information from the node information of the accessibility event * <br> * The algorithm is not exact. All the strings are recursively retrieved in the view hierarchy and then * titles are compared with the available notifications * * @param accessibilityEvent */ 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())) { // if the title is absent in the view hierarchy remove notification from available notifications iter.remove(); removed = true; } } if (removed) { Timber.d("updateNotifications: removed"); // fire event if at least one notification was removed onNotificationRemoved(); } } /** * Get all the text information from the node view hierarchy * * @param node * @return */ private Set<String> recursiveGetStrings(AccessibilityNodeInfo node) { Set<String> strings = new HashSet<>(); if (node != null) { if (node.getText() != null) { strings.add(node.getText().toString()); Timber.d("recursiveGetStrings: %1$s", node.getText().toString()); } for (int i = 0; i < node.getChildCount(); i++) { strings.addAll(recursiveGetStrings(node.getChild(i))); } } return strings; }  case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: // auto clear notifications when cleared from notifications bar (old api, Android < 4.3) if (mStatusBarWindowUtils.isStatusBarWindowEvent(accessibilityEvent)) { Timber.d("onAccessibilityEvent: status bar content changed"); updateNotifications(accessibilityEvent); } break;  /** * Remove all notifications from the available notifications with the specified package name * * @param 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: // auto clear notifications for launched application (TYPE_WINDOW_CONTENT_CHANGED not always generated // when app is clicked or cleared) Timber.d("onAccessibilityEvent: window state changed"); if (accessibilityEvent.getPackageName() != null) { String packageName = accessibilityEvent.getPackageName().toString(); Timber.d("onAccessibilityEvent: window state has been changed for package %1$s", packageName); removeNotificationsFor(packageName); } break;  /** * Check whether the accessibility event is generated by the clear all notifications button * * @param accessibilityEvent * @return */ 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: // auto clear notifications when clear all notifications button clicked (TYPE_WINDOW_CONTENT_CHANGED not always generated // when this event occurs so need to handle this manually // // also handle notification clicked event Timber.d("onAccessibilityEvent: view clicked"); if (mStatusBarWindowUtils.isStatusBarWindowEvent(accessibilityEvent)) { Timber.d("onAccessibilityEvent: status bar content clicked"); if (mStatusBarWindowUtils.isClearNotificationsButtonEvent(accessibilityEvent)) { // if clicked image view element with the clear button name content description Timber.d("onAccessibilityEvent: clear notifications button clicked"); mAvailableNotifications.clear(); // fire event onNotificationRemoved(); } else { // update notifications if another view is clicked updateNotifications(accessibilityEvent); } } break; Source: https://habr.com/ru/post/271541/
All Articles