Android端末のGoogleアシスタントから自アプリの検索を呼び出す

本日からGoogleアシスタント対応アプリを日本語で開発できるようになったようですね。 僕も最近 Google Home Mini を購入したので、日本語で使えるアプリが充実してくれると嬉しいです。

developers-jp.googleblog.com

実はAndroid端末のGoogleアシスタントでは、上記のようなGoogleアシスタント対応アプリの他に、端末内にインストールされているアプリの機能を呼び出すコマンドが実装されています。 これはGoogleアシスタントの前身、Google Nowに実装されていた Google Voice Actions という仕組みを利用したものです。 Android Developers の Common Intents ページにはこの Google Voice Actions からのインテントを受け取るための intent-filter 実装例が載っていて、この通り実装することで自作アプリで Google Voice Actions を受け取ることができます。

例として、今回は検索機能を上げてみましょう。

さきほどの Common Intents ページを見ると、search for cat videos on myvideoapp という音声指示で myvideoapp 上で cat videos を検索することができるようです。 内部的には com.google.android.gms.actions.SEARCH_ACTION アクションのインテントが発行され、それを myvideoapp が受けられる場合はアプリがこのインテントで起動され、対応していない場合はウェブ検索にフォールバックされます。 myvideoapp というのは 僕が試した限りでは AndroidManifest<application> タグ、 android:label の文字列と一致していれば認識するようで、いわゆるアプリ名と一致している必要がありました。

<application android:label="アプリ名">
    <activity android:name=".SearchActivity">
        <intent-filter>
            <action android:name="com.google.android.gms.actions.SEARCH_ACTION"/>
            <category android:name="android.intent.category.DEFAULT"/>
        </intent-filter>
    </activity>
    ...
</application>

というような定義のアプリケーションがあるとき、 SearchActivity 内で com.google.android.gms.actions.SEARCH_ACTION アクションの Intent から getStringExtra(SearchManager.QUERY) することで Google Voice Actions からの検索アクションを処理できます。 検索結果のActivityが分離されている場合、実装はとても簡単です。

…と、ここまでは良いのですが、アプリ内検索機能はまだ英語版 Googleアシスタントでしか動作しません。 この挙動を試すためには、Googleアシスタントの言語設定(Androidでは端末の言語設定を参照しています)を英語にした上で search for カニ on アプリ名 のようなキーワードを文字入力する必要があります。 幸いAndroidGoogleアシスタントは文字入力を受け付けるので日本語アプリでも呼び出すことができますが、言語設定が英語の場合、日本語アプリ名をもつアプリを起動することはできません。 この仕様は非常に残念ですね…。

しかし、Googleアシスタントが日本語対応した今、「アプリ名 で カニ を検索」のようなキーワードで日本語の端末内アプリ検索がサポートされる日も遠くないでしょう。 きっと他の Google Voice Actions も日本語対応が進んでいくはずです。 その日に備えて、Common Intents や Google Voice Actions の一覧に目を通しておきましょう。

Androidのユーザー補助サービス(Accessibility Service)は楽しい

先日クックパッドで開催された potatotips #42Android のセキュリティよくなってきた話という発表をしました。 Androidのセキュリティに関する改善の歴史と、ユーザー補助サービスによるアプリ権限奪取のデモという内容でしたが、短い時間での発表だったため、ユーザー補助サービスについてあまり十分な説明ができませんでした。

この記事では、ユーザー補助サービスのより詳細な説明や(悪用以外の)活用方法、個人的に問題として考えている部分について説明しようと思います。 発表ではネガティブな面をアピールしてしまいましたが、ユーザー補助サービスを使った開発は非常に奥深く面白いので、その楽しさを少しでも伝えられればと思います。

ユーザー補助サービスとは

ユーザー補助サービス(Accessibility Service)とは、Androidアプリとユーザーの間のやり取りをサポートするためのAndroidの機能です。 ユーザー補助サービスを利用するとアプリの切り替えや画面の更新、通知の変化を読み取って音声などの別の手段でユーザーに通知することができます。 また、逆にユーザーの操作を検出してアプリに他の種類のタッチイベントとして通知させることもできます。 どちらかの単方向ではなく、アプリとユーザーの双方向のやりとりを補助できるのが大きな特徴です。

どのように動作するのか

