Начиная с версии 4.3 в Android OS была добавлена возможность отслеживать все уведомления в системе используя NotificationListenerService. К сожалению, обратная совместимость с предыдущими версиями OS отсутствует. Что делать, если подобный функционал необходим на устройствах с более старой версией операционной системы?
В статье можно найти набор костылей и хаков для отслеживания уведомлений на Android OS версии 4.0-4.2. Не на всех устройствах результат 100% работоспособен, поэтому приходится использовать дополнительные костыли, чтобы предположить удаление уведомлений в определенных случаях.
Поиск информации в интернете по этому вопросу приводит к выводу, что необходимо использовать AccessibilityService и отслеживать событие TYPE_NOTIFICATION_STATE_CHANGED. Тестирование показало, что данное событие происходит лишь в тот момент, когда уведомление добавляется в статусную строку, но не происходит, когда уведомление удаляется. Чтение дополнительных данных о пришедшем уведомлении и отслеживание удаления и является наибольшими костылями в решении этой задачи.
Итак, уведомление пришло, получено событие TYPE_NOTIFICATION_STATE_CHANGED. Мы можем узнать package name приложения, которое послало уведомление используя метод AccessibilityEvent.getPackageName(). Само уведомление можно извлечь используя метод AccessibilityRecord.getParcelableData(), на выходе получим объект типа Notification. Но, к сожалению, набор доступных данных в извлеченном уведомлении весьма скуден. Для дальнейшего отслеживания удаления уведомления нам потребуется достать хотя бы текстовый заголовок. Для этого придется использовать reflection и другие костыли.
В вышеприведенном коде извлекаются все строковые значения и View Ids, относящиеся к объекту типа Notification. Для этого используются Reflection и чтение из Parcelable объектов. Но мы не знаем, какое View Id имеет заголовок уведомления. Для того, чтобы определить это используется следующий код:
Логика вышеприведенного кода в том, что создается тестовое уведомление с уникальным текстовым значением для заголовка. Создается View для данного уведомления при помощи LayoutInflater и рекурсивным поиском ищется дочерний TextView с ранее заданным текстом. Id найденного объекта и будет уникальным идентификатором заголовка всех входящих уведомлений.
После того, как заголовок был извлечен, сохраняем пару package, title в нашем списке активных уведомлений для дальнейших проверок.
С первой частью, вроде как, справились. Такой подход работает более менее стабильно на различных версия Android. Перейдем ко второй части, в которой мы будем пытаться отследить удаление уведомлений.
По скольку стандартным способом узнать, когда уведомление было удалено не представляется возможным, необходимо ответить на вопрос: в каких случаях оно может быть удалено? На ум приходят следующие варианты:
Сразу вынужден признать, что с последним пунктом ничего сделать пока не смог, но есть надежда, что такое поведение не слишком частое, и поэтому не слишком востребованное.
Рассмотрим каждый сценарий в отдельности.
Отследив, какие события происходят, когда пользователь смахивает уведомления, обнаружил, что генерируется событие типа TYPE_WINDOW_CONTENT_CHANGED для package name «android.system.ui» с windowId принадлежащему статусной строке. К сожалению, окно переключения между приложениями тоже имеет package name «android.system.ui» но другой windowId. WindowId это не константа, и может меняться после перезапуска устройства или на разных версиях Android.
Как же вычислить, что событие пришло именно из статусной строки? Мне пришлось изрядно поломать голову над этим вопросом. В конце концов, пришлось реализовать определенный костыль для этого. Предположил, что статусная строка должна быть развернута в момент удаления уведомления пользователем. На ней должна присутствовать кнопка очистить все уведомления с определенным accessibility description. К счастью, константа имеет одно и то же название на разных версиях Android. Теперь необходимо проанализировать иерархию вида на предмет присутствия данной кнопки, и тогда мы сможем обнаружить windowId принадлежащий статусной строке. Возможно, кто-нибудь из хабражителей знает более достоверный способ сделать это, буду благодарен, если поделитесь знаниями.
Определяем, принадлежит ли событие статусной строке:
Теперь необходимо определить, было ли удалено уведомление или все еще присутствует. Используем способ, который не обладает 100% надежностью: извлекаем все строки из статусной строки и ищем совпадения с ранее сохраненными заголовками уведомлений. Если заголовок отсутствует, считаем, что уведомление было удалено. Бывает, что приходит событие с нужным windowId но с пустым AccessibilityNodeInfo (случается, когда пользователь смахивает последнее доступное уведомление). В таком случае считаем, что все уведомления были удалены.
Было бы идеально, если бы данное поведение генерировало, как и в первом случае, событие TYPE_WINDOW_CONTENT_CHANGED для package name «android.system.ui», не пришлось бы рассматривать этот случай отдельно. Но тесты показали, что нужное событие генерируется, но не всегда: это зависит от версии Android, скорости закрытия статусной строки и еще непонятно от чего. В своем приложении мне необходимо было перестать уведомлять пользователя о пропущенном уведомлении. Было решено подстраховаться и считать, что раз пользователь открыл приложение, у которого имеются пропущенные уведомления, можно считать, что ранее сохраненные уведомления для него не важны и могут о себе не напоминать.
Когда приложение открывается, генерируется событие TYPE_WINDOW_STATE_CHANGED, откуда можно узнать packageName и удалить все отслеживаемые уведомления для него.
Тут, как и в предыдущем случае, событие TYPE_WINDOW_CONTENT_CHANGED генерируется не всегда. Пришлось предположить, что раз пользователь нажал на кнопку, то ранее полученные уведомления больше не важны и перестать о них уведомлять.
Необходимо отследить событие TYPE_VIEW_CLICKED в статусной строке и если оно принадлежит кнопке «Очистить все», перестать отслеживать все уведомления.
К сожалению, мне пока не удалось найти рабочий способ отследить удаление уведомлений. Возможность работать с ViewHierarchy в AccessibilityService была добавлена только начиная с API версии 14. Если кто-нибудь знает способ, как получить доступ к ViewHierarchy статусной строки напрямую, возможно, эту задачу удастся решить
Надеюсь, кому-нибудь интересна рассмотренная в статье тема. Буду рад услышать ваши идеи по поводу того, как улучшить результат отслеживания удаления уведомлений.
Большинство информации черпал отсюда https://github.com/minhdangoz/notifications-widget (пришлось допилить в некоторых местах)
Готовый проект https://github.com/httpdispatch/MissedNotificationsReminder — приложение напоминающее о пропущенных уведомлениях. Не забудьте выбрать v14 build variant, т.к. v18 работает через NotificationListenerService
В статье можно найти набор костылей и хаков для отслеживания уведомлений на Android OS версии 4.0-4.2. Не на всех устройствах результат 100% работоспособен, поэтому приходится использовать дополнительные костыли, чтобы предположить удаление уведомлений в определенных случаях.
Поиск информации в интернете по этому вопросу приводит к выводу, что необходимо использовать AccessibilityService и отслеживать событие TYPE_NOTIFICATION_STATE_CHANGED. Тестирование показало, что данное событие происходит лишь в тот момент, когда уведомление добавляется в статусную строку, но не происходит, когда уведомление удаляется. Чтение дополнительных данных о пришедшем уведомлении и отслеживание удаления и является наибольшими костылями в решении этой задачи.
Отслеживание входящих уведомлений с извлечение дополнительной информации
Итак, уведомление пришло, получено событие TYPE_NOTIFICATION_STATE_CHANGED. Мы можем узнать package name приложения, которое послало уведомление используя метод AccessibilityEvent.getPackageName(). Само уведомление можно извлечь используя метод AccessibilityRecord.getParcelableData(), на выходе получим объект типа Notification. Но, к сожалению, набор доступных данных в извлеченном уведомлении весьма скуден. Для дальнейшего отслеживания удаления уведомления нам потребуется достать хотя бы текстовый заголовок. Для этого придется использовать reflection и другие костыли.
Код
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;
}
В вышеприведенном коде извлекаются все строковые значения и View Ids, относящиеся к объекту типа Notification. Для этого используются Reflection и чтение из Parcelable объектов. Но мы не знаем, какое View Id имеет заголовок уведомления. Для того, чтобы определить это используется следующий код:
Код
/*
* 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;
}
}
}
}
Логика вышеприведенного кода в том, что создается тестовое уведомление с уникальным текстовым значением для заголовка. Создается View для данного уведомления при помощи LayoutInflater и рекурсивным поиском ищется дочерний TextView с ранее заданным текстом. Id найденного объекта и будет уникальным идентификатором заголовка всех входящих уведомлений.
После того, как заголовок был извлечен, сохраняем пару package, title в нашем списке активных уведомлений для дальнейших проверок.
Код
/**
* 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;
}
}
С первой частью, вроде как, справились. Такой подход работает более менее стабильно на различных версия Android. Перейдем ко второй части, в которой мы будем пытаться отследить удаление уведомлений.
Отслеживание удаления уведомлений
По скольку стандартным способом узнать, когда уведомление было удалено не представляется возможным, необходимо ответить на вопрос: в каких случаях оно может быть удалено? На ум приходят следующие варианты:
- Пользователь смахнул уведомление
- Пользователь открыл приложение по клику на уведомлении и оно исчезло.
- Пользователь нажал кнопку очистить все уведомления.
- Приложение само удалило уведомление.
Сразу вынужден признать, что с последним пунктом ничего сделать пока не смог, но есть надежда, что такое поведение не слишком частое, и поэтому не слишком востребованное.
Рассмотрим каждый сценарий в отдельности.
Пользователь смахнул уведомление
Отследив, какие события происходят, когда пользователь смахивает уведомления, обнаружил, что генерируется событие типа TYPE_WINDOW_CONTENT_CHANGED для package name «android.system.ui» с windowId принадлежащему статусной строке. К сожалению, окно переключения между приложениями тоже имеет package name «android.system.ui» но другой windowId. WindowId это не константа, и может меняться после перезапуска устройства или на разных версиях Android.
Как же вычислить, что событие пришло именно из статусной строки? Мне пришлось изрядно поломать голову над этим вопросом. В конце концов, пришлось реализовать определенный костыль для этого. Предположил, что статусная строка должна быть развернута в момент удаления уведомления пользователем. На ней должна присутствовать кнопка очистить все уведомления с определенным accessibility description. К счастью, константа имеет одно и то же название на разных версиях Android. Теперь необходимо проанализировать иерархию вида на предмет присутствия данной кнопки, и тогда мы сможем обнаружить windowId принадлежащий статусной строке. Возможно, кто-нибудь из хабражителей знает более достоверный способ сделать это, буду благодарен, если поделитесь знаниями.
Определяем, принадлежит ли событие статусной строке:
Код
/**
* 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;
}
Теперь необходимо определить, было ли удалено уведомление или все еще присутствует. Используем способ, который не обладает 100% надежностью: извлекаем все строки из статусной строки и ищем совпадения с ранее сохраненными заголовками уведомлений. Если заголовок отсутствует, считаем, что уведомление было удалено. Бывает, что приходит событие с нужным windowId но с пустым AccessibilityNodeInfo (случается, когда пользователь смахивает последнее доступное уведомление). В таком случае считаем, что все уведомления были удалены.
Код
/**
* 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;
Пользователь открыл приложение по клику на уведомлении и оно исчезло
Было бы идеально, если бы данное поведение генерировало, как и в первом случае, событие TYPE_WINDOW_CONTENT_CHANGED для package name «android.system.ui», не пришлось бы рассматривать этот случай отдельно. Но тесты показали, что нужное событие генерируется, но не всегда: это зависит от версии Android, скорости закрытия статусной строки и еще непонятно от чего. В своем приложении мне необходимо было перестать уведомлять пользователя о пропущенном уведомлении. Было решено подстраховаться и считать, что раз пользователь открыл приложение, у которого имеются пропущенные уведомления, можно считать, что ранее сохраненные уведомления для него не важны и могут о себе не напоминать.
Когда приложение открывается, генерируется событие TYPE_WINDOW_STATE_CHANGED, откуда можно узнать packageName и удалить все отслеживаемые уведомления для него.
Код
/**
* 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;
Пользователь нажал кнопку очистить все уведомления
Тут, как и в предыдущем случае, событие TYPE_WINDOW_CONTENT_CHANGED генерируется не всегда. Пришлось предположить, что раз пользователь нажал на кнопку, то ранее полученные уведомления больше не важны и перестать о них уведомлять.
Необходимо отследить событие TYPE_VIEW_CLICKED в статусной строке и если оно принадлежит кнопке «Очистить все», перестать отслеживать все уведомления.
Код
/**
* 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;
Что с Android до версии 4.0?
К сожалению, мне пока не удалось найти рабочий способ отследить удаление уведомлений. Возможность работать с ViewHierarchy в AccessibilityService была добавлена только начиная с API версии 14. Если кто-нибудь знает способ, как получить доступ к ViewHierarchy статусной строки напрямую, возможно, эту задачу удастся решить
P.S.
Надеюсь, кому-нибудь интересна рассмотренная в статье тема. Буду рад услышать ваши идеи по поводу того, как улучшить результат отслеживания удаления уведомлений.
Большинство информации черпал отсюда https://github.com/minhdangoz/notifications-widget (пришлось допилить в некоторых местах)
Готовый проект https://github.com/httpdispatch/MissedNotificationsReminder — приложение напоминающее о пропущенных уведомлениях. Не забудьте выбрать v14 build variant, т.к. v18 работает через NotificationListenerService