android.accessibilityservice.AccessibilityServiceを使用してNotificationを取得する

2012-10-14T00:00:00+00:00 Android Java

ちょっと前にDeskNotifierっていうスマフォの通知をPCにプッシュするというアプリがあったのを見て思ったのですけど、(Android)スマフォのNotificationって監視して取得したり出来る物なのかっていう所が不明だったので色々と調査

で調査結果としてAccessibilityServiceを使う事で可能な模様、という事でやってみた。まぁ詳しい事は公式ドキュメントに書いてあるんですけど

AndroidManifest.xml

<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="sample.test"
    android:versionCode="1"
    android:versionName="1.0">

    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="16" />

    <application
        android:label="@string/app_name"
        android:icon="@drawable/ic_launcher"
        android:theme="@style/AppTheme">

        <activity android:name=".MainActivity" android:label="sample" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".SampleAccessibilityService"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">

            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>

            <!-- AccessibilityService関係の設定リソースを設定 -->
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_service" />

        </service>

    </application>
</manifest>

Activityはテスト名目でただNotificationを送るだけのActivity。でAccessibilityServiceを継承したクラスに<service>に設定しておく。んでそのサービスの設定が必要になるのでそれを<meta-data>で設定しておく

res/xml/accessibility_service.xml

<?xml version="1.0" encoding="UTF-8" ?>
<accessibility-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeNotificationStateChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true" />

こいつも公式ドキュメントに書いてあるので詳しい事は省略しますが、android:packageNamesを定義すると対象とするパッケージを限定出来る模様。今回それを定義していないのでグローバルになる模様(確証は無い)

でこれを設定してインストールした場合にAndroid端末の設定画面に

というようにユーザー補助な所にサービスとして表示されるようになる。デフォルトではOFFになっているのでこれを使う場合はONにしておく

MainActivity.java

package sample.test;

import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.RemoteViews;

public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);

        final NotificationManager nm = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);

        Button btn = new Button(this);
        btn.setText("start");
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                RemoteViews rv = new RemoteViews(getPackageName(), R.layout.notify);
                rv.setTextViewText(android.R.id.title, "hoge fuga foobar");

                Notification.Builder builder = new Notification.Builder(v.getContext());
                builder.setTicker("ticker");
                builder.setContent(rv);
                builder.setSmallIcon(R.drawable.ic_launcher);

                /*
                 * 今回、カスタムなViewを使うのでここらへんは省略
                builder.setContentTitle("ABC");
                */

                Notification n = builder.build();

                nm.notify(1, n);
            }
        });

        setContentView(btn);
    }
}

恐らくはデフォルトのNotificationのレイアウト構造自体はandroid.R.layout.notification_template_base(/path/to/aosp/framework/base/core/res/res/layout/notification_template_base.xml)だと思われる

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2012 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
    android:background="@android:drawable/notification_bg"
    android:id="@+id/status_bar_latest_event_content"
    android:layout_width="match_parent"
    android:layout_height="64dp"
    internal:layout_minHeight="64dp"
    internal:layout_maxHeight="64dp"
    >
    <ImageView android:id="@+id/icon"
        android:layout_width="@dimen/notification_large_icon_width"
        android:layout_height="@dimen/notification_large_icon_height"
        android:background="@android:drawable/notification_template_icon_bg"
        android:scaleType="center"
        />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="fill_vertical"
        android:layout_marginStart="@dimen/notification_large_icon_width"
        android:minHeight="@dimen/notification_large_icon_height"
        android:orientation="vertical"
        android:paddingEnd="8dp"
        android:paddingTop="2dp"
        android:paddingBottom="2dp"
        android:gravity="top"
        >
        <LinearLayout
            android:id="@+id/line1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="6dp"
            android:layout_marginStart="8dp"
            android:orientation="horizontal"
            >
            <TextView android:id="@+id/title"
                android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:singleLine="true"
                android:ellipsize="marquee"
                android:fadingEdge="horizontal"
                android:layout_weight="1"
                />
            <ViewStub android:id="@+id/time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="0"
                android:visibility="gone"
                android:layout="@layout/notification_template_part_time"
                />
            <ViewStub android:id="@+id/chronometer"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="0"
                android:visibility="gone"
                android:layout="@layout/notification_template_part_chronometer"
                />
        </LinearLayout>
        <TextView android:id="@+id/text2"
            android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Line2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="-2dp"
            android:layout_marginBottom="-2dp"
            android:layout_marginStart="8dp"
            android:singleLine="true"
            android:fadingEdge="horizontal"
            android:ellipsize="marquee"
            android:visibility="gone"
            />
        <ProgressBar
            android:id="@android:id/progress"
            android:layout_width="match_parent"
            android:layout_height="12dp"
            android:layout_marginStart="8dp"
            android:visibility="gone"
            style="?android:attr/progressBarStyleHorizontal"
            />
        <LinearLayout
            android:id="@+id/line3"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center_vertical"
            android:layout_marginStart="8dp"
            >
            <TextView android:id="@+id/text"
                android:textAppearance="@style/TextAppearance.StatusBar.EventContent"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:layout_gravity="center"
                android:singleLine="true"
                android:ellipsize="marquee"
                android:fadingEdge="horizontal"
                />
            <TextView android:id="@+id/info"
                android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Info"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_weight="0"
                android:singleLine="true"
                android:gravity="center"
                android:paddingStart="8dp"
                />
            <ImageView android:id="@+id/right_icon"
                android:layout_width="16dp"
                android:layout_height="16dp"
                android:layout_gravity="center"
                android:layout_weight="0"
                android:layout_marginStart="8dp"
                android:scaleType="centerInside"
                android:visibility="gone"
                android:drawableAlpha="153"
                />
        </LinearLayout>
    </LinearLayout>
