内容与 Android TV 主屏幕频道集成 (Kotlin)

在此 Codelab 中,您将学习如何使用 Kotlin 和 AndroidX 库构建向 Android TV 主屏幕中添加频道和节目的应用。此 Codelab 并未涵盖主屏幕中的所有功能。请阅读相关文档,了解主屏幕的所有特性和功能。

概念

Android TV 主屏幕(或简称“主屏幕”)提供了一个以频道和节目表格的形式显示推荐内容的界面。每行对应一个频道。每个频道中包含一些卡片,每个卡片对应着该频道上提供的一个节目。您的应用可以提供任意数量的频道供用户添加到主屏幕。通常,在用户选择并批准频道后,频道才会显示在主屏幕上。

每个应用都可以创建一个默认频道。默认频道比较特殊,因为它会自动显示在主屏幕中;用户无需明确要求添加该频道。

aa0471dc91b5f815.png

概览

此 Codelab 演示如何在主屏幕上创建、添加和更新频道和节目。它使用包含合集和影片的模拟数据库。为简单起见,对于所有订阅都使用同一个影片列表。

克隆入门级项目代码库

本 Codelab 使用 Android Studio,后者是用于开发 Android 应用的 IDE。

如果您尚未安装该软件,请下载并安装它。

您可以从 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

该项目中有四个步骤。在每个步骤中,您都需要向应用中添加更多代码,在完成每个部分中的所有说明后,可以将结果与下一步中的代码进行比较。

下面是该应用中的主要组件:

  • MainActivity 是项目的入口 Activity。
  • model/TvMediaBackground 是一个对象,表示浏览影片时的背景图片。
  • model/TvMediaCollection 是一个表示影片合集的对象。
  • model/TvMediaMetadata 是一个用于存储影片信息的对象。
  • model/TvMediaDatabase 表示数据库持有者,并作为底层影片数据的主要访问点。
  • fragments/NowPlayingFragment 用于播放影片。
  • fragments/MediaBrowserFragment 是媒体浏览器 Fragment。
  • workers/TvMediaSynchronizer 是一个数据同步管理程序类,其中包含用于提取 Feed、构建对象和更新频道的代码。
  • utils/TvLauncherUtils 是一个帮助程序类,用于使用 AndroidX 库和 TV Provider 管理频道和预览节目。

运行入门级项目

尝试运行该项目。如果您遇到问题,请参阅关于使用入门的文档

  1. 连接 Android TV 或启动模拟器。
  1. 选择 step_1 配置,选择您的 Android 设备,然后按菜单栏中的 run 按钮。ba443677e48e0f00.png
  2. 您应该会看到一个简单的 TV 应用轮廓,其中包含三个视频合集。

364574330c4e90a5.png

您学到的内容

在此简介中,您学习了以下内容:

  • TV 主屏幕及其频道
  • 此 Codelab 中的项目代码结构和主要类

接下来做什么?

在主屏幕上添加频道

首先在主屏幕上添加频道。添加完频道后,您就可以在频道中插入节目了。用户可在频道配置面板中发现您的频道,并选择在主屏幕界面中显示哪些频道。此 Codelab 会为每个媒体合集创建频道:

  • Historical Feature Films
  • 1910's Feature Films
  • Charlie Chaplin Collection

以下部分将介绍如何加载数据并将其用于频道。

TvMediaSynchronizer 中的 synchronize() 方法会执行以下操作:

  1. 提取媒体 Feed,其中包含背景图片、媒体合集和视频元数据。此信息在 assets/media-feed.json 中定义
  2. 更新 TvMediaDatabase 实例,该实例用于将背景图片、媒体合集和视频数据存储到相应的对象中
  3. 使用 TvLauncherUtils 创建或更新频道和节目

不用担心此 Codelab 中的数据加载。此 Codelab 的目标是帮助您了解如何使用 AndroidX 库创建频道。为此,您需向 TvLauncherUtils 类中的若干个方法添加代码。

创建频道

