Android TV のホーム画面チャンネルとのコンテンツ統合(Kotlin)

この Codelab では、Kotlin と AndroidX のライブラリを使用して、Android TV のホーム画面にチャンネルとプログラムを追加するアプリの作成方法を学びます。この Codelab で取り上げるのは、ホーム画面の一部の機能です。ホーム画面のすべての機能については、こちらのドキュメントをご覧ください。

コンセプト

Android TV ホーム画面(あるいは単にホーム画面)では、おすすめのコンテンツをチャンネルとプログラムの表として表示する UI が提供されます。各行がチャンネルです。チャンネルには、そのチャンネルで利用可能なすべてのプログラムのカードが含まれています。アプリは、ユーザーがホーム画面に追加するチャンネルをいくつでも提供できます。ユーザーは通常、ホーム画面に表示される前に各チャンネルを選択して承認する必要があります。

すべてのアプリはデフォルト チャンネルを 1 つ作成できます。デフォルト チャンネルは特別で、自動的にホーム画面に表示されます。ユーザーが明示的に要求する必要はありません。

aa0471dc91b5f815.png

概要

この Codelab では、ホーム画面のチャンネルとプログラムを作成、追加、更新する方法について説明します。コレクションとムービーのモック データベースを使用します。わかりやすくするため、すべてのサブスクリプションに同じムービーのリストを使用します。

スターター プロジェクト リポジトリのクローンを作成する

この Codelab では、Android アプリを開発するための IDE である Android Studio を使用します。

まだインストールしていない場合は、ダウンロードしてインストールしてください。

GitHub リポジトリからソースコードをダウンロードできます。

git clone https://github.com/googlecodelabs/tv-recommendations-kotlin.git

または、ZIP ファイルとしてダウンロードできます。

ZIP をダウンロード

Android Studio を開き、メニューバーから [File] > [Open] をクリックするか、スプラッシュ画面から [Open an Existing Android Studio Project] をクリックして、最近クローンを作成したフォルダを選択します。

c0e57864138c1248.png

スターター プロジェクトについて

bd4f805254260df7.png

プロジェクトには 4 つのステップがあります。各ステップで、アプリにコードをさらに追加し、各セクションのすべての手順を完了した後、次のステップでコードと結果を比較できます。

アプリの主なコンポーネントは次のとおりです。

  • MainActivity は、プロジェクトのエントリ アクティビティです。
  • model/TvMediaBackground は、ムービー閲覧中の背景画像のオブジェクトです。
  • model/TvMediaCollection は、ムービー コレクションのオブジェクトです。
  • model/TvMediaMetadata はムービー情報を格納するオブジェクトです。
  • model/TvMediaDatabase はデータベース ホルダーであり、基となるムービーデータのメイン アクセス ポイントとして機能します。
  • fragments/NowPlayingFragment はムービーを再生します。
  • fragments/MediaBrowserFragment は、メディア ブラウザ フラグメントです。
  • workers/TvMediaSynchronizer は、フィードの取得、オブジェクトの構築、チャンネルの更新を行うためのコードを含むデータ同期クラスです。
  • utils/TvLauncherUtils は、AndroidX ライブラリとテレビ プロバイダを使用してチャンネルとプレビュー プログラムを管理するヘルパークラスです。

スターター プロジェクトを実行する

プロジェクトを実行してみてください。問題がある場合は、開始方法についてのドキュメントをご覧ください。

  1. Android TV を接続するか、エミュレータを起動します。
  1. step_1 構成を選択し、Android デバイスを選択して、メニューバーの実行ボタンを押します。ba443677e48e0f00.png
  2. 3 つの動画コレクションを含むシンプルなテレビアプリの概要が表示されます。

364574330c4e90a5.png

学習した内容

ここでは、次のことを学習しました。

  • テレビのホーム画面とチャンネル
  • この Codelab のプロジェクト コード構造と主要なクラス

次のステップ

ホーム画面にチャンネルを追加する

