검색 가능한 TV 앱 만들기

Android TV는 Android 검색 인터페이스를 사용합니다. 설치된 앱에서 콘텐츠 데이터를 가져와 사용자에게 검색 결과를 제공합니다. 앱의 콘텐츠 데이터를 이러한 결과에 포함하여 사용자가 있습니다.

앱은 Android TV가 추천 검색어를 생성할 수 있는 데이터 필드를 Android TV에 제공해야 합니다. 검색 대화상자에 문자를 입력하면 결과가 자동으로 표시됩니다. 이를 위해서는 앱에서 서비스를 제공하는 콘텐츠 제공자 콘텐츠를 설명하는 searchable.xml 구성 파일 기타 중요한 정보를 제공합니다. 또한 인코더-디코더 아키텍처를 인텐트가 실행됩니다. 대상 추가 맞춤 추천 검색어를 참고하세요. 이 가이드에서는 Android TV 앱과 관련된 주요 사항을 다룹니다.

이 가이드를 읽기 전에 Search API 가이드 검색 기능 추가도 검토하세요.

이 가이드의 샘플 코드는 <ph type="x-smartling-placeholder"></ph> Leanback 샘플 앱 에서 자세한 내용을 확인하실 수 있습니다.

열 식별

SearchManager는 예상되는 데이터 필드를 다음과 같이 표시하여 설명합니다. 로컬 데이터베이스의 열입니다. 데이터 형식에 관계없이 데이터 필드를 일반적으로 콘텐츠 데이터에 액세스하는 클래스에서 이러한 열입니다. 빌드에 대한 자세한 내용은 클래스에 대한 자세한 내용은 다음을 참조하세요. <ph type="x-smartling-placeholder"></ph> 추천 검색어 표 작성하기를 참고하세요.

SearchManager 클래스에는 Android TV와 관련된 여러 열이 포함되어 있습니다. 일부 더 중요한 열은 다음 표에 설명되어 있습니다.

설명
SUGGEST_COLUMN_TEXT_1 콘텐츠 이름 (필수)
SUGGEST_COLUMN_TEXT_2 콘텐츠의 텍스트 설명
SUGGEST_COLUMN_RESULT_CARD_IMAGE 콘텐츠의 이미지, 포스터 또는 표지
SUGGEST_COLUMN_CONTENT_TYPE 미디어의 MIME 유형
SUGGEST_COLUMN_VIDEO_WIDTH 미디어의 해상도 너비
SUGGEST_COLUMN_VIDEO_HEIGHT 미디어의 해상도 높이
SUGGEST_COLUMN_PRODUCTION_YEAR 콘텐츠 제작 연도 (필수)
SUGGEST_COLUMN_DURATION 미디어의 재생 시간(밀리초)(필수)

검색 프레임워크에는 다음 열이 필요합니다.

콘텐츠에 대한 이러한 열의 값이 다른 검색 공급자가 발견될 경우 시스템은 세부정보에 앱으로 연결되는 딥 링크가 표시됩니다. 다른 제공업체의 앱으로 연결되는 링크와 함께 콘텐츠를 볼 수 있습니다. 이에 대해서는 세부정보 화면에 표시되는 앱의 딥 링크 섹션을 확인합니다.

애플리케이션의 데이터베이스 클래스에 열이 다음과 같이 정의될 수 있습니다.

Kotlin

class VideoDatabase {
    companion object {
        // The columns we'll include in the video database table
        val KEY_NAME = SearchManager.SUGGEST_COLUMN_TEXT_1
        val KEY_DESCRIPTION = SearchManager.SUGGEST_COLUMN_TEXT_2
        val KEY_ICON = SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE
        val KEY_DATA_TYPE = SearchManager.SUGGEST_COLUMN_CONTENT_TYPE
        val KEY_IS_LIVE = SearchManager.SUGGEST_COLUMN_IS_LIVE
        val KEY_VIDEO_WIDTH = SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH
        val KEY_VIDEO_HEIGHT = SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT
        val KEY_AUDIO_CHANNEL_CONFIG = SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG
        val KEY_PURCHASE_PRICE = SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE
        val KEY_RENTAL_PRICE = SearchManager.SUGGEST_COLUMN_RENTAL_PRICE
        val KEY_RATING_STYLE = SearchManager.SUGGEST_COLUMN_RATING_STYLE
        val KEY_RATING_SCORE = SearchManager.SUGGEST_COLUMN_RATING_SCORE
        val KEY_PRODUCTION_YEAR = SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR
        val KEY_COLUMN_DURATION = SearchManager.SUGGEST_COLUMN_DURATION
        val KEY_ACTION = SearchManager.SUGGEST_COLUMN_INTENT_ACTION
        ...
    }
    ...
}

