Pages - Menu

2014年12月4日木曜日

Android Auto用音楽再生アプリの作り方


Android Auto用の音楽再生アプリを作ってみました。まずは再生、曲送りと曲戻しのみ行う簡単なものから挑戦しました。再生操作にはMediaSessionクラスを使用します。

■通常の音楽再生アプリと違う点

Android Auto用の音楽再生アプリは車載デバイスのためのオーディオサービスを提供します。このサービスによって、車載デバイスはメディアデータの一覧を参照し、カーステレオなどから音楽を再生することができます。

■専用リソースファイルのメタデータ定義

AndoridManifestファイルにはapplicationタグ内に以下のように定義し、
<meta-data
    android:name="com.google.android.gms.car.application"
    android:resource="@xml/automotive_app_desc" />
別途、専用リソースファイル\res\xml\automotive_app_desc.xmlを用意します。

■専用リソースファイルの定義

AndroidManifestファイルで定義した専用リソースファイルautomotive_app_desc.xmlには以下のように記載します。音楽を再生するためのクラスを使用する場合は"media"を定義します。メッセージアプリを使用する場合は"notification"と定義します。
<automotiveapp/>
    <uses name="media"/>
</automotiveapp/>

■サービスの実装

AndroidManifestファイルで定義したMediaBrowserService継承サービスを実装します。onGetRoot()とonLoadChildren()の実装が必須です。

■MediaSessionクラスについて

音楽再生操作を行うため、MediaSessionクラスを使用しています。MediaSessionクラスは、一連のメディア再生処理を「セッション」とし、他のクラス(MediaControllerクラス、MediaController.TransportControlsクラスなど)と紐づいて再生操作を一括して管理することができます。 関連クラスを含め、具体的に下記のことができます。

  • 再生、一時停止や曲送りなどの再生操作
  • 再生情報の表示
  • 音量変更
  • 再生状態の変更やメディアボタンのイベントをコールバックで取得
なお、再生処理そのものはMediaPlayerクラスなどで行います。

■クラス変数とonCreate()の実装

MediaBrowserService継承サービス、AndroidAutoMediaServiceクラスを定義しました。再生操作にMediaSessionクラス、再生処理にMediaPlayerクラスを使用しています。他に再生データをリスト管理するmPlayingQueue、現在の再生インデックスを保持するmCurrentQueueIndexを定義しています。onCreate()ではMediaSessionクラスの生成およびコールバックの設定などを行っています。
public class AndroidAutoMediaService extends MediaBrowserService implements 
       OnPreparedListener, OnCompletionListener, OnErrorListener{
    private MediaSession mMediaSession;                 // MediaSessionクラス
    private MediaPlayer  mMediaPlayer;                  // MediaPlayerクラス
    private List<MediaSession.QueueItem> mPlayingQueue; // 再生キューリスト
    private int mCurrentQueueIndex;                     // 再生キューのインデックス
    private static final String MEDIA_ID_ROOT = "__ROOT__";

    @Override
    public void onCreate() {
        super.onCreate();

        // 再生リストのオブジェクトを作る
        mPlayingQueue = new ArrayList<MediaSession.QueueItem>();

        // MediaSessionを生成
        mMediaSession = new MediaSession(this, "MyMediaSession");
        setSessionToken(mMediaSession.getSessionToken());
        
        // コールバックを設定
        mMediaSession.setCallback(new MyMediaSessionCallback());

        // 再生キューの位置を初期化
        mCurrentQueueIndex = 0;
    }

■onGetRoot()とonLoadChildren()の実装

onGetRoot()とonLoadChildren()の実装をします。onGetRoot()について今回はROOTを固定で返していますが、引数のclientPackageNameおよびclientUidでクライアントを判断してnullを返すことで、特定のクライアント以外からの参照を許可しないようにすることもできます。onLoadChildren()はメディア項目に関する情報を返すメソッドで、引数のresultにデータを設定します。

MediaItemの種類としてFLAG_PLAYABLEとFLAG_BROWSABLEがあります。リスト選択によって再生を開始したい場合はFLAG_PLAYABLEを、リストの表示のみを行いたい場合はFLAG_BROWSABLEを設定してください。FLAG_PLAYABLE設定時にリスト選択をすると後述のMediaSessionクラスのコールバックメソッドonPlayFromMediaId()がコールされますので、そのタイミングで再生処理を行います。
    @Override
    public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
        Bundle rootHints) {
        return new BrowserRoot(MEDIA_ID_ROOT, null);
    }

    @Override
    public void onLoadChildren(String parentId, Result <List<MediaItem>> result) {
        List<MediaBrowser.MediaItem> mediaItems = new ArrayList<MediaBrowser.MediaItem>();

        // 再生データ情報の設定
        MediaDescription.Builder mdb1 = new MediaDescription.Builder();
        
        // 再生データ1
        final Bundle mediaBundle1 = new Bundle();
        mediaBundle1.putString("Path", "mnt/sdcard/M01.mp3"); // データパス
        mediaBundle1.putInt("Index", 0);                      // 曲のインデックス
        mdb1.setMediaId("MediaID01");                         // メディアID
        mdb1.setTitle("Title01");                             // タイトル
        mdb1.setSubtitle("SubTitle01");                       // サブタイトル
        mdb1.setExtras(mediaBundle1);

        // 再生データ2
        MediaDescription.Builder mdb2 = new MediaDescription.Builder();
        final Bundle mediaBundle2 = new Bundle();
        mediaBundle2.putString("Path", "mnt/sdcard/M02.mp3"); // データパス
        mediaBundle2.putInt("Index", 1);                      // 曲のインデックス
        mdb2.setMediaId("MediaID02");                         // メディアID
        mdb2.setTitle("Title02");                             // タイトル
        mdb2.setSubtitle("SubTitle02");                       // サブタイトル
        mdb2.setExtras(mediaBundle2);
        
        mediaItems.add(new MediaBrowser.MediaItem(mdb1.build(),
                MediaBrowser.MediaItem.FLAG_PLAYABLE));        
        mediaItems.add(new MediaBrowser.MediaItem(mdb2.build(),
                MediaBrowser.MediaItem.FLAG_PLAYABLE));

        // sendResult()をコールする前にdetach()のコールが必要
        result.detach();
        result.sendResult(mediaItems);

        // 再生キューをセッションに設定
        mPlayingQueue.add(new MediaSession.QueueItem(mdb1.build(), 0));
        mPlayingQueue.add(new MediaSession.QueueItem(mdb2.build(), 1));
        mMediaSession.setQueue(mPlayingQueue);
    }

