Extract notification text from parcelable, contentView or contentIntent
Asked Answered
C

8

40

So I got my AccessibilityService working with the following code:

@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();
        }
    }
}

It works fine for reading out the text displayed when the notifcation was created (1).

enter image description here

The only problem is, I also need the value of (3) which is displayed when the user opens the notification bar. (2) is not important for me, but it would be nice to know how to read it out. As you probably know, all values can be different.

enter image description here

So, how can I read out (3)? I doubt this is impossible, but my notificationList seems to have only one entry (at least only one toast is shown).

Thanks a lot!

/edit: I could extract the notification parcel with

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

However, I have no idea how to extract the notifcation's message either from notification or notification.contentView / notification.contentIntent.

Any ideas?

/edit: To clarify what is asked here: How can I read out (3)?

Creak answered 15/2, 2012 at 10:56 Comment(0)
A
60

I've wasted a few hours of the last days figuring out a way to do what you (and, me too, by the way) want to do. After looking through the whole source of RemoteViews twice, I figured the only way to accomplish this task is good old, ugly and hacky Java Reflections.

Here it is:

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

This code worked fine on a Nexus S with Android 4.0.3. However, I didn't test if it works on other versions of Android. It's very likely that some values, especially the viewId changed. I think there should be ways to support all versions of Android without hard-coding all possible ids, but that's the answer to another question... ;)

PS: The value you're looking for (referring to as "(3)" in your original question) is the "text"-value.

Aftershaft answered 24/4, 2012 at 18:15 Comment(11)
+1 for the hard work. To get away from the hard coding, you might get more mileage out of text.values which gives you a Collection<String>. If you iterate through this I get two iterations, 1st is the sender's number, 2nd is the message.Collection<String> col = text.values(); String testStr = ""; for (String el : col) testStr = el;// 2nd iteration -> msgCallboy
+1 for both of you! It seems to work on 2.3.7 as well, but I will do more testing. Awesome! You will definitely get the bounty, but I will just leave it a couple more days so your answer gets more attention.Creak
@Zap was able to verify that this code works on 4.2, too, but: "I've verified this on 4.1.x and 4.2 and it seems to still hold fine in general. However, note that under 4.2 (as opposed to 4.1.x and 4.0.x as far as I've seen) the viewId will sometimes be null so you need to be careful in your tests!"Aftershaft
Has anyone managed to get around the issue of viewId being null on 4.2? I'm trying to extract the text from the ContentView as describedHat
It seems to me that this code does not work under GB. 16908358 is not the value for the content text. Any ideas who I can get this code work under GB?Nickelplate
Not much to add on Tom's solution except that for 4.2.2 viewIds are null because they are not contained in action.getClass().getDeclaredFields() so in order to get them you need to use action.getClass().getSuperclass().getDeclaredFields().Windshield
It works good with 1 lined notification text. It doesn't show all text of the expandable notification which contains more than 1 line of text. Please, give a tip how to fix it. Many thanks.Downs
@Aftershaft NickT thanks for nice answer it work in android 4.0 but when i test in on kitkat 4.4.4 not working.Any help?Endothelium
I have use your code inside onAccessibilityEvent, and is giving me Null pointer Exception at RemoteViews views = notification.contentViewPoetry
@SmartphoneDeveloper I am getting the same exception. Have you already found a solution?Vareck
I am also getting Null pointer Exception at RemoteViews views = notification.contentView please suggest me any alternative solution.Valued
D
23

I've spent the last week working with a similar problem and can propose a solution similar to Tom Tache's (using reflection), but might be a little bit easier to understand. The following method will comb a notification for any text present and return that text in an ArrayList if possible.

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

Because this probably looks a bit like black magic, let me explain in more detail. We first pull the RemoteViews object from the notification itself. This represents the views within the actual notification. In order to access those views, we either have to inflate the RemoteViews object (which will only work when an activity context is present) or use reflection. Reflection will work in either circumstance and is the method used here.

If you examine the source for RemoteViews here, you will see that one of the private members is an ArrayList of Action objects. This represents what will be done to the views after they are inflated. For example, after the views are created, setText() will be called at some point on each TextView that is a part of the hierarchy to assign the proper Strings. What we do is obtain access to this list of actions and iterate through it. Action is defined as follows:

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

There are a number of concrete subclasses of Action defined in RemoteViews. The one we're interested in is called ReflectionAction and is defined as follows:

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

This action is used to assign values to views. A single instance of this class would likely have the values {"setText", 10, "content of textview"}. Therefore, we're only interested in the elements of mActions that are "ReflectionAction" objects and assign text in some way. We can tell a particular "Action" is a "ReflectionAction" by examining the "TAG" field within the Action, which is always the first value to be read from the parcel. TAGs of 2 represent ReflectionAction objects.

After that, we just have to read the values from the parcel according to the order in which they were written (see the source link, if you're curious). We find any string that is set with setText() and save it in the list. (setTime() is also included, in case the notification time is also needed. If not, those lines can be safely deleted.)

While I typically oppose the use of reflection in instances like this, there are times when it is necessary. Unless there is an activity context available, the "standard" method won't work properly, so this is the only option.

Damales answered 2/12, 2013 at 6:34 Comment(4)
Great man. Can you tell me how you dig into this, like I wanted to learn how to dig into problems like this and find the solutions. I am really impressed with the explanation you provided. Great work..thanks a lot...Io
For this particular problem, necessity. I had to find an answer in order to get the functionality I wanted. Technically, the Eclipse debugger was very helpful, since it showed all the private members of all of the objects. Between that, looking through the source, and researching on SO, I eventually figured out how it worked.Damales
@Jon C. Hammer you provided perfect solution, but i am facing problem in android 4.4.4 and above version,notification data is not accessible .Have you faced same issue?Endothelium
notification.contentView it has deprecated, Is there any alternative solution available please suggest me.Valued
N
11

There's another way if you don't want to use reflection: instead of traversing the list of "actions" that are listed in the RemoteViews object, you can "replay" them on a 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);

