ブログ


スパイシー技術メモ


Google Glass用に速度計アプリを作ってみた

CATEGORY: Androidプログラム

先日SpicyWatchでレビューしたGoogle Glass用に、速度計アプリを作ってみました。

expample.png

起動するとタイムライン上に常駐し、歩いたり、自転車で移動したりした時の速度を見ることができます(タイムラインとはGoogle Glassで、スリープから復帰した際に左右フリックで表示される、横一列のカード型のUIです)。

今回は、Google Glass用のアプリ(以下Glassware)を初めて開発する上で、ポイントになると感じたところを取り上げていきたいと思います。

GlasswareはGlass Development Kit(以下GDK)を使って開発することができます。GDKとはAndroid SDKのアドオンで、Android SDKにGlass独自のUIなどを追加したものです。要はUIを除けば、基本的にはAndroidと同じように開発できるということになります。ただ独自な部分は通常のAndroidアプリとはかなり違うので、注意が必要です。

ちなみにGDKでなく、Mirror APIを使って開発する方法もあります。これはWeb API経由で、WEBサービスからGlassのタイムラインに直接情報を送信させる機能です。要はプッシュ通知ですね。まだ試してないので取り上げませんが、上手く既存サービスと連携させることができれば、なかなか面白そうだと思います(脱線)。

今回とりあげるのは、以下の三点です。

  1. Glasswareをどう起動させるか
  2. LiveCardの使い方
  3. 位置情報の取り方

Glasswareをどう起動させるか

Glasswareを起動させるには、まず音声コマンドを登録する必要があります。これを行わないと、音声で呼び出すことができないのはもちろん、ホームをタップした際に表示される実行可能アプリのリストにも表示されません。 音声コマンドはxmlで定義します。

お金を送ってほしい時は、下のように記述します。

<trigger command="SEND_MONEY" />

また併せて下のように、AmdroidManifestから音声コマンドのxmlファイルを指定する必要があります。

 
   <application
        <service
            android:name="com.spicysoft.speedmeter.SpeedMeterService"
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name"
            android:enabled="true"
            android:exported="true">
            <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>

GDKにはたくさんのコマンドが事前に定義されています。もし独自のコマンドを追加したい場合は、keyword属性を使って次のように記述します。command属性では追加されないので注意です。

<trigger keyword="Start A SpeedMeter" />

また、独自の音声コマンドを追加した場合は、AndoridManifestに下のpermissionを追加する必要があります。

    <uses-permission android:name="com.google.android.glass.permission.DEVELOPMENT" />

これは開発時のみの機能なので、正式にリリースする際は利用できません。リリースの際に独自の音声コマンドを使いたい場合(普通に有り得ると思います)、Googleに申請する必要があります。音声コマンドのチューニングや、ローカライズのために必要とのことですが、かなり不便ですね。リリース前の重要な注意点になると思います。

LiveCardの使い方

GlasswareでのUIは大きく3種類あります。Static Card、Live Card、Immersionの3つです。

Static CardとLive CardはGlassのタイムラインに表示させて使うものです。静的な情報を表示する際は、Static Cardを、動的な情報を表示する際はLive Cardを使います。

Live Cardは情報が更新されたときに、描画が更新されて、Glassのタイムライン上に表示されます。その後は画面は暗くなり、再び情報が更新された場合に、再描画されます。簡単なライフサイクルとしては、まずServiceとして起動させ、バックグラウンドに常駐させます。情報の更新時と休止時には、DirectRenderingCallbackを使って描画を更新します。これはSurfaceHolderを拡張させたもので、SurfaceViewと同じような使い方ですが、renderingPausedというLiveCardの更新/休止時の実装を行う必要があります。LiveCardはGlassWareでは最もよく使われているUIです。Immersionはゲームなどで使うUIです。常時表示させることができますが、タイムラインには表示できまません。

今回はLiveCardを使いました。またCompassアプリのサンプルコードを参考に、以下のように実装しました。