AccessibilityEventを介してアプリ更新やユーザーの操作を検知することができます。 ユーザー補助サービスを利用するアプリでは取得できるイベントや取得できるパラメータに関して細かく設定できます。 開発者は良心に基づき最小権限で実装することが求められますが、AccessibilityServiceInfoの設定項目が非常に複雑な上、ユーザーのからの許可は「ユーザー補助」として一括で扱われるため正しい設定が難しくなっています。 個人的にインストール済みアプリのユーザー補助サービス権限についてはかなり注意を払っていますが、その権限が動作に必要最小限なのかどうかは正直あまり自信がありません。

権限の話はさておき、ユーザー補助サービスが有効になったアプリではAccessibilityService(というService)を通じて様々なイベントを受け取ることができます。 Viewイベントの場合は発生源のView情報も受け取ることができますが、Viewそのものにアクセスすることはできず、あくまでユーザー補助サービスを通して可能な範囲でのデータ取得となります。

アプリ、ユーザーのイベントを受け取る

僕が特に面白いと思っているのはこの部分です。 最前面のアプリが切り替わった時にそのパッケージを取得したり、表示中画面の文字列に特定の文言が含まれているかを判定したりすることができます。 「あるアプリの情報を他のアプリから知ることができる」という部分の面白さは非常に奥深く、GoogleNowなどもこのような方法で表示中アプリの情報を得ているかもしれない、何かより面白いことができるかもしれないと思うとわくわくします。 ユーザーのタップ情報を検知して対象Viewのサイズや配置などを知ったり、アプリごとの利用時間を測ったり、簡単な実装でも色々変わったことができて非常に面白いです。

ただし、Viewの情報がわかるといってもAccessibilityNodeInfoを経由して取得できる項目は限られており、画像や色、スタイルといった情報は残念ながら取得できません…。

アプリにイベントを伝える

ユーザー補助サービスでは、ユーザーの代わりにView操作を行うことができます。 Viewのクリックや画面のスクロール、文字列の入力など、ユーザーがアプリで操作可能なことは大体行うことができます。 先日のデモではアプリの権限を許可するためのSwitchコンポーネントに対してクリック操作を行うことでユーザー操作を介さずに自アプリに権限を与えていました。 こちらのほうは汎用的に使える機能を実装するのが難しく、正直あまり使ったことがありません。 開発中のアプリの入力画面に対してテストデータの入力を補助できるような、デバッグ補助アプリのようなものを作れるかもしれません。 正直ユーザー補助サービスでやらなくても…という感じですが、別のパッケージで実装できるので本体アプリを操作する必要がないのが利点でしょうか。

利用例

例えば、ユーザー補助サービスを使うと以下のようなアプリを作ることができます。

  • 表示中のアプリのパッケージ名やViewIdから効率的に入力補助を行うパスワード管理アプリ
    • 実際に1passwordなどはこのような動作になっているようです
  • 最前面のアプリを変更するイベントを監視し、アプリの利用時間を計測するアプリ
    • RescueTimeなどはおそらくこういった実装になっています
    • 有名サービスとはいえユーザー補助サービスを許可するのは怖いので自分で実装してみるのも良いかもしれません
  • 画面をタップした際、押されたViewのサイズなどを知ることができるアプリ
    • DroidKaigi 2017 で門田さんが発表していたアプリです
    • ユーザー補助サービス自体はUIを持たないため、画面を通じてユーザーにフィードバックするのが難しくなりがちですね…

このように、アイデア次第で色々なアプリを作ることができるのが魅力です。 普通のアプリよりはるかに広範囲なものを操作できるのも楽しいですね。

問題点と今後の展望

すでに書いたとおり、もっとも大きな問題点は「ユーザー補助サービスの権限があまりに強すぎる」というところだと思います。 アプリやユーザーのイベントを受け取る、アプリの操作を行うという双方向のアクションが可能にも関わらず、設定は一箇所のみで確認も非常に簡素です。 これはユーザー補助サービス本来の目的としては正しいですが、セキュリティモデルとしては自分で開発していても心配になるレベルです。 より細かく権限設定できるようにするか、一部の頻出機能はユーザー補助サービスとは別の機能として切り出したほうが良いと感じています。

