Zezwalaj na wyszukiwanie aplikacji na telewizory

Android TV używa interfejsu wyszukiwania Androida do pobierania danych o treściach z zainstalowanych aplikacji i wyświetlania wyników wyszukiwania użytkownikowi. Dane o treściach z Twojej aplikacji mogą być uwzględniane w tych wynikach, aby użytkownik miał natychmiastowy dostęp do treści w Twojej aplikacji.

Twoja aplikacja musi udostępniać Androidowi TV pola danych, na podstawie których Android TV może generować sugerowane wyniki wyszukiwania, gdy użytkownik wpisuje znaki w oknie wyszukiwania. Aby to zrobić, Twoja aplikacja musi zaimplementować dostawcę treści, który udostępnia sugestie wraz z plikiem konfiguracyjnym searchable.xml opisującym dostawcę treści i inne ważne informacje dla Androida TV. Potrzebujesz też aktywności, która obsługuje intencję uruchamianą, gdy użytkownik wybierze sugerowany wynik wyszukiwania. Więcej informacji znajdziesz w artykule Dodawanie niestandardowych sugestii wyszukiwania. Ten przewodnik zawiera najważniejsze informacje dotyczące aplikacji na Androida TV.

Zanim przeczytasz ten przewodnik, zapoznaj się z pojęciami wyjaśnionymi w przewodniku po interfejsie Search API. Zapoznaj się też z artykułem Dodawanie funkcji wyszukiwania.

Przykładowy kod w tym przewodniku pochodzi z przykładowej aplikacji Leanback .

Określanie kolumn

Klasa SearchManager opisuje pola danych, których oczekuje, przedstawiając je jako kolumny lokalnej bazy danych. Niezależnie od formatu danych musisz zmapować pola danych na te kolumny, zwykle w klasie, która uzyskuje dostęp do danych o treściach. Informacje o tworzeniu klasy, która mapuje istniejące dane na wymagane pola, znajdziesz w artykule Tworzenie tabeli sugestii.

Klasa SearchManager zawiera kilka kolumn dla Androida TV. Niektóre z ważniejszych kolumn zostały opisane w tabeli poniżej.

Wartość Opis
SUGGEST_COLUMN_TEXT_1 Nazwa treści (wymagane)
SUGGEST_COLUMN_TEXT_2 Tekstowy opis treści
SUGGEST_COLUMN_RESULT_CARD_IMAGE Obraz, plakat lub okładka treści
SUGGEST_COLUMN_CONTENT_TYPE Typ MIME multimediów
SUGGEST_COLUMN_VIDEO_WIDTH Szerokość rozdzielczości multimediów
SUGGEST_COLUMN_VIDEO_HEIGHT Wysokość rozdzielczości multimediów
SUGGEST_COLUMN_PRODUCTION_YEAR Rok produkcji treści (wymagane)
SUGGEST_COLUMN_DURATION Czas trwania multimediów w milisekundach (wymagane)

Platforma wyszukiwania wymaga tych kolumn:

Gdy wartości tych kolumn dla Twoich treści są zgodne z wartościami tych samych treści od innych dostawców znalezionych przez serwery Google, system udostępnia precyzyjny link do Twojej aplikacji w widoku szczegółów treści oraz linki do aplikacji innych dostawców. Więcej informacji znajdziesz w sekcji Precyzyjny link do Twojej aplikacji na ekranie szczegółów.

Klasa bazy danych Twojej aplikacji może definiować kolumny w ten sposób:

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

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

Gdy tworzysz mapę z kolumn SearchManager na pola danych, musisz też określić _ID, aby każdy wiersz miał unikalny identyfikator.

Kotlin

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

W poprzednim przykładzie zwróć uwagę na mapowanie na pole SUGGEST_COLUMN_INTENT_DATA_ID. Jest to część identyfikatora URI, która wskazuje treści unikalne dla danych w tym wierszu – ostatnia część identyfikatora URI, która opisuje, gdzie są przechowywane treści. Pierwsza część identyfikatora URI, jeśli jest wspólna dla wszystkich wierszy w tabeli, jest ustawiana w pliku searchable.xml jako atrybut android:searchSuggestIntentData, jak opisano w sekcji Obsługa sugestii wyszukiwania.

