Extrait du texte de notification de partecelable, contentView ou contententent

alors j'ai eu mon AccessibilityService travaillant 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();
        }
    }
}

Il fonctionne très bien pour la lecture du texte affiché lorsque la notification a été créé (1).

enter image description here

Le seul problème est que j'ai aussi besoin de la valeur de (3) qui s'affiche lorsque l'utilisateur ouvre la barre de notification. (2) n'est pas important pour moi, mais il serait bon de savoir comment le lire. Comme vous le savez sans doute, toutes les valeurs peuvent être différentes.

enter image description here

alors, comment puis-je lire (3)<!--10? Je doute que cela soit impossible, mais mon notificationList semble n'avoir qu'une entrée (au moins un toast est montré).

Merci beaucoup!

/edit: je pourrais extraire le paquet de notification avec

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

cependant, je n'ai aucune idée de comment extraire le message de la notification soit de notification ou notification.contentView / notification.contentIntent.

des idées?

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

39
demandé sur Force 2012-02-15 14:56:25

8 réponses

j'ai perdu quelques heures des derniers jours à trouver un moyen de faire ce que vous (et, moi aussi, d'ailleurs) voulez faire. Après avoir regardé à travers toute la source de RemoteViews deux fois, j'ai pensé que le seul moyen d'accomplir cette tâche est bon vieux, laid et hacky Java Reflections.

la Voici:

    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 testé si cela fonctionne sur D'autres versions D'Android. Il est très probable que certaines valeurs, en particulier le viewId a changé. Je pense qu'il devrait y avoir des moyens de prendre en charge toutes les versions D'Android sans codage dur tous les identifiants possibles, mais c'est la réponse à une autre question... ;)

PS: La valeur que vous recherchez (se référant à "(3)" dans votre question originale) est la valeur "text".

58
répondu TomTasche 2012-04-24 18:15:42

j'ai passé la dernière semaine à travailler avec un problème similaire et je peux proposer une solution similaire à celle de Tom Tache (en utilisant la réflexion), mais qui pourrait être un peu plus facile à comprendre. La méthode suivante combinera une notification pour tout texte présent et retournera ce texte dans un ArrayList 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 un peu à de la magie noire, laissez-moi vous expliquer plus en détail. Nous retirons d'abord L'objet RemoteViews de la notification elle-même. Cela représente l' vues de l'intérieur de l'avis. Pour accéder à ces vues, nous devons soit gonfler L'objet RemoteViews (qui ne fonctionnera que lorsqu'un contexte d'activité est présent), soit utiliser la réflexion. La réflexion fonctionnera dans les deux cas et est la méthode utilisée ici.

Si vous examinez la source pour RemoteViews ici, vous verrez que l'un des membres privés est un ArrayList d'objets D'Action. Cela représente ce qui sera fait aux vues après qu'ils sont gonfler. Par exemple, après la création des vues, setText() sera appelé à un moment donné sur chaque TextView qui fait partie de la hiérarchie pour assigner les chaînes appropriées. Ce que nous faisons est d'obtenir l'accès à cette liste d'actions et d'itérer à travers elle. L'Action est définie comme suit:

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

il existe un certain nombre de sous-classes d'Action concrètes définies dans RemoteViews. Celui qui nous intéresse s'appelle ReflectionAction et est défini comme suit:

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

ce action est utilisé pour assigner des valeurs aux vues. Une seule instance de cette classe serait susceptible d'avoir les valeurs {"setText", 10, "contenu de la textview"}. Par conséquent, nous ne nous intéressons qu'aux éléments des actions qui sont des objets "ReflectionAction" et assignent du texte d'une façon ou d'une autre. Nous pouvons dire qu'une "Action" particulière est une "Réflectionaction" en examinant le champ "TAG" dans L'Action, qui est toujours la première valeur à être lue à partir du paquet. Les étiquettes de 2 représentent la Réflectionaction objet.

après cela, il suffit de lire les valeurs du paquet selon l'ordre dans lequel elles ont été écrites (voir le lien source, si vous êtes curieux). Nous trouvons n'importe quelle chaîne qui est définie avec setText() et la sauvegardons dans la liste. (setTime() est également inclus, dans le cas où le délai de notification est également nécessaire. Si non, ces lignes peuvent être supprimés en toute sécurité.)

