主屏幕上的频道

Android TV 主屏幕(或简称主屏幕)提供了一个 UI,它以频道节目表格的形式显示推荐内容。每一行是一个频道。一个频道包含该频道上所有可用节目的卡片。

TV home screen

本文档演示了如何将频道和节目添加到主屏幕、更新内容、处理用户操作,以及为用户提供最佳体验。(如果您想深入了解 API,请尝试主屏幕 Codelab 并观看I/O 2017 Android TV 会议。)

注意:推荐频道仅在 Android 8.0(API 级别 26)及更高版本中可用。您必须使用它们来为在 Android 8.0(API 级别 26)及更高版本中运行的应用提供推荐。要为在更早版本的 Android 上运行的应用提供推荐,您的应用必须改用推荐行

主屏幕 UI

应用可以创建新频道,添加、移除和更新频道中的节目,并控制频道中节目的顺序。例如,应用可以创建一个名为“新增内容”的频道,并显示新可用节目的卡片。

应用无法控制频道在主屏幕中显示的顺序。当您的应用创建新频道时,主屏幕会将其添加到频道列表的底部。用户可以重新排序、隐藏和显示频道。

“接下来观看”频道

“接下来观看”频道是主屏幕中继应用行之后的第二行。系统创建并维护此频道。您的应用可以将节目添加到“接下来观看”频道。有关更多信息,请参阅将节目添加到“接下来观看”频道

应用频道

您的应用创建的频道都遵循以下生命周期:

  1. 用户在您的应用中发现一个频道,并请求将其添加到主屏幕。
  2. 应用创建频道并将其添加到 TvProvider(此时频道不可见)。
  3. 应用请求系统显示频道。
  4. 系统要求用户批准新频道。
  5. 新频道出现在主屏幕的最后一行。

默认频道

您的应用可以提供任意数量的频道供用户添加到主屏幕。用户通常必须选择并批准每个频道,然后它才能出现在主屏幕中。每个应用都可以选择创建一个默认频道。默认频道是特殊的,因为它会自动显示在主屏幕中;用户无需明确请求它。

前提条件

Android TV 主屏幕使用 Android 的 TvProvider API 来管理您的应用创建的频道和节目。要访问提供商的数据,请将以下权限添加到您的应用清单中:

<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />

TvProvider 支持库使得使用该提供商更加容易。将其添加到您的 build.gradle 文件中的依赖项:

Groovy

implementation 'androidx.tvprovider:tvprovider:1.0.0'

Kotlin

implementation("androidx.tvprovider:tvprovider:1.0.0")

要处理频道和节目,请确保在您的程序中包含这些支持库导入:

Kotlin

import android.support.media.tv.Channel
import android.support.media.tv.TvContractCompat
import android.support.media.tv.ChannelLogoUtils
import android.support.media.tv.PreviewProgram
import android.support.media.tv.WatchNextProgram

Java

import android.support.media.tv.Channel;
import android.support.media.tv.TvContractCompat;
import android.support.media.tv.ChannelLogoUtils;
import android.support.media.tv.PreviewProgram;
import android.support.media.tv.WatchNextProgram;

频道

您的应用创建的第一个频道将成为其默认频道。默认频道会自动显示在主屏幕中。您创建的所有其他频道必须经过用户选择和接受后才能显示在主屏幕中。

创建频道

您的应用应仅在 foreground 运行时请求系统显示新添加的频道。这可以防止您的应用在用户运行其他应用时显示对话框,请求批准添加您的频道。如果您在后台运行时尝试添加频道,该活动的 onActivityResult() 方法将返回状态码 RESULT_CANCELED

