Android N 及以下版本中的推薦服務

與電視互動時,使用者通常希望在觀看內容之前,盡量不要輸入相關資料。對許多電視使用者來說,理想的情境為:坐下、開機、觀看。讓使用者輕鬆存取自己喜歡的內容,最簡單的步驟通常是他們偏好的途徑。

注意:只有在 Android 7.1 (API 級別 25) 以下版本中執行的應用程式,才能使用本文所述的 API 提供建議。如要為在 Android 8.0 (API 級別 26) 以上版本中執行的應用程式提供建議,應用程式必須使用推薦管道

Android 架構會在主畫面提供建議列,協助最低輸入互動。首次使用裝置後,內容推薦會顯示在電視主畫面的第一列。從應用程式的內容目錄中提供推薦內容,有助於吸引使用者再次使用應用程式。

圖 1 建議列範例。

本指南將說明如何建立推薦內容,並提供 Android 架構,讓使用者可以輕鬆發掘及享受您的應用程式內容。另請參閱 Leanback 範例應用程式的實作範例。

推薦功能的最佳做法

建議可協助使用者快速找到喜愛的內容和應用程式。如要打造最佳的 TV 應用程式使用者體驗,建立高品質的相關推薦內容,就是非常重要的因素。因此,您應審慎考量向使用者提供的推薦內容,並謹慎管理。

「建議」類型

建立推薦項目時,您應將使用者重新連結至未完成的觀看活動,或建議可延伸至相關內容的活動。以下是一些您應考慮的具體建議:

  • 接續內容建議,協助使用者繼續觀看下一集。或者,針對已暫停的電影、電視節目或 Podcast 採用繼續建議,讓使用者只要按幾下就能繼續觀看已暫停的內容。
  • 「新內容」建議,例如使用者看完其他系列叢書時,新的首播一集節目。此外,如果您的應用程式可讓使用者訂閱、追蹤或追蹤內容,請針對他們追蹤內容清單中的未觀看項目採用新內容推薦功能。
  • 根據使用者過往的觀看行為推薦的相關內容

如要進一步瞭解如何設計推薦資訊卡以提供最佳使用者體驗,請參閱 Android TV 設計規格中的推薦列

重新整理推薦影片

重新整理建議時,請勿只移除並重新發布建議,因為這會導致建議列末端顯示建議。播放完畢的內容項目 (例如電影) 後,請從推薦中 移除該部影片。

自訂推薦內容

您可以自訂推薦資訊卡來傳達品牌資訊,方法是設定使用者介面元素,例如資訊卡的前景和背景圖片、顏色、應用程式圖示、標題和副標題。詳情請參閱 Android TV 設計規格中的推薦列

群組建議

您可以視需要根據最佳化建議來源將最佳化建議分組。舉例來說,您的應用程式可能會提供兩種推薦內容群組:針對使用者訂閱的內容進行推薦,以及推薦使用者可能不知道的新熱門內容。

建立或更新推薦資料列時,系統會分別為每個群組排序建議及排序建議。提供群組資訊來取得推薦內容,可確保系統不會將建議排序在不相關的建議下方。

使用 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 類別,以每半小時開始定期執行推薦服務:

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);