■再生処理を実装

MediaPlayerクラスによる再生処理を行うメソッドを実装します。基本的なMediaPlayerの実装手順で問題ありません。再生データのパスはMediaSession.QueueItemで管理させています。
    private void playMusic() {
        // プレイヤー生成と準備(生成済みの場合はリセット)
        if (mMediaPlayer == null) {
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer.setOnPreparedListener(this);
            mMediaPlayer.setOnCompletionListener(this);
            mMediaPlayer.setOnErrorListener(this);
        } else {
            mMediaPlayer.reset();
        }

        // 現在のキュー位置を元に再生データのパスを取得する
        MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentQueueIndex);
        String path = queueItem.getDescription().getExtras().getString("Path");

        // MediaPlayerのデータ設定と準備(非同期)
        try {
            mMediaPlayer.setDataSource(path);
            mMediaPlayer.prepareAsync();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onPrepared(MediaPlayer mp) {
        // 再生準備完了通知を受け、再生を行う
        if(mMediaPlayer != null) {
            mMediaPlayer.start();
        }
    }

■MediaSessionクラスのコールバック

MediaSessionのコールバックを下記のように実装します。リストを選択するとonPlayFromMediaId()、曲送りを選択するとonSkipToNext()、曲戻しを選択するとonSkipToPrevious()がコールされます。



    private final class MyMediaSessionCallback extends MediaSession.Callback {
        @Override
        public void onPlay() {
            playMusic();
        }

        // リスト選択時にコールされる
        @Override
        public void onPlayFromMediaId(String mediaId, Bundle extras) {
            // MediaIDを元に曲のインデックス番号を検索し、設定する。
            for(int i = 0; i < mPlayingQueue.size(); i++) {
                MediaSession.QueueItem queueItem = mPlayingQueue.get(i);
                MediaDescription md = queueItem.getDescription();
                if(mediaId.equals(md.getMediaId())) {
                    mCurrentQueueIndex = md.getExtras().getInt("Index");
                }
            }
            playMusic();
        }

        @Override
        public void onSkipToNext() {
            // 再生キューの位置を次へ
            mCurrentQueueIndex++;
            if (mCurrentQueueIndex >= mPlayingQueue.size()) {
                mCurrentQueueIndex = 0;
            }
            playMusic();
        }

        @Override
        public void onSkipToPrevious() {
            // 再生キューの位置を前へ
            mCurrentQueueIndex--;
            if (mCurrentQueueIndex < 0) {
                mCurrentQueueIndex = 0;
            }
            playMusic();
        }

■onDestroy()の実装

終了処理を実装します。
    @Override
    public void onDestroy() {
        super.onDestroy();
        // MediaSessionの終了処理
        mMediaSession.setCallback(null);
        mMediaSession.release();
        
        // MediaPlayerの終了処理
        if(mMediaPlayer != null) {
            mMediaPlayer.setOnPreparedListener(null);
            mMediaPlayer.setOnCompletionListener(null);
            mMediaPlayer.setOnErrorListener(null);
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
    }
今回は割愛しましたが、MediaSessionクラスを正確に動作させるためには、setPlaybackState()にて再生状態や停止状態などを操作に合わせて設定する必要があります。

MediaSessioinおよびその関連クラスについては下記を参照してください。
http://developer.android.com/reference/android/media/session/package-summary.html

2014年11月27日木曜日

Android Auto用メッセージアプリの作り方


公式サイトではAndroid Autoに対応したサンプルアプリが公開されていますが、若干凝った作りの為、分かりづらい部分があります。そこでサンプルアプリを元に、簡単なメッセージアプリを作ってみました。

※公式サイトのサンプルアプリは、以下のURLからダウンロード可能です。
http://github.com/googlesamples/android-MessagingService

Android Auto用メッセージアプリでは、以下のことができます。
  • 車載機器へのNotification送信
    Android Auto用のAPIを使用して、Notificationを車載機器側(現在はシミュレーションアプリ上)に通知できます。車載機器側でNotificationを選択すると、内容を音声で読み上げてくれます。
  • 車載機器からの既読通知を受信
    車載機器側でNotificationが選択される(=音声読み上げ)と、既読になったことをIntent通知で受け取ることができます。
  • 車載機器からの返信を受信
    車載機器側でNotificationに対する返信操作が行われると、その内容をIntent通知で受け取ることができます。

それでは実際のアプリの作り方を見ていきましょう。

■AndroidManifestファイルの記載

・BroadcastReceiverの登録

既読通知と返信通知のIntent通知を受けるために、BroadcastReceiverを登録します。
    <receiver android:name=".MyMessageReadReceiver" >
        <intent-filter>
            <action android:name="com.example.androidautonotificationtest.ACTION_MESSAGE_READ" />
        </intent-filter>
    </receiver>
    <receiver android:name=".MyMessageReplyReceiver" >
        <intent-filter>
            <action android:name="com.example.androidautonotificationtest.ACTION_MESSAGE_REPLY" />
        </intent-filter>
    </receiver>

・専用リソースファイルのメタデータ定義

AndoridManifestファイルには以下のように定義し、
    <meta-data
        android:name="com.google.android.gms.car.application"
        android:resource="@xml/automotive_app_desc" />
別途、専用リソースファイルを用意します。
(プロジェクトフォルダ)\res\xml\automotive_app_desc.xml

・専用リソースファイルの定義

AndroidManifestファイルで定義した専用リソースファイルには、以下のように記載します。
    <automotiveapp>
        <uses name="notification"/>
    </automotiveapp>

■既読通知用のIntent設定

車載機器からNotificationの既読通知を受けるために、PendingIntentを生成します。
  • AndroidManifestファイルでBroadcastReceiverのIntent-filterに登録したアクションを、Intent#setAction()で設定する。
  • Notification管理用の固有IDを、IntentのExtraに設定する。
    ※今回は固定値TEST_IDを使用しています。
    int TEST_ID = 1;
    String REPLY_ID = "conversation_id";
    String READ_ACTION =
            "com.example.androidautonotificationtest.ACTION_MESSAGE_READ";

    Intent readIntent = new Intent()
                        .addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
                        .setAction(READ_ACTION)
                        .putExtra(REPLY_ID, TEST_ID);

    PendingIntent readPendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
            TEST_ID,
            readIntent,
            PendingIntent.FLAG_UPDATE_CURRENT);

■返信通知用のIntent設定

車載機器からNotificationの返信通知を受けるために、PendingIntentを生成します。
  • AndroidManifestファイルでBroadcastReceiverのIntent-filterに登録したアクションを、Intent#setAction()で設定する。
  • Notification管理用の固有IDを、IntentのExtraに設定する。
  • 返信内容のStringを受け取るためのキー(EXTRA_VOICE_REPLY)を登録する。
    String REPLY_ACTION =
            "com.example.androidautonotificationtest.ACTION_MESSAGE_REPLY";
    String EXTRA_VOICE_REPLY = "extra_voice_reply";

    Intent replyIntent = new Intent()
                    .addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
                    .setAction(REPLY_ACTION)
                    .putExtra(REPLY_ID, TEST_ID);

    RemoteInput remoteInput = new RemoteInput.Builder(EXTRA_VOICE_REPLY)
                                    .setLabel(getApplicationContext().getString(R.string.app_name))
                                    .build();

    PendingIntent replyPendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
            TEST_ID,
            replyIntent,
            PendingIntent.FLAG_UPDATE_CURRENT);

■メッセージオブジェクトの生成

UnreadConversation.Builderオブジェクトを生成します。
  • Notification送信者の設定(TEST_NAME)
  • 既読通知用/返信通知用Intentの登録
  • Notification本文の登録(TEST_MESSAGE)
    String TEST_NAME = "BRIL TARO";
    String TEST_MESSAGE = "Hello Android Auto!";

    UnreadConversation.Builder unreadConvBuilder =
            new UnreadConversation.Builder(TEST_NAME)
                .setReadPendingIntent(readPendingIntent)
                .setReplyAction(replyPendingIntent, remoteInput);

    unreadConvBuilder.addMessage(TEST_MESSAGE);

■Notificationオブジェクトの生成と送信

NotificationCompat.Builderクラスを使用して、Notificationオブジェクトを生成、送信します。
    NotificationCompat.Builder notificationBuilder =
            new NotificationCompat.Builder(getApplicationContext())
                .setSmallIcon(R.drawable.ic_launcher)
                .extend(new CarExtender()
                            .setUnreadConversation(unreadConvBuilder.build()));

    NotificationManagerCompat mNotificationManager = NotificationManagerCompat.from(getApplicationContext());
    mNotificationManager.notify(TEST_ID, notificationBuilder.build());

■既読通知用のBroadcastReceiver

既読通知用のBroadcastReceiverでは、Notification送信時に設定した管理用の固有IDを取得できます。
public class MyMessageReadReceiver  extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent) {
        int conversationId = intent.getIntExtra(MainActivity.REPLY_ID, -1);
    }
}

