SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Yahoo! JAPAN 黒帯シリーズ

Android Autoのはじめ方

Yahoo! JAPAN 黒帯シリーズ 第3回 ~ 「Android技術黒帯」による車載用Androidプラットフォームの解説

音楽再生アプリの実装

 まずは、音楽再生用のアプリを作成してみましょう。音楽再生用のAutoアプリを作成する場合、最低でも以下の2つのクラスを作成します。

  • MediaBrowserServiceを継承したService:コンテンツのリストを返却し、ユーザーに選択してもらうためのサービスです。
  • MediaSession.Callbackを継承したクラス:再生や一時停止などのイベントを受け取り、ハンドリングするためのクラスです。

 今回は例として、MediaStoreに保存されている音楽を再生するアプリを作成していきましょう。

 まずは、前述のとおり設定ファイルを作成します。ファイル名は決まっていませんが、Googleのサンプル通り、automotive_app_desc.xmlとしておき、res/xml/に配置します。音楽再生用のアプリですから、usesにはmediaを指定します。

 manifestにメタデータを追加します。また、この後MediaBrowserServiceを作成しますので、manifestファイルに忘れずに追加しておきましょう。注意点としては、exportedをtrueに設定しておくことと、"android.media.browse.MediaBrowserService"のアクションを受けとるintent-filterを追加しておくことが挙げられます。

 セキュリティ上の注意点として、exportedをtrueに設定しますので、他のまったく無関係なアプリからもこのサービスが呼び出し可能な状態になります。後述しますが、これを制御するための方法も用意されていますので、忘れないようにしましょう。

<application>

    ...
    <!-- Android Auto用メタデータ -->
    <meta-data android:name="com.google.android.gms.car.application"
     android:resource="@xml/automotive_app_desc"/>

    <!-- MediaBrowserService -->
    <service
        android:name=".auto.MyMediaBrowserService"
        android:enabled="true"
        android:exported="true" >
        <intent-filter>
            <action android:name="android.media.browse.MediaBrowserService"/>
        </intent-filter>
    </service>

</application>

MediaBrowserServiceの作成

 続いて、MediaBrowserServiceを継承したServiceを作成します。このServiceに必要な処理は、

  • onCreate()でメディアセッションを作成し、トークンを受け取ってセットする。
  • メディアセッションに、再生・一時停止などのイベントを受けとるためのコールバックを設定する
  • onGetRoot()で、パッケージ名とUIDをもとに、適切な呼び出し元であるかをチェックする。
  • onLoadChildren()で、お気に入り、最近聴いた曲、アルバムなど、ノードごとの表示の出しわけを行う
  • onDestroy()で、リソースを解放する

 です。

順を追って見ていきましょう。

メディアセッションの作成、コールバックの設定

 MediaSessionには、Contextと任意のタグを渡します。タグはログ出力時に使用されます。空文字やnullは許可されません。

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

    // メディアセッションを作成します。
    MediaSession session = new MediaSession(this, MEDIA_SESSION_TAG);
    setSessionToken(session.getSessionToken());

    // 再生、停止などのイベントを受け取るためのコールバックを設定します。
    mSessionCallback = new MyMediaSessionCallback(session);
    session.setCallback(mSessionCallback);
}

onGetRoot()で、パッケージ名とUIDをもとに、適切な呼び出し元であるかをチェックする

 onGetRoot()は、ユーザーが選択するメニュー階層構造のルートを示すIDを返します。

 また、もう一つ重要な役割があります。MediaBrowserServiceは、exportedをtrueにして、外部からの呼び出しに応じられるよう設定しておく必要があるのですが、一方そのために、使用してほしくない他のアプリからも呼び出せてしまうことになります。こうした状況に対応するため、呼び出しの許可不許可を、ここでコントロールできます。

 onGetRootでnullを返すと、MediaBrowserServiceは接続を拒否します。

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
    // 呼び出しの許可不許可を、ここでコントロールします。
    if (!isCallerAllowed(clientPackageName, clientUid)) {
        // nullを返した場合、不許可であることを意味します。
        return null;
    }

    if (ANDROID_AUTO_PACKAGE_NAME.equals(clientPackageName)) {
        // パッケージによって動作を変えたい場合も、ここでハンドリングします
        // 広告や、音楽ライブラリなどの目的を想定しているようです。
    }

    return new BrowserRoot(MEDIA_BROWSER_ROOT, null);
}

