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. Outline • Overview of AndroidTV • AndroidTV Oreo features •

    What is new in Oreo? • What is Recommendation Channels? • How to implement new recommendations & tips
  2. Oreo feature in AndroidTV • Media first • Google Asistant

    • Update home screen Today’s main!
  3. Legacy recommendation’s problem • Only one column for all apps

    • Can not change order • Use NotificationManager
  4. 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!!
  5. How to develop new recommendations? • You need careful consideration

    of Spec. • Structure of new recommendations • ContentProvider & ContentResolver • Implement recommendations
  6. 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?
  7. ContentProvider & BroadcastReceiver Android System ContentProvider Other Application BroadcastReceiver BroadcastReceiver

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

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

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

    Home Application BroadcastReceiver BroadcastReceiver Your App TV’s BroadcastReceiver BroadcastReceiver ・・・ ・・・ ・・・ ・・・ ・・・ TV contents
  11. 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
  12. 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)
  13. 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/)
  14. 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
  15. Create BroadcastReceiver class RecommendationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(c:

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

    Context, i: Intent) { if (i.action != TvContractCompat.ACTION_INITIALIZE_PROGRAMS) return RecommendationJobService.startJob(c) } } ?
  17. android.media.tv.action.INITIALIZE_PROGRAMS Android System ContentProvider Other Application BroadcastReceiver BroadcastReceiver Home Application

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

    BroadcastReceiver BroadcastReceiver Your App TV’s BroadcastReceiver BroadcastReceiver ・・・ ・・・ ・・・ ・・・
  19. 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.
  20. 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.
  21. 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
  22. 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
  23. Run on app launch class App : Application() { override

    fun onCreate() { ... RecommendationJobService.startJob(c) } } or class MainActivity : AppCompatActivity() { override fun onCreate() { ... RecommendationJobService.startJob(c) } }
  24. 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.
  25. 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() }
  26. 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() }
  27. 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() }
  28. 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() }
  29. 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() }
  30. 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() }
  31. • 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
  32. 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) }
  33. 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
  34. 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) }
  35. 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) }
  36. 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) }
  37. 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) }
  38. 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); }
  39. 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
  40. 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) }
  41. 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) }
  42. @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
  43. @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
  44. @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
  45. @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
  46. 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() }
  47. 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() }
  48. 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() }
  49. 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() }
  50. 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() }
  51. 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() }
  52. 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() }
  53. 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() }
  54. 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() }
  55. 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()) } }
  56. @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
  57. @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
  58. 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) }
  59. 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!!
  60. 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 } }
  61. 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 } }
  62. 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 } }
  63. 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 } }
  64. 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 } }
  65. 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 } }
  66. 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 } }
  67. 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 ... }
  68. 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 ... }
  69. 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()) } }
  70. 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()) } }
  71. 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()) } }
  72. 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.
  73. 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()) } }
  74. 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!!
  75. No.

  76. 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
  77. 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) } } }
  78. 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) } } }
  79. 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) } } }
  80. 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) } } }
  81. 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) } } }
  82. 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()) } }
  83. 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()) } }
  84. 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!
  85. Consider window order Your Application Global search Google Assistant Recommendation

    VideoPlayer Top VideoPlayer Top User want to see searched contents!
  86. That’s it! • Overview of AndroidTV • AndroidTV Oreo features

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