从parcelable,contentView或contentIntent中提取通知文本

时间:2012-02-15 10:56:26

标签: android notifications android-pendingintent parcelable

所以我让我的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;

但是,我不知道如何从notificationnotification.contentView / notification.contentIntent中提取通知的消息。

有什么想法吗?

/ edit:澄清此处提出的问题:如何宣读(3)

8 个答案:

答案 0 :(得分:58)

我浪费了最后几天的几个小时,想办法做你想做的事情(顺便说一下,我也想做)。在仔细查看了RemoteViews的整个源代码之后,我认为完成此任务的唯一方法是使用旧的,丑陋的hacky 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();
    }

此代码适用于Android 4.0.3的Nexus S.但是,我没有测试它是否适用于其他版本的Android。某些值很可能,尤其是viewId已更改。我认为应该有办法支持所有版本的Android而无需对所有可能的ID进行硬编码,但这是另一个问题的答案......;)

PS:您正在寻找的值(在原始问题中称为“(3)”)是“文本”值。

答案 1 :(得分:23)

我上周花了一个类似的问题,可以提出类似于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'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 here的源代码,您将看到其中一个私有成员是Action对象的ArrayList。这表示在视图夸大后将对视图执行的操作。例如,在创建视图之后,将在每个TextView上的某个点调用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,“textview”的内容}。因此,我们只对作为“ReflectionAction”对象的色素元素感兴趣并以某种方式分配文本。我们可以通过检查Action中的“TAG”字段来判断特定的“Action”是一个“ReflectionAction”,它始终是从包裹中读取的第一个值。 TAG为2表示ReflectionAction对象。

之后,我们只需要根据包裹的编写顺序读取包裹中的值(如果您感到好奇,请参阅源链接)。我们找到使用setText()设置的任何字符串并将其保存在列表中。 (如果还需要通知时间,也包括setTime()。如果不需要,可以安全删除这些行。)

虽然我通常反对在这种情况下使用反射,但有时候有必要。除非有可用的活动上下文,否则“标准”方法将无法正常工作,因此这是唯一的选择。

答案 2 :(得分:11)

如果您不想使用反射,还有另一种方法:您可以在ViewGroup上“重播”它们,而不是遍历RemoteViews对象中列出的“操作”:

/* 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下的整个层次结构并收集所有现有的TextView。

答案 3 :(得分:7)

添加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);

答案 4 :(得分:5)

回答你的问题:在你的情况下,似乎是可能的。下面我解释原因。

“可访问性事件的主要目的是为AccessibilityService公开足够的信息,以便为用户提供有意义的反馈。”在你的情况下:

  

可访问性服务可能需要更多的上下文信息   一个人在活动中付费。在这种情况下,服务可以获得   事件源,它是AccessibilityNodeInfo(视图的快照   状态),可用于探索窗口内容。 请注意   访问事件源的权限,即窗口   内容,必须明确要求。(见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)。有关可在xml文件中设置的更多选项,请参阅AccessibilityService

通常(见下文为什么不在这种情况下),应该可以调用event.getSource()并获取AccessibilityNodeInfo并遍历窗口内容,因为“可访问性事件由视图树中最顶层的视图“。

发送

虽然现在似乎可以让它正常工作,但AccessibilityEvent文档中的进一步阅读告诉我们:

  

如果辅助功能服务未请求检索窗口   内容事件不包含对其来源的引用。 也适用于   TYPE_NOTIFICATION_STATE_CHANGED类型的事件源从不   可用。

显然,这是出于安全目的......


挂钩如何从通知或notification.contentView / notification.contentIntent中提取通知的消息。我认为你不能。

contentView是RemoteView,并不提供任何获取器来获取有关通知的信息。

类似地,contentIntent是PendingIntent,它不提供任何获取器来获取有关在单击通知时将启动的意图的信息。 (例如,你不能从意图中获得额外的东西)。

此外,由于您没有提供任何有关您希望获得通知说明以及使用该通知的信息的信息,因此我无法为您提供解决此问题的解决方案。

答案 5 :(得分:2)

如果你在Android 4.2.2上尝试过TomTasche的解决方案,你会发现它不起作用。 扩展他的回答,以及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();
}

答案 6 :(得分:1)

Achep的AcDisplay项目提供了解决方案,请查看Extractor.java

中的代码

答案 7 :(得分:0)

您还可以查看Notification对象的私有字段以获取一些额外信息,例如 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;
    }