【问题标题】:Extract notification text from parcelable, contentView or contentIntent从 parcelable、contentView 或 contentIntent 中提取通知文本
【发布时间】:2012-03-06 17:09:31
【问题描述】:

所以我让我的 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 似乎只有一个条目(至少只显示了一个 toast)。

非常感谢!

/edit:我可以用

提取通知包
if (!(parcel instanceof Notification)) {
            return;
        }
        final Notification notification = (Notification) parcel;

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

有什么想法吗?

/edit:澄清这里的问题:我怎样才能读出(3)

【问题讨论】:

    标签: android notifications android-pendingintent parcelable


    【解决方案1】:

    在过去的几天里,我浪费了几个小时来寻找一种方法来做你(顺便说一下,我也是)想做的事情。在浏览了 RemoteViews 的整个源代码两次之后,我发现完成这项任务的唯一方法是古老、丑陋和 hacky 的 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();
        }
    

    此代码在装有 Android 4.0.3 的 Nexus S 上运行良好。但是,我没有测试它是否适用于其他版本的 Android。很可能某些值,尤其是 viewId 发生了变化。我认为应该有办法支持所有版本的 Android,而无需对所有可能的 id 进行硬编码,但这是另一个问题的答案...... ;)

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

    【讨论】:

    • 为辛勤工作+1。为了摆脱硬编码,您可能会从 text.values 中获得更多信息,它会为您提供一个 Collection。如果你遍历这个我得到两次迭代,第一次是发件人的号码,第二次是 message.Collection col = text.values();字符串 testStr = ""; for (String el : col) testStr = el;// 第二次迭代 -> msg
    • 你们俩+1!它似乎也适用于 2.3.7,但我会做更多的测试。惊人的!你肯定会得到赏金,但我会再放几天,这样你的答案就会得到更多关注。
    • @Zap 能够验证此代码也适用于 4.2,但是:“我已经在 4.1.x 和 4.2 上验证了这一点,它似乎仍然可以正常运行。但是,请注意在 4.2 下(据我所见,与 4.1.x 和 4.0.x 不同)viewId 有时会为空,因此您在测试中需要小心!”
    • Tom 的解决方案没什么可补充的,除了 4.2.2 viewIds 为空,因为它们不包含在 action.getClass().getDeclaredFields() 中,因此为了获得它们,您需要使用 action.getClass().getSuperclass().getDeclaredFields()
    • 我在 onAccessibilityEvent 中使用了你的代码,并在 RemoteViews 视图中给了我空指针异常 = notification.contentView
    【解决方案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'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。这表示在视图膨胀后将对视图执行什么操作。例如,在创建视图之后,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 of textview"}。因此,我们只对作为“ReflectionAction”对象并以某种方式分配文本的 mAction 元素感兴趣。我们可以通过检查 Action 中的“TAG”字段来判断特定的“Action”是“ReflectionAction”,该字段始终是从包裹中读取的第一个值。 2 的 TAG 代表 ReflectionAction 对象。

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

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

    【讨论】:

    • 伟人。你能告诉我你是如何挖掘这个的,就像我想学习如何挖掘这样的问题并找到解决方案。你提供的解释给我留下了深刻的印象。伟大的工作......非常感谢......
    • 对于这个特殊的问题,很有必要。我必须找到答案才能获得我想要的功能。从技术上讲,Eclipse 调试器非常有用,因为它显示了所有对象的所有私有成员。在此期间,通过查看源代码和研究 SO,我最终弄清楚了它是如何工作的。
    • @Jon C. Hammer 您提供了完美的解决方案,但我在 android 4.4.4 及更高版本中遇到问题,无法访问通知数据。您是否遇到过同样的问题?
    • notification.contentView 它已弃用,是否有任何替代解决方案可用,请建议我。
    【解决方案3】:

    如果您不想使用反射,还有另一种方法:您可以在 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。

    【讨论】:

    • 完美!谢谢你。我已经能够获得通知的标题和 largeImage,但到目前为止内容文本没有运气。你知道怎么做吗?
    • 我知道这很旧,但我这也可能对其他人有所帮助。您可以使用 TextView tv = (TextView) localView.findViewById(16908358);16908358 从 TomTasche 的答案中获取内容。如上所述,在某些情况下这可能会返回 nil。
    • 如果有人感兴趣,这里还有更多内容: static final int TITLE = 16908310;静态最终 int BIG_TEXT = 16909019;静态最终 int TEXT = 16908358;静态最终 int BIG_PIC = 16909021;静态最终 int INBOX_0 = 16909023;静态最终 int INBOX_1 = 16909024;静态最终 int INBOX_2 = 16909025;静态最终 int INBOX_3 =16909026;静态最终 int INBOX_4 = 16909027;静态最终 int INBOX_5 = 16909028;静态最终 int INBOX_6 = 16909029;静态最终 int INBOX_MORE = 16909030;
    • 您可以使用上面的内容来确定正在使用什么类型的 bigcontent 样式。 BIG_PIC 和 INBOX_ 分别是 bigpic 风格和收件箱风格的独特之处。
    • 原来这些是特定于设备的。请参阅下面的答案。
    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

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

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

      无障碍服务可能需要更多的上下文信息,然后 一个在事件有效载荷。在这种情况下,服务可以获得 作为 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"
       />
      

      我们明确请求访问事件源(窗口内容)的权限,并指定(使用accessibilityEventTypes)此服务希望接收的事件类型(在您的情况下仅typeNotificationStateChanged)。请参阅AccessibilityService 了解您可以在 xml 文件中设置的更多选项。

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

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

      如果无障碍服务没有请求检索窗口 content 事件将不包含对其来源的引用。 也适用于 TYPE_NOTIFICATION_STATE_CHANGED 类型的事件源是从不 可用。

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


      了解如何从 notification 或 notification.contentView / notification.contentIntent 中提取通知的消息。我不认为你可以。

      contentView 是一个RemoteView 并且不提供任何getter 来获取有关通知的信息。

      同样,contentIntent 是一个PendingIntent,它不提供任何getter 来获取有关单击通知时将启动的intent 的信息。 (例如,您无法从意图中获得额外内容)。

      此外,由于您没有提供任何信息说明您为什么要获取通知的描述以及您想用它做什么,我无法真正为您提供解决此问题的解决方案。

      【讨论】:

        【解决方案6】:

        如果您在 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();
        }
        

        【讨论】:

        • 这很好用,但它不再在 android 5 下工作了。有什么解决办法吗? secretClass.getDeclaredField("mActions");此行引发 NoSuchFieldException,但我在调试模式下看到此字段。很奇怪……请帮忙!
        • 它适用于 1 行通知文本。它不会显示包含超过 1 行文本的可扩展通知的所有文本。请给一个提示如何解决它。非常感谢。
        【解决方案7】:

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

        【讨论】:

          【解决方案8】:

          您还可以查看 Notification 对象的私有字段以获取一些额外信息,
          例如 contentTitle,contentTexttickerText

          这里是相关代码

          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;
              }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-07-28
            • 1970-01-01
            • 1970-01-01
            • 2011-07-27
            • 1970-01-01
            相关资源
            最近更新 更多