要创建频道,请按照以下步骤操作:

  1. 创建频道构建器并设置其属性。请注意,频道类型必须是 TYPE_PREVIEW。根据需要添加更多属性

    Kotlin

    val builder = Channel.Builder()
    // Every channel you create must have the type TYPE_PREVIEW
    builder.setType(TvContractCompat.Channels.TYPE_PREVIEW)
            .setDisplayName("Channel Name")
            .setAppLinkIntentUri(uri)

    Java

    Channel.Builder builder = new Channel.Builder();
    // Every channel you create must have the type TYPE_PREVIEW
    builder.setType(TvContractCompat.Channels.TYPE_PREVIEW)
            .setDisplayName("Channel Name")
            .setAppLinkIntentUri(uri);
  2. 将频道插入提供商

    Kotlin

    var channelUri = context.contentResolver.insert(
            TvContractCompat.Channels.CONTENT_URI, builder.build().toContentValues())

    Java

    Uri channelUri = context.getContentResolver().insert(
            TvContractCompat.Channels.CONTENT_URI, builder.build().toContentValues());
  3. 您需要保存频道 ID,以便稍后向频道添加节目。从返回的 URI 中提取频道 ID:

    Kotlin

    var channelId = ContentUris.parseId(channelUri)

    Java

    long channelId = ContentUris.parseId(channelUri);
  4. 您必须为您的频道添加一个徽标。使用 UriBitmap。徽标图标应为 80dp x 80dp,且必须不透明。它显示在圆形遮罩下。

    TV home screen icon mask

    Kotlin

    // Choose one or the other
    storeChannelLogo(context: Context, channelId: Long, logoUri: Uri) // also works if logoUri is a URL
    storeChannelLogo(context: Context, channelId: Long, logo: Bitmap)

    Java

    // Choose one or the other
    storeChannelLogo(Context context, long channelId, Uri logoUri); // also works if logoUri is a URL
    storeChannelLogo(Context context, long channelId, Bitmap logo);
  5. 创建默认频道(可选):当您的应用创建第一个频道时,您可以将其设置为默认频道,使其立即显示在主屏幕中,无需任何用户操作。您创建的任何其他频道在用户明确选择它们之前都不可见。

    Kotlin

    TvContractCompat.requestChannelBrowsable(context, channelId)

    Java

    TvContractCompat.requestChannelBrowsable(context, channelId);

  6. 在您的应用打开之前,让您的默认频道显示。您可以通过添加一个 BroadcastReceiver 来实现此行为,该 BroadcastReceiver 监听 android.media.tv.action.INITIALIZE_PROGRAMS 操作,该操作是主屏幕在应用安装后发送的。
    <receiver
      android:name=".RunOnInstallReceiver"
      android:exported="true">
        <intent-filter>
          <action android:name="android.media.tv.action.INITIALIZE_PROGRAMS" />
          <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </receiver>
    在开发期间旁加载您的应用时,您可以通过 adb 触发意图来测试此步骤,其中 your.package.name/.YourReceiverName 是您的应用的 BroadcastReceiver

    adb shell am broadcast -a android.media.tv.action.INITIALIZE_PROGRAMS -n \
        your.package.name/.YourReceiverName
    

    在极少数情况下,您的应用可能会在用户启动应用的同时收到广播。请确保您的代码不会尝试多次添加默认频道。

更新频道

更新频道与创建频道非常相似。

使用另一个 Channel.Builder 来设置需要更改的属性。

使用 ContentResolver 更新频道。使用您在频道最初添加时保存的频道 ID。

Kotlin

context.contentResolver.update(
        TvContractCompat.buildChannelUri(channelId),
        builder.build().toContentValues(),
        null,
        null
)

Java

context.getContentResolver().update(TvContractCompat.buildChannelUri(channelId),
    builder.build().toContentValues(), null, null);

要更新频道的徽标,请使用 storeChannelLogo()

删除频道

Kotlin

context.contentResolver.delete(TvContractCompat.buildChannelUri(channelId), null, null)

Java

context.getContentResolver().delete(TvContractCompat.buildChannelUri(channelId), null, null);

节目

将节目添加到应用频道

创建 PreviewProgram.Builder 并设置其属性。

Kotlin