■返信通知用のBroadcastReceiver

返信通知用のBroadcastReceiverでは、Notification送信時に設定した管理用の固有IDと、車載機器側が設定した返信内容を取得できます。
public class MyMessageReplyReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent) {
        int conversationId = intent.getIntExtra(MainActivity.REPLY_ID, -1);

        CharSequence reply;
        Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
        if (remoteInput != null) {
            reply = remoteInput.getCharSequence(MainActivity.EXTRA_VOICE_REPLY);
        }
    }
}

■Notificationの確認方法

上記処理を全て実装したアプリを作成して、以下の手順を行ってください。
  1. メッセージアプリ用のシミュレーションアプリ(messaging-simulator.apk )をインストール。
    ※詳細は前回記事「Android AutoのAPIが公開されました」をご参照ください。
  2. テストアプリを起動し、Notificationを送信
    ※その後、ステータスバーに表示されるNotificationは開かないでください。
  3. シミュレーションアプリを起動
成功すると、以下の画面が表示されます。
メッセージ部分をタップすると、
「New message from BRIL TARO,Hello Android Auto」
と音声で読み上げてくれます。




読み終わった後に既読通知のIntentがBroadcast送信され、以下の画面に遷移します。
ここで返信ボタンを押すと、返信通知用のIntentがBroadcast送信されます。
現在のシミュレーションアプリ上では返信内容を作成できず、アプリ側には"This is a reply"の固定文言のみが返ってきますが、将来的には音声入力などにも対応すると思われます。



