Share
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
Transcript
154 slides
-
Outline • Overview of AndroidTV • AndroidTV Oreo features •
What is new in Oreo? • What is Recommendation Channels? • How to implement new recommendations & tips -
Oreo feature in AndroidTV • Media first • Google Asistant
• Update home screen -
Oreo feature in AndroidTV • Media first • Google Asistant
• Update home screen -
Oreo feature in AndroidTV • Media first • Google Asistant
• Update home screen Today’s main! -
Legacy recommendation’s problem • Only one column for all apps
• Can not change order • Use NotificationManager -
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!! -
How to develop new recommendations? • You need careful consideration
of Spec. • Structure of new recommendations • ContentProvider & ContentResolver • Implement recommendations -
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? -
ContentProvider & BroadcastReceiver Android System ContentProvider Other Application BroadcastReceiver BroadcastReceiver
Home Application BroadcastReceiver BroadcastReceiver Your App TV’s BroadcastReceiver BroadcastReceiver ・・・ ・・・ ・・・ ・・・ -
ContentProvider & BroadcastReceiver Android System ContentProvider Other Application BroadcastReceiver BroadcastReceiver
Home Application BroadcastReceiver BroadcastReceiver Your App TV’s BroadcastReceiver BroadcastReceiver ・・・ ・・・ ・・・ ・・・ -
ContentProvider & BroadcastReceiver Android System ContentProvider Other Application BroadcastReceiver BroadcastReceiver
Home Application BroadcastReceiver BroadcastReceiver Your App TV’s BroadcastReceiver BroadcastReceiver ・・・ ・・・ ・・・ ・・・ ・・・ TV contents -
ContentProvider & BroadcastReceiver Android System ContentProvider Other Application BroadcastReceiver BroadcastReceiver
Home Application BroadcastReceiver BroadcastReceiver Your App TV’s BroadcastReceiver BroadcastReceiver ・・・ ・・・ ・・・ ・・・ ・・・ TV contents -
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 -
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) -
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/) -
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 -
Create BroadcastReceiver class RecommendationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(c:
Context, i: Intent) { if (i.action != TvContractCompat.ACTION_INITIALIZE_PROGRAMS) return RecommendationJobService.startJob(c) } } -
Create BroadcastReceiver class RecommendationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(c:
Context, i: Intent) { if (i.action != TvContractCompat.ACTION_INITIALIZE_PROGRAMS) return RecommendationJobService.startJob(c) } } ? -
android.media.tv.action.INITIALIZE_PROGRAMS Android System ContentProvider Other Application BroadcastReceiver BroadcastReceiver Home Application
BroadcastReceiver BroadcastReceiver Your App TV’s BroadcastReceiver BroadcastReceiver ・・・ ・・・ ・・・ ・・・ -
android.media.tv.action.INITIALIZE_PROGRAMS Android System ContentProvider Other Application BroadcastReceiver BroadcastReceiver Home Application
BroadcastReceiver BroadcastReceiver Your App TV’s BroadcastReceiver BroadcastReceiver ・・・ ・・・ ・・・ ・・・ -
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. -
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. -
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 -
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 -
Run on app launch class App : Application() { override
fun onCreate() { ... RecommendationJobService.startJob(c) } } or class MainActivity : AppCompatActivity() { override fun onCreate() { ... RecommendationJobService.startJob(c) } } -
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. -
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() } -
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() } -
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() } -
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() } -
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() } -
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() } -
• 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 -
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) } -
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 -
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) } -
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) } -
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) } -
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) } -
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); } -
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 -
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) } -
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) } -
@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 -
@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 -
@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 -
@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 -
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() } -
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() } -
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() } -
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() } -
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() } -
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() } -
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() } -
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() } -
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() } -
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()) } } -
@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 -
@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 -
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) } -
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!! -
5.Schedule your job 1. Prepare library and permissions 2. BroadcastReceiver
3. Create channel 4. Create programs -
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 } } -
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 } } -
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 } } -
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 } } -
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 } } -
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 } } -
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 } } -
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 ... } -
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 ... } -
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()) } } -
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()) } } -
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()) } } -
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. -
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()) } } -
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!! -
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 -
<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) -
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) } } } -
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) } } } -
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) } } } -
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) } } } -
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) } } } -
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()) } } -
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()) } } -
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! -
Consider window order Your Application Global search Google Assistant Recommendation
Top Top Top Not media first! -
Consider window order Your Application Global Search Google Assistant Recommendation
VideoPlayer Top VideoPlayer Top -
Consider window order Your Application Global search Google Assistant Recommendation
VideoPlayer Top VideoPlayer Top User want to see searched contents! -
That’s it! • Overview of AndroidTV • AndroidTV Oreo features
• What is new in Oreo? • What is Recommendation Channels? • How to implement new recommendations & tips -
Thank you! Special thanks Yoshihito Ikeda AbemaTV Android team Shohei
Kawano Shogo Sensui