val builder = PreviewProgram.Builder()
builder.setChannelId(channelId)
        .setType(TvContractCompat.PreviewPrograms.TYPE_CLIP)
        .setTitle("Title")
        .setDescription("Program description")
        .setPosterArtUri(uri)
        .setIntentUri(uri)
        .setInternalProviderId(appProgramId)

Java

PreviewProgram.Builder builder = new PreviewProgram.Builder();
builder.setChannelId(channelId)
        .setType(TvContractCompat.PreviewPrograms.TYPE_CLIP)
        .setTitle("Title")
        .setDescription("Program description")
        .setPosterArtUri(uri)
        .setIntentUri(uri)
        .setInternalProviderId(appProgramId);

根据节目类型添加更多属性。(要查看每种节目类型可用的属性,请参阅下面的表格。)

将节目插入提供商

Kotlin

var programUri = context.contentResolver.insert(TvContractCompat.PreviewPrograms.CONTENT_URI,
        builder.build().toContentValues())

Java

Uri programUri = context.getContentResolver().insert(TvContractCompat.PreviewPrograms.CONTENT_URI,
      builder.build().toContentValues());

检索节目 ID 以供以后参考。

Kotlin

val programId = ContentUris.parseId(programUri)

Java

long programId = ContentUris.parseId(programUri);

将节目添加到“接下来观看”频道

要将节目插入“接下来观看”频道,请参阅将节目添加到“接下来观看”频道

更新节目

您可以更改节目的信息。例如,您可能希望更新电影的租金,或更新显示用户已观看多少节目的进度条。

使用 PreviewProgram.Builder 设置您需要更改的属性,然后调用 getContentResolver().update 来更新节目。指定您在节目最初添加时保存的节目 ID。

Kotlin

context.contentResolver.update(
        TvContractCompat.buildPreviewProgramUri(programId),
                builder.build().toContentValues(), null, null
)

Java

context.getContentResolver().update(TvContractCompat.buildPreviewProgramUri(programId),
    builder.build().toContentValues(), null, null);

删除节目

Kotlin

context.contentResolver
        .delete(TvContractCompat.buildPreviewProgramUri(programId), null, null)

Java

context.getContentResolver().delete(TvContractCompat.buildPreviewProgramUri(programId), null, null);

处理用户操作

您的应用可以通过提供 UI 来显示和添加频道,从而帮助用户发现内容。您的应用还应该处理它们出现在主屏幕后与您的频道的交互。

发现和添加频道

您的应用可以提供一个 UI 元素,让用户选择和添加其频道(例如,一个请求添加频道的按钮)。

用户请求特定频道后,执行以下代码以获取用户将其添加到主屏幕 UI 的权限:

Kotlin

val intent = Intent(TvContractCompat.ACTION_REQUEST_CHANNEL_BROWSABLE)
intent.putExtra(TvContractCompat.EXTRA_CHANNEL_ID, channelId)
try {
  activity.startActivityForResult(intent, 0)
} catch (e: ActivityNotFoundException) {
  // handle error
}

Java

Intent intent = new Intent(TvContractCompat.ACTION_REQUEST_CHANNEL_BROWSABLE);
intent.putExtra(TvContractCompat.EXTRA_CHANNEL_ID, channelId);
try {
   activity.startActivityForResult(intent, 0);
} catch (ActivityNotFoundException e) {
  // handle error
}

系统会显示一个对话框,要求用户批准频道。在您的活动的 onActivityResult 方法中处理请求结果(Activity.RESULT_CANCELEDActivity.RESULT_OK)。

Android TV 主屏幕事件

当用户与应用发布的节目/频道互动时,主屏幕会向应用发送意图:

  • 当用户选择频道的徽标时,主屏幕会将存储在频道 APP_LINK_INTENT_URI 属性中的 Uri 发送到应用。应用应只启动其主 UI 或与所选频道相关的视图。
  • 当用户选择一个节目时,主屏幕会将存储在节目 INTENT_URI 属性中的 Uri 发送到应用。应用应播放所选内容。
  • 用户可以指示他们不再对某个节目感兴趣,并希望将其从主屏幕 UI 中移除。系统会从 UI 中移除该节目,并向拥有该节目的应用发送一个意图(android.media.tv.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED 或 android.media.tv.ACTION_WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED),其中包含该节目的 ID。应用应从提供商中移除该节目,并且不应重新插入它。

