Zezwalaj na wyszukiwanie aplikacji na telewizory

Android TV wykorzystuje interfejs wyszukiwania w Androidzie, aby pobierać dane z zainstalowanych aplikacji i dostarczać użytkownikowi wyniki wyszukiwania. Do tych wyników można dołączyć dane o treści aplikacji, aby użytkownik miał natychmiastowy dostęp do treści w aplikacji.

Twoja aplikacja musi udostępniać na Androidzie TV pola danych, z których Android TV może generować sugerowane wyniki wyszukiwania, gdy użytkownik będzie wpisywać znaki w oknie wyszukiwania. W tym celu aplikacja musi zaimplementować dostawcę treści, który będzie wyświetlać sugestie wraz z plikiem konfiguracji searchable.xml, który opisuje dostawcę treści i inne ważne informacje na temat Androida TV. Potrzebujesz też działania obsługującego zamiar wywoływany, gdy użytkownik wybierze sugerowany wynik wyszukiwania. Więcej informacji znajdziesz w artykule Dodawanie sugestii wyszukiwania niestandardowego. W tym przewodniku znajdziesz najważniejsze informacje dotyczące aplikacji na Androida TV.

Przed przeczytaniem tego przewodnika upewnij się, że znasz pojęcia opisane 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 SKAdNetwork.

Zidentyfikuj kolumny

Pole 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 do klasy, która uzyskuje dostęp do danych o treści. Informacje o tworzeniu klasy, która mapuje istniejące dane na wymagane pola, znajdziesz w sekcji Tworzenie tabeli sugestii.

Klasa SearchManager zawiera kilka kolumn dotyczących Androida TV. Niektóre z najważniejszych kolumn zostały opisane w tabeli poniżej.

Wartość Opis
SUGGEST_COLUMN_TEXT_1 Nazwa Twoich treści (wymagana)
SUGGEST_COLUMN_TEXT_2 opis tekstowy materiału;
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 (wymagany)
SUGGEST_COLUMN_DURATION Czas trwania multimediów w milisekundach (wymagany)

Struktura wyszukiwania wymaga tych kolumn:

Gdy wartości w tych kolumnach dotyczące Twoich treści są zgodne z wartościami tych samych treści pochodzących od innych dostawców znalezione na serwerach Google, system w widoku szczegółów treści udostępnia precyzyjny link do Twojej aplikacji wraz z linkami do aplikacji innych dostawców. Więcej informacji na ten temat znajdziesz w sekcji Precyzyjny link do aplikacji na ekranie z informacjami.

Klasa bazy danych aplikacji może zdefiniować 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;
...

Podczas tworzenia mapy z kolumn SearchManager na pola danych musisz też określić właściwość _ID, aby nadać każdemu wierszowi 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 pola SUGGEST_COLUMN_INTENT_DATA_ID. Jest to część identyfikatora URI wskazująca treść unikalną dla danych w tym wierszu – ostatnia część identyfikatora URI, która określa miejsce przechowywania treści. Pierwsza część identyfikatora URI, jeśli jest wspólna dla wszystkich wierszy w tabeli, jest ustawiona w pliku searchable.xml jako atrybut android:searchSuggestIntentData zgodnie z opisem w sekcji Obsługa sugestii wyszukiwania.

Jeśli pierwsza część identyfikatora URI jest inna w przypadku każdego wiersza w tabeli, zmapuj tę wartość w polu SUGGEST_COLUMN_INTENT_DATA. Gdy użytkownik wybiera tę treść, uruchamiana intencja dostarcza dane intencji pochodzące z kombinacji atrybutów SUGGEST_COLUMN_INTENT_DATA_ID i atrybutu android:searchSuggestIntentData lub wartości pola SUGGEST_COLUMN_INTENT_DATA.

Podaj dane sugestii wyszukiwania

Zaimplementuj dostawcę treści, aby zwracać sugestie wyszukiwanych haseł do okna wyszukiwania na Androidzie TV. System wysyła zapytanie do dostawcy treści o sugestie, wywołując metodę query() przy każdym pisaniu litery. W Twojej implementacji query() dostawca treści przeszukuje dane sugestii i zwraca Cursor, który wskazuje wiersze wyznaczone przez Ciebie na 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 szczególny sposób. Działanie to nie jest oznaczane jako aktywność, lecz jako <provider>. Dostawca umieszcza atrybut android:authorities, aby informować system o przestrzeni nazw dostawcy treści. Dodatkowo musisz ustawić atrybut android:exported na "true", aby wyszukiwanie globalne na Androidzie mogło korzystać ze zwróconych z niego wyników.

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

Obsługa sugestii wyszukiwania

Aby można było skonfigurować ustawienia sugestii wyszukiwania, aplikacja musi zawierać plik res/xml/searchable.xml.

W pliku res/xml/searchable.xml umieść 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 atrybucie android:authorities elementu <provider> w pliku AndroidManifest.xml.

Dołącz też etykietę, czyli nazwę aplikacji. Ustawienia wyszukiwania w systemie używają tej etykiety do numerowania aplikacji dostępnych do przeszukiwania.

Plik searchable.xml musi też zawierać element android:searchSuggestIntentAction z wartością "android.intent.action.VIEW", aby zdefiniować działanie intencji polegające na dostarczeniu niestandardowej sugestii. Różni się to od działania intencji polegającego na przesłaniu wyszukiwanego hasła, jak opisano w następnej sekcji. Inne sposoby deklarowania działania intencji w przypadku sugestii znajdziesz w sekcji Deklarowanie zamiaru działania.