자바

public class VideoDatabase {
    // The columns we'll include in the video database table
    public static final String KEY_NAME = SearchManager.SUGGEST_COLUMN_TEXT_1;
    public static final String KEY_DESCRIPTION = SearchManager.SUGGEST_COLUMN_TEXT_2;
    public static final String KEY_ICON = SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE;
    public static final String KEY_DATA_TYPE = SearchManager.SUGGEST_COLUMN_CONTENT_TYPE;
    public static final String KEY_IS_LIVE = SearchManager.SUGGEST_COLUMN_IS_LIVE;
    public static final String KEY_VIDEO_WIDTH = SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH;
    public static final String KEY_VIDEO_HEIGHT = SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT;
    public static final String KEY_AUDIO_CHANNEL_CONFIG =
            SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG;
    public static final String KEY_PURCHASE_PRICE = SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE;
    public static final String KEY_RENTAL_PRICE = SearchManager.SUGGEST_COLUMN_RENTAL_PRICE;
    public static final String KEY_RATING_STYLE = SearchManager.SUGGEST_COLUMN_RATING_STYLE;
    public static final String KEY_RATING_SCORE = SearchManager.SUGGEST_COLUMN_RATING_SCORE;
    public static final String KEY_PRODUCTION_YEAR = SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR;
    public static final String KEY_COLUMN_DURATION = SearchManager.SUGGEST_COLUMN_DURATION;
    public static final String KEY_ACTION = SearchManager.SUGGEST_COLUMN_INTENT_ACTION;
...

SearchManager 열에서 데이터 필드로의 매핑을 작성할 때 또한 _ID를 지정하여 각 행에 고유 ID를 부여해야 합니다.

Kotlin

companion object {
    ....
    private fun buildColumnMap(): MapS<tring, String> {
        return mapOf(
          KEY_NAME to KEY_NAME,
          KEY_DESCRIPTION to KEY_DESCRIPTION,
          KEY_ICON to KEY_ICON,
          KEY_DATA_TYPE to KEY_DATA_TYPE,
          KEY_IS_LIVE to KEY_IS_LIVE,
          KEY_VIDEO_WIDTH to KEY_VIDEO_WIDTH,
          KEY_VIDEO_HEIGHT to KEY_VIDEO_HEIGHT,
          KEY_AUDIO_CHANNEL_CONFIG to KEY_AUDIO_CHANNEL_CONFIG,
          KEY_PURCHASE_PRICE to KEY_PURCHASE_PRICE,
          KEY_RENTAL_PRICE to KEY_RENTAL_PRICE,
          KEY_RATING_STYLE to KEY_RATING_STYLE,
          KEY_RATING_SCORE to KEY_RATING_SCORE,
          KEY_PRODUCTION_YEAR to KEY_PRODUCTION_YEAR,
          KEY_COLUMN_DURATION to KEY_COLUMN_DURATION,
          KEY_ACTION to KEY_ACTION,
          BaseColumns._ID to ("rowid AS " + BaseColumns._ID),
          SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID to ("rowid AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID),
          SearchManager.SUGGEST_COLUMN_SHORTCUT_ID to ("rowid AS " + SearchManager.SUGGEST_COLUMN_SHORTCUT_ID)
        )
    }
}

자바

...
  private static HashMap<String, String> buildColumnMap() {
    HashMap<String, String> map = new HashMap<String, String>();
    map.put(KEY_NAME, KEY_NAME);
    map.put(KEY_DESCRIPTION, KEY_DESCRIPTION);
    map.put(KEY_ICON, KEY_ICON);
    map.put(KEY_DATA_TYPE, KEY_DATA_TYPE);
    map.put(KEY_IS_LIVE, KEY_IS_LIVE);
    map.put(KEY_VIDEO_WIDTH, KEY_VIDEO_WIDTH);
    map.put(KEY_VIDEO_HEIGHT, KEY_VIDEO_HEIGHT);
    map.put(KEY_AUDIO_CHANNEL_CONFIG, KEY_AUDIO_CHANNEL_CONFIG);
    map.put(KEY_PURCHASE_PRICE, KEY_PURCHASE_PRICE);
    map.put(KEY_RENTAL_PRICE, KEY_RENTAL_PRICE);
    map.put(KEY_RATING_STYLE, KEY_RATING_STYLE);
    map.put(KEY_RATING_SCORE, KEY_RATING_SCORE);
    map.put(KEY_PRODUCTION_YEAR, KEY_PRODUCTION_YEAR);
    map.put(KEY_COLUMN_DURATION, KEY_COLUMN_DURATION);
    map.put(KEY_ACTION, KEY_ACTION);
    map.put(BaseColumns._ID, "rowid AS " +
            BaseColumns._ID);
    map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, "rowid AS " +
            SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
    map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, "rowid AS " +
            SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
    return map;
  }
...

이전 예에서 SUGGEST_COLUMN_INTENT_DATA_ID에 대한 매핑에 주목하세요. 필드를 확인합니다. 이는 URI에서 이 호출의 데이터에 고유한 콘텐츠를 가리키는 행: URI의 마지막 부분으로, 콘텐츠가 저장되는 위치를 설명합니다. URI의 첫 번째 부분은 테이블의 모든 행에 공통인 경우 searchable.xml 파일을 <ph type="x-smartling-placeholder"></ph> android:searchSuggestIntentData 속성 추천 검색어 처리 섹션을 참조하세요.

URI의 첫 번째 부분이 테이블에서 해당 값을 SUGGEST_COLUMN_INTENT_DATA 필드와 매핑합니다. 사용자가 이 콘텐츠를 선택하면 실행되는 인텐트가 SUGGEST_COLUMN_INTENT_DATA_ID 조합 그리고 android:searchSuggestIntentData 속성 또는 SUGGEST_COLUMN_INTENT_DATA 필드 값입니다.

추천 검색어 데이터 제공

콘텐츠 제공자 구현 Android TV 검색 대화상자에 추천 검색어를 표시합니다. 시스템에서 콘텐츠를 쿼리합니다. 매번 query() 메서드를 호출하여 추천 제공자 문자를 입력합니다 query()를 구현하면 콘텐츠가 제공자가 추천 데이터를 검색하고 다음을 가리키는 Cursor를 반환합니다. 모두 표시됩니다.

Kotlin

fun query(uri: Uri, projection: Array<String>, selection: String, selectionArgs: Array<String>,
        sortOrder: String): Cursor {
    // Use the UriMatcher to see what kind of query we have and format the db query accordingly
    when (URI_MATCHER.match(uri)) {
        SEARCH_SUGGEST -> {
            Log.d(TAG, "search suggest: ${selectionArgs[0]} URI: $uri")
            if (selectionArgs == null) {
                throw IllegalArgumentException(
                        "selectionArgs must be provided for the Uri: $uri")
            }
            return getSuggestions(selectionArgs[0])
        }
        else -> throw IllegalArgumentException("Unknown Uri: $uri")
    }
}

private fun getSuggestions(query: String): Cursor {
    val columns = arrayOf<String>(
            BaseColumns._ID,
            VideoDatabase.KEY_NAME,
            VideoDatabase.KEY_DESCRIPTION,
            VideoDatabase.KEY_ICON,
            VideoDatabase.KEY_DATA_TYPE,
            VideoDatabase.KEY_IS_LIVE,
            VideoDatabase.KEY_VIDEO_WIDTH,
            VideoDatabase.KEY_VIDEO_HEIGHT,
            VideoDatabase.KEY_AUDIO_CHANNEL_CONFIG,
            VideoDatabase.KEY_PURCHASE_PRICE,
            VideoDatabase.KEY_RENTAL_PRICE,
            VideoDatabase.KEY_RATING_STYLE,
            VideoDatabase.KEY_RATING_SCORE,
            VideoDatabase.KEY_PRODUCTION_YEAR,
            VideoDatabase.KEY_COLUMN_DURATION,
            VideoDatabase.KEY_ACTION,
            SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID
    )
    return videoDatabase.getWordMatch(query.toLowerCase(), columns)
}

자바

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
        String sortOrder) {
    // Use the UriMatcher to see what kind of query we have and format the db query accordingly
    switch (URI_MATCHER.match(uri)) {
        case SEARCH_SUGGEST:
            Log.d(TAG, "search suggest: " + selectionArgs[0] + " URI: " + uri);
            if (selectionArgs == null) {
                throw new IllegalArgumentException(
                        "selectionArgs must be provided for the Uri: " + uri);
            }
            return getSuggestions(selectionArgs[0]);
        default:
            throw new IllegalArgumentException("Unknown Uri: " + uri);
    }
}

private Cursor getSuggestions(String query) {
    query = query.toLowerCase();
    String[] columns = new String[]{
        BaseColumns._ID,
        VideoDatabase.KEY_NAME,
        VideoDatabase.KEY_DESCRIPTION,
        VideoDatabase.KEY_ICON,
        VideoDatabase.KEY_DATA_TYPE,
        VideoDatabase.KEY_IS_LIVE,
        VideoDatabase.KEY_VIDEO_WIDTH,
        VideoDatabase.KEY_VIDEO_HEIGHT,
        VideoDatabase.KEY_AUDIO_CHANNEL_CONFIG,
        VideoDatabase.KEY_PURCHASE_PRICE,
        VideoDatabase.KEY_RENTAL_PRICE,
        VideoDatabase.KEY_RATING_STYLE,
        VideoDatabase.KEY_RATING_SCORE,
        VideoDatabase.KEY_PRODUCTION_YEAR,
        VideoDatabase.KEY_COLUMN_DURATION,
        VideoDatabase.KEY_ACTION,
        SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID
    };
    return videoDatabase.getWordMatch(query, columns);
}
...

매니페스트 파일에서 콘텐츠 제공업체는 특수하게 처리됩니다. 인간이 활동이 태그되면 <provider> 이 제공자는 android:authorities 속성을 포함하여 시스템에 네임스페이스에 있습니다. 또한 android:exported 속성을 "true": Android 글로벌 검색에서 반환된 결과를 사용할 수 있습니다.

<provider android:name="com.example.android.tvleanback.VideoContentProvider"
    android:authorities="com.example.android.tvleanback"
    android:exported="true" />

추천 검색어 처리

앱에 추천 검색어 설정을 구성하는 res/xml/searchable.xml 파일

res/xml/searchable.xml 파일에 다음을 포함합니다. android:searchSuggestAuthority 속성을 사용하여 시스템에 있습니다. 이 값은 android:authorities 드림 <provider>의 속성 요소에 AndroidManifest.xml 요소를 포함합니다.

라벨도 포함 애플리케이션의 이름입니다 시스템 검색 설정에서는 열거 시 이 라벨을 사용합니다. 검색 가능한 앱

searchable.xml 파일 에 도 포함해야 합니다. android:searchSuggestIntentAction: 값 "android.intent.action.VIEW" 맞춤 추천을 제공하는 인텐트 작업을 정의합니다. 이는 인텐트와는 다릅니다. 검색어를 제공하는 액션을 지정할 수 있습니다. 추천을 위한 인텐트 작업을 선언하는 다른 방법은 인텐트 작업에 전달합니다.

앱은 인텐트 작업과 함께 <ph type="x-smartling-placeholder"></ph> android:searchSuggestIntentData 속성 이 것은 URI의 첫 번째 부분이며 콘텐츠에 대한 매핑으로, 해당 매핑 테이블의 모든 행에 공통된 URI 부분을 설명합니다. 있습니다. URI에서 각 행에 고유한 부분이 SUGGEST_COLUMN_INTENT_DATA_ID 필드로 설정됩니다. 열 식별 섹션에 설명된 대로 열을 확인합니다. 추천을 위한 인텐트 데이터를 선언하는 다른 방법은 다음을 참고하세요. 선언 인텐트 데이터를 참조하세요.

android:searchSuggestSelection=" ?" 속성은 전달된 값을 지정합니다. query()selection 매개변수로 지정 메서드를 사용하여 축소하도록 요청합니다. 물음표 (?) 값이 쿼리 텍스트로 바뀝니다.

마지막으로 android:includeInGlobalSearch 속성(값 "true") 예를 들면 다음과 같습니다. searchable.xml 파일:

<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:label="@string/search_label"
    android:hint="@string/search_hint"
    android:searchSettingsDescription="@string/settings_description"
    android:searchSuggestAuthority="com.example.android.tvleanback"
    android:searchSuggestIntentAction="android.intent.action.VIEW"
    android:searchSuggestIntentData="content://com.example.android.tvleanback/video_database_leanback"
    android:searchSuggestSelection=" ?"
    android:searchSuggestThreshold="1"
    android:includeInGlobalSearch="true">
</searchable>

검색어 처리

검색 대화상자에 앱의 열 중 하나의 값과 일치하는 단어가 있으면 즉시 열 식별 섹션에 설명된 대로 열을 지정하면 시스템이 ACTION_SEARCH 인텐트에 대한 응답입니다. 이를 처리하는 앱의 활동 인텐트가 저장소에서 값에 지정된 단어가 포함된 열을 검색하고 목록을 반환합니다. 콘텐츠 항목을 더 쉽게 찾을 수 있습니다. AndroidManifest.xml 파일에서 ACTION_SEARCH를 처리하는 활동 인텐트를 실행합니다.

...
  <activity
      android:name="com.example.android.tvleanback.DetailsActivity"
      android:exported="true">

