Googleが提供しているMobile Vision APIの内、顔認識用のAPI "Face"を試してみました。
今回は、静止画を対象にした顔認識のみとなります(次回、カメラ動画で試してみたいと思います)。

環境設定

Mobile Visionを使用できるようにするには、"com.google.android.gms:play-services-vision"を依存関係として追加します。
最新のバージョンはここを見る限り、2017.08.29現在は"11.2.0"だったのですが、取得できなかったので、"11.0.0"を使用しました。

app/build.gradle
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.google.android.gms:play-services-vision:11.0.0'    // ★追加
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:recyclerview-v7:25.3.1'
}
これを設定することで、Mobile Vision APIが使用可能となります。

実装

静止画に対して、顔認識を行うのは比較的簡単な実装で行えます。
大まかな流れは以下のようになります。
  1. FaceDetector.Builderインスタンスを生成し、それから顔認識条件が設定されたFaceDetectorインスタンスを生成
  2. Frame.Builderインスタンスを生成し、それから静止画像が設定されたFrameインスタンスを生成
  3. FaceDetectorインスタンスにFrameインスタンスを設定して、顔認識を実施
顔認識を実施すると、その静止画から認識できた顔の情報を保存した、Faceインスタンスのリストが取得できます。
また、Faceインスタンスの中に、顔のパーツ(目、鼻、口、ほほなど)の位置を保存したLandmarkインスタンスのリストが格納されています。
これを駆使することで、静止画中の顔の位置と、顔のパーツの位置を知ることができます。

FaceDetector.Builderインスタンスを生成し、それから顔認識条件が設定されたFaceDetectorインスタンスを生成

app/src/main/java/jp/eq_inc/testmobilevision/fragment/FaceDetectFromPhotoFragment.java - FaceDetectTask.onPreExecute
  FaceDetector.Builder builder = new FaceDetector.Builder(activity);

  // classification
  SwitchCompat tempSwitch = (SwitchCompat) activity.findViewById(R.id.scClassification);
  if (tempSwitch.isChecked()) {
      builder.setClassificationType(FaceDetector.ALL_CLASSIFICATIONS);
  } else {
      builder.setClassificationType(FaceDetector.NO_CLASSIFICATIONS);
  }

  // landmark
  tempSwitch = (SwitchCompat) activity.findViewById(R.id.scLandmark);
  if (tempSwitch.isChecked()) {
      builder.setLandmarkType(FaceDetector.ALL_LANDMARKS);
  } else {
      builder.setLandmarkType(FaceDetector.NO_LANDMARKS);
  }

  // mode
  tempSwitch = (SwitchCompat) activity.findViewById(R.id.scDetectMode);
  if (tempSwitch.isChecked()) {
      builder.setMode(FaceDetector.FAST_MODE);
  } else {
      builder.setMode(FaceDetector.ACCURATE_MODE);
  }

  // prominent face only
  tempSwitch = (SwitchCompat) activity.findViewById(R.id.scProminentFaceOnly);
  builder.setProminentFaceOnly(tempSwitch.isChecked());

  mFaceDetector = builder.build();
設定できる条件としては、以下のものがあります。
設定名称設定名称概要設定値
classificationType目が空いているとかスマイルなどの状態を取得可否
概要
NO_CLASSIFICATIONS
デフォルト
目の開きやスマイル判定なし
ALL_CLASSIFICATIONS目の開きやスマイル判定あり
landmarkType目や鼻などの顔のパーツの取得可否
概要
NO_LANDMARKS
デフォルト
目や鼻など顔のパーツの場所特定なし
ALL_LANDMARKS目や鼻など顔のパーツの場所特定あり
proportionalMinFaceSize認識する顔の最小サイズ。小さい値を入れるほど認識率は上がるが、速度が劣化する 値は解析する画像幅を1としたときの割合。
デフォルト値はprominentFaceOnlyの指定により異なる。
prominentFaceOnly OFF: 0.1
prominentFaceOnly ON: 0.35
mode顔認識時の動作として精度を上げるか速度を上げるかのモード選択
概要
FAST_MODE
デフォルト
顔認識の速度重視
ACCURATE_MODE顔認識の正確さ重視
prominentFaceOnly一番目立つ顔のみ取得するか否か
概要
true一番目立つ顔のみ取得
false
デフォルト
認識した全ての顔情報を取得
trackingEnabled認識した顔を以後追い続けるか否か
概要
true認識した顔を追い続ける
false
デフォルト
追わない

Frame.Builderインスタンスを生成し、それから静止画像が設定されたFrameインスタンスを生成

app/src/main/java/jp/eq_inc/testmobilevision/fragment/FaceDetectFromPhotoFragment.java - FaceDetectTask.doInBackground
  Frame fullImageFrame = new Frame.Builder().setBitmap(fullImage).setRotation(rotation).build();

FaceDetectorインスタンスにFrameインスタンスを設定して、顔認識を実施

app/src/main/java/jp/eq_inc/testmobilevision/fragment/FaceDetectFromPhotoFragment.java - FaceDetectTask.doInBackground
  SparseArray detectedFaceArray = mFaceDetector.detect(fullImageFrame);

顔認識結果

いくつかの画像で、どんな結果が得られるか試してみました。

口元を隠した写真

口元を隠した写真に対して顔認識できるか否かの確認になります。
使用した写真のモデル・コピーライトなどは、以下のものとなります。
提供元ぱくたそ
モデル大川竜弥
コピーライトすしぱく
結果:
顔認識成功

怒り顔

怒り顔が写った写真に対して顔認識できるか否かの確認になります。
使用した写真のモデル・コピーライトなどは、以下のものとなります。
提供元ぱくたそ
モデル大川竜弥
コピーライトすしぱく
結果:
mode: accurateでも、顔認識できず。

横顔

横顔が写った写真に対して顔認識できるか否かの確認になります。
使用した写真のモデル・コピーライトなどは、以下のものとなります。
提供元ぱくたそ
モデル千歳
コピーライトすしぱく
結果:
顔認識成功。また、顔のパーツについても写っているものについては認識成功(写真の緑色に塗られている部分が認識している箇所になります)

目元が隠れている写真

目元が隠れている写真に対して顔認識できるか否かの確認になります。
使用した写真のモデル・コピーライトなどは、以下のものとなります。
提供元ぱくたそ
モデルゆうせい
コピーライトすしぱく
結果:
mode: accurateでも、顔認識できず。

イラスト

イラストに対して顔認識できるか否かの確認になります。
使用したイラストは、以下のものとなります。
提供元いらすとや
いらすと集合している人たちのイラスト
結果:
mode: accurateにおいて、4人だけ顔認識に成功。

mode: fastにすると、2人だけしか顔認識に成功しませんでした。

