Извлечь текст уведомления из parcelable, contentView или contentIntent - PullRequest
39 голосов
/ 15 февраля 2012

Итак, мой 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) .

enter image description here

Единственная проблема в том, что мне также нужно значение (3) , которое отображается, когда пользователь открывает панель уведомлений. (2) для меня не важно, но было бы неплохо узнать, как это зачитать.Как вы, наверное, знаете, все значения могут быть разными.

enter image description here

Итак, как мне прочитать (3) ?Я сомневаюсь, что это невозможно, но у моего notificationList, кажется, есть только одна запись (показан хотя бы один тост).

Большое спасибо!

/ edit: Я мог бы извлечь пакет уведомлений с помощью

if (!(parcel instanceof Notification)) {
            return;
        }
        final Notification notification = (Notification) parcel;

Однако я не знаю, как извлечь сообщение уведомления из notification или notification.contentView / notification.contentIntent.

Любые идеи?

/ edit: Чтобы уточнить, что здесь спрашивают: Как я могу прочитать (3) ?

Ответы [ 8 ]

58 голосов
/ 24 апреля 2012

Я потратил несколько часов последних дней, чтобы найти способ сделать то, что вы (и я, кстати, тоже) хотите сделать.Дважды просмотрев весь источник 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)" в исходном вопросе) является значением "текст".

23 голосов
/ 02 декабря 2013

Я провел последнюю неделю, работая над аналогичной проблемой, и могу предложить решение, подобное тому, которое использовал Том Таш (используя рефлексию), но, возможно, его будет немного легче понять. Следующий метод прочесывает уведомление для любого присутствующего текста и возвращает этот текст в 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. Это представляет то, что будет сделано с представлениями после того, как они надуты. Например, после того, как представления созданы, setText () будет вызываться в некоторой точке каждого TextView, который является частью иерархии, чтобы назначить правильные строки. Что мы делаем, так это получаем доступ к этому списку действий и выполняем его. Действие определяется следующим образом:

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» в действии, которое всегда является первым значением, считываемым из посылки. Тэги 2 представляют объекты ReflectionAction.

После этого нам просто нужно прочитать значения из посылки в порядке, в котором они были написаны (см. Ссылку на источник, если вам интересно). Мы находим любую строку, которая установлена ​​с помощью setText (), и сохраняем ее в списке. (setTime () также включен, если время уведомления также необходимо. Если нет, эти строки можно безопасно удалить.)

Хотя я обычно выступаю против использования отражения в подобных случаях, бывают случаи, когда это необходимо. Если нет доступного контекста действия, «стандартный» метод не будет работать должным образом, поэтому это единственный вариант.

11 голосов
/ 10 января 2013

Есть и другой способ, если вы не хотите использовать отражение: вместо обхода списка «действий», перечисленных в объекте 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.

7 голосов
/ 12 апреля 2014

Добавление ответа Реми, чтобы определить различные типы уведомлений и извлечь данные, используйте следующий код.

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);
5 голосов
/ 24 апреля 2012

Чтобы ответить на ваш вопрос: не кажется возможным в вашем случае.Ниже я объясню, почему.

«Основная цель события доступности - предоставить достаточно информации для 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 , который не предоставляет получателей для получения информации о намерении, которое будет запущено при щелчке уведомления.(т. е. вы не можете получить дополнительные сведения, например, из намерения).

Кроме того, поскольку вы не предоставили никакой информации о том, почему вы хотите получить описание уведомления и что вы хотели бы с ним сделатьЯ не могу предоставить вам решение, чтобы решить эту проблему.

2 голосов
/ 11 июля 2014

Если вы попробовали решение 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();
}
1 голос
/ 06 августа 2014

Проект Achep AcDisplay предоставил решение, проверьте код из Extractor.java

0 голосов
/ 15 ноября 2015

Вы также можете просмотреть частные поля объекта Уведомления для получения дополнительной информации:
Как 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;
    }
...