実際に作ってみると、意外と簡単にNotificationを表示することができました。
現状ではまだ未完成の部分もありますが、これからどうUpdateされていくのか楽しみですね。

2014年11月19日水曜日

Android AutoのAPIが公開されました

※Android Auto公式サイトより引用 http://www.android.com/auto/

本日、ついにAndroid AutoのAPIが公開されました。
Android Autoとは、車のカーナビのような車載デバイスとAndroid端末を連動するシステムです。Android Autoアプリを作るための環境構築方法についてまとめました。

Android Autoに対応したアプリをAndroid端末に入れて、車載デバイスと接続することで、車載デバイス側でアプリの画面を表示したり、操作することができます。

APIは以下の公式サイトで説明されています。
http://developer.android.com/training/auto/start/index.html

現状では、以下の2種類のアプリに対応しています。
オーディオ系アプリ・・・車の中でオーディオコンテンツを再生することができます。
メッセージ系アプリ・・・メールを受信するとメッセージを音声出力し、音声入力から返信メールを送信できます。

今回は、AndroidAuto向けのオーディオ系アプリを作成するまでの環境構築について、以下の流れで紹介します。

・AndroidManifestファイルの記載
・専用リソースファイルの記載
・サービスの実装
・シミュレーションアプリのインストール
・アプリの実行

AndroidManifestファイルの記載

ポイントは3つあります。

targetSdkVersion

AndroidAutoに対応するアプリは、targetSdkVersionが21以上でなければいけません。

MediaBrowserService継承サービス定義 

以下のように定義します。
    <service android:exported="true" android:name=".AndroidAutoMediaService">
        <intent-filter>
            <action android:name="android.media.browse.MediaBrowserService">
            </action>
        </intent-filter>
    </service>

専用リソースファイルのメタデータ定義

以下のように定義します。
    <meta-data
        android:name="com.google.android.gms.car.application"
        android:resource="@xml/automotive_app_desc" />

上記のように、『android:resource="@xml/automotive_app_desc"』とした場合は、以下の専用リソースファイルを用意する必要があります。
(プロジェクトフォルダ)\res\xml\automotive_app_desc.xml

 専用リソースファイルの定義

AndroidManifestファイルで定義した専用リソースファイルには、以下のように記載します。"uses name="には、オーディオ系アプリの場合は"media"、メッセージ系アプリの場合は"notification"を設定します。
今回の場合は前者です。
    <automotiveapp>
        <uses name="media"/>
    </automotiveapp>

サービスの実装