実際にAndroid Oでは文字列入力補助のためにAutofill Frameworkが導入され、必要最小限のイベント通知とフィードバックのみが行えるようになります。 このような仕組みが今後も拡張されることで、今後はユーザー補助サービスだけに頼らなくても他のアプリと協調して動作するアプリが安全に作れるようになることを期待しています。

まとめ

ユーザー補助サービス非常に楽しいのでぜひ遊びましょう。 限られたイベントとAccessibilityNodeInfoを通じてアプリと会話する経験をすることで、よりAndroid(OS)の気持ちになってアプリのことを考えることができ(ると思ってい)ます。 また、ユーザー補助サービスの何が危険でどうすれば安全なのかを理解する上でも、実際に自分でイベントの取得やフィードバックを行ってみてできることを確かめてみることが大事だと思います。

ヤドカリをハッキングするゲームが面白い

ここ最近、職場で勧められたヤドカリをハッキングできるゲームをやっている。

www.jp.playstation.com

主人公の名前は変更できなかったので、カニカマではない。

ロボヤドカリをハッキングできるという理由だけでろくに下調べもなく買ってしまったのだが、ハッキングして放置を繰り返すだけでも結構面白くてぐるぐるとマップを回ってしまう。 そろそろスキルも全部取れそうだしサブクエスト的なものもあらかた終わったのでこのゲームに関する感想をまとめておきたい。 なお、僕はアクションゲーム自体が苦手難易度イージーだけでやっているのでゲームのレビューとしては多分訳にたたないです。

ハッキング(オーバーライド)に関して

そもそも敵の種類がそこまで多くないこともあって、ストーリーボス的な一部の機械と最初から混乱状態?みたいな機械以外はすべてハッキング可能。 ハッキングした敵は他の敵に攻撃したりしなかったり、なんだか気まぐれな感じだが、そのうち同士討ちがはじまってどちらかが死ぬまで戦ってくれる。 ハッキングするためには敵と密着する必要があるが、ハッキングの最中には対象の敵は動かなくなるので、1匹ずつバレないようにハッキングしていけば僕のようなアクション下手でも簡単に敵グループを片付けることができる。とってもラクだった。

難点は敵の種類ごとに騎乗できたりできなかったり、好戦的だったりそうでもなかったりしてハッキングの結果に差がある事。 ロボヤドカリはそもそも騎乗できないし、自分に着いてきてくれることもない。これは本当に悲しかった。 エリアを切り替えてもどってくるとハッキングしたヤドカリは大抵いなくなっているので、購入時に描いていたヤドカリランド構想が完全に潰えてしまった。 ただしかなりハッキングしたヤドカリはとても好戦的で、ハッキングが終わってすぐ他のヤドカリに殴りかかったり放電したりして戦闘面では心強い。 戦闘力もめちゃくちゃ高くてヤドカリを2匹程度ハッキングできたらあとは何もしなくても他の敵が皆死んでしまう。

あとは 敵をすべて撃破 系のミッションをうけているとハッキングした機械も自分で破壊する必要があって、最初に強い敵をハッキングしてラクをしようとしていたのに結局相手をしないといけなくなって辛かった。 ハッキングした敵の自爆コマンド実装してほしい。

ゲームシステムに関して

ものを持てる量が限られていて、頻繁にものを売ったり捨てたりしないといけないのはなかなか面倒だった。 特にコイル(武器や防具の改造アイテム)に関しては、特定のレアリティ未満のものは自動的に捨てるオプションが欲しくなる。 持てる量を増やすためには特定の動物の皮とか骨とかが必要なのだが、なぜか皮も骨もレアアイテムになっていて動物を狩っても狩っても出ない場合があり苦痛だった。 終盤になるとお金は余っているのだがアイテムを持てる量が少ない状態になりがちな気がする。 動物系アイテム、かなり高額でもいいから店売りしてほしい…。

あとは時間をユーザー側で操作できないのが非常に辛い。 夜になると本当に暗くなってしまって先に進むのが困難な時もあった。 セーブポイント的なところで翌朝になるまで待機させてほしかった。

あとは収集アイテムの位置などもマップに表示されていて迷うことがほとんどなくて遊びやすかった。 なんというか、このゲームは全体的にすごく遊びやすい。最近は難しいゲームが多いなーと思っていたので、ちゃんと遊べている感があってこれは嬉しかった。

