森川敬一 CTO ブログ

unimediaでCTOやってます森川敬一です。エンジニアブログ。IoT、ウエアブルとか書いていきます。

GoogleGlassにアプリを入れてみる その4


GoogleGlassにアプリを入れてみる その3 - 森川敬一 CTO ブログ

前回、GoogleGlassのアプリを作って実行までやりました。

ただ、これだと全くGlassアプリっぽくありません。

なので、

- カード型(LiveCard)にする

  • 音声起動させる
  • ボタンに対応させる


と改修したいと思います。

Live Cardsのサンプルサイトを参考にしました。

まず、MainActivityからServiceに変える必要がありますね。

なので、MainActivity.javaを削除して、
LiveCardService.javaを作成しました。
このコードは、前述のLive Cardsのサンプルサイト

Create a service that manages the live card and renders your layout or view. This example service updates the score of an imaginary basketball game every 30 seconds.

の部分のソースコードのそのままです。

package com.example.glasstest;

import java.util.Random;

import com.google.android.glass.timeline.LiveCard;
import com.google.android.glass.timeline.LiveCard.PublishMode;

import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.widget.RemoteViews;

public class LiveCardService extends Service {

    private static final String LIVE_CARD_TAG = "LiveCardDemo";

    private LiveCard mLiveCard;
    private RemoteViews mLiveCardView;

    private int homeScore, awayScore;
    private Random mPointsGenerator;

    private final Handler mHandler = new Handler();
    private final UpdateLiveCardRunnable mUpdateLiveCardRunnable =
        new UpdateLiveCardRunnable();
    private static final long DELAY_MILLIS = 30000;

    @Override
    public void onCreate() {
        super.onCreate();
        mPointsGenerator = new Random();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (mLiveCard == null) {

            // Get an instance of a live card
            mLiveCard = new LiveCard(this, LIVE_CARD_TAG);

            // Inflate a layout into a remote view
            mLiveCardView = new RemoteViews(getPackageName(),
                    R.layout.activity_main);

            // Set up initial RemoteViews values
            homeScore = 0;
            awayScore = 0;
            mLiveCardView.setTextViewText(R.id.home_team_name_text_view,
                    getString(R.string.home_team));
            mLiveCardView.setTextViewText(R.id.away_team_name_text_view,
                    getString(R.string.away_team));
            mLiveCardView.setTextViewText(R.id.footer_text,
                    getString(R.string.game_quarter));

            // Set up the live card's action with a pending intent
            // to show a menu when tapped
            Intent menuIntent = new Intent(this, LiveCardMenuActivity.class);
            menuIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
            mLiveCard.setAction(PendingIntent.getActivity(this, 0, menuIntent, 0));
            mLiveCard.attach(this);

            // Publish the live card
            mLiveCard.publish(PublishMode.REVEAL);

            // Queue the update text runnable
            mHandler.post(mUpdateLiveCardRunnable);
        }
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        if (mLiveCard != null && mLiveCard.isPublished()) {
            //Stop the handler from queuing more Runnable jobs
            mUpdateLiveCardRunnable.setStop(true);

            mLiveCard.unpublish();
            mLiveCard = null;
        }
        super.onDestroy();
    }

    /**
     * Runnable that updates live card contents
     */
    private class UpdateLiveCardRunnable implements Runnable{

        private boolean mIsStopped = false;

        /*
         * Updates the card with a fake score every 30 seconds as a demonstration.
         * You also probably want to display something useful in your live card.
         *
         * If you are executing a long running task to get data to update a
         * live card(e.g, making a web call), do this in another thread or
         * AsyncTask.
         */
        public void run(){
            if(!isStopped()){
                // Generate fake points.
                homeScore += mPointsGenerator.nextInt(3);
                awayScore += mPointsGenerator.nextInt(3);

                // Update the remote view with the new scores.
                mLiveCardView.setTextViewText(R.id.home_score_text_view,
                        String.valueOf(homeScore));
                mLiveCardView.setTextViewText(R.id.away_score_text_view,
                        String.valueOf(awayScore));

                // Always call setViews() to update the live card's RemoteViews.
                mLiveCard.setViews(mLiveCardView);

                // Queue another score update in 30 seconds.
                mHandler.postDelayed(mUpdateLiveCardRunnable, DELAY_MILLIS);
            }
        }

        public boolean isStopped() {
            return mIsStopped;
        }

        public void setStop(boolean isStopped) {
            this.mIsStopped = isStopped;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
      /*
       * If you need to set up interprocess communication
       * (activity to a service, for instance), return a binder object
       * so that the client can receive and modify data in this service.
       *
       * A typical use is to give a menu activity access to a binder object
       * if it is trying to change a setting that is managed by the live card
       * service. The menu activity in this sample does not require any
       * of these capabilities, so this just returns null.
       */
       return null;
    }
}


59行目の辺りにメニュー設定をしている場所があります。

