Извлечь текст уведомления из parcelable, contentView или contentIntent
Таким образом, мой AccessibilityService работает со следующим кодом:
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {
List<CharSequence> notificationList = event.getText();
for (int i = 0; i < notificationList.size(); i++) {
Toast.makeText(this.getApplicationContext(), notificationList.get(i), 1).show();
}
}
}
Он отлично работает для считывания текста, отображаемого при создании уведомления (1).
Единственная проблема, мне также нужно значение (3), которое отображается, когда пользователь открывает панель уведомлений. (2) не важно для меня, но было бы неплохо узнать, как это зачитать. Как вы, наверное, знаете, все значения могут быть разными.
Итак, как я могу прочитать (3)? Я сомневаюсь, что это невозможно, но мой notificationList
Кажется, есть только одна запись (по крайней мере, только один тост).
Большое спасибо!
/ edit: я мог бы извлечь посылку уведомлений с
if (!(parcel instanceof Notification)) {
return;
}
final Notification notification = (Notification) parcel;
Тем не менее, я не знаю, как извлечь сообщение уведомления из notification
или же notification.contentView
/ notification.contentIntent
,
Есть идеи?
/ редактировать: чтобы уточнить, что здесь спрашивают: как я могу прочитать (3)?
8 ответов
Я потратил несколько часов последних дней, чтобы найти способ сделать то, что вы (и я, кстати, тоже) хотите сделать. Просмотрев весь источник RemoteViews дважды, я понял, что единственный способ выполнить эту задачу - это старые добрые, уродливые и хакерские отражения Java.
Вот:
Notification notification = (Notification) event.getParcelableData();
RemoteViews views = notification.contentView;
Class secretClass = views.getClass();
try {
Map<Integer, String> text = new HashMap<Integer, String>();
Field outerFields[] = secretClass.getDeclaredFields();
for (int i = 0; i < outerFields.length; i++) {
if (!outerFields[i].getName().equals("mActions")) continue;
outerFields[i].setAccessible(true);
ArrayList<Object> actions = (ArrayList<Object>) outerFields[i]
.get(views);
for (Object action : actions) {
Field innerFields[] = action.getClass().getDeclaredFields();
Object value = null;
Integer type = null;
Integer viewId = null;
for (Field field : innerFields) {
field.setAccessible(true);
if (field.getName().equals("value")) {
value = field.get(action);
} else if (field.getName().equals("type")) {
type = field.getInt(action);
} else if (field.getName().equals("viewId")) {
viewId = field.getInt(action);
}
}
if (type == 9 || type == 10) {
text.put(viewId, value.toString());
}
}
System.out.println("title is: " + text.get(16908310));
System.out.println("info is: " + text.get(16909082));
System.out.println("text is: " + text.get(16908358));
}
} catch (Exception e) {
e.printStackTrace();
}
Этот код отлично работал на Nexus S с Android 4.0.3. Однако я не проверял, работает ли он на других версиях Android. Весьма вероятно, что некоторые значения, особенно viewId, изменились. Я думаю, что должны быть способы поддержки всех версий Android без жесткого кодирования всех возможных идентификаторов, но это ответ на другой вопрос...;)
PS: значение, которое вы ищете (в оригинальном вопросе обозначается как "(3)"), является значением "text".
Я провел прошлую неделю, работая над подобной проблемой, и могу предложить решение, подобное тому, которое разработал Том Таш (используя рефлексию), но, возможно, его будет немного легче понять. Следующий метод прочесывает уведомление для любого присутствующего текста и возвращает этот текст в ArrayList, если это возможно.
public static List<String> getText(Notification notification)
{
// We have to extract the information from the view
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<String>();
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 (methodName == null) continue;
// Save strings
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);
}
// Save times. Comment this section out if the notification time isn't important
else if (methodName.equals("setTime"))
{
// Parameter type (5 = Long)
parcel.readInt();
String t = new SimpleDateFormat("h:mm a").format(new Date(parcel.readLong()));
text.add(t);
}
parcel.recycle();
}
}
// It's not usually good style to do this, but then again, neither is the use of reflection...
catch (Exception e)
{
Log.e("NotificationClassifier", e.toString());
}
return text;
}
Поскольку это, вероятно, немного похоже на черную магию, позвольте мне объяснить более подробно. Сначала мы извлекаем объект RemoteViews из самого уведомления. Это представляет представления в фактическом уведомлении. Чтобы получить доступ к этим представлениям, мы должны либо надуть объект RemoteViews (который будет работать только при наличии контекста активности), либо использовать отражение. Отражение будет работать в любом случае и является методом, используемым здесь.
Если вы изучите источник для RemoteViews здесь, вы увидите, что один из закрытых членов - это объект ArrayList of Action. Это представляет то, что будет сделано с представлениями после того, как они надуты. Например, после создания представлений в какой-то момент в каждом TextView, являющемся частью иерархии, будет вызываться setText() для назначения соответствующих строк. Что мы делаем, так это получаем доступ к этому списку действий и выполняем его. Действие определяется следующим образом:
private abstract static class Action implements Parcelable
{
...
}
Существует несколько конкретных подклассов Action, определенных в RemoteViews. Тот, который нас интересует, называется ReflectionAction и определяется следующим образом:
private class ReflectionAction extends Action
{
String methodName;
int type;
Object value;
}
Это действие используется для присвоения значений представлениям. Один экземпляр этого класса, вероятно, будет иметь значения {"setText", 10, "content of textview"}. Поэтому нас интересуют только элементы mActions, которые являются объектами "ReflectionAction" и каким-то образом назначают текст. Мы можем сказать конкретное "действие" как "ReflectionAction", изучив поле "TAG" в действии, которое всегда является первым значением, считываемым из посылки. TAG 2 представляют объекты ReflectionAction.
После этого нам просто нужно прочитать значения из посылки в том порядке, в котором они были написаны (см. Ссылку на источник, если вам интересно). Мы находим любую строку, которая установлена с помощью setText(), и сохраняем ее в списке. (setTime() также включен, если время уведомления также необходимо. Если нет, эти строки можно безопасно удалить.)
Хотя я обычно выступаю против использования рефлексии в подобных случаях, бывают случаи, когда это необходимо. Если нет доступного контекста действия, "стандартный" метод не будет работать должным образом, поэтому это единственный вариант.
Есть и другой способ, если вы не хотите использовать рефлексию: вместо обхода списка "действий", перечисленных в объекте RemoteViews, вы можете "воспроизвести" их в ViewGroup:
/* Re-create a 'local' view group from the info contained in the remote view */
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
ViewGroup localView = (ViewGroup) inflater.inflate(remoteView.getLayoutId(), null);
remoteView.reapply(getApplicationContext(), localView);
Обратите внимание, что вы используете remoteView.getLayoutId(), чтобы убедиться, что раздутый вид соответствует одному из уведомлений.
Затем вы можете получить некоторые из более или менее стандартных текстовых представлений с
TextView tv = (TextView) localView.findViewById(android.R.id.title);
Log.d("blah", tv.getText());
Для моей собственной цели (которая заключается в том, чтобы шпионить за уведомлениями, размещенными моим собственным пакетом), я решил пройти всю иерархию в localView и собрать все существующие TextViews.
В дополнение к ответу Remi, чтобы определить различные типы уведомлений и извлечь данные, используйте код ниже.
Resources resources = null;
try {
PackageManager pkm = getPackageManager();
resources = pkm.getResourcesForApplication(strPackage);
} catch (Exception ex){
Log.e(strTag, "Failed to initialize ids: " + ex.getMessage());
}
if (resources == null)
return;
ICON = resources.getIdentifier("android:id/icon", null, null);
TITLE = resources.getIdentifier("android:id/title", null, null);
BIG_TEXT = resources.getIdentifier("android:id/big_text", null, null);
TEXT = resources.getIdentifier("android:id/text", null, null);
BIG_PIC = resources.getIdentifier("android:id/big_picture", null, null);
EMAIL_0 = resources.getIdentifier("android:id/inbox_text0", null, null);
EMAIL_1 = resources.getIdentifier("android:id/inbox_text1", null, null);
EMAIL_2 = resources.getIdentifier("android:id/inbox_text2", null, null);
EMAIL_3 = resources.getIdentifier("android:id/inbox_text3", null, null);
EMAIL_4 = resources.getIdentifier("android:id/inbox_text4", null, null);
EMAIL_5 = resources.getIdentifier("android:id/inbox_text5", null, null);
EMAIL_6 = resources.getIdentifier("android:id/inbox_text6", null, null);
INBOX_MORE = resources.getIdentifier("android:id/inbox_more", null, null);
Чтобы ответить на ваш вопрос: это не представляется возможным в вашем случае. Ниже я объясню почему.
"Основная цель события доступности - предоставить достаточное количество информации для AccessibilityService, чтобы обеспечить значимую обратную связь с пользователем". В таких случаях, как ваш:
службе доступности может потребоваться больше контекстуальной информации, чем той, которая содержится в полезной нагрузке события. В таких случаях служба может получить источник события, который является AccessibilityNodeInfo (снимок состояния View), который может использоваться для исследования содержимого окна. Обратите внимание, что привилегия для доступа к источнику события, то есть к содержимому окна, должна быть явно запрошена. (См. AccessibilityEvent)
Мы можем явно запросить эту привилегию, установив метаданные для службы в файле манифеста Android:
<meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibilityservice" />
Где ваш XML-файл может выглядеть так:
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
android:accessibilityEventTypes="typeNotificationStateChanged"
android:canRetrieveWindowContent="true"
/>
Мы явно запрашиваем привилегию для доступа к источнику события (содержимому окна) и указываем (используя accessibilityEventTypes
) типы событий, которые этот сервис хотел бы получить (только в вашем случае typeNotificationStateChanged
). См. AccessibilityService для дополнительных параметров, которые вы можете установить в файле XML.
Обычно (см. Ниже, почему не в этом случае), должно быть возможно позвонить event.getSource()
и получить AccessibilityNodeInfo
и пройти через содержимое окна, так как "событие доступности отправляется самым верхним представлением в дереве представлений".
В то время как кажется возможным заставить это работать сейчас, дальнейшее чтение в документации AccessibilityEvent говорит нам:
Если служба специальных возможностей не запросила получение содержимого окна, событие не будет содержать ссылку на его источник. Также для событий типа TYPE_NOTIFICATION_STATE_CHANGED источник никогда не доступен.
Видимо, это из соображений безопасности...
Чтобы понять, как извлечь сообщение уведомления из уведомлений или уведомлений.contentView /tification.contentIntent. Я не думаю, что вы можете.
ContentView является RemoteView и не предоставляет никаких методов получения информации об уведомлении.
Аналогично, contentIntent - это PendingIntent, который не предоставляет получателей для получения информации о намерении, которое будет запущено при нажатии на уведомление. (например, вы не можете получить дополнительные данные из намерения, например).
Кроме того, поскольку вы не предоставили никакой информации о том, почему вы хотели бы получить описание уведомления и что вы хотели бы с ним сделать, я не могу действительно предоставить вам решение для решения этой проблемы.
Если вы попробовали решение TomTasche на Android 4.2.2, вы заметите, что оно не работает. Расширяя свой ответ и комментарий пользователя user1060919... Вот пример, который работает для 4.2.2:
Notification notification = (Notification) event.getParcelableData();
RemoteViews views = notification.contentView;
Class secretClass = views.getClass();
try {
Map<Integer, String> text = new HashMap<Integer, String>();
Field outerField = secretClass.getDeclaredField("mActions");
outerField.setAccessible(true);
ArrayList<Object> actions = (ArrayList<Object>) outerField.get(views);
for (Object action : actions) {
Field innerFields[] = action.getClass().getDeclaredFields();
Field innerFieldsSuper[] = action.getClass().getSuperclass().getDeclaredFields();
Object value = null;
Integer type = null;
Integer viewId = null;
for (Field field : innerFields) {
field.setAccessible(true);
if (field.getName().equals("value")) {
value = field.get(action);
} else if (field.getName().equals("type")) {
type = field.getInt(action);
}
}
for (Field field : innerFieldsSuper) {
field.setAccessible(true);
if (field.getName().equals("viewId")) {
viewId = field.getInt(action);
}
}
if (value != null && type != null && viewId != null && (type == 9 || type == 10)) {
text.put(viewId, value.toString());
}
}
System.out.println("title is: " + text.get(16908310));
System.out.println("info is: " + text.get(16909082));
System.out.println("text is: " + text.get(16908358));
} catch (Exception e) {
e.printStackTrace();
}
Проект Achep AcDisplay предоставил решение, проверьте код из Extractor.java
Вы также можете посмотреть на частные поля объекта Уведомления для получения дополнительной информации,
Как contentTitle,contentText и tickerText
Вот соответствующий код
Notification notification = (Notification) event.getParcelableData();
getObjectProperty(notification, "contentTitle")
getObjectProperty(notification, "tickerText");
getObjectProperty(notification, "contentText");
Вот метод getObjectProperty
public static Object getObjectProperty(Object object, String propertyName) {
try {
Field f = object.getClass().getDeclaredField(propertyName);
f.setAccessible(true);
return f.get(object);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}