/**
 * このパッケージ/UIDからの呼び出しに応じるか、チェックする
 * @param packageName 呼び出し元のパッケージ
 * @param clientUid 呼び出し元のUID
 * @return このパッケージ/UIDから使用して良い場合はtrueを返却します。
 */
private boolean isCallerAllowed(String packageName, int clientUid) {
    // 今回は自アプリと、シミュレータのみを許可します。
    return (getApplicationInfo().uid == clientUid
            || getPackageName().equals(packageName)
            || SIMULATOR_PACKAGE_NAME.equals(packageName));
}

onLoadChildren()で、ノードごとの表示の出しわけを行う

 onLoadChildren()では、メニューに表示する内容を設定します。onGetRootのあとや、ユーザーがメニューを選択した時などに呼ばれます。引数のparentMediaIdに、親のIDが渡されますので、それをもとに表示すべき内容を判断します。

 表示すべきメニューは、List<MediaItem>として引数のresultにセットします。

/**
 * onGetRootのあと、またはMediaItem.FLAG_BROWSABLEのフラグが付いている
 * MediaItemを選択した時に呼ばれます。
 * parentMediaIdで親のIDをチェックして、動作を変更します。
 */
@Override
public void onLoadChildren(final String parentMediaId,
                           final Result<List<MediaItem>> result)  {
    // Assume for example that the music catalog is already loaded/cached.
    List<MediaItem> mediaItems = new ArrayList<>();

    if (MEDIA_BROWSER_ROOT.equals(parentMediaId)) {
        // ルートIDの場合、サブメニューを表示するようにします。
        for(int i = 0; i < 5; i++) {
            MediaDescription mediaDescription
                    = (new MediaDescription.Builder())
                    .setTitle(getString(R.string.test_title, i))
                    .setSubtitle(getString(R.string.test_subtitle, i))
                    // 実際には、個別にメディアIDを変えてください
                    .setMediaId(MEDIA_BROWSER_CHILD)
                    .setDescription(null)
                    .setIconBitmap(null)
                    .build();

            MediaItem item = new MediaItem(mediaDescription,
                    // 曲自体ではなく、リストメニューなどの閲覧用のアイテムであることを示します
                    MediaItem.FLAG_BROWSABLE);

            mediaItems.add(item);
        }

    } else if (MEDIA_BROWSER_CHILD.equals(parentMediaId)) {
        // サブメニューを選択すると、楽曲を表示するようにします。
        List<MediaSession.QueueItem> playQueue = new ArrayList<>();

        ContentResolver resolver = getContentResolver();

        Cursor cursor = resolver.query(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                COLUMNS,
                null, // 検索条件。実際には、parentMediaIdによって分ける。
                null, // 検索条件に入れる、クエリパラメータ
                null); // ソート順。

        int index_of_id    = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
        int index_of_title = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
        int index_of_data  = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);

        try {
            while(cursor.moveToNext()) {
                Bundle extras = new Bundle();
                extras.putString(MyMediaSessionCallback.EXTRAS_MEDIA_PATH,
                        cursor.getString(index_of_data));

                MediaDescription description
                        = (new MediaDescription.Builder())
                        .setTitle(cursor.getString(index_of_title))
                        .setSubtitle(null)
                        .setMediaId(cursor.getString(index_of_data))
                        .setDescription(null)
                        .setIconBitmap(null)
                        .setExtras(extras)
                        .build();

                MediaItem item = new MediaItem(description,
                        // 再生用のアイテムであることを示します
                        MediaItem.FLAG_PLAYABLE);
                mediaItems.add(item);

                // キューにも追加する
                MediaSession.QueueItem queueItem
                        = new MediaSession.QueueItem(description,
                        cursor.getLong(index_of_id));
                playQueue.add(queueItem);
            }
        } finally {
        cursor.close();
    }

    mSessionCallback.setQueue(playQueue);
}

// sendResult()の前に、detach()を呼びます。
result.detach();
result.sendResult(mediaItems);

}

次のページ
MediaSession.Callbackの作成

この記事は参考になりましたか?

Yahoo! JAPAN 黒帯シリーズ連載記事一覧

もっと読む

この記事の著者

森 洋之(ヤフー株式会社)(モリ ヒロユキ)

ヤフー株式会社 ヤフオク!カンパニー開発本部 サービス開発部 黒帯 Android 技術1982年広島県生まれ。Androidアプリの個人開発などを経て、2012年ヤフー株式会社入社。「ヤフオク!」などのAndroidアプリ開発に携わる。2014年10月にAndroid技術の分野で黒帯に認定される。...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/8711 2015/06/19 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー