Android N 以前でのおすすめの表示

一般的に、ユーザーは TV を操作するとき、最小限の入力でコンテンツを視聴することを好みます。多くの TV ユーザーにとって理想的なシナリオは、「TV の前に座り、電源を入れ、コンテンツを視聴する」というものです。最小限の操作で目当てのコンテンツにたどり着くのが、ユーザーが好む一般的な手順です。

注: Android 7.1(API レベル 25)以前のバージョンの Android で実行されているアプリでおすすめを作成する場合は、ここで説明する API を使用してください。Android 8.0(API レベル 26)以降で実行されているアプリでおすすめを提供するには、アプリでおすすめのチャンネルを使用する必要があります。

Android フレームワークはホーム画面におすすめの行を表示することで、ユーザーが最小限の入力でコンテンツを視聴できるようにします。デバイスを初めて使用した後、おすすめコンテンツが TV のホーム画面の最初の行として表示されます。アプリのコンテンツ カタログからおすすめを作成すると、ユーザーのアプリの使用頻度を高めることができます。

図 1. おすすめの行の例

このレッスンでは、ユーザーがアプリのコンテンツを簡単に見つけて楽しむことができるように、おすすめを作成して Android フレームワークに提供する方法について説明します。また、Android TV の GitHub リポジトリにある Android Leanback サンプルアプリの一部のコードについて説明します。

おすすめに関するベスト プラクティス

ユーザーはおすすめを利用することで、目当てのコンテンツやアプリをすばやく見つけることができます。TV アプリのユーザー エクスペリエンスを高めるには、ユーザーとの関連性が高い高品質のおすすめ動画を作成することが重要です。このため、ユーザーに提示するおすすめを慎重に検討し、注意深く管理する必要があります。

おすすめのタイプ

おすすめを作成する際には、ユーザーを未完了の視聴アクティビティにリンクし直すか、おすすめを関連コンテンツに拡大するアクティビティを提案する必要があります。ここでは、検討すべきおすすめのタイプをいくつか紹介します。

  • 連続コンテンツ: ユーザーがシリーズの視聴を再開できるように、次のエピソードに対してこのタイプを使用します。または、一時停止した映画、テレビ番組、ポッドキャストに対してこのタイプを使用すると、ユーザーは数回クリックするだけで一時停止したコンテンツの視聴を再開できます。
  • 新規コンテンツ: ユーザーがシリーズの視聴を終了したときに、新たに封切られた別のエピソードなどに対してこのタイプを使用します。また、ユーザーがアプリを通じてコンテンツをチャンネル登録、フォロー、追跡できるようにする場合にも、追跡対象のコンテンツのリスト内にある未視聴のアイテムに対してこのタイプを使用します。
  • 関連コンテンツ: ユーザーのこれまでの視聴行動に基づいておすすめを提示します。

ユーザー エクスペリエンスを最大限に高めるおすすめカードのデザイン方法について詳しくは、Android TV デザイン仕様のおすすめの行をご覧ください。

おすすめを更新する

おすすめを更新する場合、単におすすめを削除して再投稿しないでください。このようにすると、そのおすすめがおすすめの行の最後に表示されます。映画などのコンテンツ アイテムは、いったん再生されたらおすすめから削除します。

おすすめをカスタマイズする

ユーザー インターフェースの要素(カードのフォアグラウンドおよびバックグラウンドの画像、色、アプリアイコン、タイトル、サブタイトルなど)を設定することにより、おすすめカードをカスタマイズしてブランド情報を伝えることができます。 詳しくは、Android TV デザイン仕様のおすすめの行をご覧ください。

おすすめをグループ化する

必要に応じて、おすすめのソースに基づいておすすめをグループ化できます。たとえば、おすすめの 2 つのグループ(ユーザーがチャンネル登録しているコンテンツのおすすめ、ユーザーが知らない可能性がある新しい注目コンテンツのおすすめ)をアプリで提供することもできます。

おすすめの行を作成または更新するとき、システムによっておすすめのランク付けと順位付けがグループごとに行われます。おすすめの順位が無関係のおすすめより下位にならないようにするには、おすすめのグループ情報を提供します。

NotificationCompat.Builder.setGroup() を使用して、おすすめのグループキー文字列を設定します。たとえば、新しい注目コンテンツのグループに属することを示すマークをおすすめに付けるには、setGroup("trending") を呼び出します。

おすすめサービスを作成する

コンテンツのおすすめはバックグラウンド処理で作成されます。アプリでおすすめを作成するには、アプリのカタログの掲載情報をシステムのおすすめのリストに定期的に追加するサービスを作成します。

次のコードサンプルは、IntentService を拡張してアプリのおすすめサービスを作成する方法を示しています。

Kotlin

    class UpdateRecommendationsService : IntentService("RecommendationService") {
        override protected fun onHandleIntent(intent: Intent) {
            Log.d(TAG, "Updating recommendation cards")
            val recommendations = VideoProvider.getMovieList()
            if (recommendations == null) return

            var count = 0

            try {
                val builder = RecommendationBuilder()
                        .setContext(applicationContext)
                        .setSmallIcon(R.drawable.videos_by_google_icon)

                for (entry in recommendations.entrySet()) {
                    for (movie in entry.getValue()) {
                        Log.d(TAG, "Recommendation - " + movie.getTitle())

                        builder.setBackground(movie.getCardImageUrl())
                                .setId(count + 1)
                                .setPriority(MAX_RECOMMENDATIONS - count)
                                .setTitle(movie.getTitle())
                                .setDescription(getString(R.string.popular_header))
                                .setImage(movie.getCardImageUrl())
                                .setIntent(buildPendingIntent(movie))
                                .build()
                        if (++count >= MAX_RECOMMENDATIONS) {
                            break
                        }
                    }
                    if (++count >= MAX_RECOMMENDATIONS) {
                        break
                    }
                }
            } catch (e: IOException) {
                Log.e(TAG, "Unable to update recommendation", e)
            }
        }

        private fun buildPendingIntent(movie: Movie): PendingIntent {
            val detailsIntent = Intent(this, DetailsActivity::class.java)
            detailsIntent.putExtra("Movie", movie)

            val stackBuilder = TaskStackBuilder.create(this)
            stackBuilder.addParentStack(DetailsActivity::class.java)
            stackBuilder.addNextIntent(detailsIntent)

            // Ensure a unique PendingIntents, otherwise all
            // recommendations end up with the same PendingIntent
            detailsIntent.setAction(movie.getId().toString())

            val intent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
            return intent
        }

        companion object {
            private val TAG = "UpdateRecommendationsService"
            private val MAX_RECOMMENDATIONS = 3
        }
    }
    