提取媒体数据并将其保存到本地数据库后,项目代码会将媒体 Collection 转换为频道。该代码会在 TvLauncherUtils 类的 upsertChannel() 方法中创建和更新频道。

  1. 创建 PreviewChannel.Builder() 的实例。为避免频道出现重复,此 Codelab 会检查频道是否存在,如果频道存在,则只会对其进行更新。每个视频合集都有一个关联的 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 类中的函数,将该频道插入到 TV Provider 中或更新该频道。调用 publishChannel() 会将该频道的内容值插入到 TV Provider 中。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() 方法,了解如何创建或更新频道。

将默认频道设为可见

当您向 TV Provider 添加频道时,它们是不可见的。除非用户要求,否则频道不会显示在主屏幕上。通常,在用户选择并批准频道后,频道才会显示在主屏幕上。每个应用都可以创建一个默认频道。默认频道比较特殊,因为它会自动显示在主屏幕上;用户无需明确批准添加该频道。

将以下代码添加到 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 广播消息。系统会在用户安装完 TV 应用后发送该广播,并且开发者可以在其中进行一些节目初始化。

查看示例代码中的 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

添加更多频道

Feed 包含三个合集。在 TvMediaSynchronizer 类中,为这些合集添加其他频道(在“TODO:第 5 步‘添加更多频道’”处)。

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

再次运行应用

验证是否所有三个频道均已创建完毕。点击 Customize Channels 按钮,然后点击 TV Classics。在频道面板中切换隐藏/显示按钮,以便在主屏幕上隐藏/显示这些频道。

faac02714aa36ab6.png

删除频道

如果应用不再维护某个频道,您可以将其从主屏幕中移除。

搜索第 6 步并找到 removeChannel 函数。将下面这段代码添加到该函数中(在“TODO:第 6 步‘移除频道’”处)。如需了解这段代码的运作方式,请移除 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 来更新频道。

接下来做什么?

下一部分介绍如何向频道添加节目。

向频道添加节目的过程与创建频道类似,不过,应使用 PreviewProgram.Builder 代替 PreviewChannel.Builder

您仍要使用 TvLauncherUtils 类中的 upsertChannel() 方法。

创建预览节目

在以下部分中,我们将向 step_2 添加代码。请务必在 Android Studio 项目中更改该模块的源文件。

e096c4d12a3d0a01.png

得知频道可见后,请使用带有 PreviewProgram.BuilderMetadata 对象创建一个 PreviewProgram 对象。同样,您不希望将同一节目插入某个频道中两次,因此该示例会将 metadata.id 分配给 PreviewProgramcontentId 以进行去重。在“TODO:第 7 步‘创建或查找现有的预览节目’”处添加以下代码。

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

使用媒体元数据构建构建器,然后在频道中发布/更新该构建器。(在“TODO:第 8 步‘构建预览节目并发布’”处。)

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 系统会启动该节目的 intentUriUri 应包含节目 ID,以便应用可以在用户选择该节目时从数据库中查找并播放媒体。

添加节目

在这一部分,此 Codelab 使用 AndroidX 库中的 PreviewChannelHelper 将节目插入到频道中。

使用 PreviewChannelHelper.publishPreviewProgram()PreviewChannelHelper.updatePreviewProgram() 将相应节目保存到频道中(在“TODO:第 9 步‘向频道添加预览节目’”处)。

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”。切换这三个频道,并查看日志了解发生了什么。创建频道和节目的操作在后台进行,因此您可以随意添加额外的日志语句,以帮助您跟踪所触发的事件。

您学到的内容

  • 如何向频道中添加节目。
  • 如何更新节目的属性。

接下来做什么?

向“接下来观看”频道添加节目。

“接下来观看”频道位于主屏幕顶部附近;它显示在“应用”下方,所有其他频道的上方。

44b6a6f24e4420e3.png

概念

“接下来观看”频道为您的应用提供了一种提高用户兴趣度的方法。您的应用可以向“接下来观看”频道添加以下节目:用户标记为感兴趣的节目,用户观看到中间停止的节目,或者与用户正在观看的内容相关的节目(例如电视连续剧的下一集或节目的下一季)。“接下来观看”频道有 4 种使用情形:

  • 继续观看用户尚未看完的视频。
  • 推荐下一个要观看的视频。例如,如果用户看完了第 1 集,那么您可以推荐第 2 集。
  • 显示内容来提高用户的兴趣度。
  • 维护用户添加的感兴趣视频的观看列表