alors que je m'oppose typiquement à l'utilisation de la réflexion dans des cas comme celui-ci, il y a des moments où cela est nécessaire. À moins qu'il n'y ait un contexte d'activité disponible, la méthode "standard" ne fonctionnera pas correctement, donc c'est la seule option.

23
répondu Jon C. Hammer 2013-12-02 06:34:21

il y a une autre façon si vous ne voulez pas utiliser la réflexion: au lieu de traverser la liste des "actions" qui sont listées dans L'objet RemoteViews, vous pouvez les "rejouer" sur un groupe de vue:

/* 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 s'assurer que la vue gonflée correspond à celle de la notification.

ensuite, vous pouvez récupérer certains des textviews plus ou moins standard avec

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

Pour mon propre but (qui est de l'espionnage sur les notifications postées par mon propre paquet) j'ai choisi de parcourir toute la hiérarchie sous localView et de rassembler tous les TextViews existants.

11
répondu Remi Peuvergne 2013-01-10 09:31:05

ajouter à 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
répondu black 2014-04-12 17:22:55

pour répondre À ta question: Ce n' semble possible dans votre cas. Ci-dessous je vais expliquer pourquoi.

" le but principal d'un événement d'accessibilité est d'exposer suffisamment d'information pour qu'un service D'accessibilité puisse fournir un feedback significatif à l'utilisateur."Dans un cas comme le vôtre:

un service d'accessibilité peut avoir besoin de plus d'information contextuelle que la en cas de charge. Dans ce cas, le service peut obtenir source de l'événement qui est une AccessibilityNodeInfo (instantané D'une vue de l'état) qui peut être utilisé pour explorer le contenu de la fenêtre. Notez que le privilège d'accéder à la source d'un événement, donc la fenêtre contenu, doit être explicitement demandée. (Voir Accessibility Event)

nous pouvons demander ce privilège explicitement en définissant des méta-données pour le service dans votre fichier manifeste android:

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

où votre fichier xml pourrait chercher comme:

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

nous demandons explicitement le privilège d'accéder à la source d'un événement (le contenu de la fenêtre) et nous spécifions (en utilisant accessibilityEventTypes) les types d'événements que ce service aimerait recevoir (dans votre cas seulement typeNotificationStateChanged). Voir AccessibilityService pour plus d'options que vous pouvez définir dans le fichier xml.

Normalement (voir ci-dessous, pourquoi pas dans ce cas), il devrait être possible d'appeler event.getSource() et obtenir un AccessibilityNodeInfo et traverse la fenêtre contenu, puisque "l'événement d'accessibilité est envoyé par la vue la plus haute dans l'arbre de vue".

alors que, il semble possible de le faire fonctionner maintenant, d'autres lectures dans le Accessibility Event la documentation nous dit:

si un service d'accessibilité n'a pas demandé à récupérer la fenêtre contenu l'événement ne contiendra pas de référence à sa source. également pour événements de type TYPE_NOTIFICATION_STATE_CHANGED la source est jamais disponible.

apparemment, c'est pour des raisons de sécurité...


Pour accrocher sur la façon d'extraire la notification de message à partir de la notification ou de la notification.contentView / notification.contentIntent. Je ne pense pas que vous le pouvez.

le contentView est un RemoteView et ne fournit aucun renseignement permettant d'obtenir des informations sur la notification.

de Même la contententintent is a PendingIntent, qui ne dispose pas de méthodes pour obtenir des informations sur l'intention sera lancé lorsque la notification est cliqué. (i.e. vous ne pouvez pas obtenir les extras de l'intention par exemple).

de plus, puisque vous n'avez fourni aucune information sur la raison pour laquelle vous souhaitez obtenir la description de la notification et ce que vous voulez en faire, Je ne peux pas vraiment vous fournir une solution pour résoudre ce problème.

5
répondu dennisg 2012-04-24 15:44:30

si vous avez essayé la solution de TomTasche sur Android 4.2.2, vous remarquerez qu'elle ne fonctionne pas. Développant sur sa réponse, et le commentaire de l'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
répondu Broesph 2014-07-11 16:36:01

Achep AcDisplay projet a fourni une solution, vérifiez le code de Extracteur.java

1
répondu shaobin0604 2015-11-08 07:05:43

vous pouvez aussi regarder les champs privés de L'objet Notification pour quelques informations supplémentaires,

contentTitle, contentText et tickerText

voici le code correspondant

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
répondu Humble Coder 2015-11-15 07:53:45