はじめに

先日行われた DoroidKaigi のセッションの一つとして、 ActivityRecognition を取り扱ったセッションがありました。

複数センサーシミュレーションによる行動認識 (ActivityRecognition)解剖、そして実用化 20180209 DroidKaigi2018 ActivityRecognition simulation

Activity Recognition API はデバイスで使用可能なセンサーを元に、デバイスの現在の状態を提供してくれるものです。

これまで ActivityRecognition を扱うといった機会はなかったのですが、内容として興味が持てるものであったこと、また DoroidKaigi で話を聞いただけでは実装のイメージが湧かなかった部分があったため、簡単なサンプルを作成して確認してみました。

今回作るもの

画面には Activity Recognition API を通じて取得した端末の状態を表示させます。

また実機での加速度センサーの情報も記録と表示を行い、その内容を元にエミュレータ上で ActivityRecognition の状態が再現されるかを試してみます。

サンプルとして作成するアプリではシンプルな構成とし、 Activity を継承したクラスと IntentService を継承したクラスを用意します。

  • MainActivity (Activity 継承クラス)
    • 加速度センサー
    • Activity Recognition の取得内容をブロードキャストから受け取る
    • ファイルの書き出し
  • ActivityDetectionService (IntentService 継承クラス)
    • ActivityRecognitionResult の結果を受け、 Activity Recognition の取得内容をブロードキャストする

実装

Google Play Service の準備

まず ActivityRecognition は Google Play Service で提供されているものであるため、 app の build.gradle での設定を行います。

implementation 'com.google.android.gms:play-services-location:11.2.0'

※ エミュレータで扱うバージョンに合わせてください。

また、 Androidmanifest.xml の uses-permission と meta-data に下記を追加します。

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

ファイル書き込み用のパーミッションを追加

今回ファイルの書き込みを行うため、 WRITEEXTERNALSTORAGE の uses-permission を Androidmanifest.xml に追加を行います。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

application 内に service の内容を追加

Androidmanifest.xml に service として ActivityDetectionService を設定します。

<service
  android:name=".ActivityDetectionService"
  android:label="@string/app_name"
  android:exported="false" >
</service>

ActivityDetectionService(IntentService)

IntentService を継承した ActivityDetectionService を用意します。

Override した onHandleIntent メソッド内で ActivityRecognitionResult を生成します。

1番可能性が高いものを DetectedActivity として取得し、その内容を Intent 内に納め、 sendBroadcast します。

@Override
protected void onHandleIntent(@Nullable Intent intent) {
  ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent);
  DetectedActivity mostProbableActivity = result.getMostProbableActivity();
  Intent notifyIntent = new Intent(getResources().getString(R.string.notify_intent_action));
  notifyIntent.setPackage(getPackageName());
  notifyIntent.putExtra(getResources().getString(R.string.notify_activity_type), mostProbableActivity.getType());
  notifyIntent.putExtra(getResources().getString(R.string.notify_confidence), mostProbableActivity.getConfidence());
  sendBroadcast(notifyIntent);
}

MainActivity(Activity) 

今回はファイルの書き込みを行うため、まず WRITEEXTERNALSTORAGE のパーミッションチェックの実装を用意します。

if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
  ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
}

ブロードキャストを受け取る ActivityDetectionReceiver(BroadcastReceiver)をインナークラスとして用意します。 Override を行なった onReceive 内で取得した内容を表示する実装を行います。

private class ActivityDetectionReceiver extends BroadcastReceiver  {
  @Override
  public void onReceive(Context context, Intent intent) {
    final int activityType = intent.getIntExtra(getResources().getString(R.string.notify_activity_type), -1);
    final int confidence = intent.getIntExtra(getResources().getString(R.string.notify_confidence), -1);

    MainActivity.this.runOnUiThread(new Runnable() {
      @Override
      public void run() {
        mActivityTypeTextView.setText(String.format(getResources().getString(R.string.activity_type), getNameFromType(activityType)));
        mConfidenceTextView.setText(String.format(getResources().getString(R.string.activity_confidence), confidence));
      }
    });
  }

