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

A967476c5855d593710a9a580f6b2aed?s=128

Yuichi Maekawa

February 08, 2018
Tweet

Transcript

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

  2. Outline • Overview of AndroidTV • AndroidTV Oreo features •

    What is new in Oreo? • What is Recommendation Channels? • How to implement new recommendations & tips
  3. AndroidTV

  4. AndroidTV • Simple remote controls

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

  6. AndroidTV✖Oreo

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

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

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

  10. What’s new in Oreo?

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

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

  13. Oreo feature in AndroidTV • Media first • Google Asistant

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

    • Update home screen
  15. Oreo feature in AndroidTV • Media first • Google Asistant

    • Update home screen Today’s main!
  16. Renew recommendations

  17. Legacy recommendation’s problem

  18. Legacy recommendation’s problem • Only one column for all apps

    • Can not change order • Use NotificationManager
  19. https://android-developers.googleblog.com/2017/12/phasing-out-legacy-recommendations-on.html

  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!!
  21. How to develop new recommendations? • You need careful consideration

    of Spec. • Structure of new recommendations • ContentProvider & ContentResolver • Implement recommendations
  22. Consider of Spec

  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?
  24. Structure of new recommendations

  25. ContentProvider & BroadcastReceiver Android System ContentProvider Other Application BroadcastReceiver BroadcastReceiver

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

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

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

    Home Application BroadcastReceiver BroadcastReceiver Your App TV’s BroadcastReceiver BroadcastReceiver ・・・ ・・・ ・・・ ・・・ ・・・ TV contents
  29. Understanding ContentProvider

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

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

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

  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
  34. ContentProvider(content://) MediaStore(media/) ・・・ TvContract(android.media.tv/) App(your_authority/) ContentResolver Application ContentResolver

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

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

  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)
  38. Implement recommendations

  39. 1.Prepare library and permissions

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

    ... }
  41. Add permissions to AndroidManifest.xml <uses-permission android:name= "com.android.providers.tv.permission.READ_EPG_DATA"/> <uses-permission android:name= "com.android.providers.tv.permission.WRITE_EPG_DATA"/>

  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/)
  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
  44. 2.BroadcastReceiver 1. Prepare library and permissions

  45. Create BroadcastReceiver class RecommendationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(c:

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

    Context, i: Intent) { if (i.action != TvContractCompat.ACTION_INITIALIZE_PROGRAMS) return RecommendationJobService.startJob(c) } } ?
  47. android.media.tv.action.INITIALIZE_PROGRAMS <receiver android:name=".RecommendationBroadcastReceiver"> <intent-filter> <action android:name= "android.media.tv.action.INITIALIZE_PROGRAMS"/> </intent-filter> </receiver>

  48. android.media.tv.action.INITIALIZE_PROGRAMS Android System ContentProvider Other Application BroadcastReceiver BroadcastReceiver Home Application

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

    BroadcastReceiver BroadcastReceiver Your App TV’s BroadcastReceiver BroadcastReceiver ・・・ ・・・ ・・・ ・・・
  50. android.media.tv.action.INITIALIZE_PROGRAMS document

  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.
  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.
  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
  54. We need a solution

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

  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
  57. Run on app launch class App : Application() { override

    fun onCreate() { ... RecommendationJobService.startJob(c) } } or class MainActivity : AppCompatActivity() { override fun onCreate() { ... RecommendationJobService.startJob(c) } }
  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.
  59. 3.Create channel 1. Prepare library and permissions 2. BroadcastReceiver

  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() }
  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() }
  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() }
  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() }
  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() }
  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() }
  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
  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) }
  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
  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) }
  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) }
  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) }
  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) }
  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); }
  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
  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) }
  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) }
  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
  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
  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
  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
  81. 4.Create programs 1. Prepare library and permissions 2. BroadcastReceiver 3.

    Create channel
  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() }
  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() }
  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() }
  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() }
  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() }
  87. None
  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() }
  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() }
  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() }
  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() }
  92. Insert your programs to channel @TargetApi(Build.VERSION_CODES.O) private fun insertPrograms(programs: List<PreviewProgram>):

    Int { programs.map { it.toContentValues() } .toList() .let { return cr.bulkInsert( TvContractCompat.PreviewPrograms.CONTENT_URI, it.toTypedArray()) } }
  93. @TargetApi(Build.VERSION_CODES.O) private fun insertPrograms(programs: List<PreviewProgram>): 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
  94. @TargetApi(Build.VERSION_CODES.O) private fun insertPrograms(programs: List<PreviewProgram>): Int { programs.map { it.toContentValues()

    } .toList() .let { return cr.bulkInsert( TvContractCompat.PreviewPrograms.CONTENT_URI, it.toTypedArray()) } } Insert your programs to channel
  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) }
  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!!
  97. 5.Schedule your job 1. Prepare library and permissions 2. BroadcastReceiver

    3. Create channel 4. Create programs
  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 } }
  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 } }
  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 } }
  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 } }
  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 } }
  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 } }
  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 } }
  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 ... }
  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 ... }
  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()) } }
  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()) } }
  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()) } }
  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.
  111. https://developers-jp.googleblog.com/2017/10/working-with-multiple-jobservices.html

  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()) } }
  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!!
  114. Add JobService to AndroidManifest.xml <service android:name=".DefaultChannelRecommendationJobService" android:exported="false" android:permission="android.permission.BIND_JOB_SERVICE" />

  115. Is that all?

  116. No.

  117. None
  118. None
  119. None
  120. Don’t reinsert deleted programs!

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

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

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

  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
  125. <receiver android:name=".ProgramRemovedReceiver" android:enabled="true" android:exported="false" > <intent-filter> <action android:name= "android.media.tv.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED"/> </intent-filter>

    </receiver> ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED (ACTION_WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED)
  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) } } }
  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) } } }
  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) } } }
  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) } } }
  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) } } }
  131. Insert your programs to channel @TargetApi(Build.VERSION_CODES.O) private fun insertPrograms(programs: List<PreviewProgram>):

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

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

    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!
  134. One more tip “Media first UX”

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

  136. Enable to open your app from everywhere • Recommendation

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

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

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

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

    Top Top Top
  141. Consider window order Your Application Global search Google Assistant Recommendation

    Top Top Top Not media first!
  142. Consider window order Your Application Global Search Google Assistant Recommendation

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

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

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

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

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

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

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

    VideoPlayer Top VideoPlayer Top
  150. Consider window order Your Application Global search Google Assistant Recommendation

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

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

  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
  154. Thank you! Special thanks Yoshihito Ikeda AbemaTV Android team Shohei

    Kawano Shogo Sensui