The Android Developer Challenge is back! Submit your idea before December 2.

主屏幕上的频道

Android TV 主屏幕(或简称“主屏幕”)提供了一个以“频道”和“节目”表格的形式显示推荐内容的界面。每行对应一个频道。而每个频道中则包含一些卡片,每个卡片对应着该频道上提供的一个节目:

TV 主屏幕

本文档介绍了如何向主屏幕添加频道和节目、更新内容、处理用户操作,以及为用户提供最佳体验。(如果您想深入了解该 API,请试用主屏幕 Codelab 或观看 2017 年 I/O Android TV 大会视频。)

注意:推荐频道仅适用于 Android 8.0(API 级别 26)及更高版本,它们只能用于为在 Android 8.0(API 级别 26)及更高版本中运行的应用提供推荐。要为在更低版本的 Android 系统中运行的应用提供推荐,您的应用必须改为使用推荐行

主屏幕界面

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

应用无法控制频道在主屏幕上显示的顺序。在您的应用创建新频道后,主屏幕会将其添加到频道列表末尾。用户可以重新排列频道顺序,也可以隐藏或显示频道。

“接下来观看”频道

“接下来观看”频道显示在主屏幕上的第二行,位于应用行下方。此频道由系统创建和维护。您的应用可以向“接下来观看”频道添加节目,比如用户标记为感趣的节目,用户观看到中间停止的节目或者与用户正在观看的内容相关的节目(例如电视剧的下一集或节目的下一季)。

“接下来观看”频道有一些限制:您的应用无法移动、移除或隐藏“接下来观看”频道所在的行。

应用频道

您的应用创建的频道均遵循以下流程:

  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 文件的依赖项中:

compile 'com.android.support:support-tv-provider:27.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;
    

频道

您的应用创建的第一个频道会成为它的默认频道。默认频道会自动显示在主屏幕上,而您创建的所有其他频道,则只有在用户选择并接受之后才会显示在主屏幕上。

创建频道

应用只有在前台运行时才可要求系统显示新添加的频道。这样可以防止您的应用在用户运行其他应用时显示请求批准添加频道的对话框。如果您尝试在后台运行时添加频道,则相应 Activity 的 onActivityResult() 方法会返回状态代码 RESULT_CANCELED

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

  1. 创建频道 builder 并设置其属性。请注意,频道类型必须是 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 主屏幕图标遮罩

    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,它会监听主屏幕在应用安装后发送的 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 触发 Intent 来测试这个步骤,其中 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);
    

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

将节目插入“接下来观看”频道和将节目插入您自己的频道是一样的。

有 4 种类型的节目,请选择适当的类型:

类型备注
WATCH_NEXT_TYPE_CONTINUE用户在观看过程中停止了播放。
WATCH_NEXT_TYPE_NEXT用户观看的电视剧的下一集已可供观看。例如,如果用户正在观看一部电视剧的第 3 集,则应用可以建议用户接下来观看第 4 集。
WATCH_NEXT_TYPE_NEW明确排在用户观看的内容后面的新内容现已可供观看。例如,用户正在观看一部电视剧的第 5 集,而第 6 集已可供观看。
WATCH_NEXT_TYPE_WATCHLIST由系统或应用在用户保存节目时插入。

请使用 WatchNextProgram.Builder