AndroidManifestファイルで定義したMediaBrowserService継承サービスを実際に実装します。
以下の2つのメソッドが実装必須ですが、今回は空のメソッドにしておきます。
public class AndroidAutoMediaService extends MediaBrowserService{

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {
    return null;
}

@Override
public void onLoadChildren(String parentId, Result <List<MediaItem>> result) {
}

シミュレーションアプリのインストール

AndroidAuto向けアプリの動作をAndroid端末上で確認できる、シミュレーションアプリが提供されています。
アプリはSDK Managerの「Android Auto API Simulators」からダウンロードできます。


ダウンロードが完了すると、以下のapkファイル2個が格納されます。
オーディオ系アプリ向けとメッセージ系アプリ向けに、2種類のシミュレーションアプリが用意されています。
(Android SDKフォルダ以下)\extras\google\simulators
media-browser-simulator.apk・・・オーディオ系アプリ
messaging-simulator.apk ・・・メッセージ系アプリ

今回はオーディオ系アプリを取り扱うので、media-browser-simulator.apkの方をインストールします。

それぞれのapkを起動すると、以下の画面が表示されます。

オーディオ系のシミュレーションアプリ


メッセージ系のシミュレーションアプリ



※11/25修正
なお、これらのシミュレーションアプリのサンプルコードは、以下のURLからダウンロード可能です。
http://github.com/googlesamples/android-MediaBrowserService
http://github.com/googlesamples/android-MessagingService

アプリの実行

作成したアプリをあらかじめインストールしておき、
シミュレーションアプリの「Media Sim」を起動します。
メニューボタンを押すと、作成したアプリの名前が表示されれば成功です。



今回作成したアプリでは必要最低限の実装しか行っていないため、これ以上は動作できませんが、ちゃんと実装したアプリであれば、ここから動作が確認できるはずです。

次回からは、実際に動作するAndroid Autoアプリの開発方法について、説明していきます。

2014年7月8日火曜日

Google Glass+車をサーキット場で試してみた!


技適の認証を通過し、アプリ内部には日本語が追加されるなど、どんどん日本での販売が近づいてきているGoogle Glass。空港などでも活躍の場を増やしていますが、もっと身近な車との相性はどうだろうか?危なくて使えない?意外と使える?
そんな疑問を解決するべく実際に専用のGlasswareを開発して実験してみました。

本実験は、Google Glassと車の相性を確認するものです。 Google Glassを装着して車が正常に運転できることを証明するものではありません。
また、このような運転は通常の運転に比べて危険な可能性があるため、安全性に配慮した状況で実験を行っております。








■こんなアプリを作ってみた!


まず、車の中にスマートフォンを設置します。設置したスマートフォンから加速度、GPS、そしてカメラの映像をGoogle Glassに送ります。
Google Glassは無線での通信ができませんので実際には、
スマートフォン⇔(無線)⇔PC⇔(有線)⇔Google Glass
という何ともオーバーヘッドの多い方法でやり取りしています。
Google Glassでは、それらの情報を表示する3つの画面を持つアプリを作成しました。
1つめの画面は、加速度を表示する画面です。加速度は、円の中の赤い丸で表示します
2つめの画面は、コースと現在位置を表示する画面です。取得したGPSの情報を元に現在位置を赤い丸で表示します。
3つめの画面は、カメラの映像を表示する画面です。こちらは、ゲームの画面をイメージし、カメラ映像をベースにすべての情報を表示しています。
また、GPSの情報を元に、コースタイムを計測し、取得可能な速度も参考に表示しています。
3つの画面は、タッチパッドのスワイプで切り替え、二本指のタップで最初の画面戻る事が可能になっています。

・Google Glassの操作方法でいろいろ実験

車の操作中は、両手が塞がるため、Google Glassを操作する事ができません。
その為、タッチパットの操作以外に以下の2種類の操作を追加しました。

・音声でGoogle Glassを操作する

XE18.1よりアプリ実行中でも音声を認識するContextual voice commands機能が追加されました。
この機能を利用し、音声での操作を行います。

この機能を実装すると、画面の下に”ok glass”が表示されます。

 

この状態で、”ok glass”と話すと対応している音声一覧が表示されます。




音声一覧に表示されている単語を話す事で、音声コマンドが実行されます。
では、実際のソースを見ていきます。


Activity.onCreate()でFeatureにWindowUtils.FEATURE_VOICE_COMMANDSを設定します。

    
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getWindow().requestFeature(WindowUtils.FEATURE_VOICE_COMMANDS);
}


Activity.onCreatePanelMenu()でMenuを生成します。
今回は、menuリソースの情報を用いてメニューを生成しています。
android:titile部分の文字が音声コマンドとして使用されます。
     <item
        android:id="@+id/menu_stop"
        android:title="@string/menu_stop"/>  

@Override
public boolean onCreatePanelMenu(int featureId, Menu menu) {

    if (featureId == WindowUtils.FEATURE_VOICE_COMMANDS) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
    // Pass through to super to setup touch menu.
    return super.onCreatePanelMenu(featureId, menu);
}   


Activity.onPrepareOptionsMenu()で音声認識させる項目を条件に応じて有効・無効にする事も可能です。