mode: accurate且つprominent face onlyを有効にすると、顔は認識されませんでした。
多分、顔認識はされているけども、どれを該当の顔としてよいか判断出来なかったと思われます。

おまけ

写真によっては、顔以外のものも顔として認識されることがあるようです。それも、mode: accurateの場合に。
使用した写真のモデル・コピーライトなどは、以下のものとなります。
提供元ぱくたそ
モデル河村友歌
コピーライトすしぱく
結果:
mode: fastの場合は認識されていないが、mode: accurateの場合のみ、ダッシュボード付近に顔として認識されるものが存在する。
mode: accurate mode: fast
さすがに、顔のパーツは認識されていませんが、こういったものはアプリで除去する必要がありそうです。

サンプルアプリ

ここにサンプルアプリを置きました。 試してみる場合は、以下の手順で確認してみてください。
  1. ここから環境をクローン
  2. タグ"v0.1.0"をチェックアウト
  3. ビルドしてAPKをインストール
  4. "Face Detect from Photo"を選択
  5. デバイスに格納されている写真が表示されるので、顔認識したい写真をタッチ

参考サイト

Detect Facial Features in Photos
Get Started with the Mobile Vision API
Set Up Google Play Services
今回はVR上での動画再生についてです。

プロジェクトを作成

Google VRが動作するように基本的なプロジェクトを作成します。
作成方法はここを参考にしてみてください。

VideoPlayer用のSDKを組み込む

動画再生は"Google VR for Unity" SDKを組み込んだだけでは、再生できない状態になっています。
追加でVideoPlayer用のSDKを組み込む必要があります。

VideoPlayer用のSDKは、先の"Google VR for Unity" SDKを組み込むことで、以下の場所にパッケージが置かれています。
"Assets" - "GoogleVR"配下の"GVRVideoPlayer.unitypackage"

これを"Google VR for Unity" SDKと同じようにインポートします。



GvrVideoPlayerTextureをアタッチ

動画を表示するためのGvrVideoPlayerTextureをGameObjectにアタッチします。ただし、これをアタッチするGameObjectはMesh Rendererをアタッチしている必要があるので、"Quad"などのプリミティブな"3D Object"に設定します(※)。
※ こちらでは"Quad"と"Sphere"で試してみました。
またGvrVideoPlayerTextureのプロパティの"Video Type"と"Video URL"に再生したい動画のURLの種別を設定します。
Video Type Video URLに設定できる値
Dash Dash形式のストリーミングコンテンツのURL
例: https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd
HLS HLS形式のストリーミングコンテンツのURL
Other ローカルフォルダに格納されているコンテンツのパス
例: file:///storage/emulated/0/Movies/example.mp4

あとはGvrVideoPlayerTexture.Playを実行すると再生されますが、コールするタイミングが早すぎると失敗し且つGvrVideoPlayerTexture.IsPausedもfalseが返却される状態になってしまいます。
この場合、GvrVideoPlayerTexture.Playはfalseを返却してくれるので、戻り値を見て対応した方がよいです。

VideoControls

先のGvrVideoPlayerTextureをアタッチするところまでで、再生/停止自体は可能ですが、Player用のUIが欲しい場合は、デフォルトUIとして、VideoControlsプレハブが提供されているので、これを追加すると良いです。
VideoControls.PlayerプロパティにGvrVideoPlayerTextureをアタッチしているGameObjectを設定することで、VideoControlsプレハブから動画の再生・停止などを実行することができます。

サンプルアプリ

ここにサンプルアプリを置きました。 試してみる場合は、以下の手順で確認してみてください。
  1. ここから環境をクローン
  2. サブモジュールが2つ組み込まれているので、それぞれを取り込む
  3. ブランチをタグ"v0.0.4"に切り替える
  4. 「Google VR for Unity」(v1.7.00)のパッケージをここから取得し、先の環境にインポート
  5. インポートすると、Assets/GoogleVR配下に"GVRVideoPlayer.unitypackage"ができているので、こちらもインポート
  6. 環境をビルドし、アプリをインストール
  7. 起動するとメニューが表示されるので「Video Player」を選択
  8. 画面中央の灰色の長方形か画面下部の再生(▶)ボタンをクリックすると動画の再生が始まります

参考サイト

Streaming Video Support
Android Pay APIをコールして、最後に得られるクレジットカード会社に請求する情報が記載されているPayment Method Token(FullWalletの内部で保持しているPaymentMethodTokenインスタンス)について調査したので纏めます。

これ自体はセキュアな情報のため暗号化されています。その暗号化時の鍵となるのが、MaskedWallet取得時に設定した公開鍵とGoogleで管理している公開鍵から生成した共通鍵となります。

Payment Method Tokenは以下の3要素を持つJSONとなります。
要素名値の概要
encryptedMessage文字列(base64エンコード済)暗号化されたクレジットカード情報
詳細はこちらを参照
ephemeralPublicKey文字列(base64エンコード済)共通鍵を生成する際に使用したGoogle管理のキーペアの公開鍵
これとデバイスで管理しているキーペアの秘密鍵から復号化用の共通鍵を生成
tagencryptedMessageのMAC値文字列(base64エンコード済)

encryptedMessageを復号化することで得られるJSONの構成
要素名値の概要備考
dpan文字列(数字のみ)クレジットカードの番号-
expirationMonth数字クレジットカードの期限(月)-
expirationYear数字クレジットカードの期限(年)-
authMethod文字列"3DS"固定将来的には変更される可能性有
3dsCryptogram文字列3Dセキュア用の暗号化文字列詳しい情報なし
3dsEciIndicator文字列3Dセキュア用のECIインディケータ詳しい情報なし

このクレジットカード情報を用いて、清算することになるようです。
なお、デバイス管理のキーペア(秘密鍵)とGoogleサーバ管理のキーペア(公開鍵)から、復号化用の共通鍵を作成する方法は、こちらのコード(Example: Token decryption)を参照するとよいかと。

参考文献:
https://developers.google.com/android-pay/integration/payment-token-cryptography

今回はAndroidエンジニアにとって、面倒なRuntime Permissionについてです。
ユーザがVR実行中でも、アプリは必要に応じてユーザに許可を得る必要があります。
ただ、残念なことに一旦ヘッドセットを外す必要があります(なんでコントローラから確認を行えるようにしなかったのかは不明)。
画面遷移としては以下のようになります。
画面番号 ヘッドセット装着状態画面備考
装着中
-
装着→外した状態
スマートフォンをViewerから外して、画面方向を横→縦に変更すると次のPermission確認画面に遷移
com.google.vr.vrcoreプロセス上にて表示
外した状態
com.google.android.packageinstallerプロセス上にて表示
外した状態→装着中
「RETURN TO VR」ボタンをタッチすると、VRに戻る
自アプリプロセスにて表示