Wraz z działaniem intencji aplikacja musi dostarczyć dane intencji wskazane przez Ciebie za pomocą atrybutu android:searchSuggestIntentData. Jest to pierwsza część identyfikatora URI wskazująca treść, która opisuje część identyfikatora URI wspólną dla wszystkich wierszy w tabeli mapowania tej treści. Unikalna dla każdego wiersza identyfikator URI jest tworzona w polu SUGGEST_COLUMN_INTENT_DATA_ID zgodnie z opisem w sekcji Identyfikowanie kolumn. Inne sposoby deklarowania danych dotyczących intencji na potrzeby sugestii znajdziesz w sekcji Deklarowanie danych intencji.

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

Musisz też dodać atrybut android:includeInGlobalSearch o wartości "true". Oto przykładowy plik 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 znajdzie się słowo, które pasuje do wartości w jednej z kolumn aplikacji (zgodnie z opisem w sekcji Identyfikowanie kolumn), system uruchomi intencję ACTION_SEARCH. Działanie w aplikacji, które obsługuje ten zamiar, przeszukuje repozytorium w poszukiwaniu kolumn zawierających podane słowo w wartościach i zwraca listę elementów treści z tymi kolumnami. W pliku AndroidManifest.xml wskażesz działanie, które obsługuje intencję ACTION_SEARCH, jak 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ę, którą można przeszukiwać z odniesieniem do pliku searchable.xml. Aby można było korzystać z globalnego okna wyszukiwania, plik manifestu musi zawierać opis aktywności, do której mają być wysyłane zapytania. Plik manifestu musi też opisywać element <provider> dokładnie w sposób opisany w pliku searchable.xml.

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

Jeśli skonfigurujesz konfigurację wyszukiwania zgodnie z opisem w sekcji Obsługa sugestii wyszukiwania oraz zamapujesz pola SUGGEST_COLUMN_TEXT_1, SUGGEST_COLUMN_PRODUCTION_YEAR i SUGGEST_COLUMN_DURATION zgodnie z opisem w sekcji Identyfikacja kolumn, na ekranie szczegółów, który wyświetli się, gdy użytkownik wybierze wynik wyszukiwania, na ekranie szczegółów pojawi się precyzyjny link do działania polegającego na oglądaniu Twoich treści:

Precyzyjny link na ekranie szczegółów

Gdy użytkownik wybierze link do Twojej aplikacji oznaczony przyciskiem **Dostępny w menu z informacjami**, system uruchomi działanie, które obsługuje obiekt ACTION_VIEW ustawiony jako android:searchSuggestIntentAction z wartością "android.intent.action.VIEW" w pliku searchable.xml.

Możesz też skonfigurować niestandardową intencję, aby uruchamiać aktywność. Możesz to sprawdzić w przykładowej aplikacji funkcji TalkBack. Pamiętaj, że przykładowa aplikacja uruchamia własny element LeanbackDetailsFragment, który wyświetla szczegóły wybranych multimediów. W aplikacjach uruchom działanie, które odtwarza multimedia, natychmiast, aby użytkownik mógł zapisać co najmniej jedno kliknięcie.

Sposób wyszukiwania

W Androidzie TV wyszukiwanie jest dostępne z poziomu ekranu głównego i z poziomu aplikacji. W tych 2 przypadkach wyniki wyszukiwania są różne.

Wyszukuj z ekranu głównego

Gdy użytkownik przeprowadza wyszukiwanie na ekranie głównym, na karcie jednostki wyświetla się pierwszy wynik. Jeśli istnieją aplikacje, które mogą odtworzyć te treści, u dołu karty pojawi się link do każdej z nich:

Odtwarzanie wyników wyszukiwania na telewizorze

Nie można automatycznie umieścić aplikacji na karcie elementu. Aby wyniki wyszukiwania aplikacji mogły być uwzględnione jako opcja odtwarzania, muszą być zgodne z tytułem, rokiem i czasem trwania przeszukiwanej treści.

Pod kartą może być dostępnych więcej wyników wyszukiwania. Aby je zobaczyć, użytkownik musi nacisnąć przycisk w dół na pilocie i przewinąć w dół. Wyniki dotyczące poszczególnych aplikacji pojawią się w osobnym wierszu. Nie możesz kontrolować kolejności wierszy. Na początku listy wymienione są aplikacje, które obsługują działania związane z oglądaniem.

Wyniki wyszukiwania TV

Szukaj z aplikacji

Użytkownik może też rozpocząć wyszukiwanie w aplikacji, uruchamiając mikrofon za pomocą pilota lub kontrolera pada do gier. Wyniki wyszukiwania są wyświetlane w jednym wierszu nad treścią aplikacji. Aplikacja generuje wyniki wyszukiwania, korzystając z własnego globalnego dostawcy wyszukiwania.

Wyniki wyszukiwania w TV w aplikacji

Więcej informacji

Więcej informacji o wyszukiwaniu aplikacji na telewizory znajdziesz w artykułach Integrowanie funkcji wyszukiwania na Androidzie z aplikacją oraz Dodawanie funkcji wyszukiwania.

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