Note you use remoteView.getLayoutId() to make sure the inflated view corresponds to the one of the notification.

Then, you can retrieve some of the more-or-less standard textviews with

 TextView tv = (TextView) localView.findViewById(android.R.id.title);
 Log.d("blah", tv.getText());

For my own purpose (which is to spy on the notifications posted by a package of my own) I chose to traverse the whole hierarchy under localView and gather all existing TextViews.

Norm answered 10/1, 2013 at 8:16 Comment(5)
Perfect! Thank you. I've been able to get title and largeImage of the notification, but no luck with the content text so far. Do you know how to do it?Sleet
I know this is old but I this might help others as well. You can get the content using TextView tv = (TextView) localView.findViewById(16908358); with 16908358 taken from TomTasche's answer. As stated above this might return nil in some cases.Anthropolatry
Here's more if anyone's interested: static final int TITLE = 16908310; static final int BIG_TEXT = 16909019; static final int TEXT = 16908358; static final int BIG_PIC = 16909021; static final int INBOX_0 = 16909023; static final int INBOX_1 = 16909024; static final int INBOX_2 = 16909025; static final int INBOX_3 =16909026; static final int INBOX_4 = 16909027; static final int INBOX_5 = 16909028; static final int INBOX_6 = 16909029; static final int INBOX_MORE = 16909030;Hipbone
You could use the above to identify what type of bigcontent style is being used. BIG_PIC and INBOX_ are unique for bigpic style and inbox style respectively.Hipbone
Turns out these are device specific. See my answer below.Hipbone
H
7

Adding on Remi's answer, to identify different notification types and extract data, use the below code.

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);
Hipbone answered 12/4, 2014 at 17:22 Comment(0)
O
5

To answer your question: This does not seem possible in your case. Below I explain why.

"The main purpose of an accessibility event is to expose enough information for an AccessibilityService to provide meaningful feedback to the user." In cases, such as yours:

an accessibility service may need more contextual information then the one in the event pay-load. In such cases the service can obtain the event source which is an AccessibilityNodeInfo (snapshot of a View state) which can be used for exploring the window content. Note that the privilege for accessing an event's source, thus the window content, has to be explicitly requested. (See AccessibilityEvent)

We can request this privilege explicitly by setting meta data for the service in your android manifest file:

<meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibilityservice" />

Where your xml file could look like:

<?xml version="1.0" encoding="utf-8"?>
 <accessibility-service
     android:accessibilityEventTypes="typeNotificationStateChanged"
     android:canRetrieveWindowContent="true"
 />

We explicitly request the privilige for accessing an event's source (the window content) and we specify (using accessibilityEventTypes) the event types this service would like to receive (in your case only typeNotificationStateChanged). See AccessibilityService for more options which you can set in the xml file.

Normally (see below why not in this case), it should be possible to call event.getSource() and obtain a AccessibilityNodeInfo and traverse through the window content, since "the accessibility event is sent by the topmost view in the view tree".

While, it seems possible to get it working now, further reading in the AccessibilityEvent documentation tells us:

If an accessibility service has not requested to retrieve the window content the event will not contain reference to its source. Also for events of type TYPE_NOTIFICATION_STATE_CHANGED the source is never available.

Apparently, this is because of security purposes...


To hook onto how to extract the notifcation's message either from notification or notification.contentView / notification.contentIntent. I do not think you can.

The contentView is a RemoteView and does not provide any getters to obtain information about the notification.

Similarly the contentIntent is a PendingIntent, which does not provide any getters to obtain information about the intent that will be launched when the notification is clicked. (i.e. you cannot obtain the extras from the intent for instance).

Furthermore, since you have not provided any information on why you would like to obtain the description of the notification and what you would like to do with it, I cannot really supply you with a solution to solve this.

Oftentimes answered 24/4, 2012 at 15:44 Comment(0)
H
2

If you've tried TomTasche's solution on Android 4.2.2, you'll notice it doesn't work. Expanding on his answer, and user1060919's comment... Here is an example that works for 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();
}
Hagood answered 11/7, 2014 at 16:36 Comment(2)
This works great, but its not working under android 5 anymore. Any solutions? secretClass.getDeclaredField("mActions"); this line throws an NoSuchFieldException, but i see this field in debug mode. Very strange... Please help!Raynold
It works good with 1 lined notification text. It doesn't show all text of the expandable notification which contains more than 1 line of text. Please, give a tip how to fix it. Many thanks.Downs
E
1

Achep's AcDisplay project has provided a solution, check the code from Extractor.java

Endomorphic answered 6/8, 2014 at 14:23 Comment(0)
E
0

You can also look at private fields of Notification object for some extra information,
Like contentTitle,contentText and tickerText

Here is relevant code

Notification notification = (Notification) event.getParcelableData();
getObjectProperty(notification, "contentTitle")
getObjectProperty(notification, "tickerText");
getObjectProperty(notification, "contentText");

Here is getObjectProperty method

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;
    }
Egin answered 15/11, 2015 at 6:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.