Google VR SDK for Unityとしても、Runtime Permissionに対するAPIが提供されています。

GvrPermissionsRequester

こちらの「GvrPermissionsRequester」がRuntime Permissionをユーザに確認するためのクラスとなります。
本クラスのインスタンスはInstanceプロパティで取得しますが、インスタンスが設定されていないケースが存在します。
インスタンスが設定されていないケースとしては、以下のケースがあります。
  • Unityのエミュレータ(Unity Editor)上でアクセスした場合
  • Unityのエミュレータでは、Android自体のシステムが動作しないので、こちらは使用できません。
  • FragmentがActivityにアタッチする前、もしくはデタッチした後
  • Androidの実機でも、こちらのインスタンスは別途Fragmentを生成しているので、FragmentがActivityにアタッチする前、もしくはデタッチした後は使用できないものとなります。

HasPermissionsGranted, IsPermissionGranted

確認したいPermissionが得られているか否かを確認するためのメソッドが2種類用意されています。
やっていることは一緒で、メソッドにて一括でPermission確認を実施しているか否かの違いしかないです。
メソッド名称概要備考
HasPermissionsGranted指定された複数のPermissionをユーザに一気に確認Activity.checkSelfPermissionをそれぞれのPermission毎にコールしている
IsPermissionGranted指定されたPermissionをユーザに確認Activity.checkSelfPermissionをコールしている

ShouldShowRational

指定されたPermissionに対して、ユーザに確認を行うためのUI(ダイアログ)を表示する必要があるか否かをOSに確認するためのメソッドになります。
これで確認を行わずに、RequestPermissionsを実行すると、上記の画面遷移③のユーザ確認が行われずに、そのまま画面遷移④が発生する動作になります。
そのため、ヘッドセットの着脱をした意味がなくなるので、このメソッドで実際にユーザ確認が必要か確認した方がよいでしょう。

本メソッドは内部でActivity.shouldShowRequestPermissionRationaleをコールしているだけなので、使い方はActivity.shouldShowRequestPermissionRationaleと一緒になります。

RequestPermissions

このメソッドを実行するとユーザに対してPermissionの確認が発生します(画面遷移②が表示されます)。
この先はSDK内部で処理が行われ、メソッドコール時に指定したコールバックに結果が返ってくるようなメソッドになっていますが、こちらで確認した限り、ほとんどコールされませんでした。

画面遷移②の段階で、自アプリはcom.google.vr.vrcoreアプリの裏に隠れるため、自アプリがOSによる再起動の対象になり、一旦プロセスが終了されます。その後、画面遷移④で自アプリが再起動されて、「RETURN TO VR」ボタンの表示までは意図した動作に見えるのですが、それを押下したタイミングでログに「PermissionsFragment: Permission callback object is null!」が出力されています。
これはcom.google.gvr.permissionsupport.PermissionsFragment(以下、Google VR SDK for Unity v1.70.0にて確認)の以下のメソッドで出力されているものになります。
    public static void setPermissionResult(boolean allGranted) {
        if(permissionsCallback != null) {
            permissionsCallback.onRequestPermissionResult(allGranted);
        } else {
            Log.w("PermissionsFragment", "Permission callback object is null!");
        }

    }
staticフィールドの"permissionsCallback"がnullの場合に出力されるようになっていますが、こちらはプロセスが再起動されてしまったことにより初期化されてしまっているためにコールバックができない状態になっています。

上記の点から、Permission確認をPermissionが必要なモジュールにアクセスする直前に実施すると、アプリの再起動が発生することによりユーザ操作の手戻りが多くなるので起動直後に確認した方が良さそうです。

先日「東京 Node 学園 27 時限目」に参加して Puppeteer というライブラリを知りました。
Puppeteer は Node.js から Headless Chrome を操作するためのライブラリで、Google Chrome の開発チームが出している公式のものだそうです。

Node.js から Headless Chrome を扱うには「chrome-remote-interface」というものがありました。
私も「Headless Chrome を使ったフロントエンドのテスト」という記事の中で chrome-remote-interface の使い方を紹介しています。

その記事のなかで、ページをスクロールしたり、マウスでクリックするような操作をライブラリとして用意すれば Headless Chrome を使うのは十分に現実的だという感想を書きました。
逆にいうと、そのようなライブラリを用意しないと Headless Chrome をテストで使うのは厳しいということになります。

この Puppeteer は、そういった機能を提供してくれるライブラリです。
これは試してみなくてはいけないということで、chrome-remote-interface を使って書いたテストと同じ内容のものを Puppeteer を使って書いてみることにしました。

環境構築

今回、用意したのは次のような環境です。
Node.js のバージョンが現時点での最新のものになっている以外は、chrome-remote-interface を動かしたのと同じです。

Name Version
OS Ubuntu 16.04(Xenial)64bit
Node.js 8.4.0
Puppeteer 0.10.1

いつもどおり Vagrantfile や Ansible の playbook、テストのソースコードは GitHub で公開しています。
同じような環境を構築してテストを動かしてみたいというときは、puppeteer-test を clone して試してみてください。

Puppeteer の導入

Puppeteer は npm からインストールすることが可能です。
またインストール時には、Puppeteer の package.json と同じディレクトリに .local-chromium というディレクトリを作り、最新の Chromium がダウンロードされます。
そのため、npm だけでテスト環境を構築したいという希望も実現…………、できません。

Puppeteer がダウンロードしてくれるのは Chromium だけなので、Chromium を実行するのに必要なライブラリは別途インストールする必要があります。

ライブラリが足りない状態でテストを動かすと、次のようにエラーが表示されます。
エラーが無くなるまで表示されているライブラリのインストールを繰り返すことで、テストが実行できるようになります。
ただ、必要なライブラリも多いですし、こういった作業をするのが面倒だという場合は chromium のパッケージをインストールしてしまえば一発です。

Error: Failed to launch chrome!
/vagrant/node_modules/puppeteer/.local-chromium/linux-497674/chrome-linux/chrome: error while loading shared libraries: libpangocairo-1.0.so.0: cannot open shared object file: No such file or directory


TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md

ちなみに、私が試した限り Ubuntu 16.04(Xenial)で Puppeteer を使ったテストを動かすには次の 5 つのパッケージと、これらが依存しているパッケージが必要でした。

  • libgtk-3-0
  • libnss3
  • libxss1
  • libgconf-2-4
  • libasound2

troubleshooting.md にも必要なライブラリの記載がありますので、うまく動かないという場合には参考にしてみてください。

Nightmare では xvfb を使用するため、デバッグオプションを付けて起動する必要がありましたが、Puppeteer では普通に出力されるので楽ですね。

テスト

Puppeteer を使って Protractor のサイトにあるテストのサンプルを書いてたのが次のコードです。
chrome-remote-interface を使ったときのコードだけではなく、Nightmare を使ったコードと比べてもシンプルで分かりやすいのではないでしょうか。

