Подтвердить что ты не робот

Извлечение текста извещных, 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;

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

Любые идеи?

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

4b9b3361

Ответ 1

Я потратил несколько часов в последние дни на то, чтобы выяснить, как ты (и, кстати, я тоже) хочу сделать то, что тебе нужно. Просмотрев весь источник RemoteViews дважды, я решил, что единственный способ выполнить эту задачу - это старые добрые, уродливые и взломанные Java Reflections.

Вот он:

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

Ответ 2

Я провел последнюю неделю, работая с аналогичной проблемой, и могу предложить решение, подобное Tom Tache (используя отражение), но может быть немного легче понять. Следующий метод будет расчесывать уведомление для любого присутствующего текста и возвращать этот текст в 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 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 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
{
    ...
}

В RemoteViews существует ряд конкретных подклассов Action. Тот, который нас интересует, называется ReflectionAction и определяется следующим образом:

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

Это действие используется для назначения значений представлениям. У одного экземпляра этого класса, вероятно, будут значения { "setText", 10, "content textview" }. Поэтому нас интересуют только элементы mActions, которые являются объектами ReflectionAction и каким-то образом назначают текст. Мы можем сказать, что конкретное "действие" - это "ReflectionAction", исследуя поле "TAG" внутри Action, которое всегда является первым значением, которое нужно прочитать из посылки. TAG 2 представляют объекты ReflectionAction.

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

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

Ответ 3

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

Ответ 4

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

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

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

"Основная цель события доступности - предоставить достаточную информацию для AccessibilityService для предоставления значимой обратной связи пользователю". В таких случаях, как ваш:

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

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

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

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

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

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


Чтобы подключиться к тому, как извлечь сообщение об уведомлении либо из уведомления, либо извещения .contentView/notification.contentIntent. Я не думаю, что ты можешь.

ContentView представляет собой RemoteView и не предоставляет каких-либо получателей для получения информации об уведомлении.

Аналогично, contentIntent представляет собой PendingIntent, который не предоставляет никаких геттеров для получения информации о намерении, которое будет запущено при нажатии на уведомление. (т.е. вы не можете получить дополнительные функции от намерения, например).

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

Ответ 6

Если вы пробовали решение 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();
}

Ответ 7

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

Ответ 8

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