</FrameLayout>

これでR.id.titleが定義されているので、パッケージを含めるとandroid.R.id.titleになるんだと思われる。なのでカスタムViewのレイアウトでもandroid:idな部分はandroidプレフィックスで指定する事でカスタムViewを使った場合にでもグローバルと差を無くして取得出来る(はず。これも確証無し)

res/layout/notify.xml

<?xml version="1.0" encoding="UTF-8" ?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@android:id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</FrameLayout>

上で言った通りなので詳しい事は省略。だけど、AndroidコアのNotificationのレイアウトだとFrameLayoutになってた気がするので、レイアウトベースはFrameLayoutを使用する

SampleAccessibilityService.java

package sample.test;

import android.accessibilityservice.AccessibilityService;
import android.app.Notification;
import android.util.Log;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.widget.RemoteViews;
import android.widget.TextView;
import android.widget.Toast;

public class SampleAccessibilityService extends AccessibilityService {

    private static final String TAG = "SampleAccessibilityService";

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (!(event.getClassName().toString().startsWith("android.widget.Toast"))) {
            for (CharSequence s : event.getText()) {
                Toast.makeText(this, "text: " + s, Toast.LENGTH_LONG).show();
            }
        }

        Notification n = (Notification)event.getParcelableData();

        if (n == null) {
            return;
        }

        RemoteViews rv = n.contentView;
        View view = rv.apply(this, null);

        if (view == null) {
            return;
        }

        TextView textView = (TextView)view.findViewById(android.R.id.title);

        if (textView == null) {
            return;
        }

        Log.v(TAG, "text: " + textView.getText());
    }

    @Override
    public void onInterrupt() {
    }
}

AccessibilityServiceを継承したクラスを作る。で検証はしてないんだけど、onServiceConnectedをオーバーライドして、setServiceInfo(AccessibilityServiceInfo)を設定する事で、上記で設定したaccessibility_service.xmlを使わずにサービスクラス側でコントロールする事も可能な模様(ってどっかのサイトに書いてた)

それとToastもNotificationとして認識されるのでそこ注意しておく。でToastの実体?っぽいのがandroid.widget.Toast$TNってなってるのでstartsWithでそこら辺チェックしておく。あとは取得されたNotificationからRemoteViewsを取得してそこのビューを取得してandroid.R.id.titleなUIコンポーネントにアクセスしてテキストを取得する事が可能な模様

ちなみにaccessibility_service.xmlのaccessibilityEventTypesによってはAccessibilityEventで取得出来る情報は限られている模様。そこも公式ドキュメントにしっかり書かれているので参照

公式ドキュメント: http://developer.android.com/reference/android/accessibilityservice/AccessibilityService.html

追記

Android4.3からはNotificationListenerServiceを用いる事で容易に実現可能っぽい。既に検証済み

追記2

古いAndroidのバージョンに関してはAccessibilityServiceを古いAndroidバージョンで動かすにて補足済み

追記3

追記2のリンク先にも書いてますが

PagerTitle(Tab)Stripを使ってみる SAStrutsを勉強してみる (9) - UserTransaction -