'use strict';


let browser = null;
const puppeteer = require('puppeteer');
const expect = require('expect.js');


before(async () => {
    browser = await puppeteer.launch();
});

after(() => {
    browser.close();
});


describe('Headless chrome test', function () {
    describe('Protoractor test sample', function () {
        it('Should complete test', async function () {
            const page = await browser.newPage(),
                text = 'write first protractor test';
            await page.goto('https://angularjs.org');
            await page.focus('input[ng-model="todoList.todoText"]');
            await page.type(text);

            const button_add = await page.$('input[value="add"]');
            await button_add.click();

            const list = await page.$$('li[ng-repeat="todo in todoList.todos"]');
            expect(list).to.have.length(3);
            expect(await list[2].evaluate((element) => {
                return Promise.resolve(element.querySelector('span').textContent);
            })).to.be(text);

            await list[2].evaluate((element) => {
                element.querySelector('input').click();
            });
            expect(await page.$$('li[ng-repeat="todo in todoList.todos"] .done-true')).to.have.length(2);
        });
    });
});

Puppeteer を使う際の注意点

Puppeteer を使ったテストを書くにあたって参考にしたのは README.md と、そこからリンクされている API Documentation の 2 つです。
これらの情報があれば、特にハマるようなところは無いと思います。

また、今回は確認していないのですが、問題がある場合は GitHub の issueexample ディレクトリ のファイルが参考になると思います。

他にも、これは確認しておくといいかなと思った情報を、いくつか挙げておきますので参考にしてください。

ElementHandle

page.$() メソッドを使って要素を取得すると ElementHandle が返ります。
この ElementHandle ですが、page のように「$」や「$$」といったメソッドを持っていません。
そのため、次のようにクエリーセレクターである要素を取得したあとで、さらにクエリーセレクターを使い要素を絞り込むような書き方ができません。

const list = await page.$$('li'),
    button = await list[2].$('button');
button.click();

上のコードの listbutton の両方を取得したいときには、セレクタをこのように指定して取得することになります。

const list = await page.$$('li'),
    button = await page.$('li:nth-child(3) button');
button.click();

また、取得した要素をマウスでクリックしたいといったケースでは ElementHandle.evaluate() を使うという方法もあると思います。

const list = await page.$$('li'),
    list[2].evaluate((element) => {
        element.querySelector('button').click();
    });

テキストの取得

フロントエンドのテストを書くときに、タグに含まれるテキストを取得したいという状況は多いです。
しかし、残念ながら Puppeteer には pageElementHandle などからテキストを取得するメソッドがありません。

テキストを取得したい場合は ElementHandle.evaluate() を使って、次のように書くことになります。
HTML を取得したい場合も同様に innerHTML を返すようにします。

const title = await page.$('title'),
    text = title.evaluate((element) => {
        return element.textContent;
    });

.length / .click()

これは Puppeteer を使う際の注意点というよりは Promise の話になります。
クエリーセレクターでボタンを取得してクリックしたいときに、こんな感じで書けるときれいです。

await page.$('button').click();

もちろん、このコードは思うように動作しません。
これだと Pending 状態の Promise の length を取得しようとしたり、click メソッドを呼び出してしまうことになるためです。
少し冗長に見えますが、次のようにして、先に Promise の解決をしてやる必要があります。

const button = await page.$('button');
button.click();

まとめ

さて、今回は Puppeteer を使ってフロントエンドのテストを書いてみました。
Puppeteer は公開されてから、まだそれほど経っていないライブラリとのことですが、実際にコードを書いてみたところ完成度も高く感じました。

この記事を書く 2か月ほど前には「Nightmare を使ったフロントエンドのテスト」という記事を書いています。
その記事の中で Nightmare が良さそうに思うと書いたばかりではありますが、今回 Puppeteer を試してみて、Puppeteer に乗り換えることを決めました。

機能を比べると Nightmare と Puppeteer のどちらも、テストを書くには十分な機能を持っているように見えます。
上の記事では Nightmare のメリットとして Headless Chrome を自分で立ち上げる必要が無く、ヘッドレスではない Chrome が起動中でも起動できる点を挙げました。
しかしこれは、Puppeteer を使うことでも実現することができます。

Puppeteer を使う場合システムにインストールされている Chrome ではなく、インストール時にダウンロードした Chromium が使われます。
また puppeteer.launch() で Chromium が立ち上がり browser.close() で終了します。
そのため、自分で Chromium を起動する必要はないし、システムにインストールされている Chrome が起動していても問題ありません。

機能で選ぶのであれば、どちらを選んでもそれほど変わらないのではないでしょうか。
それでも私が Puppeteer に乗り換えようとしている理由は、Puppeteer を使ったテストのコードが自分の好みに近いということが大きいです。

Nightmare ではメソッドチェインをつなげて than で受けるような形になるため、どうしてもネストが深くなってしまいますし、少し読みにくく感じてしまいます。
その点、Puppeteer は Promise を返すように設計されているため、async/await と組み合わせることでシンプルに書くことができます。

プログラムのコードは書くことよりも読まれることの方が圧倒的に多いため、コードをきれいに、読みやすく書けるというのは、大きなメリットではないでしょうか。

今のところ導入予定はないですが、折角リリースされているので、Android Pay APIについて調べてみました。

そもそもAndroid Payとは

Android Payとは電子マネー・ポイントカード・クレジットカードをAndroidデバイスに登録し、NFCを経由して電子マネーを使用したり、ポイントを追加・使用したり、クレジットカード決済を利用することができるサービスとなります。
上記のサービスを利用するために、Googleが公開しているアプリ「Android Pay」をインストールする必要があります。ただし、未だ日本ではクレジットカード会社との提携が完了していないのか、クレジットカード決済をAndroid Payから直接使用することはできないようです(2017.06現在)

サービスとしては上記のアプリにより提供されているのですが、Android Payとしてのクレジットカード決済機能を3rdPartyアプリから利用できるようにAPIが公開されています。(電子マネーやポイントカードについては公開されていない様子: 2017.06現在)。このAPIが今回のブログのターゲットとなります。

Android PayとGoogle Play In-app Billing

アプリでの料金支払いは、これまで(そしてこれからも)Google Play In-app Billingがありますが、Android Payがサービスインされたことにより、支払い方法が2パターン用意されたことになります。
Googleは以下のように記載しています。
If you wish to:
Have Google process payments for you, or
Sell digital goods such as movies or games
within your app, please use Google Play In-app Billing instead.
"https://developers.google.com/android-pay/get-started"より
なので、既にGoogle Play In-app Billingを実装済みだったり、販売するものがデジタルコンテンツだったりする場合は、これまで通りGoogle Play In-app Billingを使用するのがよいとのことです。