まず、ホーム画面にチャンネルを追加します。チャンネルを作成したら、そのチャンネルにプログラムを挿入できます。ユーザーは、チャンネル設定パネルでチャンネルを見つけて、ホーム UI に表示するチャンネルを選択できます。この Codelab では、メディア コレクションごとにチャンネルを作成します。

  • 歴史的フィーチャー映画
  • 1910 年代のフィーチャー映画
  • チャーリー チャップリン コレクション

以下のセクションでは、データの読み込み方法とチャンネル用に使用する方法について説明します。

TvMediaSynchronizersynchronize() メソッドは、次のことを行います。

  1. 背景画像、メディア コレクション、動画のメタデータを含むメディア フィードを取得します。この情報は assets/media-feed.json で定義されます
  2. TvMediaDatabase インスタンスに格納されている背景画像、メディア コレクション、動画データ オブジェクトをそれぞれ更新します
  3. TvLauncherUtils を使用して、チャンネルとプログラムを作成または更新します

この Codelab でのデータの読み込みについて心配する必要はありません。この Codelab の目標は、AndroidX ライブラリを使用してチャンネルを作成する方法について理解することです。そのために、TvLauncherUtils クラスのいくつかのメソッドにコードを追加します。

チャンネルを作成する

メディアデータを取得してローカル データベースに保存した後、プロジェクト コードはメディア Collection をチャンネルに変換します。このコードは、TvLauncherUtils クラスの upsertChannel() メソッドでチャンネルを作成し、更新します。

  1. PreviewChannel.Builder() のインスタンスを作成します。チャンネルの重複を避けるため、この Codelab では、チャンネルが存在するかどうかを確認し、存在する場合は更新のみを行います。各動画コレクションには ID が関連付けられています。この ID はチャンネルの internalProviderId として使用できます。既存のチャンネルを特定するには、その internalProviderId をコレクション ID と比較します。次のコードをコピーして upsertChannel() に貼り付けます(コードコメント // TODO: Step 1 create or find an existing channel.)。
val channelBuilder = if (existingChannel == null) {
   PreviewChannel.Builder()
} else {
   PreviewChannel.Builder(existingChannel)
}
  1. チャンネルの Builder で属性を設定します(チャンネル名、ロゴ、アイコンなど)。表示名は、ホーム画面のチャンネル アイコンのすぐ下に表示されます。Android TV は appLinkIntentUri を使用して、ユーザーがチャンネル アイコンをクリックしたときにナビゲートします。この Codelab では、この URI を使用して、アプリ内の対応するコレクションにユーザーを誘導します。次のコードをコピーして貼り付けます(コードコメント // TODO: Step 2 add collection metadata and build channel object)。
val updatedChannel = channelBuilder
       .setInternalProviderId(collection.id)
       .setLogo(channelLogoUri)
       .setAppLinkIntentUri(appUri)
       .setDisplayName(collection.title)
       .setDescription(collection.description)
       .build()
  1. PreviewChannelHelper クラスの関数を呼び出して、テレビ プロバイダにチャンネルを挿入するか、チャンネルを更新します。publishChannel() を呼び出すと、チャンネルのコンテンツ値がテレビ プロバイダに挿入されます。updatePreviewChannel は、既存のチャンネルを更新します。次のコードを挿入します(コードコメント // TODO: Step 3.1 update an existing channel)。
PreviewChannelHelper(context)
       .updatePreviewChannel(existingChannel.id, updatedChannel)
Log.d(TAG, "Updated channel ${existingChannel.id}")

下記のコードを挿入して新しいチャンネルを作成します(コードコメント // TODO: Step 3.2 publish a channel)。

val channelId = PreviewChannelHelper(context).publishChannel(updatedChannel)
Log.d(TAG, "Published channel $channelId")
channelId
  1. upsertChannel() メソッドを見直して、チャンネルの作成または更新方法を確認します。

デフォルト チャンネルを表示する

テレビ プロバイダにチャンネルを追加しても、表示されません。ユーザーがリクエストするまで、チャンネルはホーム画面に表示されません。通常は、各チャンネルがホーム画面に表示される前にユーザーが選択して承認する必要があります。すべてのアプリはデフォルト チャンネルを 1 つ作成できます。デフォルト チャンネルは特別で、自動的にホーム画面に表示されます。ユーザーが明示的に承認する必要はありません。

次のコードを upsertChannel() メソッドに追加します(TODO: step 4 make default channel visible)。

if(allChannels.none { it.isBrowsable }) {
   TvContractCompat.requestChannelBrowsable(context, channelId)
}

デフォルト以外のチャンネルに対して requestChannelBrowsable() を呼び出すと、ユーザーの同意を求めるダイアログが表示されます。

チャンネル更新のスケジュール設定

チャンネルの作成 / 更新コードを追加した後、デベロッパーは、チャンネルの作成や更新を行うために synchronize() メソッドを呼び出す必要があります。

アプリのチャンネルを作成する最適なタイミングは、ユーザーがインストールした直後です。android.media.tv.action.INITIALIZE_PROGRAMS ブロードキャスト メッセージをリッスンするブロードキャスト レシーバを作成できます。このブロードキャストは、ユーザーがテレビアプリをインストールした後に送信されます。ブロードキャストを受信したら、デベロッパーはプログラムの初期化を行うことができます。

サンプルコードの AndroidManifest.xml ファイルを確認し、ブロードキャスト レシーバのセクションを見つけます。ブロードキャスト レシーバの正しいクラス名を探します(後述します)。

<action
   android:name="android.media.tv.action.INITIALIZE_PROGRAMS" />

TvLauncherReceiver クラスを開き、次のコードブロックを見て、サンプルアプリがホーム画面チャンネルをどのように作成するかを確認します。

TvContractCompat.ACTION_INITIALIZE_PROGRAMS -> {
   Log.d(TAG, "Handling INITIALIZE_PROGRAMS broadcast")
   // Synchronizes all program and channel data
   WorkManager.getInstance(context).enqueue(
           OneTimeWorkRequestBuilder<TvMediaSynchronizer>().build())
}

チャンネルは定期的に更新するようにします。この Codelab では、WorkManager ライブラリを使用してバックグラウンド タスクを作成します。MainActivity クラスの TvMediaSynchronizer を、チャンネルの定期的な更新のスケジュール設定に使用します。

// Syncs the home screen channels hourly
// NOTE: It's very important to keep our content fresh in the user's home screen
WorkManager.getInstance(baseContext).enqueue(
       PeriodicWorkRequestBuilder<TvMediaSynchronizer>(1, TimeUnit.HOURS)
               .setInitialDelay(1, TimeUnit.HOURS)
               .setConstraints(Constraints.Builder()
                       .setRequiredNetworkType(NetworkType.CONNECTED)
                       .build())
               .build())

アプリを実行する

アプリを実行します。ホーム画面に移動します。デフォルトの(My TV App Default)チャンネルが表示されますが、プログラムはありません。エミュレータではなく実際のデバイスでコードを実行する場合、チャンネルが表示されないことがあります。

f14e903b0505a281.png

チャンネルを追加する

フィードには 3 つのコレクションが含まれています。TvMediaSynchronizer クラスにこれらのコレクションの他のチャンネルを追加します(TODO: step 5 add more channels)。

feed.collections.subList(1, feed.collections.size).forEach {
   TvLauncherUtils.upsertChannel(
           context, it, database.metadata().findByCollection(it.id))
}

アプリを再度実行する

3 つのチャンネルがすべて作成されていることを確認します。[Customize Channels] ボタンをクリックし、[TV Classics] をクリックします。チャンネル パネルの非表示 / 表示ボタンを使用して、ホーム画面上での非表示 / 表示を切り替えます。

faac02714aa36ab6.png

チャンネルを削除する

アプリで配信しなくなったチャンネルは、ホーム画面から削除できます。

ステップ 6 を検索して、removeChannel 関数を見つけます。次のセクションを追加します(TODO: step 6 remove a channel)。このコードの動作を確認するために、media-feed.json で「Charlie Chaplin Collection」というタイトルのコレクションを削除します(必ずコレクション全体を削除してください)。アプリをもう一度実行すると、数秒後にチャンネルが削除されます。

// First, get all the channels added to the home screen
val allChannels = PreviewChannelHelper(context).allChannels

// Now find the channel with the matching content ID for our collection
val foundChannel = allChannels.find { it.internalProviderId == collection.id }
if (foundChannel == null) Log.e(TAG, "No channel with ID ${collection.id}")

// Use the found channel's ID to delete it from the content resolver
return foundChannel?.let {
   PreviewChannelHelper(context).deletePreviewChannel(it.id)
   Log.d(TAG, "Channel successfully removed from home screen")

   // Remove all of the channel programs as well
   val channelPrograms =
           TvContractCompat.buildPreviewProgramsUriForChannel(it.id)
   context.contentResolver.delete(channelPrograms, null, null)

   // Return the ID of the channel removed
   it.id
}

上記の手順がすべて完了したら、アプリコードを step_2 と比較できます。

学習した内容

  • チャンネルのクエリ方法。
  • ホーム画面でチャンネルを追加または削除する方法。
  • チャンネルにロゴまたはタイトルを設定する方法。
  • デフォルト チャンネルを表示する方法。
  • WorkManager のスケジュールを設定してチャンネルを更新する方法。

次のステップ

次のセクションでは、プログラムをチャンネルに追加する方法について説明します。

プログラムをチャンネルに追加する方法は、チャンネルを作成する場合と同様です。PreviewChannel.Builder ではなく PreviewProgram.Builder を使用します。

TvLauncherUtils クラスの upsertChannel() メソッドを引き続き使用します。

プレビュー プログラムを作成する

下記のセクションで、コードを step_2 に追加します。Android Studio プロジェクトでは、そのモジュールのソースファイルに変更を加えるようにしてください。

e096c4d12a3d0a01.png

チャンネルが表示されることを確認したら、Metadata オブジェクトを PreviewProgram.Builder とともに使用して、PreviewProgram オブジェクトを作成します。繰り返しになりますが、同じプログラムを 1 つのチャンネルに 2 回挿入する必要はないため、サンプルでは重複除去の目的で metadata.idPreviewProgramcontentId に代入します。次のコードを追加します(TODO: Step 7 create or find an existing preview program)。

val existingProgram = existingProgramList.find { it.contentId == metadata.id }
val programBuilder = if (existingProgram == null) {
   PreviewProgram.Builder()
} else {
   PreviewProgram.Builder(existingProgram)
}

メディアのメタデータを使用してビルダーを作成し、チャンネルで公開 / 更新します(TODO: Step 8 build preview program and publish)。

val updatedProgram = programBuilder.also { metadata.copyToBuilder(it) }
       // Set the same channel ID in all programs
       .setChannelId(channelId)
       // This must match the desired intent filter in the manifest for VIEW action
       .setIntentUri(Uri.parse("https://$host/program/${metadata.id}"))
       // Build the program at once
       .build()

次の点にご注意ください。

  1. サンプルコードは、contentId を介してメタデータとプレビュー プログラムをリンクします。
  2. プレビュー プログラムは、PreviewProgram.Builder()setChannelId() を呼び出すことでチャンネルに挿入されます。
  3. Android TV システムは、ユーザーがチャンネルからプログラムを選択すると、プログラムの intentUri を起動します。Uri には、ユーザーがプログラムを選択したときにアプリがデータベースからメディアを見つけて再生できるように、プログラム ID を含める必要があります。

プログラムの追加

この Codelab では、AndroidX ライブラリの PreviewChannelHelper を使用して、プログラムをチャンネルに挿入します。

PreviewChannelHelper.publishPreviewProgram() または PreviewChannelHelper.updatePreviewProgram() を使用してプログラムをチャンネルに保存します(TODO: Step 9 add preview program to channel)。

try {
   if (existingProgram == null) {
       PreviewChannelHelper(context).publishPreviewProgram(updatedProgram)
       Log.d(TAG, "Inserted program into channel: $updatedProgram")
   } else {
       PreviewChannelHelper(context)
               .updatePreviewProgram(existingProgram.id, updatedProgram)
       Log.d(TAG, "Updated program in channel: $updatedProgram")
   }
} catch (exc: IllegalArgumentException) {
   Log.e(TAG, "Unable to add program: $updatedProgram", exc)
}

これで、アプリがプログラムをチャンネルに追加できるようになりました。コードを step_3 と比較できます。

アプリを実行する

構成で step_2 を選択し、アプリを実行します。

200e69351ce6a530.png

アプリを実行したら、ホーム画面の下部にある [Customize Channels] ボタンをクリックして、「TV Classics」アプリを探します。3 つのチャンネルを切り替えて、何が起きているかをログで確認します。チャンネルとプログラムの作成はバックグラウンドで行われるため、トリガーされるイベントのトレースに役立つログ ステートメントを追加しても構いません。

学習した内容

  • チャンネルにプログラムを追加する方法。
  • プログラムの属性を更新する方法。

次のステップ

Watch Next チャンネルにプログラムを追加する。

Watch Next チャンネルはホーム画面の上部にあります。[Apps] の下、その他すべてのチャンネルの上に表示されます。

44b6a6f24e4420e3.png

コンセプト

Watch Next チャンネルは、アプリがユーザーとのエンゲージメントを高める手段を提供します。アプリでは、ユーザーによって興味があるとマークされたプログラム、視聴途中のプログラム、視聴中のコンテンツに関連するプログラム(シリーズの次のエピソードやシーズンなど)を、チャンネルに追加できます。Watch Next チャンネルには 4 種類のユースケースがあります。

  • ユーザーが視聴を終えていない動画の視聴を続行する。
  • に視聴する動画を提案する。たとえば、ユーザーがエピソード 1 の視聴を終了した場合に、エピソード 2 を提案できます。
  • 新しいコンテンツを紹介してエンゲージメントを促進する。
  • ユーザーが追加した興味がある動画の観たいものリストを維持する。

このレッスンでは、Watch Next チャンネルを使用して動画の視聴を継続する方法、具体的には、ユーザーが動画を一時停止したときに Watch Next チャンネルに動画を追加する方法について説明します。動画は最後まで再生されると、Watch Next チャンネルから削除されます。

再生位置を更新する

コンテンツの再生位置を追跡する方法は複数あります。この Codelab では、スレッドを使用して最新の再生位置を定期的にデータベースに保存し、Watch Next プログラムのメタデータを更新します。step_3 を開き、下記の手順に沿ってコードを追加してください。

NowPlayingFragment で、次のコードを updateMetadataTask.run() メソッドに追加します(TODO: step 10 update progress)。

val contentDuration = player.duration
val contentPosition = player.currentPosition

// Updates metadata state
val metadata = args.metadata.apply {
   playbackPositionMillis = contentPosition
}

このコードは、再生位置が合計再生時間の 95% 未満の場合にのみ、メタデータを保存します。

次のコードを追加します(TODO: step 11 update metadata to database

val programUri = TvLauncherUtils.upsertWatchNext(requireContext(), metadata)
lifecycleScope.launch(Dispatchers.IO) {
   database.metadata().update(
           metadata.apply { if (programUri != null) watchNext = true })
}

再生位置が動画の 95% を超えた場合、Watch Next プログラムは削除され、他のコンテンツを優先できるようになります。

NowPlayingFragment で、次のコードを追加して完了した動画を Watch Next 行から削除します(TODO: step 12 remove watch next)。

val programUri = TvLauncherUtils.removeFromWatchNext(requireContext(), metadata)
if (programUri != null) lifecycleScope.launch(Dispatchers.IO) {
   database.metadata().update(metadata.apply { watchNext = false })
}

updateMetadataTask は 10 秒ごとにスケジュール設定されて、最新の再生位置が追跡されます。onResume() でスケジュール設定され、NowPlayingFragmentonPause() で停止します。このため、ユーザーが動画を視聴しているときにのみ、データが更新されます。

Watch Next プログラムの追加と更新

TvLauncherUtils はテレビ プロバイダと連携します。前のステップで、TvLauncherUtilsremoveFromWatchNextupsertWatchNext が呼び出されます。この 2 つのメソッドを実装する必要があります。AndroidX ライブラリには PreviewChannelHelper クラスが用意されており、このタスクが非常に簡単になります。

まず、WatchNextProgram.Builder のインスタンスを作成するか、既存のインスタンスを探し、次に最新の再生 metadata を使用してオブジェクトを更新します。次のコードを upsertWatchNext() メソッドに追加します(TODO: step 13 build watch next program)。

programBuilder.setLastEngagementTimeUtcMillis(System.currentTimeMillis())

programBuilder.setWatchNextType(metadata.playbackPositionMillis?.let { position ->
   if (position > 0 && metadata.playbackDurationMillis?.let { it > 0 } == true) {
       Log.d(TAG, "Inferred watch next type: CONTINUE")
       TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE
   } else {
       Log.d(TAG, "Inferred watch next type: UNKNOWN")
       WatchNextProgram.WATCH_NEXT_TYPE_UNKNOWN
   }
} ?: TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_NEXT)

// This must match the desired intent filter in the manifest for VIEW intent action
programBuilder.setIntentUri(Uri.parse(
       "https://${context.getString(R.string.host_name)}/program/${metadata.id}"))

// Build the program with all the metadata
val updatedProgram = programBuilder.build()

WatchNextProgram.Builderbuild() メソッドを呼び出すと、WatchNextProgam が作成されます。PreviewChannelHelper を使用して Watch Next 行にパブリッシュできます。

次のコードを追加します(TODO: step 14.1 create watch next program)。

val programId = PreviewChannelHelper(context)
       .publishWatchNextProgram(updatedProgram)
Log.d(TAG, "Added program to watch next row: $updatedProgram")
programId

プログラムが存在する場合は、単に更新します(TODO: step 14.2 update watch next program)。

PreviewChannelHelper(context)
       .updateWatchNextProgram(updatedProgram, existingProgram.id)
Log.d(TAG, "Updated program in watch next row: $updatedProgram")
existingProgram.id

Watch Next プログラムの削除

ユーザーが動画の再生を終了したら、Watch Next チャンネルをクリーンアップする必要があります。これは、PreviewProgram を削除する場合とほぼ同じです。

buildWatchNextProgramUri() を使用して、削除を行う Uri を作成します(PreviewChannelHelper で Watch Next プログラムを削除するために使用できる API はありません)。

TvLauncherUtils クラスの removeFromWatchNext() メソッドの既存のコードを、次のステートメントに置き換えます(TODO: step 15 remove program)。

val programUri = TvContractCompat.buildWatchNextProgramUri(it.id)
val deleteCount = context.contentResolver.delete(
       programUri, null, null)

アプリを実行する

構成で step_3 を選択し、アプリを実行します。

6e43dc24a1ef0273.png

任意のコレクションの動画を数秒間視聴し、プレーヤーを一時停止します(エミュレータを使用している場合はスペースキーを押します)。ホーム画面に戻ると、ムービーが Watch Next チャンネルに追加されていることがわかります。Watch Next チャンネルから同じムービーを選択すると、一時停止した位置から続行されます。ムービー全体を視聴すると、Watch Next チャンネルから削除されます。さまざまなユーザー シナリオを使用して Watch Next チャンネルをテストします。

学習した内容

  • プログラムを Watch Next チャンネルに追加してエンゲージメントを促進する方法。
  • Watch Next チャンネルのプログラムを更新する方法。
  • Watch Next チャンネルからプログラムを削除する方法。

次のステップ

Codelab を完了したら、自分用にアプリを作成します。メディア フィードとデータモデルを独自のものに置き換え、テレビ プロバイダのチャンネルとプログラムに変換します。

詳細については、こちらのドキュメントをご覧ください。