Android x Google Play Services #5 Location API でユーザーの行動を取得する

2013.06.25

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Activity Recognition とは

今回は Location API の Activity Recognition という機能を使ってみたいと思います。Activity Recognition はユーザーの行動を取得する API で、徒歩で移動中か、自転車で移動中かといったような「ユーザーが何をしているか」ということが把握できます。
以下のような特徴があります。

省電力に最適化

ユーザーの行動を認識するために省電力のセンサーを使用しているので、バッテリー消費が少ないです。

他サービスの強化に有効

ユーザーの動きが把握できるので、他のサービスを強化するのに有効です。例えばナビゲーションアプリに搭載する場合、車で移動中のときは頻繁に位置情報を更新し、徒歩で移動中のときは更新頻度を抑える、といった位置情報の取得頻度を調整したりすることに活用できます。

高度なアプリのための機能

Activity Recognition の取得結果には「不明」や「デバイスが傾き中」といった状態も含まれています。これは「正確に測定できない」状態ということを意味しているので、そのときは重大な処理を行わないといったような細かいカスタマイズに活用することができます。

Activity Recognition を試してみる

2014/06/11 追記
本稿で解説している実装のサンプルコードを GitHub で公開しました!ぜひ参考にしていただければと思います。
suwa-yuki/ActivityReconizer

ということで Activity Recognition を試してみたいと思います。今回はユーザーの行動を Notification で通知するアプリを作ってみます!
まず初めに AndroidManifest.xmlACTIVITY_RECOGNITION というパーミッションを追加します。ACCESS_FINE_LOCATION不要です。

<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION"/>

2014/06/11 追記
Google Play Services SDK の最新版では、AndroidManifest に Google Play Services のバージョンを記述する必要があります。この記述がないと例外が発生しますのでご注意ください。

<application>
    ...
    <meta-data
        android:name="com.google.android.gms.version"
        android:value="@integer/google_play_services_version" />
    ....
</application>

次に Activity の実装です。今回は ActivityRecognitionClient というクラスを使います。コンストラクタ引数は LocationClient と全く一緒です。

private final RecognitionActivity self = this;
private ActivityRecognitionClient mClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_recognition);
    // ActivityRecognitionClient の生成
    mClient = new ActivityRecognitionClient(self, mConnectionCallbacks, mOnConnectionFailedListener);
}

今回はレイアウトにトグルボタンを置いて ON になったときに connect()、 OFF になったときに disconnect() するようにし、 ConnectionCallbacks.onConnected() で Activity Recognition の API を叩くようにします。

private GooglePlayServicesClient.ConnectionCallbacks mConnectionCallbacks = new GooglePlayServicesClient.ConnectionCallbacks() {
    @Override
    public void onConnected(Bundle bundle) {
        Toast.makeText(self, "onConnected", Toast.LENGTH_LONG).show();
        getActivityRecognition();
    }
    @Override
    public void onDisconnected() {
        Toast.makeText(self, "onDisconnected", Toast.LENGTH_LONG).show();
    }
};

getActivityRecognition() は以下のようにします。

