Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AndroidTV Oreo Dip

AndroidTV Oreo Dip

DroidKaigi2018でAndroidTVネタで発表します | 道産子エンジニア
http://blog.kaelae.la/entry/2018/02/04/162915

What’s New for ANdroidTV(Google I/O 2017) | Youtube
https://youtu.be/LMB9B6Z__bM

Displaying Content in Recommendations Channels | Android developer's site
https://developer.android.com/training/tv/discovery/recommendations-channel.html#best_practices

Phasing out legacy recommendations on Android TV | Android Developers Blog
https://android-developers.googleblog.com/2017/12/phasing-out-legacy-recommendations-on.html

バックグラウンド実行制限 | Android developer's site
https://developer.android.com/about/versions/oreo/background.html

Implicit Broadcast Exceptions | Android developer's site
https://developer.android.com/guide/components/broadcast-exceptions.html

Code lab
https://goo.gl/t3Auwo

My sample code
https://github.com/kaelaela/TvRecommendation

Yuichi Maekawa

February 08, 2018
Tweet

More Decks by Yuichi Maekawa

Other Decks in Programming

Transcript

  1. AndroidTV Oreo Dip
    kaelaela(Yuichi Maekawa)
    AbemaTV @ CyberAgent, Inc

    View Slide

  2. Outline
    ● Overview of AndroidTV
    ● AndroidTV Oreo features
    ● What is new in Oreo?
    ● What is Recommendation Channels?
    ● How to implement new recommendations & tips

    View Slide

  3. AndroidTV

    View Slide

  4. AndroidTV
    ● Simple remote controls

    View Slide

  5. AndroidTV
    ● Simple remote controls
    ● A large screen UX

    View Slide

  6. AndroidTV✖Oreo

    View Slide

  7. Supported device in Japan
    Supported only “Nexus Player” in Japan now...

    View Slide

  8. Suppert soon
    https://www.cccair.co.jp/airstick/

    View Slide

  9. Suppert soon
    https://www.nttdocomo.co.jp/product/docomo_select/tt01/index.html

    View Slide

  10. What’s new in Oreo?

    View Slide

  11. What’s new in Oreo?
    http://blog.kaelae.la/entry/2018/02/04/162915

    View Slide

  12. Video
    https://youtu.be/LMB9B6Z__bM

    View Slide

  13. Oreo feature in AndroidTV
    ● Media first
    ● Google Asistant
    ● Update home screen

    View Slide

  14. Oreo feature in AndroidTV
    ● Media first
    ● Google Asistant
    ● Update home screen

    View Slide

  15. Oreo feature in AndroidTV
    ● Media first
    ● Google Asistant
    ● Update home screen
    Today’s main!

    View Slide

  16. Renew recommendations

    View Slide

  17. Legacy recommendation’s problem

    View Slide

  18. Legacy recommendation’s problem
    ● Only one column for all apps
    ● Can not change order
    ● Use NotificationManager

    View Slide

  19. https://android-developers.googleblog.com/2017/12/phasing-out-legacy-recommendations-on.html

    View Slide

  20. Phasing out legacy recommendations
    ● Phase out legacy recommendations over the year
    ● But, Google migrate legacies automatically now
    ○ Add each app’s channel
    ○ All legacies are added to each app's channel
    ● If your app do not update
    ○ Add all legacy recommendations to one channel
    ○ And it is added to bottom of the channel list!!

    View Slide

  21. How to develop new recommendations?
    ● You need careful consideration of Spec.
    ● Structure of new recommendations
    ● ContentProvider & ContentResolver
    ● Implement recommendations

    View Slide

  22. Consider of Spec

    View Slide

  23. Consider of Spec
    ● What recommendate content in your app?
    ● When update contents?
    ● User can delete recomenndation anytime!
    Favorite contents?
    Related contents?
    Checked artist?
    Update every day?
    New series? season?

    View Slide

  24. Structure of new
    recommendations

    View Slide

  25. ContentProvider & BroadcastReceiver
    Android System
    ContentProvider
    Other Application
    BroadcastReceiver
    BroadcastReceiver
    Home Application
    BroadcastReceiver
    BroadcastReceiver
    Your App
    TV’s
    BroadcastReceiver
    BroadcastReceiver
    ・・・
    ・・・
    ・・・
    ・・・

    View Slide

  26. ContentProvider & BroadcastReceiver
    Android System
    ContentProvider
    Other Application
    BroadcastReceiver
    BroadcastReceiver
    Home Application
    BroadcastReceiver
    BroadcastReceiver
    Your App
    TV’s
    BroadcastReceiver
    BroadcastReceiver
    ・・・
    ・・・
    ・・・
    ・・・

    View Slide

  27. ContentProvider & BroadcastReceiver
    Android System
    ContentProvider
    Other Application
    BroadcastReceiver
    BroadcastReceiver
    Home Application
    BroadcastReceiver
    BroadcastReceiver
    Your App
    TV’s
    BroadcastReceiver
    BroadcastReceiver
    ・・・
    ・・・
    ・・・
    ・・・
    ・・・
    TV contents

    View Slide

  28. ContentProvider & BroadcastReceiver
    Android System
    ContentProvider
    Other Application
    BroadcastReceiver
    BroadcastReceiver
    Home Application
    BroadcastReceiver
    BroadcastReceiver
    Your App
    TV’s
    BroadcastReceiver
    BroadcastReceiver
    ・・・
    ・・・
    ・・・
    ・・・
    ・・・
    TV contents

    View Slide

  29. Understanding ContentProvider

    View Slide

  30. Uri content://authority/path?query=value
    ContentProvider

    View Slide

  31. ContentProvider(content://XXX)
    Uri content://authority/path?query=value
    ContentProvider

    View Slide

  32. ContentProvider(content://XXX)
    MediaStore(media/)
    Uri content://authority/path?query=value
    ・・・
    TvContract(android.media.tv/) App(your_authority/)
    ContentProvider

    View Slide

  33. ContentProvider(content://XXX)
    MediaStore(media/)
    Audio
    Video
    Image
    Uri content://authority/path?query=value
    ・・・
    TvContract(android.media.tv/)
    RecodedPrograms
    PreviewPrograms
    Programs
    WatchNextPrograms
    Channels
    ・・・
    ・・・
    ・・・
    App(your_authority/)
    ContentProvider

    View Slide

  34. ContentProvider(content://)
    MediaStore(media/)
    ・・・
    TvContract(android.media.tv/) App(your_authority/)
    ContentResolver
    Application
    ContentResolver

    View Slide

  35. ContentProvider(content://)
    MediaStore(media/)
    ・・・
    TvContract(android.media.tv/) App(your_authority/)
    ContentResolver
    Application
    ContentResolver
    Query

    View Slide

  36. ContentProvider(content://)
    MediaStore(media/)
    ・・・
    TvContract(android.media.tv/) App(your_authority/)
    ContentResolver
    Application
    Query
    ContentResolver
    Cursor

    View Slide

  37. ContentResolver
    Use TvContractCompat.XXX.CONTENT_URI
    Or TvContractCompat.buildXXXUri(id)
    ● insert(Uri url, ContentValues values)
    ● bulkInsert(Uri url, ContentValues[] values)
    ● update(Uri url, ContentValues values, String where,
    String[] selectionArgs)
    ● delete(Uri url, ContentValues values, String where,
    String[] selectionArgs)
    ● query(Uri url, String[] projection, String selection,
    String[] selectionArgs, String sortOrder)

    View Slide

  38. Implement recommendations

    View Slide

  39. 1.Prepare library and permissions

    View Slide

  40. Add support library to build.gradle
    dependencies {
    ...
    implementation "com.android.support:support-tv-provider:27.0.2"
    ...
    }

    View Slide

  41. Add permissions to AndroidManifest.xml
    "com.android.providers.tv.permission.READ_EPG_DATA"/>
    "com.android.providers.tv.permission.WRITE_EPG_DATA"/>

    View Slide

  42. READ(WRITE)_EPG_DATA permission
    ContentProvider(content://XXX)
    MediaStore(media/)
    Audio
    Video
    Image
    Uri content://authority/path?query=value
    ・・・
    TvContract(android.media.tv/)
    RecodedPrograms
    PreviewPrograms
    Programs
    WatchNextPrograms
    Channels
    ・・・
    ・・・
    ・・・
    App(your_authority/)

    View Slide

  43. ContentProvider(content://XXX)
    MediaStore(media/)
    Audio
    Video
    Image
    ・・・
    ・・・
    ・・・
    ・・・
    App(your_authority/)
    READ(WRITE)_EPG_DATA permission
    Uri content://android.media.tv/~~~
    TvContract(android.media.tv/)
    RecodedPrograms
    PreviewPrograms
    Programs
    WatchNextPrograms
    Channels
    Ac e s

    View Slide

  44. 2.BroadcastReceiver
    1. Prepare library and permissions

    View Slide

  45. Create BroadcastReceiver
    class RecommendationBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(c: Context, i: Intent) {
    if (i.action != TvContractCompat.ACTION_INITIALIZE_PROGRAMS) return
    RecommendationJobService.startJob(c)
    }
    }

    View Slide

  46. Create BroadcastReceiver
    class RecommendationBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(c: Context, i: Intent) {
    if (i.action != TvContractCompat.ACTION_INITIALIZE_PROGRAMS) return
    RecommendationJobService.startJob(c)
    }
    }
    ?

    View Slide

  47. android.media.tv.action.INITIALIZE_PROGRAMS


    "android.media.tv.action.INITIALIZE_PROGRAMS"/>


    View Slide

  48. android.media.tv.action.INITIALIZE_PROGRAMS
    Android System
    ContentProvider
    Other Application
    BroadcastReceiver
    BroadcastReceiver
    Home Application
    BroadcastReceiver
    BroadcastReceiver
    Your App
    TV’s
    BroadcastReceiver
    BroadcastReceiver
    ・・・
    ・・・
    ・・・
    ・・・

    View Slide

  49. android.media.tv.action.INITIALIZE_PROGRAMS
    Android System
    ContentProvider
    Other Application
    BroadcastReceiver
    BroadcastReceiver
    Home Application
    BroadcastReceiver
    BroadcastReceiver
    Your App
    TV’s
    BroadcastReceiver
    BroadcastReceiver
    ・・・
    ・・・
    ・・・
    ・・・

    View Slide

  50. android.media.tv.action.INITIALIZE_PROGRAMS
    document

    View Slide

  51. android.media.tv.action.INITIALIZE_PROGRAMS
    document
    Broadcast Action: sent to the
    target TV input after it is
    first installed to notify the
    input to initialize its
    channels and programs to the
    system content provider.

    View Slide

  52. android.media.tv.action.INITIALIZE_PROGRAMS
    document
    Broadcast Action: sent to the
    target TV input after it is
    first installed to notify the
    input to initialize its
    channels and programs to the
    system content provider.

    View Slide

  53. android.media.tv.action.INITIALIZE_PROGRAMS
    But, we can not check on local...
    ...
    D/PackageUpdatesReceiver:
    Trying to send ACTION_INITIALIZE_PROGRAMS to ~~~(your app)
    D/PackageUpdatesReceiver:
    No permissions, blacklisted or not from the play store
    ...
    Logcat

    View Slide

  54. We need a solution

    View Slide

  55. Receive another intent action
    e.g.
    android.intent.action.BOOT_COMPLETED
    android.intent.action.MY_PACKAGE_REPLACED

    View Slide

  56. Receive another intent action
    e.g.
    android.intent.action.BOOT_COMPLETED
    android.intent.action.MY_PACKAGE_REPLACED
    NOTE!
    Background executions are limited from Oreo.
    https://developer.android.com/about/versions/
    oreo/background.html
    https://developer.android.com/guide/component
    s/broadcast-exceptions.html

    View Slide

  57. Run on app launch
    class App : Application() {
    override fun onCreate() {
    ...
    RecommendationJobService.startJob(c)
    }
    }
    or
    class MainActivity : AppCompatActivity() {
    override fun onCreate() {
    ...
    RecommendationJobService.startJob(c)
    }
    }

    View Slide

  58. Run on app launch
    class App : Application() {
    override fun onCreate() {
    ...
    RecommendationJobService.startJob(c)
    }
    }
    or
    class MainActivity : AppCompatActivity() {
    override fun onCreate() {
    ...
    RecommendationJobService.startJob(c)
    }
    }
    Be careful!
    TV apps are updated automatically by default.
    If the user app do not open, your
    recommendations will be not appear.

    View Slide

  59. 3.Create channel
    1. Prepare library and permissions
    2. BroadcastReceiver

    View Slide

  60. Create channel
    fun createChannel(): Channel {
    val appUri = Uri.parse(Config.APP_SCHEME + "://" +
    Config.AUTHORITY)
    return Channel.Builder()
    .setType(TvContractCompat.Channels.TYPE_PREVIEW)
    .setDisplayName(CHANNEL_TITLE)
    .setAppLinkIntentUri(appUri).build()
    }

    View Slide

  61. Create channel
    fun createChannel(): Channel {
    val appUri = Uri.parse(Config.APP_SCHEME + "://" +
    Config.AUTHORITY)
    return Channel.Builder()
    .setType(TvContractCompat.Channels.TYPE_PREVIEW)
    .setDisplayName(CHANNEL_TITLE)
    .setAppLinkIntentUri(appUri).build()
    }

    View Slide

  62. Create channel
    fun createChannel(): Channel {
    val appUri = Uri.parse(Config.APP_SCHEME + "://" +
    Config.AUTHORITY)
    return Channel.Builder()
    .setType(TvContractCompat.Channels.TYPE_PREVIEW)
    .setDisplayName(CHANNEL_TITLE)
    .setAppLinkIntentUri(appUri).build()
    }

    View Slide

  63. Create channel
    fun createChannel(): Channel {
    val appUri = Uri.parse(Config.APP_SCHEME + "://" +
    Config.AUTHORITY)
    return Channel.Builder()
    .setType(TvContractCompat.Channels.TYPE_PREVIEW)
    .setDisplayName(CHANNEL_TITLE)
    .setAppLinkIntentUri(appUri).build()
    }

    View Slide

  64. Create channel
    fun createChannel(): Channel {
    val appUri = Uri.parse(Config.APP_SCHEME + "://" +
    Config.AUTHORITY)
    return Channel.Builder()
    .setType(TvContractCompat.Channels.TYPE_PREVIEW)
    .setDisplayName(CHANNEL_TITLE)
    .setAppLinkIntentUri(appUri).build()
    }

    View Slide

  65. Create channel
    fun createChannel(): Channel {
    val appUri = Uri.parse(Config.APP_SCHEME + "://" +
    Config.AUTHORITY)
    return Channel.Builder()
    .setType(TvContractCompat.Channels.TYPE_PREVIEW)
    .setDisplayName(CHANNEL_TITLE)
    .setAppLinkIntentUri(appUri).build()
    }

    View Slide

  66. ● The channel is usually added by the user
    ● The default channel is not added by the
    user
    ● The default channel is added by the app
    ● The default channel is only 1/app
    ● You should never delete the default channel
    Default channel

    View Slide

  67. Check default channel existance
    fun startJob(c: Context) {
    val cr = c.contentResolver
    val channels = loadChannels(cr)
    if (channels.isEmpty()) {
    //create default channel
    val channel = createChannel(c.getString(R.string.app_name))
    val defaultChannelId = insertChannel(cr, channel)
    c.saveDefaultChannelId(defaultChannelId)
    setDefaultChannel(c, defaultChannelId)
    }
    scheduleProgramJob(c)
    }

    View Slide

  68. Check default channel existance
    fun startJob(c: Context) {
    val cr = c.contentResolver
    val channels = loadChannels(cr)
    if (channels.isEmpty()) {
    //create default channel
    val channel = createChannel()
    val defaultChannelId = insertChannel(cr, channel)
    c.saveDefaultChannelId(defaultChannelId)
    setDefaultChannel(c, defaultChannelId)
    }
    scheduleProgramJob(c)
    }
    No c n

    View Slide

  69. Check default channel existance
    fun startJob(c: Context) {
    val cr = c.contentResolver
    val channels = loadChannels(cr)
    if (channels.isEmpty()) {
    //create default channel
    val channel = createChannel()
    val defaultChannelId = insertChannel(cr, channel)
    c.saveDefaultChannelId(defaultChannelId)
    setDefaultChannel(c, defaultChannelId)
    }
    scheduleProgramJob(c)
    }

    View Slide

  70. Check default channel existance
    fun startJob(c: Context) {
    val cr = c.contentResolver
    val channels = loadChannels(cr)
    if (channels.isEmpty()) {
    //create default channel
    val channel = createChannel()
    val defaultChannelId = insertChannel(cr, channel)
    c.saveDefaultChannelId(defaultChannelId)
    setDefaultChannel(c, defaultChannelId)
    }
    scheduleProgramJob(c)
    }

    View Slide

  71. Insert channel to provider
    fun insertChannel(cr: ContentResolver, channel: Channel): Long {
    val uri = cr.insert(TvContractCompat.Channels.CONTENT_URI,
    channel.toContentValues())
    return ContentUris.parseId(uri)
    }

    View Slide

  72. Insert channel to provider
    fun insertChannel(cr: ContentResolver, channel: Channel): Long {
    val uri = cr.insert(TvContractCompat.Channels.CONTENT_URI,
    channel.toContentValues())
    return ContentUris.parseId(uri)
    }

    View Slide

  73. Insert channel to provider
    fun insertChannel(cr: ContentResolver, channel: Channel): Long {
    val uri = cr.insert(TvContractCompat.Channels.CONTENT_URI,
    channel.toContentValues())
    return ContentUris.parseId(uri)
    }
    //java
    public static long parseId(Uri contentUri) {
    String last = contentUri.getLastPathSegment();
    return last == null ? -1 : Long.parseLong(last);
    }

    View Slide

  74. Check default channel existence
    fun startJob(c: Context) {
    val cr = c.contentResolver
    val channels = loadChannels(cr)
    if (channels.isEmpty()) {
    //create default channel
    val channel = createChannel()
    val defaultChannelId = insertChannel(cr, channel)
    c.saveDefaultChannelId(defaultChannelId)
    setDefaultChannel(c, defaultChannelId)
    }
    scheduleProgramJob(c)
    }
    Sav u l

    View Slide

  75. Check default channel existence
    fun startJob(c: Context) {
    val cr = c.contentResolver
    val channels = loadChannels(cr)
    if (channels.isEmpty()) {
    //create default channel
    val channel = createChannel()
    val defaultChannelId = insertChannel(cr, channel)
    c.saveDefaultChannelId(defaultChannelId)
    setDefaultChannel(c, defaultChannelId)
    }
    scheduleProgramJob(c)
    }

    View Slide

  76. Set default channel
    @TargetApi(Build.VERSION_CODES.O)
    private fun setDefaultChannel(c: Context, channelId: Long) {
    val res = c.resources
    val logoId = R.drawable.ic_tv_channel_80dp
    val logoUri = Uri.parse(
    ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
    res.getResourcePackageName(logoId) + "/" +
    res.getResourceTypeName(logoId) + "/" +
    res.getResourceEntryName(logoId))
    ChannelLogoUtils.storeChannelLogo(c, channelId, logoUri)
    TvContractCompat.requestChannelBrowsable(c, channelId)
    }

    View Slide

  77. @TargetApi(Build.VERSION_CODES.O)
    private fun setDefaultChannel(c: Context, channelId: Long) {
    val res = c.resources
    val logoId = R.drawable.ic_tv_channel_80dp
    val logoUri = Uri.parse(
    ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
    res.getResourcePackageName(logoId) + "/" +
    res.getResourceTypeName(logoId) + "/" +
    res.getResourceEntryName(logoId))
    ChannelLogoUtils.storeChannelLogo(c, channelId, logoUri)
    TvContractCompat.requestChannelBrowsable(c, channelId)
    }
    Def ic
    ze
    Set default channel

    View Slide

  78. @TargetApi(Build.VERSION_CODES.O)
    private fun setDefaultChannel(c: Context, channelId: Long) {
    val res = c.resources
    val logoId = R.drawable.ic_tv_channel_80dp
    val logoUri = Uri.parse(
    ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
    res.getResourcePackageName(logoId) + "/" +
    res.getResourceTypeName(logoId) + "/" +
    res.getResourceEntryName(logoId))
    ChannelLogoUtils.storeChannelLogo(c, channelId, logoUri)
    TvContractCompat.requestChannelBrowsable(c, channelId)
    }
    Set default channel

    View Slide

  79. @TargetApi(Build.VERSION_CODES.O)
    private fun setDefaultChannel(c: Context, channelId: Long) {
    val res = c.resources
    val logoId = R.drawable.ic_tv_channel_80dp
    val logoUri = Uri.parse(
    ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
    res.getResourcePackageName(logoId) + "/" +
    res.getResourceTypeName(logoId) + "/" +
    res.getResourceEntryName(logoId))
    ChannelLogoUtils.storeChannelLogo(c, channelId, logoUri)
    TvContractCompat.requestChannelBrowsable(c, channelId)
    }
    Set default channel

    View Slide

  80. @TargetApi(Build.VERSION_CODES.O)
    private fun setDefaultChannel(c: Context, channelId: Long) {
    val res = c.resources
    val logoId = R.drawable.ic_tv_channel_80dp
    val logoUri = Uri.parse(
    ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
    res.getResourcePackageName(logoId) + "/" +
    res.getResourceTypeName(logoId) + "/" +
    res.getResourceEntryName(logoId))
    ChannelLogoUtils.storeChannelLogo(c, channelId, logoUri)
    TvContractCompat.requestChannelBrowsable(c, channelId)
    } Make the TYPE_PREVIEW channel browsable.
    This api is valid only once.
    Set default channel

    View Slide

  81. 4.Create programs
    1. Prepare library and permissions
    2. BroadcastReceiver
    3. Create channel

    View Slide

  82. Create program
    fun buildProgram(channelId: Long,
    content: Content
    ): PreviewProgram {
    val programUri = Uri.parse(APP_SCHEME + "://" +
    AUTHORITY + "/" +
    CONTENT_PATH)
    return PreviewProgram.Builder()
    .setChannelId(channelId)
    .setType(TvContractCompat.PreviewPrograms.TYPE_CHANNEL)
    .setTitle(content.title)
    .setDescription(content.description)
    .setPosterArtUri(content.thumbnail)
    .setIntentUri(programUri)
    .setInternalProviderId(content.id)
    .build()
    }

    View Slide

  83. Create program
    fun buildProgram(channelId: Long,
    content: Content
    ): PreviewProgram {
    val programUri = Uri.parse(APP_SCHEME + "://" +
    AUTHORITY + "/" +
    CONTENT_PATH)
    return PreviewProgram.Builder()
    .setChannelId(channelId)
    .setType(TvContractCompat.PreviewPrograms.TYPE_CHANNEL)
    .setTitle(content.title)
    .setDescription(content.description)
    .setPosterArtUri(content.thumbnail)
    .setIntentUri(programUri)
    .setInternalProviderId(content.id)
    .build()
    }

    View Slide

  84. Create program
    fun buildProgram(channelId: Long,
    content: Content
    ): PreviewProgram {
    val programUri = Uri.parse(APP_SCHEME + "://" +
    AUTHORITY + "/" +
    CONTENT_PATH)
    return PreviewProgram.Builder()
    .setChannelId(channelId)
    .setType(TvContractCompat.PreviewPrograms.TYPE_CHANNEL)
    .setTitle(content.title)
    .setDescription(content.description)
    .setPosterArtUri(content.thumbnail)
    .setIntentUri(programUri)
    .setInternalProviderId(content.id)
    .build()
    }

    View Slide

  85. Create program
    fun buildProgram(channelId: Long,
    content: Content
    ): PreviewProgram {
    val programUri = Uri.parse(APP_SCHEME + "://" +
    AUTHORITY + "/" +
    CONTENT_PATH)
    return PreviewProgram.Builder()
    .setChannelId(channelId)
    .setType(TvContractCompat.PreviewPrograms.TYPE_CHANNEL)
    .setTitle(content.title)
    .setDescription(content.description)
    .setPosterArtUri(content.thumbnail)
    .setIntentUri(programUri)
    .setInternalProviderId(content.id)
    .build()
    }

    View Slide

  86. Create program
    fun buildProgram(channelId: Long,
    content: Content
    ): PreviewProgram {
    val programUri = Uri.parse(APP_SCHEME + "://" +
    AUTHORITY + "/" +
    CONTENT_PATH)
    return PreviewProgram.Builder()
    .setChannelId(channelId)
    .setType(TvContractCompat.PreviewPrograms.TYPE_CHANNEL)
    .setTitle(content.title)
    .setDescription(content.description)
    .setPosterArtUri(content.thumbnail)
    .setIntentUri(programUri)
    .setInternalProviderId(content.id)
    .build()
    }

    View Slide

  87. View Slide

  88. Create program
    fun buildProgram(channelId: Long,
    content: Content
    ): PreviewProgram {
    val programUri = Uri.parse(APP_SCHEME + "://" +
    AUTHORITY + "/" +
    CONTENT_PATH)
    return PreviewProgram.Builder()
    .setChannelId(channelId)
    .setType(TvContractCompat.PreviewPrograms.TYPE_CHANNEL)
    .setTitle(content.title)
    .setDescription(content.description)
    .setPosterArtUri(content.thumbnail)
    .setIntentUri(programUri)
    .setInternalProviderId(content.id)
    .build()
    }

    View Slide

  89. Create program
    fun buildProgram(channelId: Long,
    content: Content
    ): PreviewProgram {
    val programUri = Uri.parse(APP_SCHEME + "://" +
    AUTHORITY + "/" +
    CONTENT_PATH)
    return PreviewProgram.Builder()
    .setChannelId(channelId)
    .setType(TvContractCompat.PreviewPrograms.TYPE_CHANNEL)
    .setTitle(content.title)
    .setDescription(content.description)
    .setPosterArtUri(content.thumbnail)
    .setIntentUri(programUri)
    .setInternalProviderId(content.id)
    .build()
    }

    View Slide

  90. Create program
    fun buildProgram(channelId: Long,
    content: Content
    ): PreviewProgram {
    val programUri = Uri.parse(APP_SCHEME + "://" +
    AUTHORITY + "/" +
    CONTENT_PATH)
    return PreviewProgram.Builder()
    .setChannelId(channelId)
    .setType(TvContractCompat.PreviewPrograms.TYPE_CHANNEL)
    .setTitle(content.title)
    .setDescription(content.description)
    .setPosterArtUri(content.thumbnail)
    .setIntentUri(programUri)
    .setInternalProviderId(content.id)
    .build()
    }

    View Slide

  91. Create program
    fun buildProgram(channelId: Long,
    content: Content
    ): PreviewProgram {
    val programUri = Uri.parse(APP_SCHEME + "://" +
    AUTHORITY + "/" +
    CONTENT_PATH)
    return PreviewProgram.Builder()
    .setChannelId(channelId)
    .setType(TvContractCompat.PreviewPrograms.TYPE_CHANNEL)
    .setTitle(content.title)
    .setDescription(content.description)
    .setPosterArtUri(content.thumbnail)
    .setIntentUri(programUri)
    .setInternalProviderId(content.id)
    .build()
    }

    View Slide

  92. Insert your programs to channel
    @TargetApi(Build.VERSION_CODES.O)
    private fun insertPrograms(programs: List): Int {
    programs.map { it.toContentValues() }
    .toList()
    .let {
    return cr.bulkInsert(
    TvContractCompat.PreviewPrograms.CONTENT_URI,
    it.toTypedArray())
    }
    }

    View Slide

  93. @TargetApi(Build.VERSION_CODES.O)
    private fun insertPrograms(programs: List): Int {
    programs.map { it.toContentValues() }
    .toList()
    .let {
    return cr.bulkInsert(
    TvContractCompat.PreviewPrograms.CONTENT_URI,
    it.toTypedArray())
    }
    }
    Insert your programs to channel
    Con t /va
    ap

    View Slide

  94. @TargetApi(Build.VERSION_CODES.O)
    private fun insertPrograms(programs: List): Int {
    programs.map { it.toContentValues() }
    .toList()
    .let {
    return cr.bulkInsert(
    TvContractCompat.PreviewPrograms.CONTENT_URI,
    it.toTypedArray())
    }
    }
    Insert your programs to channel

    View Slide

  95. Back to create channel...
    fun startJob(c: Context) {
    val cr = c.contentResolver
    val channels = loadChannels(cr)
    if (channels.isEmpty()) {
    //create default channel
    val channel = createChannel()
    val defaultChannelId = insertChannel(cr, channel)
    c.saveDefaultChannelId(defaultChannelId)
    setDefaultChannel(c, defaultChannelId)
    }
    scheduleProgramJob(c)
    }

    View Slide

  96. Want to periodic update
    fun startJob(c: Context) {
    val cr = c.contentResolver
    val channels = loadChannels(cr)
    if (channels.isEmpty()) {
    //create default channel
    val channel = createChannel()
    val defaultChannelId = insertChannel(cr, channel)
    c.saveDefaultChannelId(defaultChannelId)
    setDefaultChannel(c, defaultChannelId)
    }
    scheduleProgramJob(c)
    } If you want to periodic update, you have to make
    JobService!!

    View Slide

  97. 5.Schedule your job
    1. Prepare library and permissions
    2. BroadcastReceiver
    3. Create channel
    4. Create programs

    View Slide

  98. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    override fun onStartJob(params: JobParameters?): Boolean {
    val channelId = loadDefaultChannelId()
    val programs = createPrograms(channelId, YOUR_CONTENTS)
    bulkInsertPrograms(contentResolver, programs)
    jobFinished(params, true)
    return true
    }
    override fun onStopJob(params: JobParameters?): Boolean {
    jobFinished(params, false)
    return false
    }
    }

    View Slide

  99. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    override fun onStartJob(params: JobParameters?): Boolean {
    val channelId = loadDefaultChannelId()
    val programs = createPrograms(channelId, YOUR_CONTENTS)
    bulkInsertPrograms(contentResolver, programs)
    jobFinished(params, true)
    return true
    }
    override fun onStopJob(params: JobParameters?): Boolean {
    jobFinished(params, false)
    return false
    }
    }

    View Slide

  100. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    override fun onStartJob(params: JobParameters?): Boolean {
    val channelId = loadDefaultChannelId()
    val programs = createPrograms(channelId, YOUR_CONTENTS)
    bulkInsertPrograms(contentResolver, programs)
    jobFinished(params, true)
    return true
    }
    override fun onStopJob(params: JobParameters?): Boolean {
    jobFinished(params, false)
    return false
    }
    }

    View Slide

  101. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    override fun onStartJob(params: JobParameters?): Boolean {
    val channelId = loadDefaultChannelId()
    val programs = createPrograms(channelId, YOUR_CONTENTS)
    bulkInsertPrograms(contentResolver, programs)
    jobFinished(params, true)
    return true
    }
    override fun onStopJob(params: JobParameters?): Boolean {
    jobFinished(params, false)
    return false
    }
    }

    View Slide

  102. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    override fun onStartJob(params: JobParameters?): Boolean {
    val channelId = loadDefaultChannelId()
    val programs = createPrograms(channelId, YOUR_CONTENTS)
    bulkInsertPrograms(contentResolver, programs)
    jobFinished(params, true)
    return true
    }
    override fun onStopJob(params: JobParameters?): Boolean {
    jobFinished(params, false)
    return false
    }
    }

    View Slide

  103. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    override fun onStartJob(params: JobParameters?): Boolean {
    val channelId = loadDefaultChannelId()
    val programs = createPrograms(channelId, YOUR_CONTENTS)
    bulkInsertPrograms(contentResolver, programs)
    jobFinished(params, true)
    return true
    }
    override fun onStopJob(params: JobParameters?): Boolean {
    jobFinished(params, false)
    return false
    }
    }

    View Slide

  104. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    override fun onStartJob(params: JobParameters?): Boolean {
    val channelId = loadDefaultChannelId()
    val programs = createPrograms(channelId, YOUR_CONTENTS)
    bulkInsertPrograms(contentResolver, programs)
    jobFinished(params, true)
    return true
    }
    override fun onStopJob(params: JobParameters?): Boolean {
    jobFinished(params, false)
    return false
    }
    }

    View Slide

  105. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    companion object {
    @JvmStatic fun startJob(c: Context) {
    //load default channel

    scheduleProgramJob(c)
    }
    }
    override fun onStartJob(params: JobParameters?): Boolean ...
    override fun onStopJob(params: JobParameters?): Boolean ...
    }

    View Slide

  106. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    companion object {
    @JvmStatic fun startJob(c: Context) {
    //load default channel

    scheduleProgramJob(c)
    }
    }
    override fun onStartJob(params: JobParameters?): Boolean ...
    override fun onStopJob(params: JobParameters?): Boolean ...
    }

    View Slide

  107. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    @JvmStatic fun startJob(c: Context) …
    override fun onStartJob(params: JobParameters?): Boolean …
    override fun onStopJob(params: JobParameters?): Boolean …
    fun scheduleProgramJob(c: Context) {
    val service = ComponentName(c,
    DefaultChannelRecommendationJobService::class.java)
    val scheduler = c.getSystemService(Context.JOB_SCHEDULER_SERVICE)
    val channelId = loadDefaultChannelId()
    val jobId = JobIdManager.createId(JobIdManager.TYPE_CHANNEL_PROGRAMS, channelId)
    val builder = JobInfo.Builder(jobId, service)
    scheduler.schedule(builder.setPeriodic(TimeUnit.MINUTES.toMillis(15)).build())
    }
    }

    View Slide

  108. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    @JvmStatic fun startJob(c: Context) ...
    override fun onStartJob(params: JobParameters?): Boolean ...
    override fun onStopJob(params: JobParameters?): Boolean …
    fun scheduleProgramJob(c: Context) {
    val service = ComponentName(c,
    DefaultChannelRecommendationJobService::class.java)
    val scheduler = c.getSystemService(Context.JOB_SCHEDULER_SERVICE)
    val channelId = loadDefaultChannelId()
    val jobId = JobIdManager.createId(JobIdManager.TYPE_CHANNEL_PROGRAMS, channelId)
    val builder = JobInfo.Builder(jobId, service)
    scheduler.schedule(builder.setPeriodic(TimeUnit.MINUTES.toMillis(15)).build())
    }
    }

    View Slide

  109. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    @JvmStatic fun startJob(c: Context) ...
    override fun onStartJob(params: JobParameters?): Boolean ...
    override fun onStopJob(params: JobParameters?): Boolean …
    fun scheduleProgramJob(c: Context) {
    val service = ComponentName(c,
    DefaultChannelRecommendationJobService::class.java)
    val scheduler = c.getSystemService(Context.JOB_SCHEDULER_SERVICE)
    val channelId = loadDefaultChannelId()
    val jobId = JobIdManager.createId(JobIdManager.TYPE_CHANNEL_PROGRAMS, channelId)
    val builder = JobInfo.Builder(jobId, service)
    scheduler.schedule(builder.setPeriodic(TimeUnit.MINUTES.toMillis(15)).build())
    }
    }

    View Slide

  110. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    @JvmStatic fun startJob(c: Context) ...
    override fun onStartJob(params: JobParameters?): Boolean ...
    override fun onStopJob(params: JobParameters?): Boolean …
    fun scheduleProgramJob(c: Context) {
    val service = ComponentName(c,
    DefaultChannelRecommendationJobService::class.java)
    val scheduler = c.getSystemService(Context.JOB_SCHEDULER_SERVICE)
    val channelId = loadDefaultChannelId()
    val jobId = JobIdManager.createId(JobIdManager.TYPE_CHANNEL_PROGRAMS, channelId)
    val builder = JobInfo.Builder(jobId, service)
    scheduler.schedule(builder.setPeriodic(TimeUnit.MINUTES.toMillis(15)).build())
    }
    }
    You have to create unique jobId for each job.

    View Slide

  111. https://developers-jp.googleblog.com/2017/10/working-with-multiple-jobservices.html

    View Slide

  112. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    @JvmStatic fun startJob(c: Context) ...
    override fun onStartJob(params: JobParameters?): Boolean ...
    override fun onStopJob(params: JobParameters?): Boolean …
    fun scheduleProgramJob(c: Context) {
    val service = ComponentName(c,
    DefaultChannelRecommendationJobService::class.java)
    val scheduler = c.getSystemService(Context.JOB_SCHEDULER_SERVICE)
    val channelId = loadDefaultChannelId()
    val jobId = JobIdManager.createId(JobIdManager.TYPE_CHANNEL_PROGRAMS, channelId)
    val builder = JobInfo.Builder(jobId, service)
    scheduler.schedule(builder.setPeriodic(TimeUnit.MINUTES.toMillis(15)).build())
    }
    }

    View Slide

  113. Create Jobservice
    Class DefaultChannelRecommendationJobService : JobService() {
    @JvmStatic fun startJob(c: Context) ...
    override fun onStartJob(params: JobParameters?): Boolean ...
    override fun onStopJob(params: JobParameters?): Boolean …
    fun scheduleProgramJob(c: Context) {
    val service = ComponentName(c,
    DefaultChannelRecommendationJobService::class.java)
    val scheduler = c.getSystemService(Context.JOB_SCHEDULER_SERVICE)
    val channelId = loadDefaultChannelId()
    val jobId = JobIdManager.createId(JobIdManager.TYPE_CHANNEL_PROGRAMS, channelId)
    val builder = JobInfo.Builder(jobId, service)
    scheduler.schedule(builder.setPeriodic(TimeUnit.MINUTES.toMillis(15)).build())
    }
    } The periodic job is not work when the period is less
    than 15 minute!!

    View Slide

  114. Add JobService to AndroidManifest.xml
    android:exported="false"
    android:permission="android.permission.BIND_JOB_SERVICE"
    />

    View Slide

  115. Is that all?

    View Slide

  116. No.

    View Slide

  117. View Slide

  118. View Slide

  119. View Slide

  120. Don’t reinsert deleted programs!

    View Slide

  121. Don’t reinsert deleted programs!!
    User can delete program anytime

    View Slide

  122. Don’t reinsert deleted programs!!
    OK, I delete it.

    View Slide

  123. Don’t reinsert deleted programs!!
    After 15min...

    View Slide

  124. Save deleted content data in your app
    ● Handling program delete event
    ● Then save content data(id, name and so on)
    ● Filter content in your job

    View Slide

  125. android:name=".ProgramRemovedReceiver"
    android:enabled="true"
    android:exported="false"
    >

    "android.media.tv.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED"/>


    ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED
    (ACTION_WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED)

    View Slide

  126. Create BroadcastReceiver
    class ProgramRemovedReceiver : BroadcastReceiver() {
    override fun onReceive(c: Context, i: Intent) {
    if (i.action ==
    TvContractCompat.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED) {
    // save content ids
    val contents = loadPrograms(c)
    contents.filter { !it.isBrowsable }
    .map { it.id }
    .toList()
    .let { deletedProgramIds ->
    saveIds(deletedProgramIds)
    }
    }
    }

    View Slide

  127. Create BroadcastReceiver
    class ProgramRemovedReceiver : BroadcastReceiver() {
    override fun onReceive(c: Context, i: Intent) {
    if (i.action ==
    TvContractCompat.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED) {
    // save content ids
    val contents = loadPrograms(c)
    contents.filter { !it.isBrowsable }
    .map { it.id }
    .toList()
    .let { deletedProgramIds ->
    saveIds(deletedProgramIds)
    }
    }
    }

    View Slide

  128. Create BroadcastReceiver
    class ProgramRemovedReceiver : BroadcastReceiver() {
    override fun onReceive(c: Context, i: Intent) {
    if (i.action ==
    TvContractCompat.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED) {
    // save content ids
    val contents = loadPrograms(c)
    contents.filter { !it.isBrowsable }
    .map { it.id }
    .toList()
    .let { deletedProgramIds ->
    saveIds(deletedProgramIds)
    }
    }
    }

    View Slide

  129. Create BroadcastReceiver
    class ProgramRemovedReceiver : BroadcastReceiver() {
    override fun onReceive(c: Context, i: Intent) {
    if (i.action ==
    TvContractCompat.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED) {
    // save content ids
    val contents = loadPrograms(c)
    contents.filter { !it.isBrowsable }
    .map { it.id }
    .toList()
    .let { deletedProgramIds ->
    saveIds(deletedProgramIds)
    }
    }
    }

    View Slide

  130. Create BroadcastReceiver
    class ProgramRemovedReceiver : BroadcastReceiver() {
    override fun onReceive(c: Context, i: Intent) {
    if (i.action ==
    TvContractCompat.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED) {
    // save content ids
    val contents = loadPrograms(c)
    contents.filter { !it.isBrowsable }
    .map { it.id }
    .toList()
    .let { deletedProgramIds ->
    saveIds(deletedProgramIds)
    }
    }
    }

    View Slide

  131. Insert your programs to channel
    @TargetApi(Build.VERSION_CODES.O)
    private fun insertPrograms(programs: List): Int {
    programs.map { it.toContentValues() }
    .toList()
    .let {
    return cr.bulkInsert(
    TvContractCompat.PreviewPrograms.CONTENT_URI,
    it.toTypedArray())
    }
    }

    View Slide

  132. Insert your programs to channel
    @TargetApi(Build.VERSION_CODES.O)
    private fun insertPrograms(programs: List): Int {
    val removedIds = loadRemovedIds(this)
    programs.map { it.toContentValues() }
    .toList()
    .let {
    return cr.bulkInsert(
    TvContractCompat.PreviewPrograms.CONTENT_URI,
    it.toTypedArray())
    }
    }

    View Slide

  133. Insert your programs to channel
    @TargetApi(Build.VERSION_CODES.O)
    private fun insertPrograms(programs: List): Int {
    val removedIds = loadRemovedIds(this)
    programs.filter { !removedIds.contains(it.id) }
    .map { it.toContentValues() }
    .toList()
    .let {
    return cr.bulkInsert(
    TvContractCompat.PreviewPrograms.CONTENT_URI,
    it.toTypedArray())
    }
    }
    100!

    View Slide

  134. One more tip
    “Media first UX”

    View Slide

  135. Enable to open your app from everywhere
    ● Global search

    View Slide

  136. Enable to open your app from everywhere
    ● Recommendation

    View Slide

  137. Enable to open your app from everywhere
    ● Google Assistant

    View Slide

  138. Consider window order
    Your Application
    Global
    Search
    Google
    Assistant
    Recommendation

    View Slide

  139. Consider window order
    Your Application
    Global
    Search
    Google
    Assistant
    Recommendation
    Top Top Top

    View Slide

  140. Consider window order
    Your Application
    Global
    Search
    Google
    Assistant
    Recommendation
    Top Top Top

    View Slide

  141. Consider window order
    Your Application
    Global
    search
    Google
    Assistant
    Recommendation
    Top Top Top
    Not media first!

    View Slide

  142. Consider window order
    Your Application
    Global
    Search
    Google
    Assistant
    Recommendation

    View Slide

  143. Consider window order
    Your Application
    Global
    Search
    Google
    Assistant
    Recommendation
    Video Video Video

    View Slide

  144. Consider window order
    Your Application
    Global
    Search
    Google
    Assistant
    Recommendation
    Video

    View Slide

  145. Consider window order
    Your Application
    Global
    Search
    Google
    Assistant
    Recommendation
    Video
    Top

    View Slide

  146. Consider window order
    Your Application
    Global
    Search
    Google
    Assistant
    Recommendation
    Top

    View Slide

  147. Consider window order
    Your Application
    Global
    Search
    Google
    Assistant
    Recommendation
    Video
    Video

    View Slide

  148. Consider window order
    Your Application
    Global
    Search
    Google
    Assistant
    Recommendation
    Video
    Top
    Video
    Top

    View Slide

  149. Consider window order
    Your Application
    Global
    Search
    Google
    Assistant
    Recommendation
    VideoPlayer
    Top
    VideoPlayer
    Top

    View Slide

  150. Consider window order
    Your Application
    Global
    search
    Google
    Assistant
    Recommendation
    VideoPlayer
    Top
    VideoPlayer
    Top
    User want to see
    searched contents!

    View Slide

  151. Consider window order
    Your Application
    Global
    Search
    Google
    Assistant
    Recommendation
    Video
    Video

    View Slide

  152. Samples
    My sample
    https://github.com/kaelaela/TvRecommendation
    Code lab
    https://goo.gl/t3Auwo

    View Slide

  153. That’s it!
    ● Overview of AndroidTV
    ● AndroidTV Oreo features
    ● What is new in Oreo?
    ● What is Recommendation Channels?
    ● How to implement new recommendations & tips

    View Slide

  154. Thank you!
    Special thanks
    Yoshihito Ikeda
    AbemaTV Android team
    Shohei Kawano
    Shogo Sensui

    View Slide