Glassware (Live Card)のスケルトン

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](https://qiita-image-store.s3.amazonaws.com/0/21963/d0e7cd3e-3532-8be8-6ac9-62855335d005.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コマンドでプロジェクトを生成します。

```console
$ 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
```

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

```console
$ 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* に移動し、それ以外を削除します。

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

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

```console
$ 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
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.xml* 、 *res/values/styles.xml* に定義)

```xml:res/values/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="black">#ff000000</color>
  <color name="white">#ffffffff</color>
</resources>
```

```xml: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()`します。

```java: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()`します。

```java: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 #<呼び出された回数>"という文字列を返すだけの小さなクラスです。

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

### *AndroidManifest.xml*

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

```xml: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* に記述します。

```xml:AndroidManifest.xml
        <activity android:name="LiveCardMenuActivity"
                  android:label="@string/app_name"
                  android:theme="@style/MenuTheme"
                  android:exported="true" />
```

```xml: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/* に配置するだけです。

```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>
```

```xml:res/xml/voice_trigger_start.xml
<?xml version="1.0" encoding="utf-8"?>
<trigger keyword="@string/voice_trigger_start" />
```

```xml: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としてローンチすることができない、ということです。

# 実行結果

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

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

まず、voice triggerは動いているでしょうか。
![voice_menu.png](https://qiita-image-store.s3.amazonaws.com/0/21963/de703d48-01ed-c911-6ee1-6909c452404b.png)
最後にインストールしたものは先頭に出るようです。

一方、"ok glass"をタップした場合は次のような選択画面が表示されます。
![voice_trigger_start.png](https://qiita-image-store.s3.amazonaws.com/0/21963/dd608f1e-55f8-cf14-2306-0acc84f92c01.png)
アイコンは *res/drawable/ic_launcher.png* です。

この画面をタップ、またはvoice triggerでサービスを呼び出すと、cardが表示されます。
![livecard.png](https://qiita-image-store.s3.amazonaws.com/0/21963/e1c49a67-d847-514a-3a40-7d4f77db0669.png)

次に、このcardをタップしてメニューを呼び出してみましょう。
![menu_update.png](https://qiita-image-store.s3.amazonaws.com/0/21963/b0c2910a-4c4f-9739-3392-78770f2cf6fc.png)

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

![livecard_updated.png](https://qiita-image-store.s3.amazonaws.com/0/21963/c2438ef7-0cc0-1e90-fe74-899adffc53d7.png)

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

最後に、メニューでupdateから右にスワイプするとexitを選択することができます。
![menu_exit.png](https://qiita-image-store.s3.amazonaws.com/0/21963/e6ac4560-598d-0381-f8d1-6e889131b119.png)

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