      <!-- Receives the search request. -->
      <intent-filter>
          <action android:name="android.intent.action.SEARCH" />
          <!-- No category needed, because the Intent will specify this class component -->
      </intent-filter>

      <!-- Points to searchable meta data. -->
      <meta-data android:name="android.app.searchable"
          android:resource="@xml/searchable" />
  </activity>
...
  <!-- Provides search suggestions for keywords against video meta data. -->
  <provider android:name="com.example.android.tvleanback.VideoContentProvider"
      android:authorities="com.example.android.tvleanback"
      android:exported="true" />
...

활동은 또한 다음을 참조하여 검색 가능한 구성을 설명해야 합니다. searchable.xml 파일 전체 검색 대화상자를 사용하려면 다음 안내를 따르세요. 매니페스트는 검색어를 수신해야 하는 활동을 설명해야 합니다. 매니페스트는 또한 <provider> 설명 요소에 포함해야 합니다.searchable.xml

세부정보 화면에 표시되는 앱의 딥 링크

검색 처리 섹션과 SUGGEST_COLUMN_TEXT_1 매핑됨, SUGGEST_COLUMN_PRODUCTION_YEARSUGGEST_COLUMN_DURATION 필드 열 식별 섹션, 콘텐츠 시청 작업으로 연결되는 딥 링크가 세부정보 화면에 표시됩니다. 사용자가 검색결과를 선택할 때:

세부정보 화면에 표시되는 딥 링크

사용자가 앱의 링크를 선택하면 아래의 **Available On** 버튼으로 표시됩니다. 세부정보 화면, 시스템에서 ACTION_VIEW를 처리하는 활동을 시작함 (으)로 설정 android:searchSuggestIntentAction: 값이 "android.intent.action.VIEW" searchable.xml 파일을 참고하세요.

맞춤 인텐트를 설정하여 활동을 시작할 수도 있습니다. 이는 <ph type="x-smartling-placeholder"></ph> Leanback 샘플 앱 에서 자세한 내용을 확인하실 수 있습니다. 샘플 앱은 자체 LeanbackDetailsFragment를 실행하여 다음을 수행합니다. 선택한 미디어에 관한 세부정보를 표시합니다. 앱에서 미디어를 재생하는 활동을 실행합니다. 즉시 사용자가 클릭 한 두 번을 줄일 수 있습니다.

검색 동작

Android TV의 홈 화면과 앱 내부에서 검색이 가능합니다. 검색결과 이 두 경우에 대해 서로 다릅니다.

홈 화면에서 검색

사용자가 홈 화면에서 검색하면 첫 번째 결과가 항목 카드에 표시됩니다. 만약 앱이 콘텐츠를 재생할 수 있는 경우 각 앱의 링크가 카드 하단에 표시됩니다.

TV 검색결과 재생

프로그래매틱 방식으로 앱을 항목 카드에 배치할 수 없습니다. 앱의 검색결과는 동영상의 제목, 연도 및 기간과 검색 콘텐츠

카드 아래에 더 많은 검색결과가 표시될 수 있습니다. 알림을 보려면 사용자가 아래로 스크롤하세요 각 앱의 결과는 별도의 행에 표시됩니다. 사용자는 포드의 상태를 행 정렬 다음을 지원하는 앱 시청 작업이 먼저 나열됩니다.

TV 검색결과

앱에서 검색

사용자는 리모컨이나 리모컨에서 마이크를 시작하여 앱 내에서 검색을 시작할 수도 있습니다. 게임패드 컨트롤러입니다. 검색결과는 앱의 콘텐츠 상단에 단일 행으로 표시됩니다. 앱에서는 자체 글로벌 검색 제공자를 사용하여 검색결과를 생성합니다.

TV 인앱 검색결과

자세히 알아보기

TV 앱을 검색하는 방법을 자세히 알아보려면 다음을 참조하세요. 앱에 Android 검색 기능 통합 검색 기능 추가

SearchFragment로 인앱 검색 환경을 맞춤설정하는 방법에 관한 자세한 내용은 다음을 참고하세요. TV 앱 내에서 검색