ただし、Google Play In-app Billingは売り上げの30%を手数料としてGoogleに支払う必要があるので、クレジットカード会社と契約している企業においては、手数料の兼ね合いでどちらを使用すべきかを検討するのも面白いかと。
なお、Android Payも競合するApple PayもGoogle(出典)/Apple(出典)への手数料は発生しないようです。

アプリからのAPIコール

アプリからAndroid PayのAPIをコールして、料金を徴収する方法です。 ただし、残念ながら、日本ではクレジットカード会社との提携が完了していないのか、クレジットカード決済は使用できないので、以下に記載する内容は試せないです(2017.06現在)。
上記の点が、調査を進めている途中で分かり、記事にするのを止めようかと思いましたが、とりあえずGoogleが公開しているサンプルアプリとそれに対する説明からコード解析を行いたいと思います。

コードを見たところ、要点は以下の点となります。
  1. Android Pay APIを使用できる状態か否かの判定
  2. 支払いに"Android Pay"を使用することを選択するためのボタンを表示
  3. "Android Pay"で実際に支払いを行う

Android Pay APIを使用できる状態か否かの判定

必ず実施しないとならない機能ではない(使用できないときは、その先でエラーになる)です。
com.google.android.gms.wallet.Wallet.Payments.isReadyToPay()の結果をResultCallbackインスタンスで受けることができます。
コード: com.google.android.gms.samples.wallet.CheckoutActivity.onCreate
    @Override
    protected void onCreate(Bundle savedInstanceState) {
                      :省略
        // Android Payの使用可否を確認するためのGoogleApiClientインスタンスの生成と設定
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addApi(Wallet.API, new Wallet.WalletOptions.Builder()
                        .setEnvironment(Constants.WALLET_ENVIRONMENT)
                        .build())
                .enableAutoManage(this, this)
                .build();
                      :省略
        Wallet.Payments.isReadyToPay(mGoogleApiClient).setResultCallback(
                new ResultCallback<booleanresult>() {
                    @Override
                    public void onResult(@NonNull BooleanResult booleanResult) {
                        hideProgressDialog();

                        if (booleanResult.getStatus().isSuccess()) {
                            if (booleanResult.getValue()) {
                                // ★Android Payが使える
                            } else {
                                // ★Android Payが使えない
                            }
                        } else {
                            // Error making isReadyToPay call
                            Log.e(TAG, "isReadyToPay:" + booleanResult.getStatus());
                        }
                    }
                });
    }

支払いに"Android Pay"を使用することを選択するためのボタンを表示

先の確認により、動作しているデバイス上でAndroid Payが使用できることが判明したので、ユーザにAndroid Payでの支払い方法があることを以下のようなボタンにより提示します。
Google社公開のアプリ"androidpay-quickstart"の
スクリーンショットを加工

コード: com.google.android.gms.samples.wallet.CheckoutActivity.createAndAddWalletFragment
// [START fragment_style_and_options]
WalletFragmentStyle walletFragmentStyle = new WalletFragmentStyle()
        .setBuyButtonText(WalletFragmentStyle.BuyButtonText.BUY_WITH)
        .setBuyButtonAppearance(WalletFragmentStyle.BuyButtonAppearance.ANDROID_PAY_DARK)
        .setBuyButtonWidth(WalletFragmentStyle.Dimension.MATCH_PARENT);

WalletFragmentOptions walletFragmentOptions = WalletFragmentOptions.newBuilder()
        .setEnvironment(Constants.WALLET_ENVIRONMENT)
        .setFragmentStyle(walletFragmentStyle)  // ★設定したWalletFragmentStyleをWalletFragmentOptionsインスタンスへ設定
        .setTheme(WalletConstants.THEME_DARK)
        .setMode(WalletFragmentMode.BUY_BUTTON)
        .build();

このAndroid Payボタンを表示する際に、以下の設定を行えます。
  • Android Payボタンの見た目
  • Android Payボタンに表示する文字列
  • Android Payボタンが格納されているフラグメントの幅・高さ
  • Android Payが動作する環境

Android Payボタンの見た目

API: com.google.android.gms.wallet.fragment.WalletFragmentStyle.setBuyButtonAppearance(int)
WalletFragmentStyle.BuyButtonAppearance.
ANDROID_PAY_DARK
WalletFragmentStyle.BuyButtonAppearance.
ANDROID_PAY_LIGHT
WalletFragmentStyle.BuyButtonAppearance.
ANDROID_PAY_LIGHT_WITH_BORDER

Android Payボタンに表示する文字列

API: com.google.android.gms.wallet.fragment.WalletFragmentStyle.setBuyButtonText(int)
WalletFragmentStyle.BuyButtonText.
BUY_WITH
WalletFragmentStyle.BuyButtonText.
LOGO_ONLY
WalletFragmentStyle.BuyButtonText.
DONATE_WITH
またWalletFragmentOptions.newBuilder.setModeで設定する値は、Android Payボタンを表示する際の値は、WalletFragmentMode.BUY_BUTTON固定とする必要があります。変更すると以下のような表示になります。

Android Payボタンが格納されているフラグメントの幅・高さ

API(pixel指定): com.google.android.gms.wallet.fragment.WalletFragmentStyle.setBuyButtonWidth/Height(int width)
設定したい幅・高さをPixelで指定。ただし、以下の値を設定することで相対設定が可能です。
  • WalletFragmentStyle.Dimension.MATCH_PARENT
  • ViewGroup.LayoutParams.MATCH_PARENTと同じような動作になる。
  • WalletFragmentStyle.Dimension.WRAP_CONTENT
  • ViewGroup.LayoutParams.WRAP_CONTENTと同じような動作になる。
API(値・単位指定): com.google.android.gms.wallet.fragment.WalletFragmentStyle.setBuyButtonWidth/Height(int unit, float width)
設定する単位とを指定することで設定。単位は以下のものが使用可能です。
単位名称概要
WalletFragmentStyle.Dimension.UNIT_DIPdip(device independent pixels)指定
WalletFragmentStyle.Dimension.UNIT_INインチ指定
WalletFragmentStyle.Dimension.UNIT_MMミリメートル指定
WalletFragmentStyle.Dimension.UNIT_PTポイント指定
WalletFragmentStyle.Dimension.UNIT_PXピクセル指定
WalletFragmentStyle.Dimension.UNIT_SPsp(scaled pixel)指定


Android Payが動作する環境

