AndroidでApp Actionsの音声制御を試してみて、うまく動かなかった話 → 動きました

# 概要

Androidアプリを音声で制御する方法を調べ、[App Actions](https://developers.google.com/assistant/app/overview)のサンプルを実装してみた結果、期待通りの動作をしなかった話です。(ただし、現在対応方法を調査中です。)

# 目標

アプリの特定の機能を音声から制御できるような仕組みを実装することを目標としました。
用途としては、両手が空いてないような状況、具体的には車の運転や、料理をしているような状況で、音声で起動して任意の動作をさせて、作業の補助をさせることを想定したものです。
実装的には、特定の音声（「OK、Google、○○して」みたいな感じ）に反応してパラメータを渡してアプリを起動できれば良い、ぐらいに考えていました。

# Androidのアプリを音声で制御する仕組み

調べたところ、Androidで音声制御をする方法にはいくつかの手法があったので、少し整理してみました。

## Speech Recognizer

[SpeechRecognizer  \|  Android Developers](https://developer.android.com/reference/android/speech/SpeechRecognizer)

これは、Android標準の音声解析機能を用いる方法です。音声を認識した結果をテキストとして取得できるので、実装自体は難しくないです。ただし、アプリが音声を拾う必要があるため、アプリが起動し続ける必要があります。

アプリを起動し続けるのが挙動としてあまり望ましくないので、これは不採用としました。

## System Voice Actions

[Get Started with System Voice Actions  \|  Google Developers](https://developers.google.com/voice-actions/system/)

Android側で決められたアクションを音声で操作できる仕組みです。[一般的なインテント](https://developer.android.com/guide/components/intents-common)で、「Google Voice Actions」が付けられたIntentが対象になります。
実装としては、Android ManifestのIntent Filterで既定のIntent (&lt;action android:name="android.intent.action.SET_ALARM"/&gt;など)を付けておくと、既定の音声コマンドで呼び出す事ができます。アプリ起動時にパラメータが渡ってくるので、アプリ側に適切な処理を実装します。いくつかのアクションはインタラクション(対話)の実装が必要になります。
[アクションの一覧](https://developers.google.com/voice-actions/system)を見ると分かりますが、使えるアクションが限定的なので使い所が難しいです。最も汎用的な「Open Application」ですが、これは音声操作のデフォルトの挙動で、(Androidがアプリ名を認識できれば)特に何も実装しなくても動きます。ただし、これを使ってアプリを呼び出してもパラメータが渡ってこないので、アプリアイコンをタップして起動した時と区別が付きません。なので、音声で操作というのは難しいように思いました。

こちらは、[Google Now](https://ja.wikipedia.org/wiki/Google_Now)で実装された機能ですが、現在は後述のGoogle AssistantのApp Actionsがあり、あえて使う必要は無いかと思いました。よって、不採用としました。

## Actions on Google (App Actions)

[App Actions  \|  Google Developers](https://developers.google.com/assistant/app)

Androidの音声アシスタントであるGoogle Assistantを使って、アプリを制御する仕組みです。Actions on GoogleはGoogle Assistantの開発プラットフォームのことを指します。Google Assistantには、Webサービス側で処理を行ったり、スマートスピーカーなどのホームデバイスに命令を実行させたりする機能がありますが、そのうちの一つでAndroid端末内のアプリやアクションを実行する機能をApp Actionsと言います。
実行できるアクションは、[Built\-in intents](https://developers.google.com/assistant/app/reference/built-in-intents/)で定義されているものになります。ざっと見ると、System Voice Actionsの機能と同じようなものがいくつかあり、App Actionsの仕組みで再実装されたような印象を受けます。System Voice Actions同様、用途がかなり限定されているので使いにくいですが、今後増えていくことに期待したいところです。
現状では、対応言語は英語(en-US, en-GB, en-CA, en-IN, en-BE, en-SG, and en-AU)のみとなるため、Google Assistantの対応言語に英語を追加する必要があります。

今回は、これを使って実装する事としました。



# Fit Actionsを実装してみる

[App Actionsの実装ガイド](https://developers.google.com/assistant/app/get-started)で述べられている、[Codelabs](https://codelabs.developers.google.com/codelabs/appactions/#0)の手順に従い、App Actionsの実装サンプルとなる、Fit Actionsの実装を行ってみました。Fit Actionsのコードが[GitHubにある](https://github.com/actions-on-google/appactions-fitness-kotlin)ので、こちらを用います。

作業には次の環境が必要になります。

* gitが使える環境
* Android Studioの最新バージョン
* Google Play ConsoleにアクセスできるGoogleアカウント
* アプリアクションをテストする物理的なAndroidデバイス (Android 5以降)

##  Codelabsの実装

実装手順については、[Codelabsの内容](https://codelabs.developers.google.com/codelabs/appactions/#0)を読めば良いので省略します。
基本的にGitHubのコードをアプリケーションIDを変更した後、ビルドして動かすのですが、自分はいくつか引っかかった点があったので、メモしておきます。

* 実行する時、リリースビルドを作成してGoogle Play Consoleの内部テスト版トラックにAPK(AAB)をアップロードする必要があります。なので、鍵の生成やアプリ登録を行う必要があります。アップロードまで出来れば良く、ストア情報の入力や公開などは不要です。
* Codelabsには書かれていないようですが、Google Play Console上で「Actions on Google」の同意にチェックを入れることと、利用規約のURLを登録しないと、2回目以降のアップロードでエラーになります(初回のアップロードが大丈夫だったのは謎)。公開する予定が無いなら、利用規約のURLは何らかのURLを入れておけば良さそうです。
* 動作テストを行う端末は、Play Consoleで操作したアカウントと紐付いている必要があります。
* 端末上で動かす実行ファイルは、Google Play Consoleでアップロードしたものと別物でも大丈夫そうです(actions.xmlの定義が一致してる必要はあると思いますが)。デバッグビルドでも動作しました。
* アクションを定義するactions.xmlのフォーマットのチェックはAndroid Studio上で行われず、APKのアップロード時にチェックされます。(画像参照)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/4ed8501d-2ce5-9a9a-5ee4-2a6453addda2.png)


## App Actions Test Toolによる動作確認

### 準備

端末にアプリをインストールした後、動作を確認します。動作確認には、[App Actions Test Tool](https://developers.google.com/assistant/app/test-tool)を使いますので、Pluginからインストールしておく必要があります。
この作業は、Android StudioがGoogle Play Consoleのアカウントでログインしている必要があります。また、繰り返しになりますが、端末はGoogle Play Consoleのアカウントに紐付いている必要があります。

### App Actions Test Toolの起動

Android StudioからTest Toolを起動すると、次の画面が表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/c18b7834-b25d-e628-dd6e-6056c366e04a.png" width="240" />

* Invocation name: アプリ名を入れます。これがApp Actionsで認識される名前になるので、適当な値を入れるとアプリ名に反応しません。
* Locale: Google Assistantの言語を入れますが、現状英語のみ対応なのでen-USと入れるか、とりあえず未入力で良いかと思います。

上記を入力し、Create Previewを押してプレビューを作成します。
なお、プレビューを一度入力するとこの画面は次回以降は出てこなくなりますので、再設定したい場合は次の画面で一度プレビューを削除します。

### App Actions Test Toolから実行確認

以下の画面が表示されますので、この画面でテストを実行します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/993fd62b-8485-7cf0-b370-cfb0f43798b7.png" width="240" />

* Configure: actions.xmlで定義されたアクションが表示されますので、動作確認したいアクションを選択します。
* exercise: 実行パラメータを設定します。
* Select Target Device: 動作対象のデバイスを選択します。

上記を入力し、Runを押すと端末でGoogle Assistantが起動し、それを介してアプリが実行されます。
例えば、Configureにactions.intent.START_EXERCISEを設定した場合、端末上で走行距離のカウントが開始されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/81892b92-5f0f-cb09-73d9-8efab6554211.png" width="240" />


### 端末から実行確認

端末のGoogle Assistantから実行してみます。
ただその前に、Google Assistantの言語設定に英語を追加する必要があります(端末の言語設定ではありません)。機種にもよりますが、設定画面より「Google」→「アカウントサービス」→「検索、アシスタントと音声」→「Googleアシスタント」→「アシスタント」タブ→「言語」の順で開き、English (United States)を追加して一番上にくるように設定します(Google Assistantの言語は2言語まで設定できます)。

Google Assistantを起動し(Voice Match設定が有効の場合は「OK, Google」も良いです)、「Start running in Fit Actions」と話してみると、テストツールで確認したactions.intent.START_EXERCISEと同じ挙動をすることが確認できます。



# アクションを追加してみる

Fit Actionsのアプリに対して、新しいアクションを追加してみます。とりあえず、最も汎用的な[Open app feature](https://developers.google.com/assistant/app/reference/built-in-intents/common/open-app-feature)を利用します。このアクションは仕様上、パラメータとしてfeatureの値を渡す事ができます。

## actions.xmlの編集

actions.xmlに次の値を追加します。

```
    <action intentName="actions.intent.OPEN_APP_FEATURE">
        <fulfillment urlTemplate="https://fit-actions.firebaseapp.com/launch{?appFeature}">
            <parameter-mapping
                intentParameter="feature"
                urlParameter="appFeature" />
        </fulfillment>
    </action>
```

**https://fit-actions.firebaseapp.com/**は、Fit Actionsアプリ側で定義済みのDeepLinkのURIです。これを使って、Open app featureアクションのパラメータを渡してアプリを起動します。

## App Actions Test Toolから実行確認

actions.xmlを変更した後、リリースビルドを作成してGoogle Play Consoleに再度アップロードします(バージョンが被るとアップロードできないので、バージョンを上げるか、アーティファクト ライブラリの画面から前回の物を削除しておきます)。

再度App Actions Test Toolを起動し、一度「Update Preview」ボタンを押すことで、actions.xmlの変更を反映します。
その後、Configureからactions.intent.OPEN_APP_FEATUREを選択し、featureの欄に任意の値を設定して実行すると、Google Assistantを介してアプリ起動し、featureに設定したパラメータが渡されていることがデバッガなどで確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/0f9e09e4-ffdd-e101-2eb5-0f6b3662456d.png" width="240" />

## 端末から実行

端末のGoogle Assistantから追加したアクションを実行してみます。Open app featureをパラメータ付きで実行するには、「Open Fit Actions ○○」「Open ○○ on Fit Actions」「Open ○○  from Fit Actions」といった音声が該当するようです。

…が、試してみたところうまく動きません。何度やっても、GoogleアシスタントにはGoogleの検索結果が表示されます。

## 上手く行かない原因

色々調査したところ、以下の情報がヒットしました。

[OPEN\_APP\_FEATURE works on test tool only \[135714718\] \- Visible to Public \- Issue Tracker](https://issuetracker.google.com/issues/135714718)

ざっと読んだところ特に有効な解決策は無かったのですが、5日前の5/6に以下の書き込みがあり、現在調査の真っ最中という感じでした。

> Regarding OPEN_APP_FEATURE working on test tool but not via voice triggering, the App Actions team is looking into this and may reach out to you for additional information.

というわけで、タイミングが良いのか悪いのか分からないですが、現在この機能は上手く動かないようです。

**[2020-05-13]** 動くようになりました。追記参照。

## 他のbuilt-in intentについて

恐らく他のbuilt-in intentを使うことで、パラメータ付きのアプリ実行は可能だと思います。ただ、本来と異なる機能の使い方をした場合はストアからリジェクトされる可能性もあり、最悪はGoogle Play Consoleから弾かれる懸念も想定されるので、一旦保留とします。Open app featureの動作が有効になった時、改めて試したいと思います。


# 最後に

長くなりましたが、Android端末の音声制御について、調べた事や試した事について書いてみました。結果としては上手く行かなかったのですが、とりあえずまだ今後の改善が見込めそうなので、一旦保留とします。
個人的に運転中に音声で動かせるアプリが作りたいと思っているので。時期を見てまた試したいと思います。

# 2020-05-13 追記

本日、上記スレッド([OPEN\_APP\_FEATURE works on test tool only \[135714718\] \- Visible to Public \- Issue Tracker](https://issuetracker.google.com/issues/135714718))にて5/13にGoogleの方の報告があり、問題が修正されたようです。

> All, we have addressed this issue. If you were previously running into OPEN_APP_FEATURE working on test tool but not via voice triggering, it should be fixed now.

試してみたところ、きちんとGoogle Assistantから反応しました。この場合、Historyという単語に反応しています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/53341e6c-227a-ea9f-3141-cdcfddb4f14b.png" width="240px" />

ちなみに、「Open fit actions history」「Open history in fit actions」「Open history on fit actions」などで反応しました（「Open history from fit actions」はダメ）。

## 反応する単語を限定する

上記スレッドにて書かれていましたが、パラメータとなる単語を限定するには、actions.xmlに次のように書きます。このように定義することで、「history」「records」という単語のみに反応するようです。

```xml
<actions>
    <action intentName="actions.intent.OPEN_APP_FEATURE">
        <!-- Each parameter can reference an entity set using a custom ID. -->
        <parameter name="feature">
            <entity-set-reference entitySetId="FeatureEntitySet"/>
        </parameter>

        <fulfillment urlTemplate="https://fit-actions.firebaseapp.com/openappfeature{?appFeature}">
            <parameter-mapping intentParameter="feature" urlParameter="appFeature" />
        </fulfillment>
    </action>
    <entity-set entitySetId="FeatureEntitySet">
        <entity identifier="HISTORY" name="history"/>
        <entity identifier="RECORDS" name="records"/>
    </entity-set>
</actions>
```

## 気付いたこと

何度か音声入力を試してみましたが、私の発音が悪いのでアプリ名の反応率があまり良くないです。「Fit actions」というアプリ名は、よく「feet actions」に化けてしまい、なかなかアプリが反応しません。今回のような音声入力でアプリ名を指定する場合、誤認識の少ないアプリ名の設計が重要な要素になることが分かりました。将来的に日本語対応された場合は少し事情が変わってくるかもしれませんが。
ちなみに、START_EXERCISEアクションを試していた時はそこまで誤入力は無かった気がするのですが、恐らく音声の内容からアクションを特定し、それに対応したアプリを類推して補正していたのではという気がします。OPEN_APP_FEATUREアクションはアプリ名を正確に述べないと、対象アプリが特定できないので、認識率が悪いのではないかと。なので、可能な限り対応したbuilt-in intentを使った方が、アプリの使い勝手は良くなるのではないかと思います。

# 2020-06-06 追記

また動かなくなったので、調べてみたらやっぱりまだ調査中のようです。
現在Googleの中の人が調査中のようです。

https://issuetracker.google.com/issues/157893912
https://issuetracker.google.com/issues/157913527

現在色々試してみたところ、Test Tool経由なら動きますが、Google Assistantからは"HISTORY"というパラメータにのみ反応するなど、妙に不安定な動作が見られます。