Jeśli pierwsza część identyfikatora URI jest inna dla każdego wiersza w tabeli, zmapuj tę wartość na pole SUGGEST_COLUMN_INTENT_DATA. Gdy użytkownik wybierze te treści, uruchomiona intencja udostępni dane intencji z połączenia SUGGEST_COLUMN_INTENT_DATA_ID oraz atrybutu android:searchSuggestIntentData lub wartości pola SUGGEST_COLUMN_INTENT_DATA.

Udostępnianie danych sugestii wyszukiwania

Zaimplementuj dostawcę treści , aby zwracać sugestie wyszukiwanych haseł do okna wyszukiwania Androida TV. System wysyła zapytania do dostawcy treści o sugestie, wywołując metodę query() za każdym razem, gdy użytkownik wpisze literę. W implementacji query() dostawca treści przeszukuje dane sugestii i zwraca Cursor, który wskazuje wiersze wyznaczone jako sugestie.

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

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

W pliku manifestu dostawca treści jest traktowany w specjalny sposób. Zamiast być oznaczony jako aktywność, jest opisany jako <provider>. Dostawca zawiera atrybut android:authorities, który informuje system o przestrzeni nazw dostawcy treści. Musisz też ustawić jego atrybut android:exported na wartość "true", aby wyszukiwanie globalne Androida mogło korzystać z zwracanych przez niego wyników.

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

Obsługa sugestii wyszukiwania

Twoja aplikacja musi zawierać plik res/xml/searchable.xml, aby skonfigurować ustawienia sugestii wyszukiwania.

W pliku res/xml/searchable.xml uwzględnij atrybut android:searchSuggestAuthority, aby poinformować system o przestrzeni nazw dostawcy treści. Musi on być zgodny z wartością ciągu znaków określoną w android:authorities atrybucie elementu <provider> w pliku AndroidManifest.xml.

Uwzględnij też etykietę, czyli nazwę aplikacji. Ustawienia wyszukiwania systemowego używają tej etykiety podczas wyliczania aplikacji, w których można wyszukiwać.

Plik searchable.xml musi też zawierać atrybut android:searchSuggestIntentAction z wartością "android.intent.action.VIEW" , aby zdefiniować działanie intencji w przypadku udostępniania niestandardowej sugestii. Różni się to od działania intencji w przypadku udostępniania wyszukiwanego hasła, jak opisano w następnej sekcji. Inne sposoby deklarowania działania intencji w przypadku sugestii, zobacz Deklarowanie działania intencji.

Oprócz działania intencji Twoja aplikacja musi udostępniać dane intencji, które określasz za pomocą atrybutu android:searchSuggestIntentData. Jest to pierwsza część identyfikatora URI, która wskazuje treści i opisuje część identyfikatora URI wspólnego dla wszystkich wierszy w tabeli mapowania tych treści. Część identyfikatora URI, która jest unikalna dla każdego wiersza, jest określana za pomocą pola SUGGEST_COLUMN_INTENT_DATA_ID, jak opisano w sekcji Określanie kolumn. Inne sposoby deklarowania danych intencji w przypadku sugestii znajdziesz w artykule Deklarowanie danych intencji.

Atrybut android:searchSuggestSelection=" ?" określa wartość przekazywaną jako parametr selection metody query(). Znak zapytania (?) jest zastępowany tekstem zapytania.

Na koniec musisz też uwzględnić atrybut android:includeInGlobalSearch z wartością "true". Oto przykład pliku 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>

Obsługa wyszukiwanych haseł

Gdy tylko w oknie wyszukiwania pojawi się słowo, które pasuje do wartości w jednej z kolumn Twojej aplikacji, jak opisano w sekcji Określanie kolumn, system uruchomi intencję ACTION_SEARCH. Aktywność w Twojej aplikacji, która obsługuje tę intencję, przeszukuje repozytorium pod kątem kolumn z danym słowem w ich wartościach i zwraca listę elementów treści z tymi kolumnami. W pliku AndroidManifest.xml wyznacz aktywność, która obsługuje intencję ACTION_SEARCH, jak pokazano w tym przykładzie:

...
  <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" />
...