API: com.google.android.gms.wallet.fragment.WalletFragmentOptions.setEnvironment(int)
名称概要
WalletConstants.ENVIRONMENT_PRODUCTION本番環境。クレジットカード会社と契約された企業との取引以外できないと思われる
WalletConstants.ENVIRONMENT_TESTテスト環境。動作確認などに使用できる
WalletConstants.ENVIRONMENT_SANDBOX非推奨の値。WalletConstants.ENVIRONMENT_TESTを使用すべきとのこと
WalletConstants.ENVIRONMENT_STRICT_SANDBOX非推奨の値。WalletConstants.ENVIRONMENT_TESTを使用すべきとのこと

上記の設定を行うことでボタンの設定を行うことができるので、それをフラグメント化します。
コード: com.google.android.gms.samples.wallet.CheckoutActivity.createAndAddWalletFragment
mWalletFragment = SupportWalletFragment.newInstance(walletFragmentOptions);

MaskedWalletを取得するために、MaskedWalletRequestインスタンスを生成。
コード: com.google.android.gms.samples.wallet.CheckoutActivity.createAndAddWalletFragment
// Now initialize the Wallet Fragment
String accountName = ((BikestoreApplication) getApplication()).getAccountName();
MaskedWalletRequest maskedWalletRequest;
if (mUseStripe) {
    // Stripe integration
    maskedWalletRequest = WalletUtil.createStripeMaskedWalletRequest(
            Constants.ITEMS_FOR_SALE[mItemId],
            getString(R.string.stripe_publishable_key),
            getString(R.string.stripe_version));
} else {
    // Direct integration
    maskedWalletRequest = WalletUtil.createMaskedWalletRequest(
            Constants.ITEMS_FOR_SALE[mItemId],
            getString(R.string.public_key));
}

作成したMaskedWalletRequestをフラグメントに設定するために、WalletFragmentInitParamsをインスタンス化し、それを初期パラメータとしてフラグメントに設定。
また、このとき、WalletFragmentInitParams.Builder.setMaskedWalletRequestCodeによりAndroid Payボタン押下の結果をActivity.onActivityResultで受ける際のリクエストコードを指定します。
コード: com.google.android.gms.samples.wallet.CheckoutActivity.createAndAddWalletFragment
WalletFragmentInitParams.Builder startParamsBuilder = WalletFragmentInitParams.newBuilder()
        .setMaskedWalletRequest(maskedWalletRequest)  // ★WalletFragmentInitParamsにMaskedWalletRequestインスタンスを設定
        .setMaskedWalletRequestCode(REQUEST_CODE_MASKED_WALLET)
        .setAccountName(accountName);
mWalletFragment.initialize(startParamsBuilder.build()); // ★フラグメントにWalletFragmentInitParamsインスタンスを設定

設定が完了したフラグメント(未だボタンを表示している状態)を表示するために、リソースID: R.id.dynamic_wallet_button_fragmentにフラグメントを設定。
これによりAndroid Payボタンが表示されます。このボタンをユーザが選択すると、このフラグメントで隠蔽された中で処理され、その結果がActivity.onActivityResultに通知されます。
この際に指定されるリクエストコードは、先にWalletFragmentInitParams.Builder.setMaskedWalletRequestCodeで設定した値となります。
Activity.onActivityResultでこのリクエストコードをハンドリングして、成功した際は引数で渡されたIntentからMaskedWalletを取得できるので、それを使用してユーザに使用するクレジットカード情報の確認を行います。
※ サンプルアプリでは、このタイミングでConfirmationActivity(引数に取得したMaskedWalletインスタンス)を表示します。
コード: com.google.android.gms.samples.wallet.CheckoutActivity.createAndAddWalletFragment
// add Wallet fragment to the UI
getSupportFragmentManager().beginTransaction()
        .replace(R.id.dynamic_wallet_button_fragment, mWalletFragment)  // ★フラグメントを表示
        .commit();

"Android Pay"で実際に支払いを行う

取得したMaskedWalletの情報をユーザに確認してもらい、使用するクレジットカードや商品の配送先を決定する契機になります。
なお、使用するクレジットカードや配送先の表示については、SDKにて表示してくれます。
https://developers.google.com/android-pay/payment-flows
上記のサイトから引用

上記の画像内の境界線から"Confirm order"ボタンより上までのクレジットカード情報と商品の配送先情報を表示するフラグメントを生成します。
その際、以下の設定が可能となっています。
  • クレジットカード情報と商品の配送先情報の文字サイズや文字色
  • クレジットカード情報と商品の配送先情報のヘッダ部分の文字サイズや文字色
  • クレジットカード情報と商品の配送先情報内のボタンの背景
  • クレジットカード情報と商品の配送先情報の背景
  • ロゴ画像
定義されているリソースIDや値を設定することでカスタマイズが可能となっています。
ロゴ画像は以下のものが設定可能ですが、非推奨を除くとWalletFragmentStyle.LogoImageType.ANDROID_PAYのみとなります。
名称概要
WalletFragmentStyle.LogoImageType.ANDROID_PAYAndroid Payのロゴマーク。
 
WalletFragmentStyle.LogoImageType.GOOGLE_WALLET_CLASSIC 非推奨の値。
恐らく、このようなマーク。
WalletFragmentStyle.LogoImageType.GOOGLE_WALLET_MONOCHROME 非推奨の値。
恐らく、このようなマーク。



クレジットカード情報と商品の配送先情報の文字サイズや文字色などの設定
コード: com.google.android.gms.samples.wallet.ConfirmationActivity.createAndAddWalletFragment
WalletFragmentStyle walletFragmentStyle = new WalletFragmentStyle()
        .setMaskedWalletDetailsTextAppearance(
                R.style.BikestoreWalletFragmentDetailsTextAppearance)
        .setMaskedWalletDetailsHeaderTextAppearance(
                R.style.BikestoreWalletFragmentDetailsHeaderTextAppearance)
        .setMaskedWalletDetailsBackgroundColor(
                getResources().getColor(R.color.bikestore_white))
        .setMaskedWalletDetailsButtonBackgroundResource(
                R.drawable.bikestore_btn_default_holo_light);

モードに"WalletFragmentMode.SELECTION_DETAILS"を設定することで、クレジットカード情報や商品の配送先情報の選択用のフラグメントになります。
コード: com.google.android.gms.samples.wallet.ConfirmationActivity.createAndAddWalletFragment
WalletFragmentOptions walletFragmentOptions = WalletFragmentOptions.newBuilder()
        .setEnvironment(Constants.WALLET_ENVIRONMENT)
        .setFragmentStyle(walletFragmentStyle)
        .setTheme(WalletConstants.THEME_LIGHT)
        .setMode(WalletFragmentMode.SELECTION_DETAILS)
        .build();

作成したWalletFragmentOptionsを使用してフラグメントを作成
コード: com.google.android.gms.samples.wallet.ConfirmationActivity.createAndAddWalletFragment
mWalletFragment = SupportWalletFragment.newInstance(walletFragmentOptions);

