Сделайте телевизионные приложения доступными для поиска

Android TV использует поисковый интерфейс Android для получения данных о контенте из установленных приложений и предоставления результатов поиска пользователю. Данные о контенте вашего приложения могут быть включены в эти результаты, чтобы предоставить пользователю мгновенный доступ к контенту в вашем приложении.

Ваше приложение должно предоставлять Android TV поля данных, на основе которых Android TV сможет генерировать предлагаемые результаты поиска по мере ввода пользователем символов в диалоговом окне поиска. Для этого ваше приложение должно реализовать Content Provider , который предоставляет предложения, а также файл конфигурации searchable.xml , описывающий Content Provider и другую важную информацию для Android TV. Вам также потребуется Activity, которая обрабатывает Intent, срабатывающий, когда пользователь выбирает предлагаемый результат поиска. Подробнее см. раздел «Добавление пользовательских предложений поиска» . Это руководство охватывает основные моменты, специфичные для приложений Android TV.

Перед прочтением этого руководства убедитесь, что вы знакомы с понятиями, изложенными в руководстве по API поиска . Также ознакомьтесь с разделом «Добавление функции поиска» .

Пример кода в этом руководстве взят из демонстрационного приложения Leanback .

Определите столбцы

SearchManager описывает ожидаемые поля данных, представляя их в виде столбцов локальной базы данных. Независимо от формата ваших данных, вы должны сопоставить ваши поля данных с этими столбцами, обычно в классе, который обращается к вашим данным контента. Информацию о создании класса, который сопоставляет ваши существующие данные с необходимыми полями, см. в разделе «Создание таблицы подсказок» .

Класс 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 Длительность вашего медиафайла в миллисекундах (обязательно)

Для работы поисковой системы необходимы следующие столбцы:

Когда значения этих столбцов для вашего контента совпадают со значениями для того же контента от других поставщиков, найденных серверами Google, система предоставляет прямую ссылку на ваше приложение в подробном представлении контента, а также ссылки на приложения других поставщиков. Подробнее об этом рассказывается в разделе «Прямая ссылка на ваше приложение на экране сведений» .

Класс базы данных вашего приложения может определять столбцы следующим образом:

Котлин

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
        ...
    }
    ...
}

Java

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 , чтобы присвоить каждой строке уникальный идентификатор.

Котлин

companion object {
    ....
    private fun buildColumnMap(): Map<String, 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)
        )
    }
}

Java

...
  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 как атрибут android:searchSuggestIntentData , как описано в разделе «Обработка поисковых подсказок» .

Если первая часть URI отличается для каждой строки в таблице, сопоставьте это значение с полем SUGGEST_COLUMN_INTENT_DATA . Когда пользователь выбирает этот контент, срабатывающее намерение предоставляет данные намерения, полученные из комбинации SUGGEST_COLUMN_INTENT_DATA_ID и либо атрибута android:searchSuggestIntentData , либо значения поля SUGGEST_COLUMN_INTENT_DATA .

Предоставьте данные для поисковых подсказок.

Реализуйте поставщика контента для возврата подсказок по поисковым запросам в диалоговое окно поиска Android TV. Система запрашивает у вашего поставщика контента подсказки, вызывая метод query() каждый раз, когда вводится буква. В вашей реализации метода query() ваш поставщик контента ищет данные подсказок и возвращает Cursor , указывающий на строки, которые вы указали для подсказок.

Котлин

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

Java

@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" , чтобы определить действие Intent для предоставления пользовательских подсказок. Это отличается от действия Intent для ввода поискового запроса , описанного в следующем разделе. Другие способы объявления действия Intent для подсказок см. в разделе «Объявление действия Intent» .

Помимо действия, определяемого намерением, ваше приложение должно предоставлять данные намерения, которые вы указываете с помощью атрибута android:searchSuggestIntentData . Это первая часть URI, указывающая на контент, которая описывает часть URI, общую для всех строк в таблице сопоставления для этого контента. Часть URI, уникальная для каждой строки, устанавливается с помощью поля SUGGEST_COLUMN_INTENT_DATA_ID , как описано в разделе « Идентификация столбцов» . Другие способы объявления данных намерения для подсказок см. в разделе «Объявление данных намерения» .

Атрибут android:searchSuggestSelection=" ?" задает значение, передаваемое в качестве параметра selection метода query() . Значение вопросительного знака ( ? ) заменяется текстом запроса.

Наконец, необходимо также добавить атрибут 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_YEAR и SUGGEST_COLUMN_DURATION , как описано в разделе «Идентификация столбцов» , то на экране сведений, который открывается при выборе пользователем результата поиска, появится прямая ссылка на действие отслеживания для вашего контента:

Прямая ссылка на экране с подробной информацией
Рисунок 1. Прямая ссылка на экране с подробной информацией.

Когда пользователь выбирает ссылку на ваше приложение, обозначенную кнопкой **Доступно в** на экране сведений, система запускает активность, которая обрабатывает параметр ACTION_VIEW , установленный как android:searchSuggestIntentAction со значением "android.intent.action.VIEW" в файле searchable.xml .

Вы также можете настроить собственный Intent для запуска вашей активности. Это показано в примере приложения Leanback . Обратите внимание, что в примере приложения запускается собственный LeanbackDetailsFragment для отображения подробной информации о выбранном медиафайле; в ваших приложениях запускайте активность, которая воспроизводит медиафайл немедленно, чтобы сэкономить пользователю один или два клика.

Поисковое поведение

Поиск доступен на Android TV как с главного экрана, так и из самого приложения. Результаты поиска в этих двух случаях будут разными.

Поиск с главного экрана

Когда пользователь выполняет поиск с главного экрана, первый результат отображается в виде карточки объекта. Если существуют приложения, способные воспроизводить контент, внизу карточки появляется ссылка на каждое из них:

Воспроизведение результатов поиска телепередач
Рисунок 2. Результаты поиска на главном экране.

Добавить приложение в карточку сущности программным способом невозможно. Чтобы приложение было включено в список вариантов воспроизведения, результаты поиска приложения должны соответствовать названию, году и продолжительности искомого контента.

Дополнительные результаты поиска могут отображаться ниже карточки. Чтобы их увидеть, пользователю необходимо нажать и прокрутить список на пульте дистанционного управления. Результаты для каждого приложения отображаются в отдельной строке. Вы не можете изменить порядок строк. Приложения, поддерживающие действия на часах, отображаются первыми.

Дополнительные результаты поиска телепередач
Рисунок 3. Просмотр дополнительных результатов поиска.

Найдите нужный пункт в приложении.

Пользователь также может начать поиск из вашего приложения, активировав микрофон с помощью пульта дистанционного управления или геймпада. Результаты поиска отображаются в одной строке поверх содержимого приложения. Ваше приложение генерирует результаты поиска, используя собственный глобальный поставщик поиска .

Результаты поиска по ТВ в приложении
Рисунок 4. Результаты поиска в приложении.

Узнать больше

Чтобы узнать больше о поиске в приложении для телевизора, прочтите статьи «Интеграция функций поиска Android в ваше приложение» и «Добавление функциональности поиска» .

Для получения дополнительной информации о том, как настроить поиск внутри приложения с помощью SearchFragment , прочитайте раздел «Поиск в приложениях для ТВ» .