Zezwalaj na wyszukiwanie aplikacji na telewizory

Android TV wykorzystuje interfejs wyszukiwania Androida do pobierania danych o treściach z zainstalowanych aplikacji i dostarczania użytkownikowi wyników wyszukiwania. W tych wynikach możesz uwzględniać dane o treści aplikacji, aby zapewnić użytkownikom natychmiastowy dostęp do treści w Twojej aplikacji.

Aplikacja musi udostępniać na Androidzie TV pola danych, na podstawie których Android TV może generować sugerowane wyniki wyszukiwania, gdy użytkownik wpisuje znaki w oknie wyszukiwania. W tym celu musisz wdrożyć w aplikacji dostawcę treści, który będzie wyświetlał sugestie wraz z plikiem konfiguracyjnym searchable.xml opisującym dostawcę treści i inne ważne informacje dotyczące 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 niestandardowych sugestii wyszukiwania. W tym przewodniku omawiamy główne zagadnienia związane z aplikacjami na Androida TV.

Zanim przeczytasz ten przewodnik, upewnij się, że znasz pojęcia wyjaśnione 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śl kolumny

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

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

Wartość Opis
SUGGEST_COLUMN_TEXT_1 Nazwa treści (wymagana)
SUGGEST_COLUMN_TEXT_2 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 Rozdzielczość – szerokość 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 Twoich treści odpowiadają wartościom dotyczącym tych samych treści pochodzących od innych dostawców znalezionych przez serwery Google, system poda w widoku szczegółów precyzyjny link do Twojej aplikacji wraz z linkami do aplikacji innych dostawców. Więcej informacji znajdziesz w sekcji Precyzyjny link do aplikacji na ekranie szczegółów.

Klasa bazy danych aplikacji może definiować kolumny w następujący 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 na podstawie kolumn SearchManager na pola danych musisz też określić _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 danym 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 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 w każdym wierszu tabeli, zmapuj tę wartość na pole SUGGEST_COLUMN_INTENT_DATA. Gdy użytkownik wybiera te treści, uruchamiająca się intencja dostarcza danych o zamiarach pochodzących z kombinacji atrybutu 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ł w oknie wyszukiwania na Androidzie TV. System wysyła zapytania do dostawcy treści w poszukiwaniu sugestii, wywołując metodę query() przy każdym pisaniu litery. W Twojej implementacji funkcji query() dostawca treści przeszukuje dane sugestii i zwraca element Cursor wskazujący wiersze, które wskażesz na potrzeby sugestii.

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. Opis nie jest oznaczany jako aktywność, ale opisany jako <provider>. Dostawca zawiera atrybut android:authorities, który informuje system o przestrzeni nazw dostawcy treści. Dodatkowo musisz ustawić atrybut android:exported na "true", aby wyszukiwanie globalne Androida mogło używać zwróconych 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 wskazać systemowi przestrzeń 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ę, która jest nazwą aplikacji. Ustawienia wyszukiwania w systemie używają tej etykiety przy wyliczaniu aplikacji dostępnych do przeszukiwania.

Plik searchable.xml musi też zawierać obiekt android:searchSuggestIntentAction z wartością "android.intent.action.VIEW", aby zdefiniować działanie intencji przekazywania niestandardowej sugestii. Różni się to od intencji polegającej na przesłaniu wyszukiwanego hasła, co zostało opisane w następnej sekcji. Inne sposoby deklarowania działania intencji na potrzeby sugestii znajdziesz w artykule Deklarowanie działania intencji.

Wraz z działaniem intencji aplikacja musi dostarczać dane intencji, które określasz za pomocą atrybutu android:searchSuggestIntentData. Jest to pierwsza część identyfikatora URI wskazująca treść. Opisuje część identyfikatora URI wspólną dla wszystkich wierszy w tabeli mapowania dotyczącej danej treści. Unikalna część identyfikatora URI, która jest unikalna dla każdego wiersza, jest tworzona w polu SUGGEST_COLUMN_INTENT_DATA_ID zgodnie z opisem w sekcji Identyfikowanie kolumn. Inne sposoby deklarowania danych 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.

Na koniec 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 słów

Gdy w oknie wyszukiwania pojawi się słowo, które odpowiada 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 tę intencję, wyszukuje w repozytorium kolumny z danym słowem w wartościach i zwraca listę elementów treści z tymi kolumnami. W pliku AndroidManifest.xml określasz 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ę dostępną do wyszukiwania w odniesieniu do pliku searchable.xml. Aby skorzystać z okna wyszukiwania globalnego, plik manifestu musi zawierać informacje o aktywności, która ma otrzymywać wyszukiwane hasła. Plik manifestu musi też opisywać element <provider> dokładnie w sposób opisany w pliku searchable.xml.

Precyzyjny link do aplikacji na ekranie szczegółów

Jeśli konfiguracja wyszukiwania została skonfigurowana zgodnie z opisem w sekcji Obsługa sugestii dotyczących wyszukiwania oraz zmapowane pola SUGGEST_COLUMN_TEXT_1, SUGGEST_COLUMN_PRODUCTION_YEAR i SUGGEST_COLUMN_DURATION zgodnie z opisem w sekcji Identyfikowanie kolumn, precyzyjny link do działania dotyczącego oglądania Twoich treści pojawi się na ekranie szczegółów, który wyświetli się, gdy użytkownik wybierze wynik wyszukiwania:

Precyzyjny link na ekranie szczegółów

Gdy użytkownik kliknie link do aplikacji oznaczony przyciskiem **Dostępny w** na ekranie szczegółów, system uruchomi aktywność, która 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ę uruchomioną przez Twoją aktywność. Zostało to pokazane w przykładowej aplikacji Leanback. Pamiętaj, że przykładowa aplikacja uruchamia własną właściwość LeanbackDetailsFragment, która wyświetla szczegóły wybranych multimediów. W aplikacji uruchom działanie, które spowoduje natychmiastowe odtworzenie multimediów, aby zapisać użytkownika jeszcze raz lub dwa kliknięcia.

Sposób wyszukiwania informacji

W Androidzie TV wyszukiwanie jest dostępne na ekranie głównym oraz w aplikacji. W tych 2 przypadkach wyniki wyszukiwania różnią się.

Wyszukuj z ekranu głównego

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

Odtwarzanie wyników wyszukiwania programów telewizyjnych

Nie można automatycznie umieszczać aplikacji na karcie jednostki. Aby aplikacja mogła pojawić się jako opcja odtwarzania, wyniki wyszukiwania muszą pasować do tytułu, roku i czasu trwania szukanych treści.

Pod kartą może być dostępnych więcej wyników wyszukiwania. Aby je zobaczyć, użytkownik musi nacisnąć przycisk na pilocie i przewinąć w dół. Wyniki dla poszczególnych aplikacji pojawią się w osobnym wierszu. Nie da się kontrolować kolejności wierszy. Aplikacje, które obsługują działania zegarka, są wymienione na początku.

Wyniki wyszukiwania programów telewizyjnych

Wyszukiwanie z poziomu aplikacji

Użytkownik może też rozpocząć wyszukiwanie z poziomu 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 usług własnego globalnego dostawcy wyszukiwania.

Wyniki wyszukiwania w aplikacji TV

Więcej informacji

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

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