@Override  
public boolean onPrepareOptionsMenu(Menu menu) { 

    //条件に応じて、項目の有効、無効、表示、非表示を変更する
    setMenuState(menu.findItem(R.id.start),  !mGameInfo.isTimeCount);
    setMenuState(menu.findItem(R.id.stop),  mGameInfo.isTimeCount);     
    return super.onPrepareOptionsMenu(menu); 
}

private static void setMenuState(MenuItem menuItem, boolean enabled) {

    menuItem.setVisible(enabled);
    menuItem.setEnabled(enabled);
}


Activity.onMenuItemSelected()に音声認識時の処理を記述します。

@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {

    if (featureId == WindowUtils.FEATURE_VOICE_COMMANDS) {
        switch (item.getItemId()) {
        case R.id.menu_start:
            break;
        case R.id.menu_stop:
            break;
        case R.id.menu_home:
            break;
        default:
            return true;
        }
        return true;
    }

    return super.onMenuItemSelected(featureId, item);     
}
      


・顔のジェスチャーで操作する

Google Glassの加速度センサーを利用して、画面の切り替えを行います。
今回のアプリでは右に振る事で画面の切り替え、下に頷く事で初期画面を表示します。

顔が振られたかどうかの判断は、かなりシンプルです。
指定された軸に一定以上の力が発生したかどうかで判断しています。
右方向に振られたかどうかはX軸、下に頷いたかどうかはY軸の情報をチェックします。


Y軸の頷いたかどうかのチェックは、タッチパットをタッチした場合にも同様の加速度が発生するため判断が難しく実際に使用するにはもう少し認識方法を考える必要があると思います。

センサーの初期化および開始と終了の処理です。(Androidで加速度センサーを利用する方法と同じです)

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);       
    setContentView(R.layout.main);
    mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
}

@Override
protected void onResume(){
    super.onResume();
     
    if(mSensorManager != null){
        List<sensor> sensors = mSensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER);
        if(sensors.size()>0){
            mSensorManager.registerListener(this, sensors.get(0), SensorManager.SENSOR_DELAY_UI);
        }
    }
}

@Override
protected void onPause(){
    super.onPause();

    if(mSensorManager != null){
         mSensorManager.unregisterListener(this);
    }  
       
}


加速度の情報をローパスフィルタを利用し、重力を除きます。
加速度の強さが一定以上の場合は、ジェスチャーが実行されたと判断し処理を実行します。
今回は実験のため、緩めの数値にしています。値が高い場合、かなり強く振らないと認識しなくなるためです。ただ、弱すぎる場合は、思わぬ顔振りで画面が切り替わったり、Glassの取り外しで画面が変わってしまう事もありました。上手く使用するにはON/OFFの設定が必要かもしれません。

@Override
public void onSensorChanged(SensorEvent event) {

    if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {

        currentOrientationValues[0] = event.values[0] * 0.1f + currentOrientationValues[0] * (1.0f - 0.1f);
        currentOrientationValues[1] = event.values[1] * 0.1f + currentOrientationValues[1] * (1.0f - 0.1f);
        currentOrientationValues[2] = event.values[2] * 0.1f + currentOrientationValues[2] * (1.0f - 0.1f);
   
        float acceleration_x = event.values[0] - currentOrientationValues[0];
        float acceleration_y = event.values[1] - currentOrientationValues[1];
        float acceleration_z = event.values[2] - currentOrientationValues[2];
     
        //x軸に4以上の加速度が発生した場合は、右振りと判断する
        if(acceleration_x > 4){
           return;
        }
   
        //y軸に5.5以上の加速度が発生した場合は、頷きと判断する
        //タッチパッド操作の誤認識を避けるために右振りよりも少し数値を強めにしています。
        if(acceleration_y > 5.5){
          return;    
        }
    }
}


■実際に試してみました!


っということで、実際にサーキット場で車(ラジコン)を運転してみました!



ラジコンには、データを配信するためのスマートフォンを搭載します。
そのままだとカメラが写らないので、鏡を使って無理やり前を映しています。
また、落ちないようにテープでぐるぐる巻きにしています。
※鏡の位置がポイントです。ここに設置しないとボディーが閉まりません。



残念ながら天気が悪く雨の中を走らせました。



ゆっくり走らせましたが最後は、水溜りにはまり帰らぬ車となりました。。
※スマホは防水のため無事でした。



・実際に走らせてみて

ラジコン用のコースでは、GPSの誤差による影響が大きく、コースから外れた場所を表示する事が多かったです。ただ、ある程度誤差を考慮に入れていたタイム測定は動いてくれました。正確な測定は難しいかもしれませんが、遊び程度には使えそうな気がします。

いろいろ実装した操作は、顔ジェスチャーが使いやすかったです。音声は、私の発音がダメでなかなか認識してくれませんでした。次回は、音声+ジェスチャーの組み合わせも試してみたいと思います。

期待していたカメラの映像は、個人的にはいい感じで、予想以上に車っぽい映像になっていました。
今回は、一台で試しましたが、複数台にこのシステム載せて走らせたら、リアルタイムでの順位や、前方の車までの距離の表示などまるでゲームのような表現も可能になるのではないかな?と考えています。
また、すべての車の映像を一括で表示すれば、観客にも今までとは違った楽しみ方を提案できるのではないかなと思います。