Aktywność musi też opisywać konfigurację wyszukiwania za pomocą odniesienia do pliku searchable.xml. Aby można było używać okna wyszukiwania globalnego, manifest musi opisywać, która aktywność ma otrzymywać zapytania. Manifest musi też opisywać element <provider> dokładnie tak, jak jest on opisany w pliku searchable.xml.

Precyzyjny link do Twojej aplikacji na ekranie szczegółów

Jeśli skonfigurujesz wyszukiwanie zgodnie z opisem w sekcji Obsługa sugestii i zmapujesz pola SUGGEST_COLUMN_TEXT_1, SUGGEST_COLUMN_PRODUCTION_YEAR, i SUGGEST_COLUMN_DURATION zgodnie z opisem w sekcji Określanie kolumn, na ekranie szczegółów, który pojawi się, gdy użytkownik wybierze wynik wyszukiwania, pojawi się precyzyjny link do działania oglądania Twoich treści:

Precyzyjny link na ekranie szczegółów
Rysunek 1. Precyzyjny link na ekranie szczegółów.

Gdy użytkownik wybierze link do Twojej aplikacji, oznaczony przyciskiem **Dostępne w** na ekranie szczegółów, system uruchomi aktywność, która obsługuje ACTION_VIEW ustawioną jako android:searchSuggestIntentAction z wartością "android.intent.action.VIEW" w pliku searchable.xml.

Możesz też skonfigurować niestandardową intencję, aby uruchamiać aktywność. Pokazujemy to w przykładowej aplikacji Leanback . Pamiętaj, że przykładowa aplikacja uruchamia własny fragment LeanbackDetailsFragment, aby wyświetlić szczegóły wybranych multimediów. W swoich aplikacjach uruchamiaj aktywność, która odtwarza multimedia, aby użytkownik nie musiał klikać dodatkowo.

Zachowania związane z wyszukiwaniem

Wyszukiwanie jest dostępne na Androidzie TV na ekranie głównym i w Twojej aplikacji. Wyniki wyszukiwania są różne w tych 2 przypadkach.

Wyszukiwanie z ekranu głównego

Gdy użytkownik wyszukuje z ekranu głównego, pierwszy wynik pojawia się na karcie encji. Jeśli są aplikacje, które mogą odtwarzać treści, u dołu karty pojawi się link do każdej z nich:

Odtwarzanie wyników wyszukiwania w telewizji
Rysunek 2. Wynik wyszukiwania na ekranie głównym.

Nie możesz programowo umieścić aplikacji na karcie encji. Aby aplikacja była uwzględniana jako opcja odtwarzania, wyniki wyszukiwania muszą pasować do tytułu, roku i czasu trwania wyszukiwanych treści.

Pod kartą mogą być dostępne dodatkowe wyniki wyszukiwania. Aby je zobaczyć, użytkownik musi nacisnąć przycisk w dół na pilocie i przewinąć. Wyniki dla każdej aplikacji pojawiają się w osobnym wierszu. Nie możesz kontrolować kolejności wierszy. Najpierw wyświetlają się aplikacje, które obsługują działania oglądania.

Dodatkowe wyniki wyszukiwania w telewizji
Rysunek 3. Wyświetlanie dodatkowych wyników wyszukiwania.

Wyszukiwanie z aplikacji

Użytkownik może też rozpocząć wyszukiwanie z poziomu aplikacji, uruchamiając mikrofon za pomocą pilota lub gamepada. Wyniki wyszukiwania są wyświetlane w jednym wierszu u góry treści aplikacji. Twoja aplikacja generuje wyniki wyszukiwania za pomocą własnego dostawcy wyszukiwania globalnego.

Wyniki wyszukiwania w aplikacji na telewizorze
Rysunek 4. Wyniki wyszukiwania w aplikacji.

Więcej informacji

Więcej informacji o wyszukiwaniu w aplikacji na telewizor znajdziesz w artykułach Integrowanie funkcji wyszukiwania Androida z aplikacją i Dodawanie funkcji wyszukiwania.

Więcej informacji o dostosowywaniu wyszukiwania w aplikacji za pomocą SearchFragment znajdziesz w artykule Wyszukiwanie w aplikacjach na telewizor.