googleglass

Glassware (Live Card)のスケルトン

More than 1 year has passed since last update.

Google GlassでTimeline上で動的にコンテンツを表示するLive Cardを使ったアプリケーションのつくりかたについてまとめました。

Live Cardとは

Google Glassのアプリケーション(glassware)には、大きな分類として以下の2つがあります。
1. TimelineにCardとして表示するアプリケーション(Live Card)
2. Timelineとは関係なく実行するアプリケーション(Immersion)
ここでは、これらのうちより基本的なアプリケーションであるLive Cardアプリケーションを作ってみることにします。

なお、この記事で作成したLive Cardはgithubに公開しているので、スケルトンとして自由に使用してください。
https://github.com/blackaplysia/livecard

Live Cardアプリケーションの基本的な構造

まず、典型的なLive Cardアプリケーションは以下に図示するような構造をとります。

livecard-summary.png

Google Glassを起動、あるいはグラスをかけることでスリープから戻ると、まず時刻と"ok glass"と表示された画面が表示されます。ここから左右にスワイプする(操作としてはglassの右のフレームを指で前後になぞる)ことでさまざまな画面が表示されますが、このそれぞれの画面をcard、全体の流れをtimelineと呼びます。Timelineは最近更新されたものほど左に表示され、もっとも左にはシステム設定があります。

Live Cardは、このtimeline上のcardにコンテンツを表示するglasswareです。それでは、このglasswareがどのような画面遷移になるのかを説明しましょう。

まず、"ok glass"のcardで"ok glass"と呼びかけると、main voice menuが表示されます。これはインストールされているglasswareのリストです。このリストの中から、該当する表示どおりに"google"、"take a picture"などと呼びかけると、該当するglasswareが呼び出されます。このしくみがvoice triggerです。なお、画面遷移は必ずしも音声を使わずとも、タップとスワイプだけで遷移することができますが、voice triggerを登録していなければリストに表示されず、したがって、glassの操作によって起動することができません。

Live Cardはパッケージが任意のタイミングでコンテンツを更新するため、大抵の場合、activityではなくserviceとして実装します。Serviceからcardを更新する方法は頻度によって2つの方法が推奨されています。1つは更新頻度が高くない場合で、RemoteViewを使います。通常のandroid端末でnotificationを更新する場合と同様の方法です。いまひとつはDirectRenderingCallback (OpenGLの場合はGlRenderer)を実装する方法です。今回はRemoteViewを使用します。

また、glasswareは通常cardをタップするとオプションメニューを表示します。そこで、そのためのmenu activityが必要となります。Menu activityの実装には、glassware特有のことは何もありません。

Androidコマンドによる前準備

今回は、glasswareの特徴を明確にするために、普通のandroidアプリケーションのテンプレートを変更していくことにします。

まず、Android SDKのandroidコマンドでプロジェクトを生成します。

$ mkdir livecard
$ cd livecard
$ android -v create project -n livecard -p . -t "Google Inc.:Glass Development Kit Preview:19" -k com.blackaplysia.livecard -a LiveCardMenuActivity

以下のようなファイル群が生成されます。

$ tree .
----------------------------------------------------------------
.
├── AndroidManifest.xml
├── ant.properties
├── bin
├── build.xml
├── libs
├── local.properties
├── proguard-project.txt
├── project.properties
├── res
│   ├── drawable-hdpi
│   │   └── ic_launcher.png
│   ├── drawable-ldpi
│   │   └── ic_launcher.png
│   ├── drawable-mdpi
│   │   └── ic_launcher.png
│   ├── drawable-xhdpi
│   │   └── ic_launcher.png
│   ├── layout
│   │   └── main.xml
│   └── values
│       └── strings.xml
└── src
    └── com
    └── blackaplysia
            └── livecard
                └── LiveCardMenuActivity.java

13 directories, 13 files
----------------------------------------------------------------

