39 votes

Extraire le texte de notification de parcelable, contentView ou contentIntent

J'ai donc obtenu mon AccessibilityService de travail 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 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 mon notificationList semble n'avoir qu'une seule entrée (au moins un seul toast est indiqué).

Merci beaucoup!

/edit: j'ai pu extraire la notification colis avec

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

Cependant, je n'ai aucune idée de comment faire pour extraire la notification de message à partir d' notification ou notification.contentView / notification.contentIntent.

Des idées?

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

58voto

TomTasche Points 2268

J'ai gâché quelques heures de la dernière jours de trouver un moyen de faire ce que vous (et moi aussi, par la voie) veulent faire. Après en regardant à travers l'ensemble de la source de RemoteViews deux fois, j'ai pensé que la seule façon d'accomplir cette tâche est de la bonne vieille, laide et hacky Java Réflexions.

Ici, il est:

    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 tester si ça marche sur d'autres versions d'Android. Il est très probable que certaines valeurs, en particulier la viewId changé. Je pense qu'il devrait y avoir des moyens pour supporter toutes les versions d'Android sans coder en dur tous les possibles de l'ids, mais c'est la réponse à une autre question... ;)

PS: La valeur que vous recherchez (en se référant à "(3)" dans votre question initiale) est le "texte"de la valeur.

23voto

Jon C. Hammer Points 222

J'ai passé la dernière semaine de travail avec un problème similaire et peut proposer une solution similaire à Tom Taché (à l'aide de la réflexion), mais peut-être un peu plus facile à comprendre. La méthode suivante sera peigne une notification pour n'importe quel texte présent et de renvoyer ce texte en 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 c'est probablement ce qui ressemble un peu à de la magie noire, laissez-moi vous expliquer plus en détail. Nous avons d'abord tirer la RemoteViews objet de la notification elle-même. Ceci représente le point de vue à l'intérieur de la notification réel. Pour accéder à ces points de vue, nous avons pour gonfler le RemoteViews objet (qui ne fonctionne que lorsqu'un contexte d'activité est présent) ou utiliser la réflexion. La réflexion va travailler dans les deux cas et c'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 une liste de tableaux, d'objets d'Action. Cela représente ce qui va être fait à l'opinion une fois qu'ils sont gonflés. Par exemple, après les vues sont créées, setText() sera appelée à un certain point à chaque TextView qui est une partie de la hiérarchie à affecter le bon Chaînes. 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 y a un certain nombre de sous-classes concrètes d'Action définies dans RemoteViews. Celui qui nous intéresse est appelé ReflectionAction et est définie comme suit:

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

Cette action est utilisée pour assigner des valeurs aux points de vue. Une seule instance de cette classe serait susceptible d'avoir les valeurs {"setText", 10, "contenu de la textview"}. Donc, nous sommes seulement intéressés par les éléments de mActions qui sont "ReflectionAction" objets et affecter un texte d'une certaine façon. Nous pouvons dire à un particulier de "l'Action" est un "ReflectionAction" en examinant le champ "TAG" dans l'Action, ce qui est toujours le premier de lire la valeur de la parcelle. Les balises de 2 représentent ReflectionAction objets.

Après cela, il nous suffit de lire les valeurs de la parcelle selon l'ordre dans lequel ils ont été écrits (voir le lien vers la source, si vous êtes curieux). Nous trouvons toute la chaîne qui est définie avec setText() et de l'enregistrer 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 généralement s'opposer à l'utilisation de la réflexion dans des cas comme cela, il y a des moments où il est nécessaire. Sauf si il y a un contexte d'activité disponibles, le "standard" méthode ne fonctionne pas correctement, c'est donc la seule option.

11voto

Remi Peuvergne Points 199

Il y a une autre façon, si vous ne voulez pas utiliser la réflexion: au lieu de parcourant la liste des "actions" qui sont répertoriés dans la RemoteViews objet, vous pouvez "replay" 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);

Remarque vous utilisez remoteView.getLayoutId() assurez-vous que le gonflés vue correspond à celui de la notification.

Ensuite, vous pouvez récupérer une partie de la plus-ou-moins standard textviews avec

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

Pour mon propre usage (qui est d'espionner les notifications affichées par un paquet de mon propre) j'ai choisi de parcourir l'ensemble de la hiérarchie, sous localView et de recueillir tous les TextViews.

7voto

Sebouh Points 149

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

5voto

dennisg Points 3398

Pour répondre à ta question: Ce n'est pas envisageable dans votre cas. Ci-dessous je vais expliquer pourquoi.

"Le but principal de l'accessibilité de l'événement, c'est s'exposer suffisamment d'informations pour une AccessibilityService de fournir une rétroaction à l'utilisateur." Dans un cas comme le vôtre:

une accessibilité des services peuvent avoir besoin de plus d'information contextuelle, puis le en cas de charge. Dans de tels cas, le service peut obtenir la source de l'événement qui est une AccessibilityNodeInfo (capture d'écran d'une Vue de l'état) qui peut être utilisé pour explorer le contenu de la fenêtre. Notez que le privilège pour l'accès à un événement de la source, donc la fenêtre contenu, doit être explicitement demandée. (Voir AccessibilityEvent)

Nous pouvons demander à ce privilège explicitement par la mise en 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 ressembler à:

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

Nous avons explicitement la demande du privilège pour l'accès à un événement de la source (le contenu de la fenêtre) et de nous préciser (à l'aide d' accessibilityEventTypes), les types d'événements de ce service désirez 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 d'obtenir un AccessibilityNodeInfo et de parcourir à travers le contenu de la fenêtre, car "l'accessibilité de l'événement est envoyé par le premier affichage dans la vue de l'arbre".

Alors, il semble possible de le faire maintenant, de plus amples informations dans le AccessibilityEvent de la documentation nous dit:

Si l'un d'accessibilité des services n'a pas demandé à récupérer la fenêtre contenu de l'événement ne sera pas contenir de référence à sa source. Aussi pour les événements de type TYPE_NOTIFICATION_STATE_CHANGED la source est de ne jamais disponible.

Apparemment, c'est parce que des raisons de sécurité...


À 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 pas de méthodes pour obtenir de l'information sur la notification.

De même, la contentIntent est un PendingIntent, qui ne dispose pas de méthodes pour obtenir des informations sur l'intention sera lancé lorsque la notification est cliqué. (c'est à dire vous ne pouvez pas obtenir les extras de l'intention par exemple).

En outre, puisque vous n'avez pas fourni d'informations sur la raison pour laquelle vous désirez obtenir la description de la notification et de ce que vous voulez faire avec elle, je ne peux pas vraiment vous fournir une solution pour résoudre ce.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X