Android N 및 이전 버전의 권장사항

TV와 상호작용할 때 사용자는 일반적으로 콘텐츠를 보기 전에 최소한의 입력만 하는 것을 선호합니다. 많은 TV 사용자에게 이상적인 시나리오는 앉아서 TV를 켜고 시청하는 것입니다. 일반적으로 사용자가 좋아하는 콘텐츠로 안내하기 위한 가장 적은 단계는 일반적으로 사용자가 선호하는 경로입니다.

참고: 여기에 설명된 API를 사용하여 Android 7.1 (API 수준 25) 이하의 Android 버전에서만 실행되는 앱에 추천하세요. Android 8.0 (API 수준 26) 이상에서 실행되는 앱을 위한 추천을 제공하려면 앱에서 추천 채널을 사용해야 합니다.

Android 프레임워크는 홈 화면에 추천 행을 제공하여 최소 입력 상호작용을 지원합니다. 콘텐츠 추천은 기기를 처음 사용한 후 TV 홈 화면의 첫 번째 행으로 표시됩니다. 앱의 콘텐츠 카탈로그에서 추천 항목을 제공하면 사용자를 앱으로 다시 유도할 수 있습니다.

그림 1. 추천 행의 예

이 강의에서는 사용자가 앱 콘텐츠를 쉽게 찾고 즐길 수 있도록 추천을 만들고 Android 프레임워크에 제공하는 방법을 설명합니다. 이번 논의에서는 Android TV GitHub 저장소에 있는 Android Leanback 샘플 앱의 일부 코드를 설명합니다.

추천 권장사항

추천을 통해 사용자는 자신이 즐기는 콘텐츠와 앱을 빠르게 찾을 수 있습니다. 사용자와 관련된 고품질 추천을 만드는 것은 TV 앱에서 훌륭한 사용자 환경을 만드는 데 중요한 요소입니다. 따라서 사용자에게 어떤 추천을 제공할지 신중하게 고려하고 이를 면밀히 관리해야 합니다.

추천 유형

추천을 만들 때는 사용자를 완료되지 않은 시청 활동으로 다시 연결하거나 이를 관련 콘텐츠로 확장하는 활동을 추천해야 합니다. 다음은 고려해야 할 특정 유형의 권장사항입니다.

  • 사용자가 시리즈를 다시 볼 수 있도록 다음 에피소드에 관한 연속 콘텐츠 추천 또는 일시중지된 영화, TV 프로그램 또는 팟캐스트에 연속 추천을 사용하여 사용자가 클릭 몇 번만으로 일시중지된 콘텐츠를 다시 시청할 수 있도록 합니다.
  • 사용자가 다른 시리즈 시청을 끝냈다면 첫 방송된 새로운 에피소드와 같은 새로운 콘텐츠를 추천합니다. 또한 앱에서 사용자가 콘텐츠를 구독, 팔로우 또는 추적할 수 있는 경우 추적된 콘텐츠 목록에서 시청하지 않은 항목에 새 콘텐츠 추천을 사용하세요.
  • 사용자의 과거 시청 행동에 기반한 관련 콘텐츠를 추천합니다.

최상의 사용자 환경을 위해 추천 카드를 디자인하는 방법에 관한 자세한 내용은 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()를 호출합니다.

또한 NotificationCompat.BigPictureStyle 알림이 다른 기기에 표시되지 않도록 setLocalOnly()를 호출해야 합니다.

다음 코드 예에서는 추천을 빌드하는 방법을 보여줍니다.

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 클래스의 이러한 구현은 설치된 TV 기기가 시작된 후에 실행되어야 합니다. 이를 위해 기기 부팅 프로세스의 완료를 수신 대기하는 인텐트 필터로 앱 매니페스트에 이 클래스를 등록하세요. 다음 샘플 코드에서는 이 구성을 매니페스트에 추가하는 방법을 보여줍니다.

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