web-dev-qa-db-fra.com

Extraire le texte de notification de colisable, contentView ou contentIntent

Donc, mon AccessibilityService fonctionne avec le code suivant: 

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

Cela fonctionne bien pour lire le texte affiché lors de la création de la notification (1) .

enter image description here

Le seul problème est que j'ai également besoin de la valeur (3) qui est affichée lorsque l'utilisateur ouvre la barre de notification. (2) n'est pas important pour moi, mais ce serait bien de savoir comment le lire. Comme vous le savez probablement, toutes les valeurs peuvent être différentes.

enter image description here

Alors, comment puis-je lire (3) ? Je doute que cela soit impossible, mais ma notificationList ne semble avoir qu'une entrée (au moins un pain grillé est affiché).

Merci beaucoup!

/edit: Je pourrais extraire le colis de notification avec

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

Cependant, je ne sais pas comment extraire le message de notifcation à partir de notification ou notification.contentView/notification.contentIntent.

Des idées?

/edit: Pour clarifier ce qui est demandé ici: Comment puis-je lire (3) ?

39
Force

J'ai perdu quelques heures au cours des derniers jours à trouver un moyen de faire ce que vous (et moi-même aussi) voulez faire. Après avoir parcouru deux fois l'intégralité de la source de RemoteViews, je me suis dit que le seul moyen d'accomplir cette tâche était de faire de bonnes réflexions Java anciennes et laides.

C'est ici:

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

Ce code a bien fonctionné sur un Nexus S avec Android 4.0.3. Cependant, je n'ai pas vérifié si cela fonctionnait sous d'autres versions d'Android. Il est très probable que certaines valeurs, en particulier le viewId, aient changé. Je pense qu'il devrait exister des moyens de prendre en charge toutes les versions d'Android sans coder en dur tous les identifiants possibles, mais c'est la réponse à une autre question ...;)

PS: la valeur que vous recherchez (en référence à "(3)" dans votre question initiale) est la valeur "text".

58
TomTasche

J'ai passé la semaine dernière à travailler sur un problème similaire et je peux proposer une solution similaire à celle de Tom Tache (en utilisant la réflexion), mais elle est peut-être un peu plus facile à comprendre. La méthode suivante consiste à combiner une notification pour tout texte présent et à renvoyer ce texte dans une liste de tableaux, si 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;
}

Parce que cela ressemble probablement à de la magie noire, laissez-moi vous expliquer plus en détail. Nous extrayons d'abord l'objet RemoteViews de la notification elle-même. Cela représente les vues dans la notification réelle. Afin d'accéder à ces vues, nous devons soit gonfler l'objet RemoteViews (qui ne fonctionnera que lorsqu'un contexte d'activité est présent) ou utiliser une réflexion. La réflexion fonctionne dans les deux cas et est la méthode utilisée ici. 

Si vous examinez le source pour RemoteViews ici , vous verrez que l’un des membres privés est un ArrayList of Action objects. Cela représente ce qui sera fait aux vues une fois qu’elles sont gonflées. Par exemple, après la création des vues, setText () sera appelée à un moment donné sur chaque TextView faisant partie de la hiérarchie pour affecter les chaînes appropriées. Ce que nous faisons, c'est obtenir l'accès à cette liste d'actions et le parcourir. L'action est définie comme suit:

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

Un certain nombre de sous-classes concrètes d’Actions sont définies dans RemoteViews. Celui qui nous intéresse s’appelle ReflectionAction et se définit comme suit:

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

Cette action est utilisée pour affecter des valeurs aux vues. Une seule instance de cette classe aurait probablement les valeurs {"setText", 10, "content of textview"}. Par conséquent, nous ne nous intéressons qu'aux éléments de mActions qui sont des objets "ReflectionAction" et affectent le texte d'une manière ou d'une autre. Nous pouvons dire qu'une "Action" particulière est une "ReflectionAction" en examinant le champ "TAG" de l'Action, qui est toujours la première valeur à lire dans la parcelle. Les balises de 2 représentent des objets ReflectionAction.

Après cela, il suffit de lire les valeurs de la parcelle en fonction de l'ordre dans lequel elles ont été écrites (voir le lien source, si vous êtes curieux). Nous trouvons n'importe quelle chaîne définie avec setText () et l'enregistrons dans la liste. (setTime () est également inclus, si le délai de notification est également nécessaire. Sinon, ces lignes peuvent être supprimées en toute sécurité.)

Bien que je sois généralement opposé à l’utilisation de la réflexion dans des cas comme celui-ci, il est parfois nécessaire. Sauf si un contexte d'activité est disponible, la méthode "standard" ne fonctionnera pas correctement. Il s'agit donc de la seule option.

23
Jon C. Hammer

Il existe un autre moyen de ne pas utiliser la réflexion: au lieu de parcourir la liste des "actions" répertoriées dans l'objet RemoteViews, vous pouvez les "rejouer" sur un 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);

Notez que vous utilisez remoteView.getLayoutId () pour vous assurer que la vue gonflée correspond à celle de la notification.

Ensuite, vous pouvez récupérer certaines des vues de texte standard plus ou moins avec

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

Pour mon propre but (qui consiste à espionner les notifications postées par un paquet de mon choix), j'ai choisi de parcourir toute la hiérarchie sous localView et de rassembler tous les TextViews existants.

11
Remi Peuvergne

En ajoutant la réponse de Remi, pour identifier différents types de notification et extraire des données, utilisez le code ci-dessous.

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);
7
black

Si vous avez essayé la solution de TomTasche sur Android 4.2.2, vous remarquerez que cela ne fonctionne pas…. Développer sa réponse et le commentaire de user1060919 ... Voici un exemple qui fonctionne pour 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();
}
2
Broesph

Le projet AcDisplay d'Achep a fourni une solution. Vérifiez le code dans Extractor.Java .

1
shaobin0604

Vous pouvez également consulter les champs privés de l'objet Notification pour obtenir des informations supplémentaires,
Comme contentTitle, contentText et tickerText

Voici le code pertinent

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

Voici la méthode 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;
    }
0
Zain Ali