アクションに関して

途中何度もさせられる壁登りアクションが面倒だった。 次の足場がある方向にスティックを倒すだけなのだが、主人公が装備しているARデバイス的なものでは次の足場がどこなのかわからないし、普段黄色く表示されている足場も夜になると全然目立たないので夜の壁登りは本当に苦痛だった。 夜の壁登りは避けようと思っても時間スキップもできないので待つしか無い。辛い。 敵や人間の足跡は夜でもハイライトできるのに黄色で目立つ足場がハイライトできないARデバイスは本当にポンコツすぎる…

壁登り以外は特にアクション面での不満はなかった。難易度もかなり下げられてとても遊びやすい。

まとめ

残念なところはいっぱいあるけれど、ヤドカリをハッキングできるゲームはとても珍しいのでそれだけでも買う価値があります。 特になついてくれないところも甲殻類っぽいと思えばそれはそれで良いかもしれない。

良いかもしれないといったけどそんなリアルさはゲームに求めていないので、メーカーさんはヤドカリに騎乗できる、または自分についてきてくれるDLCをお願いします。 自分がヤドカリになる続編でもいいです。お願いします。

なぜプレイヤーキャラクターの名前がカニカマなのか

プレイヤーキャラクターの名前を自由入力させるゲームを遊ぶときはキャラクターの名前をカニカマにしています。

なぜ本名やハンドルネームではないのか? また、なぜカニではないのか? という点について嫁から突っ込まれたので、現時点での僕の考えを記録しておこうと思いました。

まず、なぜ本名ではないのか。

これは単純でプレイヤーキャラは僕ではないからです。 昔ラブプラスというゲームをやったときに律儀に自分の名前をプレイヤーキャラクターの名前にしていたのですが、 主人公が僕のようなコミュ障ではないので段々自分と乖離してきて感情移入できなくなってしまったからです。 そのうち自分と同姓同名のキャラクターとネネさんがイチャイチャしているのを観るのが辛くなってきたのでこのゲームを起動することはなくなりました。

なぜハンドルネームではないのか。

ハンドルネームは名前ではないからです。 エヌ・イー・アイ・エヌ・サン・ナナさんはすべての住民がIDで管理された一部のディストピア以外の世界観には馴染めません。 というか、そもそも長すぎて入力できない場合がほとんどです。 nein37 は日本語のシナリオ中に出てくると違和感があるので使っていません。

なぜカニではないのか。

僕がカニを好きで、ゲームキャラはカニではないからです。 カニという名前で呼ばれた時、ハサミもなく、歩脚が4対でなく、眼柄ももたないキャラクターでありたくないんです。 また、たとえ操作キャラクターがカニのゲームであっても理由なく残虐な行為に及ぶカニであれば「カニはそんなことしない」と思ってしまいそうな気がします。 もし理想のカニになってカニらしい暮らしができるゲームができたらカニという名前になるかもしれませんが、その機会はおそらくないでしょう。 カニに求めるものが多すぎて気軽にカニになれていません。

なぜカニカマなのか。

カニカマはカニではなく、誰からもカニの代用品として認知されていて、それでいて僕自身が深い興味を持っていないものだからです。 カニでなくカニカマだと思えばどんなキャラクターであっても、どんな性格でも受け入れられますし、 僕自信が「こうでなければ」という強迫観念から離れて自由に遊ぶことができます。 例えば、カニカマは時に理由なく残虐な行為に及ぶことがありますが、カニじゃなくてカニカマなのでしょうがない。 魚肉練り製品じゃあしょうがないなあ。という感じです。 今日もカニカマのおかげで現実とゲームの区別を付けて楽しく遊んでいられます。

TextAppearance のプレビュー画面を作る

AndroidのリソースXMLを読み書きできない人に TextAppearance の定義内容を共有するためには、わかりやすく定義内容を一覧できる仕組みが必要です。 できれば、以下のように実際のアプリ内の表示とあわせて見られるのが一番良さそうです。

f:id:nein37:20170328151636p:plain

このプレビュー画面を作るためには「TextAppearane の一覧を取得する」「TextAppearane の定義内容を取得する」という処理が必要になるので、それぞれ方法を紹介します。

TextAppearane の一覧を取得する