private void getActivityRecognition() {
    // PendingIntent の生成
    Intent intent = new Intent(self, RecognitionIntentService.class);
    PendingIntent pendingIntent = PendingIntent.getService(self, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    mClient.requestActivityUpdates(1000, pendingIntent);
}

処理結果の取得は Geofencing と同様 PendingIntent を使います。前回は Activity に返していましたが、今回は IntentService に返してみます。RecognitionIntentService は以下のように実装します。

RecognitionIntentService.java

package jp.classmethod.android.sample.locationapi;

import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v4.app.NotificationCompat;

import com.google.android.gms.location.ActivityRecognitionResult;
import com.google.android.gms.location.DetectedActivity;

public class RecognitionIntentService extends IntentService {

    private final RecognitionIntentService self = this;

    public RecognitionIntentService() {
        super("RecognitionIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        if (ActivityRecognitionResult.hasResult(intent)) {
            ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent);
            DetectedActivity mostProbableActivity = result.getMostProbableActivity();
            int confidence = mostProbableActivity.getConfidence();
            int activityType = mostProbableActivity.getType();
            notification(getTypeName(activityType));
        }
    }

    private void notification(String message) {
        // Intent の作成
        Intent intent = new Intent(self, RecognitionActivity.class);
        PendingIntent contentIntent = PendingIntent.getActivity(self, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        Bitmap largeIcon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
        // NotificationBuilderを作成
        NotificationCompat.Builder builder = new NotificationCompat.Builder(self);
        builder.setContentIntent(contentIntent);
        builder.setTicker(message);
        builder.setSmallIcon(R.drawable.ic_launcher);
        builder.setContentTitle("ActivityRecognition");
        builder.setContentText(message);
        builder.setLargeIcon(largeIcon);
        builder.setWhen(System.currentTimeMillis());
        builder.setAutoCancel(true);
        // Notificationを作成して通知
        NotificationManager manager = (NotificationManager) getSystemService(Service.NOTIFICATION_SERVICE);
        manager.notify(0, builder.build());
    }

    /**
     * activity type をテキストに変換する.
     * @param activityType activity type
     * @return activity type のテキスト
     */
    private String getTypeName(int activityType) {
        switch(activityType) {
            case DetectedActivity.IN_VEHICLE:
                return "車で移動中";
            case DetectedActivity.ON_BICYCLE:
                return "自転車で移動中";
            case DetectedActivity.ON_FOOT:
                return "徒歩で移動中";
            case DetectedActivity.STILL:
                return "待機中";
            case DetectedActivity.UNKNOWN:
                return "不明";
            case DetectedActivity.TILTING:
                return "デバイスが傾き中";
        }
        return null;
    }
}

重要なのは25行〜31行の onHandleIntent() メソッド内の実装です。ここで Intent のインスタンスを元に ActivityRecognitionResult を取得し、ユーザーの行動のデータを取得しています。あとは行動の種類 (int) を getTypeName() メソッドのような感じで判別します。今回は String に変換して Notification で通知するようにしました。
あと、この IntentService クラスを AndroidManifest.xml に忘れずに登録しましょう。

<service
    android:name=".RecognitionIntentService"
    android:exported="false">
</service>

以上で実装完了です!最後に Activity の全ソースを載せておきます。

RecognitionActivity.java

package jp.classmethod.android.sample.locationapi;

import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient;
import com.google.android.gms.location.ActivityRecognitionClient;
import com.google.android.gms.location.ActivityRecognitionResult;
import com.google.android.gms.location.DetectedActivity;

public class RecognitionActivity extends FragmentActivity {

    private final RecognitionActivity self = this;
    private ActivityRecognitionClient mClient;
    private ToggleButton toggleButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recognition);

        // ActivityRecognitionClient の生成
        mClient = new ActivityRecognitionClient(self, mConnectionCallbacks, mOnConnectionFailedListener);

        toggleButton = (ToggleButton) findViewById(R.id.toggle);
        toggleButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
                if (b) {
                    mClient.connect();
                } else {
                    mClient.disconnect();
                }
            }
        });
    }
    
    /**
     * Activity Recognition を取得する.
     */
    private void getActivityRecognition() {
        // PendingIntent の生成
        Intent intent = new Intent(self, RecognitionIntentService.class);
        PendingIntent pendingIntent = PendingIntent.getService(self, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        mClient.requestActivityUpdates(1000, pendingIntent);
    }
    
    /**
     * 接続時・切断時のコールバック.
     */
    private GooglePlayServicesClient.ConnectionCallbacks mConnectionCallbacks = new GooglePlayServicesClient.ConnectionCallbacks() {
        @Override
        public void onConnected(Bundle bundle) {
            Toast.makeText(self, "onConnected", Toast.LENGTH_LONG).show();
            getActivityRecognition();
        }
        @Override
        public void onDisconnected() {
            Toast.makeText(self, "onDisconnected", Toast.LENGTH_LONG).show();
        }
    };
    
    /**
     * 接続失敗時のコールバック.
     */
    private GooglePlayServicesClient.OnConnectionFailedListener mOnConnectionFailedListener = new GooglePlayServicesClient.OnConnectionFailedListener() {
        @Override
        public void onConnectionFailed(ConnectionResult connectionResult) {
            Toast.makeText(self, "onConnectionFailed", Toast.LENGTH_LONG).show();
        }
    };  
}

では実行してみましょう。とりあえずデバッグ開始時はもちろん「待機中」です。

recognition01

デバイスを持つと「デバイスが傾き中」になりました。そりゃそうですね。この状態だとユーザーの行動が正確にとれないようです。

recognition02

外に持って歩いてみたら「徒歩で移動中」になりました!(ちなみに AWS 認定試験会場に向かってるときに試してみました)

recognition03

電車は…あまり動かさないようにしておくと「車で移動中」になります。ですがちょっと傾けたり操作したりしてると「不明」になっちゃいます。

recognition05

recognition04

まとめ

ということで Activity Recognition を試してみました。ユーザーが何をしているかわかるというのはかなり画期的ですし、いろいろな使い道がありそうですね。この記事を参考にぜひ使ってみてください。

参考