Java

    public class UpdateRecommendationsService extends IntentService {
        private static final String TAG = "UpdateRecommendationsService";
        private static final int MAX_RECOMMENDATIONS = 3;

        public UpdateRecommendationsService() {
            super("RecommendationService");
        }

        @Override
        protected void onHandleIntent(Intent intent) {
            Log.d(TAG, "Updating recommendation cards");
            HashMap<String, List<Movie>> recommendations = VideoProvider.getMovieList();
            if (recommendations == null) return;

            int count = 0;

            try {
                RecommendationBuilder builder = new RecommendationBuilder()
                        .setContext(getApplicationContext())
                        .setSmallIcon(R.drawable.videos_by_google_icon);

                for (Map.Entry<String, List<Movie>> entry : recommendations.entrySet()) {
                    for (Movie movie : entry.getValue()) {
                        Log.d(TAG, "Recommendation - " + movie.getTitle());

                        builder.setBackground(movie.getCardImageUrl())
                                .setId(count + 1)
                                .setPriority(MAX_RECOMMENDATIONS - count)
                                .setTitle(movie.getTitle())
                                .setDescription(getString(R.string.popular_header))
                                .setImage(movie.getCardImageUrl())
                                .setIntent(buildPendingIntent(movie))
                                .build();

                        if (++count >= MAX_RECOMMENDATIONS) {
                            break;
                        }
                    }
                    if (++count >= MAX_RECOMMENDATIONS) {
                        break;
                    }
                }
            } catch (IOException e) {
                Log.e(TAG, "Unable to update recommendation", e);
            }
        }

        private PendingIntent buildPendingIntent(Movie movie) {
            Intent detailsIntent = new Intent(this, DetailsActivity.class);
            detailsIntent.putExtra("Movie", movie);

            TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
            stackBuilder.addParentStack(DetailsActivity.class);
            stackBuilder.addNextIntent(detailsIntent);
            // Ensure a unique PendingIntents, otherwise all
            // recommendations end up with the same PendingIntent
            detailsIntent.setAction(Long.toString(movie.getId()));

            PendingIntent intent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
            return intent;
        }
    }
    

システムがこのサービスを認識して実行できるようにするには、アプリ マニフェストを使用してサービスを登録します。次のコード スニペットは、このクラスをサービスとして宣言する方法を示しています。

    <manifest ... >
      <application ... >
        ...

        <service
                android:name="com.example.android.tvleanback.UpdateRecommendationsService"
                android:enabled="true" />
      </application>
    </manifest>
    

おすすめを作成する

おすすめサービスは、実行が開始されたら、おすすめを作成して Android フレームワークに渡す必要があります。フレームワークはおすすめを Notification オブジェクトとして受け取ります。このオブジェクトは特定のテンプレートを使用し、特定のカテゴリのマークが付けられます。

値を設定する

おすすめカードの UI 要素の値を設定するには、下記のビルダー パターンに従うビルダークラスを作成します。まず、おすすめカードの要素の値を設定します。

Kotlin

    class RecommendationBuilder {
        ...

        fun setTitle(title: String): RecommendationBuilder {
            this.title = title
            return this
        }

        fun setDescription(description: String): RecommendationBuilder {
            this.description = description
            return this
        }

        fun setImage(uri: String): RecommendationBuilder {
            imageUri = uri
            return this
        }

        fun setBackground(uri: String): RecommendationBuilder {
            backgroundUri = uri
            return this
        }

    ...
    

Java

    public class RecommendationBuilder {
        ...

        public RecommendationBuilder setTitle(String title) {
                this.title = title;
                return this;
            }

            public RecommendationBuilder setDescription(String description) {
                this.description = description;
                return this;
            }

            public RecommendationBuilder setImage(String uri) {
                imageUri = uri;
                return this;
            }

            public RecommendationBuilder setBackground(String uri) {
                backgroundUri = uri;
                return this;
            }
    ...
    

通知を作成する

値を設定したら、ビルダークラスから通知に値を割り当て、NotificationCompat.Builder.build() を呼び出して、通知を作成します。

また、setLocalOnly() を呼び出して、NotificationCompat.BigPictureStyle 通知が他のデバイスに表示されないようにします。

次のコードサンプルは、おすすめを作成する方法を示しています。

Kotlin

    class RecommendationBuilder {
        ...

        @Throws(IOException::class)
        fun build(): Notification {
            ...

            val notification = NotificationCompat.BigPictureStyle(
            NotificationCompat.Builder(context)
                    .setContentTitle(title)
                    .setContentText(description)
                    .setPriority(priority)
                    .setLocalOnly(true)
                    .setOngoing(true)
                    .setColor(context.resources.getColor(R.color.fastlane_background))
                    .setCategory(Notification.CATEGORY_RECOMMENDATION)
                    .setLargeIcon(image)
                    .setSmallIcon(smallIcon)
                    .setContentIntent(intent)
                    .setExtras(extras))
                    .build()

            return notification
        }
    }
    

Java

    public class RecommendationBuilder {
        ...

        public Notification build() throws IOException {
            ...

            Notification notification = new NotificationCompat.BigPictureStyle(
                    new NotificationCompat.Builder(context)
                            .setContentTitle(title)
                            .setContentText(description)
                            .setPriority(priority)
                            .setLocalOnly(true)
                            .setOngoing(true)
                            .setColor(context.getResources().getColor(R.color.fastlane_background))
                            .setCategory(Notification.CATEGORY_RECOMMENDATION)
                            .setLargeIcon(image)
                            .setSmallIcon(smallIcon)
                            .setContentIntent(intent)
                            .setExtras(extras))
                    .build();

            return notification;
        }
    }
    

おすすめサービスを実行する

アプリのおすすめサービスは、最新のおすすめを作成するために、定期的に実行する必要があります。サービスを実行するには、タイマーを実行してサービスを定期的に呼び出すクラスを作成します。次のコードサンプルでは、BroadcastReceiver クラスを拡張して、30 分ごとにおすすめサービスを実行しています。

Kotlin

    class BootupActivity : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            Log.d(TAG, "BootupActivity initiated")
            if (intent.action.endsWith(Intent.ACTION_BOOT_COMPLETED)) {
                scheduleRecommendationUpdate(context)
            }
        }

        private fun scheduleRecommendationUpdate(context: Context) {
            Log.d(TAG, "Scheduling recommendations update")
            val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
            val recommendationIntent = Intent(context, UpdateRecommendationsService::class.java)
            val alarmIntent = PendingIntent.getService(context, 0, recommendationIntent, 0)
            alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                    INITIAL_DELAY,
                    AlarmManager.INTERVAL_HALF_HOUR,
                    alarmIntent
            )
        }

        companion object {
            private val TAG = "BootupActivity"
            private val INITIAL_DELAY:Long = 5000
        }
    }
    

