2018年7月13日

Android

GYAO! Android TVアプリでのOSホーム画面に対する取り組み

  • このエントリーをはてなブックマークに追加

はじめに

こんにちは。メディアカンパニーGYAO本部の渡邊です。
動画配信サービスGYAO!のAndroid TVアプリや動画プレーヤーの開発を担当しています。
Android 8.0からAndroid TVのホーム画面が大きく変わりました。
今回はGYAO!のTVアプリがおこなっているホーム画面への対応についてご紹介させていただきたいと思います。

Android TVとは

日本で最初のAndroid TV端末であるNexus Playerが3年前の2015年2月に発売されました。
利用できる端末が限られていたこともあり、当時のAndroid TV利用ユーザー数はそれほど多くありませんでした。
しかしその後、比較的安価なスティックタイプの端末が発売され、また、ソニー、シャープなどの大手メーカーが家庭用テレビのOSとして採用したことで、利用者は大きく増え続けています。
デバイスがテレビであることから特に映像アプリとは相性が良く、GYAO!も2015年の端末販売開始とほぼ同時期にTVアプリをリリースしました。

ホーム画面の変更

リリース当時のOSバージョンは5.0でしたが、TVのOSも毎年更新されており、間もなくP(9.0)がリリースされる予定です。
その中でも昨年リリースされた8.0ではAndroid TVリリース以来最大とも言える変更がなされました。

下の画像は「7.0以前のホーム画面」と「8.0のホーム画面」を比べたものです。
アプリのアイコンが並ぶこれまでのホーム画面が一新され、画面の多くの表示領域をコンテンツが占めています。

7.0以前のホーム画面
7.0以前のホーム画面

8.0のホーム画面
8.0のホーム画面

Googleはこの変更について「コンテンツファースト」という言葉をしばしば使っています。
「どのアプリを使うのか」ではなく「(どのアプリかは意識せず)興味のあるコンテンツに出会う」ことにユーザーの最大の関心があるという考え方です。

現在(2018年7月)、この新しいホーム画面を利用できる端末はほぼNexus Playerだけに限られていますが、6月末に発売されたシャープのAQUOSではこちらのホーム画面が採用されています。
http://www.sharp.co.jp/corporate/news/180607-b.html
また、既に販売されている多くの端末も今年以降Android 8.0にOSアップデートされることが予想され、ホーム画面がガラっと変わるデバイスがこれから続々と増えていくと思われます。

Android 7.0以前のホーム画面

Android 7.0以前のホーム画面にも、各アプリがおすすめするコンテンツを表示する仕組みはありました。
それがホーム画面一番上の「レコメンド行」と呼ばれる領域(白枠で囲った部分)です。

2018-06-22_16-37-07

各アプリはプッシュ通知の仕組みを使ってレコメンド行におすすめのコンテンツを表示します。
TV用のSupportLibraryにヘルパークラスが用意されており、こちらを利用することで容易にこの機能を実現できます。

implementation 'com.android.support:recommendation:27.1.1'
val recommendation = ContentRecommendation.Builder()
    .setBadgeIcon(R.drawable.badge_icon) // 右下のロゴ
    .setColor(color) // タイトル部分の背景色
    .setTitle(title) // タイトル部分の1行目
    .setText(text) // タイトル部分の2行目
    .setContentImage(image) // 画像
    .setContentIntentData(ContentRecommendation.INTENT_TYPE_ACTIVITY, intent, id, null) // 選択された際に起動するアプリ内コンテンツのUri(DeepLink)
    .setBackgroundImageUri(backGroundImageUri) // フォーカスされた際に背景に表示する画像
    .build()