Goole Glassは今のところ1種類しかありませんので、さまざまな端末用のdrawableリソースは不要です。1つを汎用のディレクトリ res/drawable に移動し、それ以外を削除します。

$ mv res/drawable-hdpi/ res/drawable
$ rm -r res/drawable-xhdpi
$ rm -r res/drawable-mdpi
$ rm -r res/drawable-ldpi

次に、リリースのためのキーストアを生成します。キーストアとエイリアス名は ant.properties に追記しておきましょう。

$ keytool -genkeypair -v -keystore ../.keystore/livecard_keystore -storepass pass -dname "CN=blackaplysia, OU=Unknown, O=blackaplysia, L=shinjuku, ST=tokyo, C=jp" -alias livecard -keypass pass -keyalg RSA -validity 10000
$ echo "key.store=../.keystore/livecard_keystore" >> ant.properties 
$ echo "key.alias=livecard" >> ant.properties

Glasswareの実装

ここまでは、通常のandroidアプリケーションととくに違いがありませんでした。ここからは、glasswareの実装をしていきます。

レイアウト

まず最初にレイアウト( res/layout/main.xml )を変更します。今回の例はスケルトンですので、layoutは最低限の修正のみ施しています。Androidコマンドが生成したファイルとの差異を表示してみましょう。

diff --context ../original/res/layout/main.xml res/layout/main.xml 
*** ../original/res/layout/main.xml 2014-05-30 08:43:14.097590445 +0900
--- res/layout/main.xml 2014-06-03 09:02:40.079083868 +0900
***************
*** 3,13 ****
--- 3,16 ----
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
+     android:background="@color/black"
      >
  <TextView
+     android:id="@+id/contents"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:text="Hello World, LiveCardMenuActivity"
+     style="@style/CardText"
      />
  </LinearLayout>

TextViewを更新するためのidを設定したほか、背景色を@color/blackに、テキストのスタイルを@style/CardTextにしています。(それぞれ、 res/values/colors.xmlres/values/styles.xml に定義)

res/values/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="black">#ff000000</color>
  <color name="white">#ffffffff</color>
</resources>
res/values/styles.xml
  <style name="CardText">
    <item name="android:textColor">@color/white</item>
    <item name="android:textSize">54px</item>
  </style>

Service

次にserviceを実装します。Serviceの実装は、起動時処理、終了時処理、更新処理の3点です。

onStartCommand()

Cardに対応するRemoteViewを準備し、setViews()でcardに紐付けます。同様に、menu activityはPendingIntentの対象としてsetAction()で紐付けます。最後に、このService自体をattach()でcardに紐付け、publish()します。

LiveCardService.java
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        Log.i(getServiceTag(), "LiveCard Service started");

        if (_card == null) {
            _card = new LiveCard(this, CARD_TAG);
            _views = new RemoteViews(getPackageName(), R.layout.main);
            _card.setViews(_views);

            Intent menuIntent = new Intent(this, LiveCardMenuActivity.class);
            menuIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
            _card.setAction(PendingIntent.getActivity(this, 0, menuIntent, 0));
            _card.attach(this);
            _card.publish(PublishMode.REVEAL);
            update();
        } else {
            _card.navigate();
        }

        return START_STICKY;
    }

onDestroy()

onDestroy()では、cardをunpublish()します。

LiveCardService.java
    @Override
    public void onDestroy() {
        if (_card != null && _card.isPublished()) {
            _card.unpublish();
            _card = null;
        }
        super.onDestroy();
    }

コンテンツの更新

任意のタイミングでRemoteViewに対してコンテンツの更新を行い、setViews()でcardに反映します。今回の例では、menu activityから呼び出すためのupdate()というメソッドを定義しています。LiveCardServiceContentsは、"Contents #<呼び出された回数>"という文字列を返すだけの小さなクラスです。

LiveCardService.java
    public void update() {
        _views.setTextViewText(R.id.contents, LiveCardServiceContents.get());
        _card.setViews(_views);
    }

AndroidManifest.xml

