たるこすの日記

読者です 読者をやめる 読者になる 読者になる

たるこすの日記

リアルからバーチャルへ、バーチャルからリアルへ

「OK Google」音声コマンドを取得してTVの電源をON,OFFするAndroidアプリを作る

Android おうちハック

こんにちは、たるこすです。
今回はAndroidを使ったおうちハックネタです。

Android ユーザのみなさん、「OK Google」機能は使っていますか?
どの画面からでも、「OK Google」と話しかければ音声検索ができるので便利ですよね。

この音声検索の発言内容を自作アプリで取得できるということを知ったので、以前 Raspberry Pi で作ったTVコントローラと繋げてみます。
「OK Google TVつけて」と話しかけて、TV の電源をつけられるようにすることが目標です。

アプリの名前は、森博嗣さんの「すべてがFになる」に出てくるデボラにちなんで、deborahとしました。

アプリで音声コマンドを取得する

以下のページを参考にしました。

空のAndroidアプリを作ったら、以下の3つを行います。

  1. AccesibilityService を継承したクラスを作成
  2. AndroidManifest.xml を編集
  3. accessibility_service_config.xml を作成

1. AccesibilityService を継承したクラスを作成

新しくクラスを作成し、以下のように記入します。

package mydomain.deborah;

import android.accessibilityservice.AccessibilityService;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;

public class VoiceCommandService extends AccessibilityService {
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        AccessibilityNodeInfo accessibilityNodeInfo = event.getSource();
        if (null == accessibilityNodeInfo) {
            return;
        }

        String className = accessibilityNodeInfo.getClassName().toString();
        final CharSequence text = accessibilityNodeInfo.getText();
        if(className.indexOf("com.google.android.apps.gsa.searchplate") == -1 || text == null) {
            return;
        }

        Toast.makeText(getApplicationContext(), text.toString(), Toast.LENGTH_SHORT).show();
    }
    @Override
    public void onInterrupt() {
    }
}

2. AndroidManifest.xml を編集

AndroidManifest.xml を編集し、アプリが AccessiblityService のイベントを受け取れるようにします。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="mydomain.deborah">
    <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service
            android:name="mydomain.deborah.VoiceCommandService"
            android:label="@string/app_name"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_service_config" />
        </service>
    </application>

</manifest>

3. accessibility_service_config.xml を作成

accessibility service の設定ファイルです。
res ディレクトリの中に、xml ディレクトリを作成し、その中に accessibility_service_config.xml を作成します。
accessibility_service_config.xml の内容は以下のように書きます。

<?xml version="1.0" encoding="UTF-8" ?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:description="@string/accessibility_description"
    />

description で指定している@string/accessibility_description は res/values/strings.xml に以下のように書いておきます。

    <string name="accessibility_description">OK Google からの音声コマンドを取得します</string>

アプリを実行してみる

ここで、このアプリをAndroid端末で実行してみます。
うまくいけば、OK Google で発言した内容がトーストで表示されるはずです。

あれ? 出ない。

そういえば、アプリに対して AccessibilityService 機能の許可を忘れていました。

設定 > ユーザー補助 を開くと、サービス欄に作成したアプリの名前が出ています。
これをタップして開いた画面で、 OFF のスイッチを ON にします。

f:id:tarukosu:20161218153439p:plain:w300  f:id:tarukosu:20161218153436p:plain:w300

これで大丈夫だと思ったのですが、やはり反応はなし...

一旦諦めていたのですが、数日後端末を再起動したらトーストが表示されるようになっていました。
原因はよくわかりませんが動いたのでよしとします。

デバッグ実行してみたところ、onAccessibilityEvent は認識途中の文字についても呼ばれてしまうようです。
例えば、「テレビ消して」と発言したところ、以下のように計7回もonAccessibilityEventが呼ばれていました。

  • テレ
  • テレビ
  • テレビ家
  • テレビ消して
  • テレビ消して
  • テレビ消して
  • テレビ消して

これへの対処は以下のようにしました。

何度も呼ばれるonAccessibilityEventに対処する

上にも書きましたが、onAccessibilityEvent は認識途中の文字についても呼ばれてしまうようです。
認識途中のものか認識完了後のものかを判断できれば良いのですが、その方法はわかりませんでした。

そこで、イベントが呼ばれた時点ですぐに処理を行わず、Handlerを使って一定時間後に処理するよう設定します。 また、それ以前に Handler に設定されていた処理はキャンセルするようにします。

これにより、連続してイベントが呼ばれても、最後の1回のみに対して処理が行われるようになります。

具体的には以下のようなコードになりました。

package mydomain.deborah;

import android.accessibilityservice.AccessibilityService;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;

public class VoiceCommandService extends AccessibilityService {
    Handler _handler = new Handler();
    int WAIT_TIME = 1000;

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        AccessibilityNodeInfo accessibilityNodeInfo = event.getSource();
        if (null == accessibilityNodeInfo) {
            return;
        }

        String className = accessibilityNodeInfo.getClassName().toString();
        final CharSequence text = accessibilityNodeInfo.getText();
        if(className.indexOf("com.google.android.apps.gsa.searchplate") == -1 || text == null) {
            return;
        }

        _handler.removeCallbacksAndMessages(null);
        _handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                execVoiceCommand(text.toString());
            }
        }, WAIT_TIME);
    }
    @Override
    public void onInterrupt() {
    }

    public void execVoiceCommand(String command){
        Log.d("voice command", command);
        // write your command
    }
}

Raspberry Pi の TV コントローラと繋げる

Raspberry Pi ではTVコントローラ用のWebサーバが立っていて、 GETリクエストを投げればTVのコントロールができるようになっています。 TVのコントロールはTVリモコンと同じように、赤外線を出して実現しています。

詳細は省略しますが、上記コードの execVoiceCommand 内から GETリクエストを投げる処理を呼ぶことで TVのON, OFFができるようになりました。

実行してみる

実行させてみた様子です。
「テレビつけて」と「テレビ消して」を使い分けていますが、TVにはON, OFFを切り替えるという操作しかないので どちらもその操作を実行しています。

OK Google コマンドでテレビをON, OFFする

以前作ったTVを操作できるWebページは、結局普通のリモコンの方が使いやすくてあまり使わなくなりました。
今回作成したものはページを開く必要が無いため、それよりは便利に使えるはずです。
今後も使いつづけられるのか、結局使わなくなるのか、どうなるのか楽しみです。