TextAppearance や style は R.style. 以下に id が定数として定義されているので、これを利用してリフレクションで一覧を作ります。

今回は AppCompat の TextAppearance を対象にするので、TextAppearance_ の prefix を持つ style を対象にしました。

final List<Field> textAppearanceList = new ArrayList<>();
for (Field field : R.style.class.getFields()) {
    String fieldName = field.getName();
    if (fieldName.startsWith("TextAppearance_")) {
        textAppearanceList.add(field);
    }
}

これで R.style.TextAppearance_* の定数のリストができました。 ここで取得したリストではもともとxml に定義していた名前の ._ に変換されているので注意しましょう。

idの値は以下のどちらかの方法で取り出すことができます。

Field から直接値を取り出す

int styleId = 0;
try {
    styleId = field.getInt(null);
} catch (IllegalAccessException e) {
}

もとの名前から id を検索する

String styleName = field.getName().replace("_", ".");
int styleId = context.getResources().getIdentifier(styleName, "style", context.getContext().getPackageName());

TextAppearance の定義内容を知る

id がわかれば style の定義内容を取得することができます。

TypedArray typedArray = context.getTheme().obtainStyledAttributes(styleId, R.styleable.TextAppearance);

あとは TypedArray から値を取得するだけ… と思っていたら、TextView などが参照している com.android.internal.R.styleable.TextAppearance_textColor は外からアクセスできず、カスタムViewでもないので package.R.styleable にもなくてどうしよう、という感じでした。

悩んだ末、今回は作っていたのが preview ツールだったので TextViewに一度適用して TextView の setter から各デザイン情報を得るという乱暴な方法で実現しました。 TypedArray から直接属性を取る方法知りたい…

// 新しいAPIだが TextViewCompat 経由で setTextAppearance を呼べる
TextViewCompat.setTextAppearance(textView, styleId);

// textColor は ColorStateList だったりするが getCurrentTextColor が初期表示の色を取るのに便利
int textColor = (0xFFFFFFFF & textView.getCurrentTextColor());

// textSize は pixel で取れるので sp に変換する
// style定義時の値が dp か sp かを実行時に知る方法はない
float textSize = textView.getTextSize();
int textSizeSp = Math.round(textSize / context.getResources().getDisplayMetrics().scaledDensity);

// 太字/イタリック
boolean isBold = false;
boolean isItalic = false;
if (textView.getTypeface() != null) {
    isBold = textView,getTypeface().isBold();
    isItalic = textView.getTypeface().isItalic();
}

// ALLCAPS(後述)
boolean isAllCaps = false;
if (textView.getTransformationMethod() != null) {
    if (TextUtils.equals(textView.getTransformationMethod().getClass().getSimpleName(), "AllCapsTransformationMethod")) {
        isAllCaps = true;
    }
}

ALLCAPS 属性は set した瞬間に AllCapsTransformationMethod を TransformationMethod に set という乱暴な処理で扱われていて、isAllCaps() のような getter が存在しません。 しかも互換性のために AllCapsTransformationMethod が2種類(SDKとsupport)用意されていて、両方 @hide なので instanceof させることもできませんでした。 仕方がないのでクラス名だけの比較にしていますが、かなり厳しいですね…。

まとめ

かなり無理やりな処理ですが、一応なんとかなったのでサンプルとして公開しました。 アプリ内で TextAppearance に共通する prefix や suffix が決められていれば同じような処理で TextAppearance のプレビュー画面が作れると思います。

github.com

スプレッドシートでよく使う関数メモ

意外とスプレッドシートを良く使うので、最近使って便利だった関数をメモしておきます。

JOIN

配列要素を結合するのに使う。 char(10) が改行文字なので、取ってきたデータを1セルの中で表示したい時は JOIN(char(10), 配列) みたいな感じにすることが多い。

https://support.google.com/docs/answer/3094077?hl=ja

QUERY

特定の範囲からデータを取ってくるときに便利。 QUERY(A2:H100,"select A where D like 'kani%'") のようにSQLっぽい記法でデータを抽出できる。

INDEX と MATCH でも似たようなことが出来るけど、QUERYだと簡単に書ける上に範囲指定が1回だけで良いので楽。 https://support.google.com/docs/answer/3093343?hl=ja

IMAGE