取得したMaskedWalletをフラグメントに設定するために、WalletFragmentInitParamsをインスタンス化し、それを初期パラメータとしてフラグメントに設定。
このとき、WalletFragmentInitParams.Builder.setMaskedWalletRequestCodeにより同フラグメントからActivity遷移が発生したときに通知されるリクエストコードを指定します。
また、使用するログイン済みGoogleアカウント名称(メールアドレス)を一緒に設定することができますが、設定していない場合でも、その後選択可能なようです。
コード: com.google.android.gms.samples.wallet.ConfirmationActivity.createAndAddWalletFragment
// Now initialize the Wallet Fragment
String accountName = ((BikestoreApplication) getApplication()).getAccountName();
WalletFragmentInitParams.Builder startParamsBuilder = WalletFragmentInitParams.newBuilder()
        .setMaskedWallet(mMaskedWallet)
        .setMaskedWalletRequestCode(REQUEST_CODE_CHANGE_MASKED_WALLET)
        .setAccountName(accountName);
mWalletFragment.initialize(startParamsBuilder.build());

設定が完了したフラグメントを表示するために、リソースID: R.id.dynamic_wallet_masked_wallet_fragmentにフラグメントを設定。
これによりクレジットカード情報や商品の配送先情報が表示されます。 コード: com.google.android.gms.samples.wallet.ConfirmationActivity.createAndAddWalletFragment
// add Wallet fragment to the UI
getSupportFragmentManager().beginTransaction()
        .replace(R.id.dynamic_wallet_masked_wallet_fragment, mWalletFragment)
        .commit();

ユーザが確認を終えて、"Confirm order"ボタンを押下すると、FullWalletを取得するとために、FullWalletRequestインスタンスを作成します。
このとき、ユーザが選択したWallet情報がMaskedWalletとして取得している(変更されたときはActivity.onActivityResultで通知され更新している)ので、そこに設定されているGoogleTransactionIDを取得し、FullWalletRequestに設定する。
また、この際請求する金額を商品の金額、配送費、税金の項目に分割して、それぞれをLineItemインスタンス化する。
コード: com.google.android.gms.samples.wallet.WalletUtil.createFullWalletRequest
public static FullWalletRequest createFullWalletRequest(ItemInfo itemInfo,
        String googleTransactionId) {

    List lineItems = buildLineItems(itemInfo, false);

    String cartTotal = calculateCartTotal(lineItems);

    // [START full_wallet_request]
    FullWalletRequest request = FullWalletRequest.newBuilder()
            .setGoogleTransactionId(googleTransactionId)
            .setCart(Cart.newBuilder()
                    .setCurrencyCode(Constants.CURRENCY_CODE_USD)
                    .setTotalPrice(cartTotal)
                    .setLineItems(lineItems)
                    .build())
            .build();
    // [END full_wallet_request]

    return request;
}

取得したFullWalletRequestを使用して、com.google.android.gms.wallet.Wallet.Payments.loadFullWalletを用いて、FullWalletの取得を試みる。
この際、結果を受けるためのリクエストコードを指定する。
コード: com.google.android.gms.samples.wallet.FullWalletConfirmationButtonFragment.getFullWallet
private void getFullWallet() {
    FullWalletRequest fullWalletRequest = WalletUtil.createFullWalletRequest(mItemInfo,
            mMaskedWallet.getGoogleTransactionId());

    // [START load_full_wallet]
    Wallet.Payments.loadFullWallet(mGoogleApiClient, fullWalletRequest,
            REQUEST_CODE_RESOLVE_LOAD_FULL_WALLET);
    // [END load_full_wallet]
}

FullWalletの取得要求の結果が、Activity.onActivityResultに渡される。成功していれば、渡されたIntentからFullWalletを取得できる。
このFullWalletに請求に必要な全クレジットカード情報が暗号化されて記載されているので、それを実際にクレジットカード会社に請求する自社サーバなどに送信して請求することが求められています。
FullWalletに記載されている情報について、長くなったので別途記載します。

参考文献:
https://developers.google.com/android-pay/tutorial
https://developers.google.com/android-pay/diagrams
https://codelabs.developers.google.com/codelabs/android-pay/index.html
Google VRでのSpatial Audio(音再生)についてです。

Spatial Audioとは

VRは名前のとおり、仮想的な現実空間を構築することを目的としています。現実空間においては音源の方向に向いていれば、音ははっきり聞こえるし、壁や床からの反射音も含めた音を聞いています。
VRで聞こえる音についても音源からの距離や向きなどにより聞こえ方が変わることを目的とした機能となります。

Unityの"Spatializer Plugin"の設定

最初に、この"Spatializer Plugin"の設定を行う必要があります。
これを設定していないと、音空間の設定が出来ないためか、全く音が聞こえないです。
(これを知らなかったために約1日悩みましたorz)

設定はUnityのメニュー"Edit" - "Project Settings" - "Audio"の"Spatializer Plugin"を「GVR Audio Spatializer」に変更します。

GvrAudioSourceを追加

Unityでは音源を追加するとき、AudioSourceを使用していますが、Google VRで音を再生する場合もAudioSourceで音を再生することができます。ただし、Spatial Audioに対応する場合、GvrAudioSourceで音を再生させる必要があります。
※ AudioSourceでは音は聞こえますが、Spatial Audioに対応していないため、音源に近づいても音量が増加しないです。
GvrAudioSourceの使い方はAudioSourceとほぼ同じで、GameObjectにアタッチして、「AudioClip」プロパティに音源ファイルを設定することで使用可能となります。

GvrAudioListenerをGameObjectにアタッチ

Spatial Audioが有効な音を聞くために、GvrAudioListenerをアタッチする必要があります。 ここを見ると、AudioListenerをアタッチしているGameObjectにGvrAudioListenerをアタッチする必要があるように見えますが、他のGameObjectにアタッチしても音自体は聞こえます。ただ、通常はAudioListenerと同じGameObjectにアタッチするのが良いと思いますが。 また、AudioListenerはそのままアタッチしておく必要があります。外すとSpatial Audioが無効になってしまいます。

サンプルアプリ

ここにサンプルアプリを置きました。
試してみる場合は、以下の手順で確認してみてください。
  1. ここから環境をクローン
  2. サブモジュールが2つ組み込まれているので、それぞれを取り込む
  3. ブランチをタグ"v0.0.2"に切り替える
  4. 「Google VR for Unity」(v1.7.00)のパッケージをここから取得し、先の環境にインポート
  5. 環境をビルドし、アプリをインストール
  6. 起動するとメニューが表示されるので「Spatial Audio」を選択
  7. タッチパッドをクリックすると灰色の直方体が表示され、そこから猫の鳴き声が聞こえます(直方体をクリックすると直方体が消えて、鳴き声も聞こえなくなります)

image-ghost とは

