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

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

Ваше приложение должно предоставить Android TV поля данных, из которых Android TV может генерировать предлагаемые результаты поиска по мере того, как пользователь вводит символы в диалоговом окне поиска. Для этого в вашем приложении должен быть реализован поставщик контента , который предоставляет предложения вместе с файлом конфигурации searchable.xml , который описывает поставщика контента и другую важную информацию для Android TV. Вам также понадобится действие, которое обрабатывает намерение, которое срабатывает, когда пользователь выбирает предложенный результат поиска. Более подробную информацию см. в разделе Добавление предложений пользовательского поиска . В этом руководстве рассматриваются основные моменты, относящиеся к приложениям 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
        ...
    }
    ...
}

Ява

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

Ява

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

Ява

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

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

Глубокая ссылка на экране сведений

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

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

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

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

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

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

Воспроизведение результатов поиска ТВ

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

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

Результаты поиска ТВ

Поиск из вашего приложения

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

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

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

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

Дополнительные сведения о том, как настроить поиск в приложении с помощью SearchFragment , см. в статье Поиск в телевизионных приложениях .