  private String getNameFromType(int activityType) {
    switch(activityType) {
      case DetectedActivity.IN_VEHICLE:
        return getResources().getString(R.string.in_vehicle);
      case DetectedActivity.ON_BICYCLE:
        return getResources().getString(R.string.on_bicycle);
      case DetectedActivity.ON_FOOT:
        return getResources().getString(R.string.on_foot);
      case DetectedActivity.STILL:
        return getResources().getString(R.string.still);
      case DetectedActivity.UNKNOWN:
        return getResources().getString(R.string.unknown);
      case DetectedActivity.TILTING:
        return getResources().getString(R.string.tilting);
      case DetectedActivity.WALKING:
        return getResources().getString(R.string.walking);
      case DetectedActivity.RUNNING:
        return getResources().getString(R.string.running);
    }
    return getResources().getString(R.string.unknown) + getResources().getString(R.string.with_space_hyphen) + activityType;
  }
}

このように用意した ActivityDetectionReceiver を onCreate などで生成し、 registerReceiver を行います。

また ActivityDetectionService を元とした PendingIntent を作ります。

mActivityDetectionReceiver = new ActivityDetectionReceiver();
registerReceiver(mActivityDetectionReceiver, new IntentFilter(getResources().getString(R.string.notify_intent_action)));
ActivityRecognitionClient client = ActivityRecognition.getClient(getApplicationContext());
Intent intent = new Intent(getApplicationContext(), ActivityDetectionService.class);
PendingIntent pendingIntent = PendingIntent.getService(
  getApplicationContext(),
  0,
  intent,
  PendingIntent.FLAG_UPDATE_CURRENT);
  Task<Void> task = client.requestActivityUpdates(0, pendingIntent);
task.addOnSuccessListener(new OnSuccessListener<Void>() {
  @Override
  public void onSuccess(Void result) {}
});

task.addOnFailureListener(new OnFailureListener() {
  @Override
  public void onFailure(@NonNull Exception e) {}
});

これで、 ActivityRecognition の内容がどのようになっているか確認できるようになったかと思います。

加速度センサーの内容を取得、表示

MainActivity の implements に SensorEventListener を追加します。 SensorManager を取得し registerListener を行なった上で onSensorChanged を用意します。 今回は加速度を表すデータをインナークラスとして用意して扱うことにし、センサー更新の度に List に格納を行なって行きます。

エミュレータで再現されるかを試す

加速度センサーのデータをエミュレータに流し込み、エミュレータ上で ActivityRecognition の内容が反映されるかを確認します。

エミュレータをコマンドラインから操作する

まず emulator Console に telnet で接続してみます。 ※ 接続方法は、自身の環境に合わせてください

$ telnet localhost 5554

「 Authentication required 」といった内容が出てきますが、 auth_token の場所も表示されているので、そちらの内容を確認し、

auth <auth_token の内容>

を行うことで認証を完了させます。

help コマンドを実行することでどのような内容が扱えるかを確認できます。

今回は加速度センサーの値を変えて反映させて行きたいため、

sensor set acceleration 0.000000:0.000000:0.000000

このような形で実際の加速度を入れていくことになります。

コマンド自動化

アプリ側で List に格納した加速度の一連の情報を.sh に書き出して行きます。

格納していく内容として、まず先ほどの認証の内容を入れておきます。

auth_token=$(cat ~/.emulator_console_auth_token)
sleep 1
echo auth $auth_token

telnet の接続完了を待つため、「 sleep 1」を入れておきます。

その後はこのような内容を追加して行きます。

sleep <前回取得からの経過時間>
echo sensor set acceleration <sensorEvent.values[0]の値>:<sensorEvent.values[1]の値>:<sensorEvent.values[2]の値>

上記の内容を実機で sh ファイルとして作成し、エミュレータで起動を行なった上で試してみます。

$ sh <用意した.sh のパス> | telnet localhost 5554

先ほどまで実端末で表示されていた内容がエミュレータ上でも反映され、 ActivityRecognition の内容も更新されることが確認できました。

終わりに

今回は ActivityRecognition に触れてみて、また Android の emulator Console から再現させるということを行なってみました。

色々な使用ケースにおいての精度や調整などまではやれていませんが、少し試した感じでは歩行や停止状態などは取れており、精度が上がれば面白いサービスの開発に活かせると思いました。

また、エミュレータの状態を自動化にするというものも、 AR 、 VR などが盛んになっている昨今では端末を360度変更する際の挙動テストなどで使える内容であるのではと感じました。

今回使ったサンプルはこちらです。

https://github.com/gaprot/ActivityRecognitionTest

※ 今回は ActivityRecognition の内容を確認する目的の実装のため、異常系の処理などは省略しています。また動作確認の端末は Galaxy S8 Android7.0で行なっています。