Java

    public class BootupActivity extends BroadcastReceiver {
        private static final String TAG = "BootupActivity";

        private static final long INITIAL_DELAY = 5000;

        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d(TAG, "BootupActivity initiated");
            if (intent.getAction().endsWith(Intent.ACTION_BOOT_COMPLETED)) {
                scheduleRecommendationUpdate(context);
            }
        }

        private void scheduleRecommendationUpdate(Context context) {
            Log.d(TAG, "Scheduling recommendations update");

            AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
            Intent recommendationIntent = new Intent(context, UpdateRecommendationsService.class);
            PendingIntent alarmIntent = PendingIntent.getService(context, 0, recommendationIntent, 0);

            alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                    INITIAL_DELAY,
                    AlarmManager.INTERVAL_HALF_HOUR,
                    alarmIntent);
        }
    }
    

この BroadcastReceiver クラスの実装は、インストール対象のテレビデバイスの起動後に実行する必要があります。そのためには、デバイスの起動プロセスの完了をリッスンするインテント フィルタを使用して、アプリ マニフェストにこのクラスを登録します。次のコードサンプルは、この構成をマニフェストに追加する方法を示しています。

    <manifest ... >
      <application ... >
        <receiver android:name="com.example.android.tvleanback.BootupActivity"
                  android:enabled="true"
                  android:exported="false">
          <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED"/>
          </intent-filter>
        </receiver>
      </application>
    </manifest>
    

重要: 起動完了の通知を受け取るには、アプリは RECEIVE_BOOT_COMPLETED 権限をリクエストする必要があります。詳細については、ACTION_BOOT_COMPLETED をご覧ください。

おすすめサービスクラスの onHandleIntent() メソッドで、次のようにしておすすめをマネージャーに送信します。

Kotlin

    val notification = notificationBuilder.build()
    notificationManager.notify(id, notification)
    

Java

    Notification notification = notificationBuilder.build();
    notificationManager.notify(id, notification);