Google I/O 2013 で発表された行動認識(Activity Recognition)を使ってみる

Google I/O で発表された Android の行動認識(動作認識)機能ですが、これは Google Play Services で提供されているので、新しい API Ver でなくても(Froyo でも!)使えます、すばらしい!

というわけで、早速使ってみました。

1. SDK の Google Play Services を更新する

image1

SDK Manager で、Google Play service を最新に更新します。 私は勢いで Android SDK Tools なども最新にしてしまいましたが、これが必要だったかは定かでないです。また SDK Tools を更新したら Eclipse のプラグインも更新する必要がありました。

2. Eclipse でプロジェクトを作る

Android Studio は使ってません(まだよくわからないので) Eclipse で、いつもどおりに Android のプロジェクトを作ります。 Fragment も使いませんよ、古きよき、BlankActivity なプロジェクトです。 名前はここでは ActivityRecognizingSample とします。

3. プロジェクトに google-play-services_lib を追加する

Google Play services を使うために、SDK のフォルダにある google-play-services_lib が必要です。

Ecplise の Import で {your sdk location}/extras/google/google_play_services/libproject/google-play-services_lib を選択します。自分の Workspace に Copy しておいた方が無難でしょう。

コピーしたら、ActivityRecognizingSample で、 google-play-services_lib をライブラリ参照します。

image2

次から ActivityRecognizingSample の実装です。

4. IntentService クラスの用意

行動認識結果は IntentService で受け取ります。そのためのクラス ReceiveRecognitionIntentService を作成します。

//ReceiveRecognitionIntentService.java
package com.example.activityrecognizingsample;

import android.app.IntentService;

public class ReceiveRecognitionIntentService extends IntentService {

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

	@Override
	protected void onHandleIntent(Intent intent) {
        // まだ未実装
	}
}

5. AndroidManifest.xml の編集

行動認識を使うための権限 com.google.android.gms.permission.ACTIVITY_RECOGNITION をマニフェストに追加します。 あと、ReceiveRecognitionIntentService も忘れずに追加しておきます。 細かいところでは、MainActivity の画面の向きを縦(Portrait)に固定しておきます。試している時に画面の向きが変わると面倒なので。

//AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.activityrecognizingsample"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />
    <uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION"/>

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" 
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <service 
            android:name=".ReceiveRecognitionIntentService"   
            android:label="@string/app_name"
    		android:exported="false" />
    </application>

</manifest>

6. 行動認識クラス(ActivityRecognitionClient)を使う

ここから、一気に行きます。

行動認識には ActivityRecognitionClient を使います。

大雑把な使い方は、

  1. インスタンスを生成する
  2. connect を呼ぶ -> ConnectionCallbacks.onConnected がコールバックされる
  3. requestActivityUpdatesReceiveRecognitionIntentService を仕掛けて、呼ぶ。 -> ReceiveRecognitionIntentService.onHandleIntent が呼ばれる
  4. onHandleIntentActivityRecognitionResult にて結果を取得する

です。

その後、認識結果を画面に表示するために、

  1. 予め、MainActivityBroadcastReceiver を仕掛けておく
  2. ReceiveRecognitionIntentService.onHandleIntent で取得した認識結果を、Broadcast する。

とします。

では全コードをどうぞ。

//MainActivity.jara
package com.example.activityrecognizingsample;

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

import android.os.Bundle;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.text.format.DateFormat;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity {
    private ActivityRecognitionClient _recClient; // 行動認識のメインクラス
	private TextView _textResult; // 認識結果を表示するところ
	
	// 認識結果は PendingIntent で通知してくれる
	//  PendingIntent に、Service を起動する Intent を仕込んでおいて、
	//  認識結果の取得はそっちで行う。 > ReceiveRecognitionIntentService.java
    private PendingIntent _receiveRecognitionIntent; 
    
    // ReceiveRecognitionIntentService で取得した認識結果は、Broadcast で通知されるので、
    // それを受け取る Receiver 。ここで画面に認識結果を表示する。
	private final BroadcastReceiver _receiveFromIntentService = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			final int activityType = intent.getIntExtra("activity_type", 0);
			final int confidence = intent.getIntExtra("confidence", -1);
			final long time = intent.getLongExtra("time", 0);
			
			MainActivity.this.runOnUiThread(new Runnable() {
				@Override
				public void run() {
					String text = _textResult.getText().toString();
					text = DateFormat.format("hh:mm:ss.sss", time) + " - " 
							+ getNameFromType(activityType) + "(" +
							+ confidence + ")" + "\n" + text;
					
					_textResult.setText(text);
				}
			});
		}
		
		// http://developer.android.com/training/location/activity-recognition.html
		// からパクってきた関数
		 /**
	     * Map detected activity types to strings
	     *@param activityType The detected activity type
	     *@return A user-readable name for the type
	     */
	    private String getNameFromType(int activityType) {
	        switch(activityType) {
	            case DetectedActivity.IN_VEHICLE:
	                return "in_vehicle";
	            case DetectedActivity.ON_BICYCLE:
	                return "on_bicycle";
	            case DetectedActivity.ON_FOOT:
	                return "on_foot";
	            case DetectedActivity.STILL:
	                return "still";
	            case DetectedActivity.UNKNOWN:
	                return "unknown";
	            case DetectedActivity.TILTING:
	                return "tilting";
	        }
	        return "unknown - " + activityType;
	    }
	};
	
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        _textResult = (TextView)findViewById(R.id.text_results);
        
        // IntentService から Broadcast される認識結果を受け取るための Receiver を登録しておく
        registerReceiver(_receiveFromIntentService, new IntentFilter("receive_recognition"));
        
        final Button buttonStart = (Button)findViewById(R.id.button_start);
        buttonStart.setOnClickListener(new OnClickListener() {
        	private boolean _isStarted = false;
        	
			@Override
			public void onClick(View v) {
				
				if (!_isStarted) {
					startReckoning();
					buttonStart.setText("Stop");
				} else {
					stopReckoning();
					buttonStart.setText("Start");
				}
				
				_isStarted = !_isStarted;
			}
		});
    }
    
    @Override
    protected void onDestroy() {
    	stopReckoning();
		// ConnectionCallbacks.onDisconnected が呼ばれるまで待った方がいい気がする
    	unregisterReceiver(_receiveFromIntentService);
    	super.onDestroy();
    }
    
    private void startReckoning() {
    	_recClient = new ActivityRecognitionClient(this, new ConnectionCallbacks() {
			
			@Override
			public void onConnected(Bundle bundle) {
				Intent intent = new Intent(
		                MainActivity.this, ReceiveRecognitionIntentService.class);
				_receiveRecognitionIntent = PendingIntent.getService(
						MainActivity.this, 0, intent,
		                PendingIntent.FLAG_UPDATE_CURRENT);
				
				// 2. 行動認識開始!
				//  1秒間隔で認識間隔を通知。
				//  認識したら ReceiveRecognitionIntentService が呼び出されるようにしている。
				_recClient.requestActivityUpdates(1000, _receiveRecognitionIntent);
			}

			@Override
			public void onDisconnected() {
				_recClient = null; // NOTE disconnect してもここにこないよ?
			}

    	}, new OnConnectionFailedListener() {
			@Override
			public void onConnectionFailed(ConnectionResult result) {
				// 接続でエラーが発生したらここにくるらしい
			}
		});
    	
    	// 1. 行動認識サービスに接続!
    	_recClient.connect();
    }
    
    private void stopReckoning() {
    	if (_recClient == null || !_recClient.isConnected()) {
    		return;
    	}
    	
    	_recClient.removeActivityUpdates(_receiveRecognitionIntent);
		_recClient.disconnect(); 
		// ConnectionCallbacks.onDisconnected が呼ばれるまで待った方がいい気がする
    }
    
    
}
//ReceiveRecognitionIntentService.java
package com.example.activityrecognizingsample;

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