Kotlin

    val builder = WatchNextProgram.Builder()
    builder.setType(TvContractCompat.WatchNextPrograms.TYPE_CLIP)
            .setWatchNextType(TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
            .setLastEngagementTimeUtcMillis(time)
            .setTitle("Title")
            .setDescription("Program description")
            .setPosterArtUri(uri)
            .setIntentUri(uri)
            .setInternalProviderId(appProgramId)

    val watchNextProgramUri = context.contentResolver
            .insert(TvContractCompat.WatchNextPrograms.CONTENT_URI,
                    builder.build().toContentValues())
    

Java

    WatchNextProgram.Builder builder = new WatchNextProgram.Builder();
    builder.setType(TvContractCompat.WatchNextPrograms.TYPE_CLIP)
            .setWatchNextType(TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
            .setLastEngagementTimeUtcMillis(time)
            .setTitle("Title")
            .setDescription("Program description")
            .setPosterArtUri(uri)
            .setIntentUri(uri)
            .setInternalProviderId(appProgramId);

    Uri watchNextProgramUri = context.getContentResolver()
            .insert(TvContractCompat.WatchNextPrograms.CONTENT_URI, builder.build().toContentValues());
    

请使用 TvContractCompat.buildWatchNextProgramUri(long watchNextProgramId) 来创建更新“接下来观看”节目所需的 Uri

当用户向“接下来观看”频道添加节目时,系统会将该节目添加到相应的行。系统会发送 Intent TvContractCompat.ACTION_PREVIEW_PROGRAM_ADDED_TO_WATCH_NEXT 通知应用添加了节目。该 Intent 包含两个提取项:复制的节目 ID 和在“接下来观看”频道中为相应节目创建的节目 ID。

更新节目

您可以更新节目信息。例如,您可能要更新电影的租借价格,或更新显示用户观看进度的进度条。

请使用 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);
    

处理用户操作

您的应用可以提供用于显示和添加频道的界面来帮助用户发现内容。在频道显示在主屏幕上之后,您的应用还需处理用户与这些频道的交互。

发现并添加频道

您的应用可以提供一个界面元素,供用户选择并添加应用的频道(例如,要求添加频道的按钮)。

在用户请求特定频道后,请执行此代码,请求用户同意将该频道添加到主屏幕界面中:

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
    }
    

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

Android TV 主屏幕事件

当用户与应用发布的节目/频道进行交互时,主屏幕会向应用发送 Intent:

  • 当用户选择某个频道的徽标后,主屏幕会将存储在该频道的 APP_LINK_INTENT_URI 属性中的 Uri 发送给应用。此时,应用应启动其主界面或与所选频道相关的视图。
  • 当用户选择某个节目后,主屏幕会将存储在该节目的 INTENT_URI 属性中的 Uri 发送给应用。此时,应用应播放所选的内容。
  • 用户可以表示其对某个节目不再感兴趣,想要从主屏幕界面中移除该节目。系统会从界面中移除该节目,并将该节目的 ID 以及一个 Intent(android.media.tv.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED 或 android.media.tv.ACTION_WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED)一起发送给拥有该节目的应用。应用应该从提供程序中移除该节目,并且不能重新插入该节目。

请务必针对主屏幕为用户交互发送的所有 Uris 创建 Intent 过滤器;例如:

<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>
    

最佳做法

  • 许多电视应用都会要求用户登录。在这种情况下,监听 android.media.tv.action.INITIALIZE_PROGRAMSBroadcastReceiver 应该向未经身份验证的用户推荐频道内容。例如,您的应用开始时可以显示最优质的内容或当前流行的内容。在用户登录后,可以显示个性化的内容。这是应用在用户登录前进行追加销售的好机会。leanback-homescreen-channels 应用示例介绍了在用户安装应用或者设置设备(如果已预装了应用)后如何加载频道。
  • 当您的应用未在前台运行时,如果您需要更新某个频道或节目,可以使用 JobScheduler 来调度此操作(请参阅:JobSchedulerJobService)。
  • 如果您的应用出现不良行为(例如:持续向提供程序发送垃圾内容),系统可能会撤销您应用的提供程序许可。请务必使用 try-catch 语句封装访问提供程序的代码,以处理安全异常。
  • 在更新节目和频道之前,请向提供程序查询需要更新的数据并对数据进行调整。例如,不需要更新用户想从界面中移除的节目。请使用后台作业,该作业会查询现有数据并请求批准您的频道,然后再将您的数据插入/更新到提供程序。您可以在应用启动时以及应用需要更新数据时运行该作业。

    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 在用户选择频道徽标后,系统会发送 Intent 以启动提供与频道相关的内容的 Activity。请将此属性设为该 Activity 的 Intent 过滤器中使用的 URI。

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

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

节目属性

请参阅每种节目类型所对应的属性的说明页面:

代码示例

要详细了解如何构建能够与 Android TV 主屏幕交互并向主屏幕添加频道和节目的应用,请参阅我们的主屏幕 CodelabGitHub 示例