URLで指定した画像をセル内に描画できる。便利。 対象のURLに画像が存在するかどうかの分岐はできない(エラーにならない)のが残念。

https://support.google.com/docs/answer/3093333?hl=ja&ref_topic=3105411

DEC2HEX、HEX2DEC

10進数を16進数に変換するやつとその逆。

配列の結合

配列A,配列B,配列C,を結合したい時、{配列A;配列B;配列C} とやると配列の次元を保ったまま結合できる。 {配列A,配列B,配列C} だと3次元配列になり、データ件数が違うとエラーが出る。

応用

A列、B列、C列の中から空文字でない要素をひとつのセルの中で改行して並べる場合、以下のように書ける。

= JOIN(char(10),QUERY({A:A;B:B;C:C},"select Col1 where Col1 <>''"))

簡単。

Genymotion VD for AWS を試したメモ

Genymotion VD for AWS を少し触ったのでメモとして残しておきます。

Genymotion とは

Genymotionとは、Androidの非公式エミュレータの一種です。 その昔Androidの公式エミュレータが遅かった時代に流行しましたが、いまはx86仮想化を利用した高速な公式エミュレータがあるので若干人気が落ちてきた印象です。 一応、公式エミュレータにくらべて実機に近いテンプレートや録画などのサポートツールが充実しているという利点があります。

Genymotion VD for AWS とは

Genymotionの仮想デバイスをec2インスタンスで動作可能にしたものです。 公式のx86/x64エミュレータIntel HAXMやKVMがないと動作しないため、これまでAndroidエミュレータはec2環境で動かすことが難しい状態でした。 (ARM版エミュレータは動作させられますが起動も動作もかなり遅く、テスト環境としてはあまり安定していません) Genymotion VD for AWSを利用することで(独立したインスタンスにはなりますが)ec2環境でAndroidを動作させることが可能になるため、様々なメリットがあります(後述)。

導入準備

動画付きの導入説明が非常に充実しているので、動画を見ながらやれば簡単にインスタンスを作成できます。 (AWSのコンソールが若干見た目がかわってますが類推できる範囲だと思います) また、チュートリアルにadb接続のやり方や解像度の変更方法も書いてあります。

以下、試したときの導入手順+メモを書き出してみます。

1. GenymotionのEC2インスタンスを用意する

CIのテスト端末向けとしては m4.large が良いようです

2. 作成したGenymotionインスタンスのadb接続を有効にする

$ssh -i key.pem root@instance_ip
(ssh接続成功後)
$setprop persist.sys.usb.config adb

key.pem はインスタンス作る時に指定した keypare のものを利用します。 instance_ip は ec2-hogehoge.compute.amazonaws.com みたいなやつ(PublicDNS)を利用します。

3. 作成したGenymotionインスタンスとadbで接続する

直接接続すると危ないのでsshポートフォワードで特定ポートを繋ぐことが推奨されています。

$ssh -i key.pem -NL 5555:localhost:5555 root@instance_ip

うまく行けば上記手順でつながるのであとはadb経由でなんでもできます

もし問題が起きたり、端末設定変更のために画面を確認したいときは https://instance_ip を直接開くとデバイス画面にアクセスできます。 認証プロンプトが表示されますが、 genymotion/instanceId(AWSコンソールから見れるやつ) でログインできます。 おそらくインスタンスサイズによると思いますが、 m4.large では画面からの操作は若干もたつく感じでした。

メリット

  • スケーラブル(インスタンス数・インスタンスサイズ)
  • 言語や画面サイズをあらかじめ設定しておける(スナップショット)
  • 公式のARMエミュレータと比べて圧倒的に早い
  • AWS Device Farm などのプールされた実機をレンタルするサービスと違って接続待ち時間などが発生しない

デメリット

まとめ

使ってみるまではインスタンスの管理は Genymotion 側でやっていてそれを時間単位でレンタルできるだけなのかなと思っていたのですが、Genymotion 仮想デバイス自体のインスタンスを自前で管理できるので使い方を細かくコントロールできて良さそうな気がしました。 短時間ならかなり低価格で気軽に試せるので、CI環境に実機を使っていて並列度を上げたいがスケールが難しい、仕方なくARMエミュレータを使っているが遅い…という部分で悩んでいる方はちょっとだけ使ってみると良いかなと思います。