import android.app.IntentService;
import android.content.Intent;
import android.text.format.DateFormat;
import android.util.Log;

/**
 * 行動認識結果を取得するための IntentService 
 * 
 * ActivityRecognitionClient.requestActivityUpdates に仕込んでおくと
 * 認識結果を受信する度にこれが呼ばれる。
 * 
 */
public class ReceiveRecognitionIntentService extends IntentService {
	private static final String TAG = "ReceiveRecognitionIntentService";

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

	@Override
	protected void onHandleIntent(Intent intent) {
		if (!ActivityRecognitionResult.hasResult(intent)) {
			// 行動認識結果持ってないよ
			return;
		}
		
		// 認識結果を取得する
		ActivityRecognitionResult result = 
				ActivityRecognitionResult.extractResult(intent);
		
		DetectedActivity mostProbableActivity = result.getMostProbableActivity();
		int activityType = mostProbableActivity.getType();
		int confidence = mostProbableActivity.getConfidence();
		
		Log.d(TAG, "Receive recognition.");
		Log.d(TAG, " activityType - " + activityType); // 行動タイプ
		Log.d(TAG, " confidence - " + confidence); // 確実性(精度みたいな)
		Log.d(TAG, " time - " + DateFormat.format("hh:mm:ss.sss", result.getTime())); // 時間
		Log.d(TAG, " elapsedTime - " + DateFormat.format("hh:mm:ss.sss", result.getElapsedRealtimeMillis())); // よく分からん

		// 画面に結果を表示するために、Broadcast で通知。
		//  MainActivity にしかけた BroadcastReceiver で受信する。
		Intent notifyIntent = new Intent("receive_recognition");
		notifyIntent.setPackage(getPackageName());
		notifyIntent.putExtra("activity_type", activityType);
		notifyIntent.putExtra("confidence", confidence);
		notifyIntent.putExtra("time", result.getTime());
		sendBroadcast(notifyIntent);
	}
}

7. 動くのか!?

HTC J(not蝶) で動かしてみました。

image3

on_foot ってのが「歩いてる」ってやつですね。 感覚としてはズボンの尻ポケに入れて、5mくらいは歩かないと認識されない感じ。結構 unknown が多いですね。

クルマは、ダッシュボードに放置して運転してみたもの。開始から1分経たずに認識できています。

自転車は、最初クルマと誤認したものの、その後認識しました。

ここでは教科書通りの使い方をしましたが、なかなか感動します。

2013.5.21追記 電車だとどうなる?

東京に出張する機会があったので、新幹線と在来線でも試してみました。

image4

新幹線は窓側の席だったので、窓の机に端末を放置していました。 在来線は座ることができなかったので、立った状態で端末は尻ポケ、カベに持たれてなるべく動かないようにしていました。

結果は、在来線ではなんとか vehicle と認識されましたが、新幹線では認識できませんでした。 恐らく、新幹線は揺れが少なすぎるのだと思います。優秀ですね、日本の新幹線。

2013.5.27 追記 位置情報を許可すると精度が上がる?

@sos_jp さんの記事、

によりますと、

位置情報があるとより正確な判定になるようです

とのことなので、もしかしたら FINE(+COURSE)_LOCATION とか付けとくと新幹線でも vechicle と認識デキるかもしれません。いずれ試してみます。

まとめ

公式のコンプリートな Getting Started は

にありますので、こちらを読まれた方が確実です。

ここで作ったサンプルは、

に置いておきます。