确保为主屏幕因用户互动而发送的所有 Uris 创建意图过滤器;例如:

<receiver
   android:name=".WatchNextProgramRemoved"
   android:enabled="true"
   android:exported="true">
   <intent-filter>
       <action android:name="android.media.tv.ACTION_WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED" />
   </intent-filter>
</receiver>

最佳实践

  • 许多 TV 应用要求用户登录。在这种情况下,监听 android.media.tv.action.INITIALIZE_PROGRAMSBroadcastReceiver 应为未经身份验证的用户建议频道内容。例如,您的应用最初可以显示最佳内容或当前热门内容。用户登录后,它可以显示个性化内容。这是应用在用户登录前向上销售用户的好机会。
  • 当您的应用不在前台且您需要更新频道或节目时,请使用 JobScheduler 来调度工作(请参阅:JobSchedulerJobService)。
  • 如果您的应用行为不当(例如:持续向提供商发送垃圾数据),系统可能会撤销您应用的提供商权限。请务必将访问提供商的代码用 try-catch 语句包装起来,以处理安全异常。
  • 在更新节目和频道之前,查询提供商以获取您需要更新的数据并协调数据。例如,无需更新用户希望从 UI 中移除的节目。使用后台作业,在查询现有数据后将数据插入/更新到提供商中,然后请求批准您的频道。您可以在应用启动时以及应用需要更新数据时运行此作业。

    Kotlin

    context.contentResolver
      .query(
          TvContractCompat.buildChannelUri(channelId),
              null, null, null, null).use({
                  cursor-> if (cursor != null and cursor.moveToNext()) {
                               val channel = Channel.fromCursor(cursor)
                               if (channel.isBrowsable()) {
                                   //update channel's programs
                               }
                           }
              })

    Java

    try (Cursor cursor = context.getContentResolver()
          .query(
              TvContractCompat.buildChannelUri(channelId),
              null,
              null,
              null,
              null)) {
                  if (cursor != null && cursor.moveToNext()) {
                      Channel channel = Channel.fromCursor(cursor);
                      if (channel.isBrowsable()) {
                          //update channel's programs
                      }
                  }
              }
  • 对所有图像(徽标、图标、内容图像)使用唯一的 Uri。当您更新图像时,请务必使用不同的 Uri。所有图像都经过缓存。如果您在更改图像时未更改 Uri,旧图像将继续显示。

  • 请记住,不允许使用 WHERE 子句,并且使用 WHERE 子句调用提供商将抛出安全异常。

属性

本节分别描述了频道和节目属性。

频道属性

您必须为每个频道指定这些属性:

属性 备注
TYPE 设置为 TYPE_PREVIEW
DISPLAY_NAME 设置为频道名称。
APP_LINK_INTENT_URI 当用户选择频道的徽标时,系统会发送一个意图以启动一个活动,该活动显示与该频道相关的内容。将此属性设置为该活动意图过滤器中使用的 Uri。

此外,频道还保留了六个字段供内部应用使用。这些字段可用于存储键或其他值,以帮助应用将频道映射到其内部数据结构:

  • INTERNAL_PROVIDER_ID
  • INTERNAL_PROVIDER_DATA
  • INTERNAL_PROVIDER_FLAG1
  • INTERNAL_PROVIDER_FLAG2
  • INTERNAL_PROVIDER_FLAG3
  • INTERNAL_PROVIDER_FLAG4

节目属性

请参阅各个页面,了解每种节目类型的属性:

示例代码

要详细了解如何构建与主屏幕交互并将频道和节目添加到 Android TV 主屏幕的应用,请参阅我们的主屏幕Codelab