public class SpeedMeterRenderer implements DirectRenderingCallback {
    private final SpeedDetector.OnChangedListener mSpeedMeterListener =
            new SpeedDetector.OnChangedListener() {
        @Override
        public void onSpeedChanged(double speed) {
            mSpeedMeterView.setText(makeSpeedLabel(speed));
        }
    };
    public SpeedMeterRenderer(Context context, SpeedDetector speedDetector) {
        LayoutInflater inflater = LayoutInflater.from(context);
        mLayout = (FrameLayout) inflater.inflate(R.layout.speedmeter, new FrameLayout(context));
        mLayout.setWillNotDraw(false);
        mSpeedMeterView = (TextView) mLayout.findViewById(R.id.speed);
        mSpeedDetector = speedDetector;
        mSpeedMeterView.setText("----km/H");
    }
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
        mSurfaceWidth = width;
        mSurfaceHeight = height;
        doLayout();
    }
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mRenderingPaused = false;
        mHolder = holder;
        updateRenderingState();
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mHolder = null;
        updateRenderingState();
    }
    @Override
    public void renderingPaused(SurfaceHolder holder, boolean paused) {
        mRenderingPaused = paused;
        updateRenderingState();
    }
    private void updateRenderingState() {
        boolean shouldRender = (mHolder != null) && !mRenderingPaused;
        boolean isRendering = (mRenderThread != null);
        if (shouldRender != isRendering) {
            if (shouldRender) {
                mSpeedDetector.addOnChangedListener(mSpeedMeterListener);
                mSpeedDetector.start();
                if (mSpeedDetector.hasLocation()) {
                    mSpeedMeterView.setText(makeSpeedLabel(mSpeedDetector.getSpeed()));
                }
                mRenderThread = new RenderThread();
                mRenderThread.start();
            } else {
                mRenderThread.quit();
                mRenderThread = null;
                mSpeedDetector.removeOnChangedListener(mSpeedMeterListener);
                mSpeedDetector.stop();
                mSpeedMeterView.setText("--km/H");
            }
        }
    }
    private void doLayout() {
        int measuredWidth = View.MeasureSpec.makeMeasureSpec(mSurfaceWidth,
                View.MeasureSpec.EXACTLY);
        int measuredHeight = View.MeasureSpec.makeMeasureSpec(mSurfaceHeight,
                View.MeasureSpec.EXACTLY);
        mLayout.measure(measuredWidth, measuredHeight);
        mLayout.layout(0, 0, mLayout.getMeasuredWidth(),
                mLayout.getMeasuredHeight());
    }
    private synchronized void draw() {
        Canvas canvas = mHolder.lockCanvas();
        if (canvas != null) {
            canvas.drawColor(Color.BLACK);
            mLayout.draw(canvas);
            mHolder.unlockCanvasAndPost(canvas);
        }
    }
    private class RenderThread extends Thread {
        private boolean mShouldRun;
        public RenderThread() {
            mShouldRun = true;
        }
        private synchronized boolean shouldRun() {
            return mShouldRun;
        }
        public synchronized void quit() {
            mShouldRun = false;
        }
        @Override
        public void run() {
            Log.d(TAG, "run");
            while (shouldRun()) {
                draw();
                SystemClock.sleep(FRAME_TIME_MILLIS);
            }
        }
    }
}

LiveCardはGDKで一番良く使われるUIなので、まずはこれを検討すると良いと思います。

位置情報の取り方

まず大前提として、GoolgeGlassはGPSが内蔵されておらず、位置情報を取ることができません。 Glassは位置情報をBlueToothで接続したスマートフォンから取得します。スマートフォンは一緒に持ち歩く必要があります。

スマートフォンから取得した場合のProviderは"remote_gps"とか"remote_network"といった、LocationMangerで定数として定義されているものとは違った名前になっています。そのためLocationManager.GPS_PROVIDERを直接指定して呼びだそうとすると、例外が発生するので注意が必要です。

GDKのドキュメントでは、位置情報の取得にはCriteriaを使うことが推奨されています。

次のようにしてCriteriaを使って、位置情報をアップデートさせます。

Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_FINE);
criteria.setBearingRequired(false);
List<String> providers =
    mLocationManager.getProviders(criteria, true /* enabledOnly */);
    for (String provider : providers) {
    mLocationManager.requestLocationUpdates(provider,
    MILLIS_BETWEEN_LOCATIONS, METERS_BETWEEN_LOCATIONS, mLocationListener,
    Looper.getMainLooper());
}

デバッグが運動になるアプリを作ったのは初めてでしたw。通常のAndroidアプリならエミュレーターでダミーの位置情報を送ることができると思うのですが、Glasswareは実機でしかデバッグできないので、なかなか大変です。当初Location.getSpeed()を使って速度を取得しようとしていたのですが、Providerの精度の問題なのか、なぜか速度が取得できなかったので、自前で緯度と経度を比較して、距離と時間から速度を計算しました。これはGDKとは関係ないかもしれません。まあやっていることは同じはずなので、よしとしました。

締め

今回開発したコードはgithubに上げました。 Glasswareはまだ始まったばかりで、画期的と思えるようなアプリもそんなに多くなく、これから面白くなっていく分野です。GDKのUIはかなり特殊ですが、挑戦しがいのある分野だと思います。ぜひ試してみてください。






プレスリリース

人気ブログ記事

採用情報

最新ブログ記事








ページの先頭へ