■最後に・・

今回の実験は、安全を考慮してラジコンという車を運転しました。
しかし、実際の車で運転する場合は、道路交通法の問題や、そもそも運転自体に危険が伴う可能性もあります。
公道での使用は、ご遠慮いただきますよう宜しくお願い致します。

もし公道で使用したら・・
こんな怪しい人に追いかけられたり・・

こんな恐ろしい事故を起こすかもしれません。


くれぐれも公道での使用はご遠慮ください。


※Google Glassは、無線による通信を行っていません。すべて有線を使用しています。



㈱ブリリアントサービス 大阪開発部 開発課 田中雅也

2014年6月18日水曜日

BluetoothSMARTデバイスをmbed で開発する(4)



はじめに


これまで3回に渡ってnRF51822ブランクモジュールをARM mbed 対応機器にするための方法について掲載してきましたが、今回から mbedによるBLEプログラミング実践編をスタートいたします。

2014年6月4日水曜日

BluetoothSMARTデバイスをmbed で開発する(3)



はじめに

前回はARM mbedを用いたソフトウェア開発手法を紹介いたしましたが、今回は連載3回目として mbed対応の要となるハードウェアの作成方法について紹介させていただきます。

2014年5月27日火曜日

BluetoothSMARTデバイスをmbed で開発する(2)


はじめに

前回のブログで弊社の取り組みについて紹介させていただきましたが、今回は第二回目として実際にmbedを使った開発について紹介させて頂きます。


2014年5月23日金曜日

BluetoothSMARTデバイスをmbed で開発する(1)


はじめに

NordicのnRF51ブランクモジュールは、法的面でBluetooth SMARTアクセサリ製造への敷居を下げてくれるという期待感から注目されていましたが、モジュール使用にあたって技術面でのハードルが高いところがあり、まだユーザーが増えていないのが現状のようです。

そこで今回は個人レベルでも試作が可能となるような試みとして、nRF51ブランクモジュールのDIP基板化と ARM® mbed™ を使った開発について紹介いたします。

2014年4月19日土曜日

[コラム] Google Glass XE16 サンプルコードから見る変更点



ついに来ました。やっと来ました。XE16が公開されました。
以前から言われていましたがベースをICS4.0.3から、Kitkat4.4へ変更されています。
追加機能や、変更機能は、すでに他のサイトで紹介されていますので、どのように変わったのか古いバージョンのサンプルソースを利用して調べてみました。


■スクリーン録画って使えるの?

いきなり開発とは関係ない話になります。
スクリーン録画は、Android Kitkat4.4から追加された機能で、その名の通り動作中の動画撮れる機能です。
Google GlassもKitkat 4.4になったんだし、この機能も使えるんじゃないの?って事で試してみました。

実際に録画した動画がこちらになります。


どうでしょう?いい感じではないでしょうか?

Google Glassは内容を人に伝えづらい端末です。
録画した動画を見て頂くことで、Google Glassでの動きも伝えやすくなると思います。


■XE12からの変更点

では、本題です。
XE12から何が変わったのかを探っていきます。

まずは、GDKから。GoogleGlass XE16用のGDKは、アップデート前から配信されています。



実は、サンプルコードも配信される前からXE16用になってます。

※今回は、よく参考に使われるstopwatchを利用しています。


これらをもとに古いバージョンのサンプルを最新のGDKでビルドしてみました。
結果はこんな感じです。



・LiveCardの生成方法

StopwatchService.javaでエラーが発生している箇所を見てみると、LiveCardの生成するためのTimelineManagerクラスが削除されました。

[StopwatchService.java]

XE16以前の記述方法
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (mLiveCard == null) {
            Log.d(TAG, "Publishing LiveCard");
            mLiveCard = mTimelineManager.createLiveCard(LIVE_CARD_TAG);

            // Keep track of the callback to remove it before unpublishing.
            mCallback = new ChronometerDrawer(this);
            mLiveCard.setDirectRenderingEnabled(true).getSurfaceHolder().addCallback(mCallback);

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

            mLiveCard.publish(PublishMode.REVEAL);
            Log.d(TAG, "Done publishing LiveCard");
        } else {
            // TODO(alainv): Jump to the LiveCard when API is available.
        }

        return START_STICKY;
    }

こちらが新しいバージョンの記述方法です。
   @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (mLiveCard == null) {
            mLiveCard = new LiveCard(this, LIVE_CARD_TAG);

            // Keep track of the callback to remove it before unpublishing.
            mCallback = new ChronometerDrawer(this);
            mLiveCard.setDirectRenderingEnabled(true).getSurfaceHolder().addCallback(mCallback);

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

        return START_STICKY;
    }


TimelineManagerからLiveCardを生成するのではなく、LiveCardを直接生成する方法に変わっています。

LiveCardの生成方法を変更することで、とりあえずはビルドが通る状態となりました。
では早速、Glassで動作確認をしてみます。


