【Kotlin】音楽アプリにAndroid Autoのアイテム表示を実装してみた

## はじめに

この記事は [レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku) の2日目の記事となります。  

はじめまして、レコチョクでAndroidアプリを開発している杉山といいます。
10月3日にTOWER RECORDS MUSICではAndroid Auto対応を行いリリースしました。
今回はその中で行ったタブとアイテムの表示に関する実装について紹介したいと思います。
※前提として音楽プレイヤーアプリとして開発されているものにAndroid Autoの対応を追加するため、一部説明を省いている部分があります。

## Android Autoとは

> Android Auto は Android スマートフォンで Android Auto アプリを使用しており、互換性のある車またはアフターマーケット ステレオ システムを持っているユーザーに対して、ドライバー向けに最適化されたアプリ エクスペリエンスを提供します。車載ディスプレイにスマートフォンを接続して、直接アプリを使用できます。ドライバー向けに最適化されたインターフェースを表示するための Android Auto 用サービスを作成することで、Android Auto とスマートフォン アプリを接続できるようになります。

[自動車向け Android の概要](https://developer.android.com/training/cars?hl=ja) より

Android Autoは、車のディスプレイを利用してスマホアプリの一部機能を使用可能にでき、Googleアシスタントを使うことでハンズフリーで操作ができたりします。Android OS 10以上ではデフォルトで組み込まれているため、自動車やカーステレオが対応していれば、簡単に接続が可能です。

## [TOWER RECORDS MUSIC](https://music.tower.jp/)とは

> タワーレコードとレコチョクがお届けする音楽アプリ。
1億曲以上の楽曲と10万本のミュージックビデオが広告なしで聴き放題＆見放題！

※ストア掲載文より一部抜粋

TOWER RECORDS MUSIC(以降TRMと略します)はタワーレコード様とレコチョクで提供している音楽サブスクリプションサービスになります。
今回の記事で気になった方はぜひ利用してみてください！

Android Autoで、以下の項目が選択、再生できます。

- プレイリスト
  - Myプレイリスト
  - あなたにおすすめ
  - お気に入り
  - 再生履歴
- アルバム
  - ニューリリース
  - あなたにおすすめ
  - お気に入り
 - 再生履歴
- 楽曲
  - ランキング
  - お気に入り
  - 再生履歴
  - オフライン

![image-20231122110128242.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298978/1c4f37e0-ebce-e54f-f416-d811456fc319.png)

※画像はプレイリストタブの一覧を表示したエミュレータの画面になります

## Android Auto対応宣言

Android Auto はメディアブラウザサービスからの情報を使用するため、マニフェストファイルに構成を追記する必要があります。
下記の宣言をマニフェストファイルに追記してください。

```kotlin
<application>
    ...
    <service android:name=".MyMediaBrowserService"
             android:exported="true">
        <intent-filter>
            <action android:name="android.media.browse.MediaBrowserService"/>
        </intent-filter>
    </service>
    ...
</application>
```



## タブを作成する
今回紹介するタブとは図に示す部分のことを指します。

![image-20231122111247584.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298978/b7da3c12-dcd9-b066-bc68-e83a3492653b.png)
※画像はエミュレータの画面になります

`MediaBrowserServiceCompat`クラスの`onGetRoot()`にて設定したルートIDで追加された最初のアイテムがタブとして表示されます。
タブ表示には制限があり、タブの数は4が上限になります。また直接再生可能なアイテムは表示することができません。

```kotlin
override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle?
): BrowserRoot? {
    return if (clientPackageName.contains(applicationContext.packageName) ||
        clientPackageName.contains(PACKAGE_ANDROID_AUTO)
    ) {
        BrowserRoot(ID_ROOT, null)
    } else {
        null
    }
}
```

`clientPackageName`にAndroid Autoのタイプが含まれているかどうかで処理を分岐します。
含まれている場合は`BrowserRoot()`にAndroid AutoのルートIDを設定して返すことで、`onLoadChildren()`の処理を変えることができます。

```kotlin
override fun onLoadChildren(
    parentId: String,
    result: Result<List<MediaBrowserCompat.MediaItem>>
) {
    if (parentId == "player_parent_id") {
      // 通常再生時の処理を追加しています
      // 今回は省略しています
    } else {
        result.detach()
        launch(start = CoroutineStart.UNDISPATCHED) {
            launch(Dispatchers.IO) {
                try {
                    // ここでAndroid Auto上で表示するアイテムを制御する
                    result.sendResult(androidAutoUseCase.browse(parentId))
                } catch (e: RuntimeException) {
                    Log.e(TAG, "Failed to load children for $parentId", e)
                }
            }
        }
    }
}
```

TRMではUseCaseクラスの`browse()`でAndroid Autoの表示アイテムの制御をしています。以下にコードを示します。

```kotlin
suspend fun browse(parentId: String): List<MediaBrowserCompat.MediaItem> {
    val results = mutableListOf<MediaBrowserCompat.MediaItem>() // 表示するアイテムリスト
    val parentIdUri = parentId.toUri() // ファイルパスをIDにしているためUrlに変換
    when (removeQuery(parentIdUri).toString()) { // 共通部分を削除して分岐判断
        // トップメニュー
        ID_ROOT -> {
          val playlistMediaDesc = MediaDescriptionCompat.Builder() // プレイリスト
          .setMediaId(ID_PLAYLIST)
          .setTitle("プレイリスト")
          .setIconBitmap("プレイリストアイコンのBitmap")
          .build()
          results.add(MediaBrowserCompat.MediaItem(playlistMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
          val albumMediaDesc = MediaDescriptionCompat.Builder() // アルバム
          .setMediaId(ID_ALBUM)
          .setTitle("アルバム")
          .setIconBitmap("アルバムアイコンのBitmap")
          .build()
          results.add(MediaBrowserCompat.MediaItem(albumMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
          val musicMediaDesc = MediaDescriptionCompat.Builder() // 楽曲
          .setMediaId(ID_MUSIC)
          .setTitle("楽曲")
          .setIconBitmap("楽曲アイコンのBitmap")
          .build()
          results.add(MediaBrowserCompat.MediaItem(musicMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
        }
        else -> {
            // nop
        }
    }
    return results
}
```

各タブの項目のMediaID、タイトル、アイコンを設定して配列に追加します。その配列を返して表示します。

 例えばプレイリストの場合

```kotlin
val playlistMediaDesc = MediaDescriptionCompat.Builder() 
          .setMediaId(ID_PLAYLIST) // MediaID
          .setTitle("プレイリスト") // タイトル
          .setIconBitmap("プレイリストアイコンのBitmap") // アイコン
          .build()
          results.add(MediaBrowserCompat.MediaItem(playlistMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)) // 表示する配列に追加
```

といった形になります。表示したいアイテムを`MediaBrowserCompat.MediaItem`として配列に追加することで表示されます。以下のフラグを設定することでアイテムの種類を判断するようにします。

| アイテムの種類フラグ                        |                                      |
| ------------------------------------------- | ------------------------------------ |
| MediaBrowserCompat.MediaItem.FLAG_BROWSABLE | ブラウズ可能なアイテムとして配置する |
| MediaBrowserCompat.MediaItem.FLAG_PLAYABLE  | 再生可能なアイテムとして配置する     |

上記で設定することで画像のようにタブの表示ができるようになります。

![image-20231122111401440.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298978/68177336-65c4-9c6c-1caf-f6cf0c702446.png)

※画像はエミュレータの画面になります

## アイテムを作成する

タブ以下のアイテムの作成に関してもタブアイテム作成とほぼ同じになります。

先程のルートIDから選択されたアイテムに付与されているMediaIDを元に分岐処理で表示するものを設定します。

以下はプレイリストを選択した場合の処理です。

```kotlin
ID_PLAYLIST -> {
    val myPlaylistMediaDesc = MediaDescriptionCompat.Builder()
        .setMediaId(ID_MY_PLAYLIST)
        .setTitle("Myプレイリスト")
        .build()
    results.add(MediaBrowserCompat.MediaItem(myPlaylistMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
    val recommendPlaylistMediaDesc = MediaDescriptionCompat.Builder()
        .setMediaId(ID_RECOMMEND_PLAYLIST)
        .setTitle("あなたにおすすめ")
        .build()
    results.add(MediaBrowserCompat.MediaItem(recommendPlaylistMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
    val favoritePlaylistMediaDesc = MediaDescriptionCompat.Builder()
        .setMediaId(ID_FAVORITE_PLAYLIST)
        .setTitle("お気に入り")
        .build()
    results.add(MediaBrowserCompat.MediaItem(favoritePlaylistMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
    val historyPlaylistMediaDesc = MediaDescriptionCompat.Builder()
        .setMediaId(ID_HISTORY_PLAYLIST)
        .setTitle("再生履歴")
        .build()
    results.add(MediaBrowserCompat.MediaItem(historyPlaylistMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
}
```

基本的には先程と同様にMediaIDとタイトルを設定したものを配列に追加していきます。ここではまだ各項目のプレイリストを表示するために、`MediaBrowserCompat.MediaItem.FLAG_BROWSABLE`で表示します。

プレイリストタブが選択されているときの画面

![image-20231122111428964.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298978/91726aef-0961-bd13-1fd7-b820dcc21798.png)
※画像はエミュレータの画面になります

```kotlin
ID_MY_PLAYLIST -> {
    // Myプレイリストの一覧を同様に追加します
   // 以下プレイリストの数だけ追加しています。
  val mediaId = ID_MY_PLAYLIST_DETAIL.toUri()
                .buildUpon()
                .appendQueryParameter(MEDIA_ID, it.myPlaylistId.toString())
                .toString() // 各MyプレイリストのIDをMediaIDに設定しています。
            val desc = MediaDescriptionCompat.Builder()
                .setMediaId(mediaId)
                .setTitle("プレイリスト名")
                .setIconUri("アイコンのUri") //  Uriで設定可能
                .build()
            results.add(MediaBrowserCompat.MediaItem(desc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
}
```
前述 ではアイコンを`.setIconBitmap()` を使ってBitmapで設定していましたが、` .setIconUri()` でUriを用いて設定することも可能です。
またプレイリストの各楽曲から再生できるアイテムを作成する場合は`MediaBrowserCompat.MediaItem.FLAG_PLAYABLE`に設定すると再生処理に分岐されます。

Myプレイリスト一覧画面

![image-20231122111447355.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298978/bf6b7888-6e54-1aef-bdc8-d5dfaf7edf60.png)

※画像はエミュレータの画面になります

Myプレイリスト詳細画面

![image-20231122111501326.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298978/44d1a58c-93c3-1af9-933b-20690ecc0092.png)

※画像はエミュレータの画面になります

先頭のシャッフル再生のように、特定のアイテムを追加することもできます。

## まとめ

今回は音楽アプリにAndroid Auto対応を導入して、タブとアイテムを表示するまでの実装を簡潔に紹介させていただきました。
また機会があれば再生の実装に関しても紹介できればと思います！

明日の [レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku) は3日目【Stylelint】コード検証ツールでCSSの見落としをなくすです。お楽しみに！ 

この記事はレコチョクのエンジニアブログの記事を転載したものとなります。 

https://techblog.recochoku.jp/9832