            Intent menuIntent = new Intent(this, LiveCardMenuActivity.class);
            menuIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
            mLiveCard.setAction(PendingIntent.getActivity(this, 0, menuIntent, 0));
            mLiveCard.attach(this);


なので、LiveCardMenuActivity.javaを実装します。

package com.example.glasstest;

import android.app.Activity;
import android.content.Intent;
import android.os.Handler;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import java.lang.Runnable;

public class LiveCardMenuActivity extends Activity
{
    private final Handler mHandler = new Handler();

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        openOptionsMenu();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle item selection.
        switch (item.getItemId()) {
            case R.id.exit:
                // Stop the service at the end of the message queue for proper options menu
                // animation. This is only needed when starting a new Activity or stopping a Service
                // that published a LiveCard.
                post(new Runnable() {

                    @Override
                    public void run() {
                        stopService(new Intent(LiveCardMenuActivity.this, LiveCardService.class));
                    }
                });
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onOptionsMenuClosed(Menu menu) {
        // Nothing else to do, closing the Activity.
        finish();
    }

    /**
     * Posts a {@link Runnable} at the end of the message loop, overridable for testing.
     */
    protected void post(Runnable runnable) {
        mHandler.post(runnable);
    }
}

次に同じくLive Cardsのサンプルサイトの下記の部分にある通りにactivity_main.xmlを修正します。

Create the layout or view that you want to render. The following example shows a layout for an imaginary basketball game:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.glasstest.LiveCardMenuActivity" >

<TextView
     android:id="@+id/home_team_name_text_view"
     android:layout_width="249px"
     android:layout_height="wrap_content"
     android:layout_alignParentRight="true"
     android:gravity="center"
     android:textSize="40px" />

 <TextView
     android:id="@+id/away_team_name_text_view"
     android:layout_width="249px"
     android:layout_height="wrap_content"
     android:layout_alignParentLeft="true"
     android:gravity="center"
     android:textSize="40px" />

 <TextView
     android:id="@+id/away_score_text_view"
     android:layout_width="249px"
     android:layout_height="wrap_content"
     android:layout_alignLeft="@+id/away_team_name_text_view"
     android:layout_below="@+id/away_team_name_text_view"
     android:gravity="center"
     android:textSize="70px" />

 <TextView
     android:id="@+id/home_score_text_view"
     android:layout_width="249px"
     android:layout_height="wrap_content"
     android:layout_alignLeft="@+id/home_team_name_text_view"
     android:layout_below="@+id/home_team_name_text_view"
     android:gravity="center"
     android:textSize="70px" />

 <TextView
     android:id="@+id/footer_text"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_alignParentBottom="true"
     android:layout_alignParentLeft="true"
     android:layout_marginBottom="33px"
     android:textSize="26px" />
</RelativeLayout>

menu/main.xmlも合わせて変更します。
この際、iconは、さっきのLive Cardsのサンプルサイトにある通りに50 × 50 pixelのiconを作成しときます。

Creating menu resources

Creating menu resources is the same as on the Android platform, but follow these guidelines for Glass:

For each menu item, provide a 50 × 50 pixel menu item icon. The menu icon must be white in color on a transparent background. See the Glass menu item icons for an example or to download them for your own use.
Use a short name that describes the action and is in title case. An imperative verb works well (for example, Share or Reply all).
Glass does not display live cards without a menu item. At the very least, provide a Stop menu item, so users can remove the live card from the timeline.
The CheckBox widget is not supported.
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/exit"
        android:title="@string/menu_app_exit"
        android:icon="@drawable/ic_exit" />
</menu>

合わせてstyle.xmlも編集します。MenuThemeを追加です

<resources>

    <!--
        Base application theme, dependent on API level. This theme is replaced
        by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
    -->
    <style name="AppBaseTheme" parent="android:Theme.Light">
        <!--
            Theme customizations available in newer API levels can go in
            res/values-vXX/styles.xml, while customizations related to
            backward-compatibility can go here.
        -->
    </style>

    <!-- Application theme. -->
    <style name="AppTheme" parent="AppBaseTheme">
        <!-- All customizations that are NOT specific to a particular API-level can go here. -->
    </style>
    <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>

</resources>

最後にAndroidManifest.xmlを編集します。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.glasstest"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="17"
        android:targetSdkVersion="19" />

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

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.glasstest.LiveCardMenuActivity"
            android:label="@string/app_name"
            android:theme="@style/MenuTheme"
            android:enabled="true" >
        </activity>

        <service android:name="LiveCardService"
                 android:label="@string/app_name"
                 android:exported="true">
            <intent-filter>
                <category android:name="android.intent.category.LAUNCHER" />
                <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>

com.google.android.glass.action.VOICE_TRIGGER
を使う場合は、

パーミッションが必須の様です。


で指定した言葉で起動する事が出来る様になります。

ビルドして実行すると下記の画面が出てきます。
ランダムで点数が入っていくアプリです。仕事しながら好きなサッカーな試合状況が見れるみたいな便利ですね。
f:id:m-kei1:20141225190143p:plain
f:id:m-kei1:20141225190221p:plain

タップするとメニュー画面でexit画面が出ます。
f:id:m-kei1:20141225190234p:plain

最後にgithubのソースを上げといたので必要あれば、clone下さい。

k1morikawa/googleGlass · GitHub