本课程将介绍如何使用“接下来观看”频道继续观看视频,具体而言,就是如何在用户暂停观看某个视频时将该视频加入到“接下来观看”频道中。当该视频播放结束后,它应该从“接下来观看”频道中移除。

更新播放位置

可以通过多种方法跟踪播放内容的播放位置。此 Codelab 使用一个线程定期将最新的播放位置保存到数据库中,并更新“接下来观看”节目的元数据。请打开 step_3,然后按照以下说明添加相应代码。

NowPlayingFragment 中,将以下代码添加到 updateMetadataTask.run() 方法中(在“TODO:第 10 步‘更新进度’”处):

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

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

仅当播放进度小于总时长的 95% 时,该代码才会保存元数据。

添加以下代码(在“TODO:第 11 步‘更新数据库中的元数据’”处)。

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

如果播放进度已超过视频的 95%,系统将移除该“接下来观看”节目,以便优先播放其他内容。

NowPlayingFragment 中添加以下代码,以便从“接下来观看”行中移除已看完的视频(在“TODO:第 12 步 [移除‘接下来观看’]”处)。

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

updateMetadataTask 每隔 10 秒调度一次,以确保跟踪到最新的播放位置。此方法在 onResume() 中调度并会在 NowPlayingFragmentonPause() 中停止,因此,相关数据仅在用户观看视频时才会更新。

添加/更新“接下来观看”节目

TvLauncherUtils 会与 TV Provider 进行互动。在上一步中,调用了 TvLauncherUtils 中的 removeFromWatchNextupsertWatchNext。现在,您需要实现这两个方法。AndroidX 库提供了 PreviewChannelHelper 类,可以让此任务变得很简单。

首先,创建或查找 WatchNextProgram.Builder 的现有实例,然后使用最新的播放 metadata 更新该对象。在 upsertWatchNext() 方法中添加以下代码(在“TODO:第 13 步 [构建‘接下来观看’节目]”处)。

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.Builder 调用 build() 方法后,系统将创建 WatchNextProgam。您可以使用 PreviewChannelHelper 将它发布到“接下来观看”行。

添加以下代码(在“TODO:第 14.1 步 [创建‘接下来观看’节目]”处):

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

或者,如果该节目已存在,则更新它(在“TODO:第 14.2 步 [更新‘接下来观看’节目]”处)

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

移除“接下来观看”节目

当用户播放完视频时,您应该清理“接下来观看”频道。这与移除 PreviewProgram 几乎相同。

使用 buildWatchNextProgramUri() 创建一个执行删除操作的 Uri。(我们无法在 PreviewChannelHelper 中使用 API 来移除“接下来观看”节目。)

TvLauncherUtils 类的 removeFromWatchNext() 方法中的现有代码替换为以下语句(在“TODO:第 15 步‘移除节目’”处):

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

运行应用

在配置中选择 step_3,然后运行应用。

6e43dc24a1ef0273.png

观看您的任一合集中的视频几秒钟,然后暂停播放器(如果您使用的是模拟器,请按空格键)。返回到主屏幕后,您应该会看到该影片已添加至“接下来观看”频道。从“接下来观看”频道中选择同一部影片,它应该会从您上次暂停的位置继续播放。在您看完整部影片后,它应该从“接下来观看”频道中移除。在不同的用户场景中试用“接下来观看”频道。

您学到的内容

  • 如何向“接下来观看”频道中添加节目以提高用户兴趣度。
  • 如何更新“接下来观看”频道中的节目。
  • 如何从“接下来观看”频道中移除节目。

接下来做什么?

完成 Codelab 后,将该应用变成您自己的应用。将媒体 Feed 和数据模型替换为您自己的数据,并将其转换为 TV Provider 的频道和节目。

如需了解详情,请访问相关文档