web-dev-qa-db-ja.com

parcelable、contentView、またはcontentIntentから通知テキストを抽出する

だから私は私の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();
        }
    }
}

通知が作成されたときに表示されたテキストを読み取る場合は正常に機能します(1)

enter image description here

唯一の問題は、ユーザーが通知バーを開いたときに表示される(3)の値も必要なことです。 (2)は私にとって重要ではありませんが、それを読み取る方法を知っておくといいでしょう。ご存知のように、すべての値が異なる場合があります。

enter image description here

だから、どうすれば(3)を読み取ることができますか?これは不可能だと思いますが、私のnotificationListにはエントリが1つしかないようです(少なくとも1つのトーストしか表示されません)。

どうもありがとう!

/ edit:で通知パーセルを抽出できます

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

ただし、notificationまたはnotification.contentView/notification.contentIntentから通知のメッセージを抽出する方法がわかりません。

何か案は?

/ edit:ここで何が求められているかを明確にするために:どうすれば読み込めますか(3)

39
Force

私はあなた(そして私もちなみに)がやりたいことをする方法を考え出す最後の日の数時間を無駄にしました。 RemoteViewのソース全体を2度調べた後、このタスクを実行する唯一の方法は古くて醜​​く、ハッキーなJavaリフレクションです。

ここにあります:

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

このコードはAndroid 4.0.3のNexus Sで正常に機能しました。ただし、他のバージョンのAndroidで機能するかどうかはテストしていません。一部の値、特にviewIdが変更された可能性が高いです。Androidのすべてのバージョンをサポートして、考えられるすべてのIDをハードコーディングせずにサポートする方法があるはずですが、それが別の質問の答えです...;)

PS:探している値(元の質問では「(3)」と呼ばれています)は「テキスト」値です。

58
TomTasche

私は先週、同様の問題に取り組み、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;
}

これはおそらく黒魔術のように見えるので、詳しく説明します。最初に、通知自体からRemoteViewsオブジェクトをプルします。これは、実際の通知内のビューを表します。これらのビューにアクセスするには、RemoteViewsオブジェクトを膨らませる(アクティビティコンテキストが存在する場合にのみ機能する)か、リフレクションを使用する必要があります。リフレクションはどちらの状況でも機能し、ここで使用される方法です。

RemoteViews here のソースを調べると、プライベートメンバーの1つがActionオブジェクトのArrayListであることがわかります。これは、ビューが膨らんだ後にビューに対して何が行われるかを表しています。たとえば、ビューが作成された後、階層の一部である各TextViewのある時点でsetText()が呼び出され、適切な文字列が割り当てられます。私たちが行うことは、このアクションのリストへのアクセス権を取得し、それを反復処理することです。アクションは次のように定義されます。

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

RemoteViewsで定義されているActionの具体的なサブクラスがいくつかあります。対象となるのはReflectionActionと呼ばれるもので、次のように定義されています。

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

このアクションは、ビューに値を割り当てるために使用されます。このクラスの単一のインスタンスの値は、おそらく{"setText"、10、 "content of textview"}です。したがって、私たちは「ReflectionAction」オブジェクトであるmActionsの要素にのみ関心があり、何らかの方法でテキストを割り当てます。特定の「アクション」が「ReflectionAction」であることは、常にパーセルから読み取られる最初の値であるアクション内の「TAG」フィールドを調べることで確認できます。 2のタグは、ReflectionActionオブジェクトを表します。

その後、値が書き込まれた順序に従って小包から値を読み取る必要があります(知りたい場合は、ソースリンクを参照してください)。 setText()で設定された文字列を見つけて、リストに保存します。 (通知時間も必要な場合に備えて、setTime()も含まれています。必要でない場合は、これらの行を安全に削除できます。)

私は通常、このような場合のリフレクションの使用に反対していますが、必要な場合があります。利用可能なアクティビティコンテキストがない限り、「標準」メソッドは正しく機能しないため、これが唯一のオプションです。

23
Jon C. Hammer

リフレクションを使用したくない場合は、別の方法があります。RemoteViewsオブジェクトにリストされている「アクション」のリストをトラバースする代わりに、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);

remoteView.getLayoutId()を使用して、膨張したビューが通知の1つに対応していることを確認してください。

次に、多かれ少なかれ標準のテキストビューのいくつかを取得することができます

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

私自身の目的(自分のパッケージによって投稿された通知をスパイすること)のために、localViewの下の階層全体をトラバースし、すべての既存のTextViewを収集することを選択しました。

11
Remi Peuvergne

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

あなたの質問に答えるには:これはあなたのケースではできないようです。以下にその理由を説明します。

「ユーザー補助イベントの主な目的は、AccessibilityServiceがユーザーに意味のあるフィードバックを提供するために十分な情報を公開することです。」あなたのような場合:

アクセシビリティサービスでは、イベントペイロードのコンテキスト情報よりも多くのコンテキスト情報が必要になる場合があります。このような場合、サービスは、ウィンドウのコンテンツの探索に使用できるAccessibilityNodeInfo(ビューステートのスナップショット)であるイベントソースを取得できます。 イベントのソース、つまりウィンドウコンテンツにアクセスするための特権は明示的に要求される必要があることに注意してください。AccessibilityEvent を参照)

Androidマニフェストファイルでサービスのメタデータを設定することにより、この特権を明示的に要求できます。

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

Xmlファイルは次のようになります。

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

イベントのソース(ウィンドウコンテンツ)にアクセスするための特権を明示的に要求し、このサービスが受信するイベントタイプ(accessibilityEventTypesを使用)を指定します(この場合はtypeNotificationStateChangedのみ)。 xmlファイルで設定できるその他のオプションについては、 AccessibilityService を参照してください。

通常(この場合は以下の理由を参照してください)、event.getSource()を呼び出してAccessibilityNodeInfoを取得し、ウィンドウをトラバースできるはずです「アクセシビリティイベントはビューツリーの最上位のビューによって送信される」からです。

それを今すぐ機能させることは可能であるように思われますが、 AccessibilityEvent のドキュメントをさらに読むと、次のことがわかります。

ユーザー補助機能サービスがウィンドウコンテンツの取得を要求していない場合、イベントにはそのソースへの参照が含まれません。 タイプTYPE_NOTIFICATION_STATE_CHANGEDのイベントの場合も、ソースはnever使用できません。

どうやら、これはセキュリティ上の目的によるものです...


Notificationまたはnotification.contentView/notification.contentIntentから通知のメッセージを抽出する方法をフックする。できないと思います。

ContentViewは RemoteView であり、通知に関する情報を取得するゲッターを提供しません。

同様に、contentIntentは PendingIntent であり、通知がクリックされたときに起動されるインテントに関する情報を取得するゲッターを提供しません。 (たとえば、インテントからエキストラを取得することはできません)。

さらに、通知の説明を取得する理由とそれをどうしたいのかについての情報を提供していないため、これを解決するための解決策を実際に提供することはできません。

5
dennisg

Android 4.2.2でTomTascheのソリューションを試した場合、それが機能しないことに気づくでしょう。彼の答えとuser1060919のコメントを展開すると... 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

Achepの AcDisplay プロジェクトがソリューションを提供しています。 Extractor.Java のコードを確認してください

1
shaobin0604

追加情報については、Notificationオブジェクトのプライベートフィールドを確認することもできます。
同様contentTitle、contentTextおよびtickerText

ここに関連するコードがあります

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

これが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