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