Извлечение текста уведомления из 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();
        }
    }
}

он отлично работает для чтения текста, отображаемого при создании notifcation (1).

enter image description here

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

enter image description here

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

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

/ edit: я мог бы извлечь посылку уведомления с

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

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

какие идеи?

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

8 ответов


вот это:

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


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

private abstract static class Action implements Parcelable
{
    ...
}

существует ряд конкретных подклассов действий, определенных в RemoteViews. Мы заинтересованы в называется ReflectionAction и определяется как следует:

private class ReflectionAction extends Action
{
    String methodName;
    int type;
    Object value;
}

Это действие используется для присвоения значений видом. Один экземпляр этого класса, вероятно, будет иметь значения {"setText", 10, "content of textview"}. Поэтому нас интересуют только элементы mActions, которые являются объектами "ReflectionAction" и каким-то образом назначают текст. Мы можем сказать, что конкретное " действие "является" отражением", изучив поле" тег " в действии, которое всегда является первым значением, считываемым из посылки. Теги 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());

для моей собственной цели (которая spy on the notifications posted by a package of my own) я решил пересечь всю иерархию под 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, чтобы обеспечить значимую обратную связь с пользователем.- В случаях, подобных вашему:--9-->

доступность услуг может потребоваться больше контекстной информации, то один в случае оплаты нагрузки. В таких случаях сервис может получить источник события, который AccessibilityNodeInfo (снимок представления state), который можно использовать для изучения содержимого окна. обратите внимание, что привилегию для доступа к источнику события, таким образом, окно содержание, должно быть явно запрошено. (см. 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"
 />

мы явно запрашиваем privilige для доступа к источнику события (содержимое окна), и мы указываем (используя accessibilityEventTypes) типы событий, которые эта служба хотела бы получить (в вашем случае только typeNotificationStateChanged). См.AccessibilityService дополнительные параметры, которые можно задать в XML-файл.

обычно (см. ниже, почему бы и нет в этом случае), должно быть возможно вызвать event.getSource() и получения AccessibilityNodeInfo и пройти через окно содержимое, так как"событие доступности отправляется самым верхним представлением в дереве представлений".

в то время как, кажется, можно заставить его работать сейчас, дальнейшее чтение в AccessibilityEvent документация говорит нам:

если служба специальных возможностей не запросила получение окна содержание событие не будет содержать ссылки на его источник. также для события типа TYPE_NOTIFICATION_STATE_CHANGED источник никогда доступный.

видимо, это из-за целей безопасности...


чтобы зацепиться за то, как извлечь сообщение notifcation либо из уведомления или уведомления.странице просмотр содержимого / уведомлений.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 проект предоставил решение, проверьте код из экстрактор.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;
    }