(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
  .notify(id, recommendation.getNotificationObject(context))

SupportLibraryの各APIについて詳細を知りたい方は以下のドキュメントを参照ください。
https://developer.android.com/reference/android/support/app/recommendation/package-summary
また、実装例を確認したい方はGoogleが公開しているサンプルの以下のクラスを参照ください。
https://github.com/googlesamples/androidtv-Leanback/blob/master/app/src/main/java/com/example/android/tvleanback/recommendation/UpdateRecommendationsService.java

アプリを横断しておすすめのコンテンツを紹介する仕組みはAndroid TVの特徴の1つですが、実際にはレコメンド行には以下のような問題がありました。

  • 多くのアプリがインストールされている状況では各アプリで場所の取り合いになる
  • 端末メーカーの特集など、ビジネス的に優先されるカードが表示されることがある
    • それらはコンテンツでない場合もある
  • 表示順に関するロジックがブラックボックスであり、いつどこに表示されるか分からない

結果として興味のないコンテンツが羅列されてしまったり、例えインストールしているアプリに探していたコンテンツがあったとしても、気付くことができない状況が多々ありました。

Android 8.0のホーム画面

Android 8.0でのUIの変更は、レコメンド行での問題を解決することで、ホーム画面のユーザー価値を高めるという目的があったと考えられます。
また、それを実現するための技術的な仕様も変わりました。

チャンネルとプログラム

新しいホーム画面では、各行を「チャンネル」と呼び、チャンネルに含まれる各カードを「プログラム」と呼びます。
アプリがインストールされたタイミングで、各アプリは1つだけ自動でチャンネルをホーム画面に表示させることができます。このチャンネルを「デフォルトチャンネル」と呼びます。

一方で、これまでとは異なり、アプリをインストールした際にホーム画面のアプリ一覧にアイコンが自動で登録されません。

2018-07-11_07-23-58
アプリインストール直後

2018-07-11_07-26-06
ユーザーが「+」からアプリを選択して初めてホーム画面にアプリアイコンが表示される

2018-07-11_07-26-16

そのため、自動で登録されるデフォルトチャンネルは、ユーザーとの接点としてとても重要です。

チャンネルおよびプログラムは、Content Providerと呼ばれるAndroidのデータ管理の仕組みを用いて実現します。
https://developer.android.com/guide/topics/providers/content-providers
お手持ちのAndroidスマートフォンで撮影した画像ファイルや、端末に登録してある電話帳などをアプリで表示する際にもこの仕組みが用いられています。
contentprovider

チャンネルを作成し、Content Providerのチャンネル用領域にデータを保存することで、そのチャンネルがホーム画面に表示されます。
こちらについてもTvContractCompatなどの専用のヘルパークラスが用意されています。
TvContractCompatにはチャンネルのIdとContent Providerで扱うUriとの関係が記述されているため、チャンネルのIdを用いた単純なメソッド呼び出しで各操作が完了します。

implementation 'com.android.support:support-tv-provider:27.1.1'

チャンネル作成

// 登録するチャンネルを作成
val channel = Channel.Builder()
    .setType(TvContractCompat.Channels.TYPE_PREVIEW) // 固定値
    .setDisplayName(title) // 「今日のPICK UP」
    .setAppLinkIntentUri(intentUri) // チャンネルロゴを選択した際の遷移先Uri
    .build()

// Content Providerにチャンネルを書き込み、そのアドレスとなるUriを取得
val uri = context.contentResolver.insert(TvContractCompat.Channels.CONTENT_URI, channel.contentValues())
val channelId = ContentUris.parseId(uri)

// Content ProvicerのUriではなく、Idを用いてヘルパークラス経由でアクセスする
ChannelLogoUtils.storeChannelLogo(context, channelId, logo) // チャンネルのロゴを登録
TvContractCompat.requestChannelBrowsable(context, channelId) // ホーム画面に表示(デフォルトチャンネルのみ)

チャンネル更新

val uri = TvContractCompat.buildChannelUri(channelId) // チャンネルId → Uri
context.contentResolver.delete(uri, contentValues, null, null);

チャンネル削除

// チャンネルのIdだけ知っていれば削除できる
context.contentResolver.delete(TvContractCompat.buildChannelUri(channelId), null, null);

但し、チャンネルを削除してしまうと、それまでホーム画面に表示されていたチャンネルがいきなり消えてしまうので、実際に使う機会はなさそうです。

おすすめコンテンツの定期更新

GYAO!ではおすすめの映像を毎日更新しているため、表示されているプログラムを1日に1回更新するようJobSchedulerを設定しています。
テレビの電源を入れると前日とは別の映像がホーム画面で紹介されるので、興味のあるコンテンツにより出会いやすくなっています。

定期更新のためのJobSchedulerの記述はモバイルアプリでのJobServiceの実装方法と同じです。

@TargetApi(Build.VERSION_CODES.O)
class UpdateProgramsJobService : JobService() {
    companion object {
        fun startAndSchedulePeriodicJob(context: Context, channel: Channel) {
            // 中略
            val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
            val periodicJobInfo = JobInfo.Builder(jobId, componentName)
                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                .setPeriodic(TimeUnit.DAYS.toMillis(1)) // 1日1回更新する
                .build()
            scheduler.schedule(periodicJobInfo)
        }
    }

    override fun onStartJob(params: JobParameters): Boolean {
        // Content Providerを介してプログラムの更新処理を記述
    }
}

JobServiceについてはデベロッパーガイドを参照ください。
https://developer.android.com/reference/android/app/job/JobService

また今回は割愛させていただきましたが、Content Providerを介したプログラムの更新方法につきましては、以下のデベロッパーガイドを参照ください。
https://developer.android.com/training/tv/discovery/recommendations-channel#programs

チャンネル表示/非表示での更新

チャンネルロゴで左を押すと表示される「-」アイコンを選択すると、チャンネルそのものを非表示にすることができます。

2018-06-22_17-24-15s

非表示になっているにもかかわらずコンテンツを定期的に更新し続けることは、システムに不要な負荷を与えます。
逆に、設定画面から再度チャンネルを表示させた際には、最新のおすすめをユーザーに紹介する必要があります。
そのため、チャンネルの表示状態を監視し、変更があった場合にはその変更に応じた処理を行うようにスケジュールしておきます。
JobSchedulerにはこれを簡単に実現する仕組みがあるので、それを利用しています。

@TargetApi(Build.VERSION_CODES.O)
class UpdateChannelJobService : JobService() {
    companion object {
        fun scheduleJob(context: Context, channel: Channel) {
            // 中略
            val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler

            // 監視するContent ProviderのUriをチャンネルのIdから取得
            val triggerContentUri = JobInfo.TriggerContentUri(
                    TvContractCompat.buildChannelUri(channel.id),
                    JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)

            val jobInfo = JobInfo.Builder(jobId, componentName)
                    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                    .addTriggerContentUri(triggerContentUri) // チャンネルの状態変更をトリガーにする
                    .setTriggerContentMaxDelay(0L)
                    .setTriggerContentUpdateDelay(0L)
                    .build()
            scheduler.schedule(jobInfo)
        }
    }

    override fun onStartJob(params: JobParameters): Boolean {
        // チャンネルがホーム画面に表示されていれば最新の状態に更新し、定期更新を再開する
        // チャンネルがホーム画面で非表示であればプログラムを削除して、定期更新を止める
    }
}

複数チャンネル

各アプリは、デフォルトチャンネル以外にも任意のチャンネルを作成できます。
例えばYouTubeであれば以下のようにテーマごとのチャンネルが用意されています。

2018-06-21_19-33-31s

GYAO!アプリではコンテンツを映画、アニメ、韓流、などの「ジャンル」に分類しています。
GYAO!のユーザーにはいろいろなジャンルの映像に興味を持たれている方ももちろんいらっしゃいますが、特定ジャンルの映像を毎日視聴されている方もたくさんいらっしゃいます。
そういったユーザーがより少ない操作で好きなコンテンツを視聴できるように、各ジャンルに対応したチャンネルを用意しています。
チャンネルを用いることで以下の要件を満たすことができるからです。

  • アプリのジャンルトップ画面をホーム画面から直接開くことができる
  • 好きなジャンルのおすすめコンテンツを、ホーム画面から直接視聴開始できる

チャンネルのロゴを選択するとアプリのジャンルトップ画面が開き、プログラムを選択するとそのコンテンツの再生がすぐに始まります。

2018-06-22_19-10-27s

デフォルトチャンネル以外のチャンネルをホーム画面に表示するためには、ユーザーの操作や同意が必要です。
そのための操作として以下の2つのパターンがあります。

(1)「チャンネルをカスタマイズ」から選択

ホーム画面の一番下にある「チャンネルをカスタマイズ」から各チャンネルの表示非表示を切り替えることができます。
あらかじめContent Providerにチャンネルを登録しておくと、この画面にチャンネル名が表示されます。
この画面についてはOSが全てハンドリングしてくれるので、先に書いた「チャンネル表示/非表示での更新」が正しく実装されていれば、アプリ側では他にすることはありません。

2018-06-21_19-38-58s

(2)アプリ内から追加

(1)の設定画面だけではほとんどのユーザーに気付いてもらえないため、GYAO!アプリでは、ジャンルトップ画面からそれぞれのジャンルのチャンネル追加できるようになっています。

2018-06-21_19-39-35s
2018-06-21_19-39-50s

この場合にはアプリ内で、ダイアログを表示するためのIntentの作成と、ユーザー操作のコールバックを受けるコードを記述する必要があります。

/**
 * チャンネル追加確認ダイアログを表示する
 */
private fun promptUserToDisplayChannel(channelId: Long) {
    val intent = Intent(TvContractCompat.ACTION_REQUEST_CHANNEL_BROWSABLE)
    intent.putExtra(TvContractCompat.EXTRA_CHANNEL_ID, channelId) // ホーム画面に追加したいチャンネルId
    startActivityForResult(intent, 0)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (resultCode == Activity.RESULT_OK) {
        Toast.makeText(activity, "チャンネルが追加されました", Toast.LENGTH_LONG).show()
    }
}

さいごに

手元にAndroid TV端末をお持ちの方は、OSが8.0に更新されましたら新しいホーム画面をぜひ体験してみてください。
実は新しいホーム画面ではチャンネル以外にももう一つ大きな変更点がありました。こちらについては次の機会にご紹介させていただけたらと思っています。

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加