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

For*_*rce 39 notifications android parcelable android-pendingintent

所以我让我的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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它可以很好地读出创建通知时显示的文本(1).

在此输入图像描述

唯一的问题是,我还需要在用户打开通知栏时显示的值(3).(2)对我来说并不重要,但知道如何阅读它会很好.您可能知道,所有值都可能不同.

在此输入图像描述

那么,我怎么读出来(3)?我怀疑这是不可能的,但我notificationList似乎只有一个条目(至少只显示一个吐司).

非常感谢!

/ edit:我可以使用提取通知包裹

if (!(parcel instanceof Notification)) {
            return;
        }
        final Notification notification = (Notification) parcel;
Run Code Online (Sandbox Code Playgroud)

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

有任何想法吗?

/编辑:澄清这里的问题:我如何读出(3)

Tom*_*che 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();
    }
Run Code Online (Sandbox Code Playgroud)

此代码在带有Android 4.0.3的Nexus S上运行良好.但是,我没有测试它是否适用于其他版本的Android.某些值很可能,尤其是viewId已更改.我觉得应该有办法支持Android的所有版本没有硬编码的所有可能的ID,但是这是回答另一个问题...;)

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

  • 在Tom的解决方案上添加的内容不多,除了4.2.2之外,viewIds为null,因为它们不包含在`action.getClass().getDeclaredFields()中,所以为了得到它们,你需要使用`action.getClass(). getSuperclass之类().getDeclaredFields()`. (4认同)
  • +1努力工作.为了摆脱硬编码,你可能会从text.values中获得更多的里程数,它们会为你提供Collection <String>.如果你遍历这个我得到两次迭代,第一个是发送者的号码,第二个是消息.Collection <String> col = text.values(); String testStr =""; for(String el:col)testStr = el; // 2nd iteration - > msg (3认同)
  • 我在onAccessibilityEvent中使用你的代码,并在RemoteViews views = notification.contentView给我Null指针Exception (2认同)

小智 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;
}
Run Code Online (Sandbox Code Playgroud)

因为这可能看起来有点像黑魔法,让我更详细地解释一下.我们首先从通知本身中提取RemoteViews对象.这表示实际通知中的视图.为了访问这些视图,我们要么必须膨胀RemoteViews对象(只有在存在活动上下文时才能工作)或使用反射.反射将适用于任何一种情况,并且是这里使用的方法.

如果在此处检查RemoteView的源,您将看到其中一个私有成员是Action对象的ArrayList.这表示在视图夸大后将对视图执行的操作.例如,在创建视图之后,将在每个TextView上的某个点调用setText(),该TextView是层次结构的一部分,用于分配正确的字符串.我们所做的是获取对此操作列表的访问权并迭代它.行动定义如下:

private abstract static class Action implements Parcelable
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

RemoteViews中定义了许多Action的具体子类.我们感兴趣的是ReflectionAction,定义如下:

private class ReflectionAction extends Action
{
    String methodName;
    int type;
    Object value;
}
Run Code Online (Sandbox Code Playgroud)

此操作用于为视图指定值.此类的单个实例可能具有值{"setText",10,"textview"的内容}.因此,我们只对作为"ReflectionAction"对象的色素元素感兴趣并以某种方式分配文本.我们可以通过检查Action中的"TAG"字段来判断特定的"Action"是一个"ReflectionAction",它始终是从包裹中读取的第一个值.TAG为2表示ReflectionAction对象.

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

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

  • @Jon C. Hammer你提供了完美的解决方案,但我在android 4.4.4及以上版本中遇到问题,通知数据无法访问.你遇到过同样的问题吗? (3认同)

Rem*_*gne 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);
Run Code Online (Sandbox Code Playgroud)

请注意,您使用remoteView.getLayoutId()来确保膨胀的视图对应于其中一个通知.

然后,您可以检索一些或多或少的标准文本视图

 TextView tv = (TextView) localView.findViewById(android.R.id.title);
 Log.d("blah", tv.getText());
Run Code Online (Sandbox Code Playgroud)

出于我自己的目的(监视由我自己的包发布的通知),我选择遍历localView下的整个层次结构并收集所有现有的TextView.


bla*_*ack 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);
Run Code Online (Sandbox Code Playgroud)


den*_*isg 5

要回答你的问题:这并没有看起来可能你的情况.下面我解释一下原因.

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

可访问性服务可能需要更多的上下文信息,然后是事件中的一个上下载信息.在这种情况下,服务可以获得事件源,该事件源是可用于探索窗口内容的AccessibilityNodeInfo(视图状态的快照).请注意,必须明确请求访问事件源的权限,即窗口内容.(请参阅AccessibilityEvent)

我们可以通过在您的android清单文件中设置服务的元数据来显式请求此权限:

<meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibilityservice" />
Run Code Online (Sandbox Code Playgroud)

您的xml文件可能如下所示:

<?xml version="1.0" encoding="utf-8"?>
 <accessibility-service
     android:accessibilityEventTypes="typeNotificationStateChanged"
     android:canRetrieveWindowContent="true"
 />
Run Code Online (Sandbox Code Playgroud)

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

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

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

如果辅助功能服务未请求检索窗口内容,则该事件将不包含对其源的引用.此外,对于TYPE_NOTIFICATION_STATE_CHANGED类型的事件,源永远 不可用.

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


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

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

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

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