Androidコマンドが生成した AndroidManifest.xml にはserviceに関する記述がありませんので、これを追加しておく必要があります。

AndroidManifest.xml
        <service android:name="LiveCardService"
                 android:label="@string/app_name"
                 android:exported="true">
        ... Voice triggerに関する記述(後述) ...
        </service>

Menu activity

前述したように、menu activityの実装にはglassware特有のことは一切ありません。Android一般と同様のoption menuを実装します。今回の例では、service側でコンテンツを更新するために、IBinderを経由して直接LiveCardServiceのインスタンスを取得し、後でメソッドを呼び出す方法をとっています。

Menu activityには特徴的なことはありませんが、メニューアイテムの見た目に関しては、何も指定しないとこれも通常のandroidアプリによく似た見た目になってしまうため、 AndroidManifest.xml でテーマを指定します。この例では、デフォルトのテーマであるTheme.DeviceDefaultを少し変更したテーマMenuThemeを定義して利用しています。テーマの実体は res/values/style.xml に記述します。

AndroidManifest.xml
        <activity android:name="LiveCardMenuActivity"
                  android:label="@string/app_name"
                  android:theme="@style/MenuTheme"
                  android:exported="true" />
res/values/styles.xml
  <style name="MenuTheme" parent="@android:style/Theme.DeviceDefault">
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:colorBackgroundCacheHint">@null</item>
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowAnimationStyle">@null</item>
  </style>

Voice trigger

最後に、voice triggerを実装します。これは音声認識を活用するわりには非常に簡単で、AndroidManifest.xmlでpermissionおよびintent-filterの設定を行い、triggerの対象となる文字列リソースを res/xml/ に配置するだけです。

AndroidManifest.xml
<manifest>
...
  <uses-permission android:name="com.google.android.glass.permission.DEVELOPMENT" />
...
  <application>
    <service>
...
      <intent-filter>
        <action android:name="com.google.android.glass.action.VOICE_TRIGGER" />
      </intent-filter>
      <meta-data
          android:name="com.google.android.glass.VoiceTrigger"
          android:resource="@xml/voice_trigger_start"
          />
...
    </service>
  </application>
</manifest>
res/xml/voice_trigger_start.xml
<?xml version="1.0" encoding="utf-8"?>
<trigger keyword="@string/voice_trigger_start" />
res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="voice_trigger_start">Card Sample</string>
</resources>

なお、permissionについては、 https://developers.google.com/glass/develop/gdk/starting-glassware に以下のように記述されています。(XE16~XE17で確認)

If you want to use voice commands that are not available in VoiceTriggers.Command, you can request an Android permission to do so.

Note: This feature is for development purposes only, so you cannot launch your Glassware in MyGlass until you submit a new command for approval. Do this as soon as possible, before you finish your Glassware. It takes time for us to approve and build a model for it).

つまり、正式にはGoogleに依頼してVoiceTriggers.Commandに登録してもらう必要があり、それを行わなければ正式なglasswareとしてローンチすることができない、ということです。

実行結果

それでは実行してみます。

$ ant release
$ adb install -r bin/livecard-release.apk

まず、voice triggerは動いているでしょうか。
voice_menu.png
最後にインストールしたものは先頭に出るようです。

一方、"ok glass"をタップした場合は次のような選択画面が表示されます。
voice_trigger_start.png
アイコンは res/drawable/ic_launcher.png です。

この画面をタップ、またはvoice triggerでサービスを呼び出すと、cardが表示されます。
livecard.png

次に、このcardをタップしてメニューを呼び出してみましょう。
menu_update.png

コンテンツが透過されています。
ここでタップするとコンテンツが更新されます。

livecard_updated.png

コンテンツが表示されているときに左右にスワイプすると、このcardがtimeline上にあることを確認できます。

最後に、メニューでupdateから右にスワイプするとexitを選択することができます。
menu_exit.png

これをタップするとcardが削除されます。Timelineをスワイプしてももはやcardは出てきません。