・音声で起動しない

インストールした後、「start a stopwatch」と話しても起動してくれません。
音声起動どころか、音声起動の一覧にすら表示されていません。

調べてみると音声起動の設定方法が変わっていました。
今回のバージョンより、決まった音声のみの起動可能となってます。

それだとテストも出来ないじゃないかということで、以下のuses permissionを利用することで今までと同じように音声での起動も可能となります。

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


ただし、開発時のオプションのため、リリース時には、Googleの承認が必要となりそうです。
音声起動の部分は、今後公開するうえでネックになりそうに思います。



これで起動もできたので、ストップウォッチの動作を確認します。



動作も問題なしだったので終了したところ・・


まさかの異常終了。
エラーログを見るとこんな感じに。



・openOptionMenuの呼び出しタイミング

問題を調べてみると、openOptionMenuが呼び出されるタイミングが変わっていました。


[MenuActivity.java]

XE16以前の記述方法
@Override
    public void onResume() {
        super.onResume();
        openOptionsMenu();
    }

こちらが新しいバージョンの記述方法です。
@Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        openOptionsMenu();
    }


ここまで来ると以前と同じように動作するようになりました。





今回調査をしてみてLiveCard周りの変更が大きいと感じました。
説明できていませんが、追加されている部分も多くあります(attachやnavigateなど)
音声起動は、着々と一般販売に向けて制限しているのかなという感じです。

Bluetooth周りも大幅に変わったと聞きます。
以前のバージョンでは、かなり苦労したので今回のバージョンアップで、少しでも素直になってくれたらと期待しています。
LiveCardの追加分も含めて、別の機会で報告できればと思います。





㈱ブリリアントサービス 大阪開発部 開発課 田中雅也



2014年4月17日木曜日

GDKでの動的なGlassware作成


GDKで動的なGlasswareを作成する方法には、LiveCardを使用する方法とImmersionという方法が存在します。その二つの方法で同じアプリを作成してみたところ、処理の構築に大きな差異があることがわかりました。

その具体的な内容については本文から。



2014年3月20日木曜日

Android Wear Developer Preview 開発環境構築



f:id:bs-android:20140319172604p:image

早くも発売が待ち遠しいAndroid Wearの端末ですが、端末を買ってから環境を整えていたら開発者として楽しめません。

エミュレータを使って開発ができるので環境構築手順を紹介します。

[コラム] Android Wearニュース記事まとめ



f:id:bs-android:20140319172211j:image

本日公開されたばかりなのに記事がかなり多いですね。

久々の勢いを感じます。


[コラム] ウェアラブル機器開発の本命、Android Wear登場



f:id:bs-android:20140319135838j:image

2週間近く前にGoogleのSundar Pichai氏がSXSWのイベントでAndroidのウェアラブル機器のSDKをリリースするという話をしていたのですが、宣言通りに開発者向けプレビュー版のSDKが本日公開されました。

公式ブログでのAndroid Wear発表

Android Wear開発者向けサイト

Android Wearとは


Android WearとはAndroid搭載スマートフォンと連携して動作するウェアラブルデバイスのことを指すようです。

現段階では腕時計だけが対象のようですが、今後他のウェアラブル機器にも広げていくとのこと。

音声でのテキスト入力、タッチパネルでの操作が可能なようです。

詳細なハードウェア仕様はまだ公開されていません。




恐らく6/25-6/26にサンフランシスコで開催されるGoogle I/O 2014でデバイスがお目見えすることになると思います。




モトローラが夏にAndroid Wear端末Moto 360を発売すると発表しているので恐らくこのモデルがGoogle I/O参加者に配布されることになるのではないかと予想しています。

まずは利用シーンの紹介動画。


D

開発者向け動画


D

下の画像は動画から抜き出したものですが、Google Nowで使える機能がほとんど使えるようです。天気予報、経路検索、音声入力でHangoutでテキストチャットもできている模様。


f:id:bs-android:20140319144846p:image:w360

f:id:bs-android:20140319144845p:image:w360

f:id:bs-android:20140319144844p:image:w360

f:id:bs-android:20140319144843p:image:w360

f:id:bs-android:20140319144842p:image:w360

f:id:bs-android:20140319144840p:image:w360

f:id:bs-android:20140319144841p:image:w360

f:id:bs-android:20140319144839p:image:w360


UIの動作イメージはこんな感じです。

画面が小さいので基本操作はスワイプ。音声入力にも対応しています。

「Ok google」と話しかけると、そこからアプリとインターネットへの道が開きます。


f:id:bs-android:20140319153819g:image




f:id:bs-android:20140319153820g:image




f:id:bs-android:20140319154729g:image


まずは概要だけお届けしましたが、開発者視点の記事を随時更新していきますのでお楽しみに。







ちなみにシンクロニシティなのかどうかわかりませんが、偶然にも(?)2日ほど前にTizen陣営からもTizen Software Developer Kit for Wearablesというスマートウォッチ向けのSDKがリリースされています。