image-ghost は複数の画像を一括でリサイズできるツールで、実行環境には Node.jsGraphicsMagick が必要です。npm 経由で image-ghost をインストールすることができます。
インストールの仕方についてはこちらから確認できます。

更新内容

image-ghost の version 1.0.8 をリリースしました。対応した点は以下になります。

コマンド名で実行できるようになった

npm install <name> の実行時にグローバルインストールの場合は {prefix}/lib/node_modules/(※1) 、ローカルインストールの場合は ./node_modules/.bin にシンボリックリンクが作成されるようになりました。

画像が一枚の場合

グローバルインストール

  1. imageghost-resize -w 290 ./readPath/imgs/dummy.png ./exportPath/image.png

ローカルインストール

  1. ./node_modules/.bin/imageghost-resize -w 290 ./readPath/imgs/dummy.png ./exportPath/image.png

画像が複数の場合

グローバルインストール

  1. imageghost-task ./readPath/task.json

ローカルインストール

  1. ./node_modules/.bin/imageghost-task ./readPath/task.json
※1
{prefix}は npm prefix [-g] を叩いた時に出力される値のことです。


image-ghost の使い方について

https://developers.blog.eq-inc.jp/2017/05/image-ghost.html

npm ライブラリ

https://www.npmjs.com/package/@eq-inc/image-ghost

今回は前回記載したRoboテストを実施した結果について記載します。
Robo試験で試験すると、以下のような画面が表示されます。

ただし、2017.06現在、仮想端末で試験を実施すると、以下のように必ず"テストでの問題"が発生するようです。エラー内容見て試験と関係なさそうでしたら無視しましょう。

今回Robo試験に使用したアプリは以下のような画面遷移を行えるものとなります。
第一階層
第二階層
文字入力欄を持つActivity
ListViewを持つActivity
ScrollViewを持つActivity
ListViewにSpinnerが子要素として含まれるActivity
SpinnerのみのActivity
第三階層
文字入力結果を確認するActivity


また以下に記載しているlogcatログやスクリーンショットや動画などは画面右上辺りの「ソースファイルを表示」を選択することで表示される「Google Cloud Platform」から取得可能です。

ログ


logcatログが出力されます。特筆すべきことはありませんので、省略。

スクリーンショット


画面遷移時や指定された文字の入力時などに取得されるようです。また取得されたスクリーンショットの並び方は画面遷移順とは関連がないものとなっています。
なので、手順の詳細を確認したいときは、動画を確認した方がよいです。

アクティビティマップ


アクティビティ間の遷移図のようなものが表示されます。全てのアクティビティに意図通り遷移したかの確認がすぐに出来ます。

動画


試験時の操作が全て記録された動画になります。意図したように操作されていたかも後追いで確認が可能となっています。

パフォーマンス


試験実施中のCPU使用率、メモリ使用量、通信量が記録され、時系列でそれぞれの値を確認することができます。
仮想端末では参照できないので注意が必要です。

注意点

  • 試験が複雑だとデフォルトの試験タイムアウト時間: 5分で終わらずに試験が中断されてしまうことがある
  • 特にEditTextが多いと予想以上に時間が掛かるようで、以下のようなActivityを持つアプリで試験を実施したとき、このActivityだけで試験が終わってしまいました。
Google VRでコントローラからのインプットについてです。

開発環境

名称バージョン
Unity 5.6.2f1 64bit
Google VR for Unity(GVR Unity) 1.7.00

コントローラの部位の名称と役割

先にコントローラの部位の名称を明記します。なぜなら、クリックやタッチが混在しているため、分かり難いからです。

コントローラの部位の名称は以下のようになります。
※ Daydream Viewに付属されているコントローラで説明していますが、Daydreamに準拠しているViewerであれば同じはずです。

名称 概要
タッチパッド VR上でスクロールやクリックする際に使用 本部位をカチッとなるまで押下するとクリックイベントが発生し、そのクリックイベントによってVR上のボタンを押下することができる
Appボタン アプリで自由に使用できるボタン
Daydreamアプリではバックボタンとして動作するように定義している
HOMEボタン VR上のホームアプリ(Daydreamアプリ)を呼び出すボタン
通常使用(非VR)でのHOMEボタンと同じ役割をVR上で行う
ボリュームダウンボタン 音量を下げるボタン
ボリュームアップボタン 音量を上げるボタン

タッチパッドで行える操作は大きく、以下の2点あります。
  • クリック
  • タッチ

クリック

タッチパッドをカチッと音が鳴るまで押下することでクリックイベントが発生します。
クリックイベントの発生有無は以下のプロパティにて検出可能です。
クリック状態 GvrController.
ClickButton
GvrController.
ClickButtonDown
GvrController.
ClickButtonUp
クリックダウン True True False
クリックアップ
(クリックが終わった直後)
False False True
クリックなし False False False

クリックした位置の取得方法が若干面倒でした。正しいやり方なのか分かりませんが、GvrLaserPointerのreticleのpositionを使用するようしました。
reticleは以下の部分を表示しているGameObjectとなっています。また、reticleは表示しないことも可能なので、その場合はLineEndPointを使用するようにしました。
実装イメージ:
  public Vector3 GetPointerPosition()
  {
    GameObject controllerPointerGO = GameObject.Find("GvrControllerPointer");
    Transform laserTF = controllerPointerGO.transform.Find("Laser");
    GameObject laserGO = laserTF.gameObject;
    GvrLaserPointer laserPointer = laserGO.GetComponent();

    return laserPointer.reticle != null ? laserPointer.reticle.transform.position : laserPointer.LineEndPoint;
  }

タッチ

タッチパッドという名称の通り、タッチ(指がタッチパッドに触れている状態)も検出可能となっています。
タッチイベントの発生有無は以下のプロパティにて検出可能です。
タッチ状態 GvrController.
IsTouching
GvrController.
TouchDown
GvrController.
TouchUp
タッチ中 True True False
タッチ終了
(タッチが終わった直後)
False False True
タッチなし False False False
タッチパッド上のタッチしている位置はGvrController.TouchPosにて取得可能です。
またこの値は、以下の座標系における位置となります。

サンプルアプリ

ここにサンプルアプリを置きました。
試してみる場合は、以下の手順で確認してみてください。
  1. ここから環境をクローン
  2. サブモジュールが2つ組み込まれているので、それぞれを取り込む
  3. ブランチをタグ"v0.0.1"に切り替える【2017.08.31追記】
  4. 「Google VR for Unity」(v1.7.00)のパッケージをここから取得し、先の環境にインポート
  5. 環境をビルドし、アプリをインストール
  6. 起動するとメニューが表示されるので「Touchpad Test」を選択
  7. タッチパッドをクリックすると緑色の球体がクリックした位置に表示され、それをクリックすると消えます