Membuat aplikasi TV dapat ditelusuri

Android TV menggunakan antarmuka penelusuran Android untuk mengambil data konten dari aplikasi terinstal dan menampilkan hasil penelusuran kepada pengguna. Aplikasi Anda data konten dapat disertakan dengan hasil ini untuk memberi pengguna akses instan ke konten di aplikasi Anda.

Aplikasi Anda harus menyediakan kolom data ke Android TV yang dapat digunakan Android TV untuk membuat penelusuran yang disarankan hasil saat pengguna memasukkan karakter dalam dialog penelusuran. Untuk melakukannya, aplikasi Anda harus mengimplementasikan sebuah Penyedia Konten yang menayangkan saran beserta File konfigurasi searchable.xml yang mendeskripsikan konten dan informasi penting lainnya untuk Android TV. Anda juga memerlukan aktivitas yang menangani yang diaktifkan saat pengguna memilih hasil penelusuran yang disarankan. Sebagai detail selengkapnya, lihat Menambahkan saran penelusuran kustom. Panduan ini membahas poin-poin utama khusus terkait aplikasi Android TV.

Sebelum membaca panduan ini, pastikan Anda telah memahami konsep yang dijelaskan dalam Panduan Search API. Selain itu, tinjau Menambahkan fungsi penelusuran.

Kode contoh dalam panduan ini berasal dari Aplikasi contoh Leanback kami.

Mengidentifikasi kolom

SearchManager menjelaskan kolom data yang diharapkan dengan mewakilinya sebagai kolom dari {i>database<i} lokal. Terlepas dari format data, Anda harus memetakan {i>field<i} data Anda untuk kolom ini, biasanya di kelas yang mengakses data konten Anda. Untuk informasi tentang membangun class yang memetakan data yang ada ke kolom yang wajib diisi, lihat Membuat tabel saran.

Class SearchManager menyertakan beberapa kolom untuk Android TV. Beberapa kolom yang lebih penting dijelaskan dalam tabel berikut.

Nilai Deskripsi
SUGGEST_COLUMN_TEXT_1 Nama konten Anda (wajib diisi)
SUGGEST_COLUMN_TEXT_2 Deskripsi teks dari konten Anda
SUGGEST_COLUMN_RESULT_CARD_IMAGE Gambar, poster, atau sampul untuk konten Anda
SUGGEST_COLUMN_CONTENT_TYPE Jenis MIME media Anda
SUGGEST_COLUMN_VIDEO_WIDTH Lebar resolusi media Anda
SUGGEST_COLUMN_VIDEO_HEIGHT Tinggi resolusi media Anda
SUGGEST_COLUMN_PRODUCTION_YEAR Tahun produksi konten Anda (wajib)
SUGGEST_COLUMN_DURATION Durasi media Anda dalam milidetik (wajib)

Framework penelusuran membutuhkan kolom berikut:

Ketika nilai kolom ini untuk konten Anda cocok dengan nilai untuk konten yang sama dari yang ditemukan oleh server Google, sistem tersebut menyediakan deep link ke aplikasi Anda secara mendetail melihat konten, beserta link ke aplikasi penyedia lain. Hal ini dibahas lebih lanjut dalam di bagian Deep link ke aplikasi Anda di layar detail.

Class database aplikasi Anda mungkin menentukan kolom sebagai berikut:

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

Saat membuat peta dari kolom SearchManager ke kolom data, Anda juga harus menentukan _ID untuk memberikan ID unik ke setiap baris.

Kotlin

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

Pada contoh sebelumnya, perhatikan pemetaan ke SUGGEST_COLUMN_INTENT_DATA_ID kolom tersebut. Ini adalah bagian dari URI yang menunjuk ke konten yang unik bagi data dalam —bagian terakhir URI, yang menjelaskan tempat konten disimpan. Bagian pertama dari URI, ketika umum untuk semua baris dalam tabel, diatur dalam searchable.xml sebagai android:searchSuggestIntentData, seperti yang dijelaskan dalam Bagian Menangani saran penelusuran.

Jika bagian pertama URI berbeda untuk setiap baris dalam tabel, petakan nilai tersebut dengan kolom SUGGEST_COLUMN_INTENT_DATA. Bila pengguna memilih konten ini, intent yang diaktifkan akan memberikan data intent dari kombinasi dari SUGGEST_COLUMN_INTENT_DATA_ID dan atribut android:searchSuggestIntentData atau Nilai kolom SUGGEST_COLUMN_INTENT_DATA.

Memberikan data saran penelusuran

Mengimplementasikan Penyedia Konten untuk mengembalikan saran istilah penelusuran ke dialog penelusuran Android TV. Sistem mengkueri konten Anda untuk mendapatkan saran dengan memanggil metode query() setiap kali huruf ketika diketik. Dalam implementasi query(), konten Anda penyedia menelusuri data saran Anda dan menampilkan Cursor yang mengarah ke baris yang telah Anda tentukan untuk saran.

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

Dalam file manifes Anda, penyedia konten akan menerima perlakuan khusus. Alih-alih menjadi ditandai sebagai suatu aktivitas, hal itu digambarkan sebagai <provider> Tujuan penyedia menyertakan atribut android:authorities untuk memberi tahu sistem tentang namespace penyedia konten Anda. Selain itu, Anda harus menetapkan atribut android:exported miliknya ke "true" sehingga penelusuran global Android dapat menggunakan hasil yang ditampilkan dari penelusuran tersebut.

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

Menangani saran penelusuran

Aplikasi Anda harus menyertakan res/xml/searchable.xml untuk mengonfigurasi setelan saran penelusuran.

Dalam file res/xml/searchable.xml, sertakan android:searchSuggestAuthority untuk memberi tahu sistem tentang namespace penyedia konten lainnya. Ini harus cocok dengan nilai string yang Anda tentukan dalam kolom android:authorities dari <provider> dalam file AndroidManifest.xml Anda.

Sertakan juga label, yang merupakan nama aplikasi. Setelan penelusuran sistem menggunakan label ini saat menghitung aplikasi yang dapat ditelusuri.

File searchable.xml juga harus menyertakan android:searchSuggestIntentAction dengan nilai "android.intent.action.VIEW" mendefinisikan tindakan intent untuk memberikan saran khusus. Hal ini berbeda dengan maksud tindakan untuk memberikan istilah penelusuran, seperti yang dijelaskan di bagian berikut. Untuk cara lain mendeklarasikan tindakan intent untuk saran, lihat Mendeklarasikan tindakan intent.

Bersamaan dengan tindakan intent, aplikasi Anda harus menyediakan data intent, yang Anda tetapkan dengan android:searchSuggestIntentData. Ini adalah bagian pertama dari URI yang menunjuk ke konten, yang menjelaskan bagian URI yang umum untuk semua baris dalam tabel pemetaan untuk saat ini. Bagian URI yang unik untuk setiap baris dibuat dengan kolom SUGGEST_COLUMN_INTENT_DATA_ID, seperti yang dijelaskan di bagian Mengidentifikasi kolom. Untuk cara lain mendeklarasikan data intent untuk saran, lihat Mendeklarasikan data intent.

Atribut android:searchSuggestSelection=" ?" menentukan nilai yang diteruskan sebagai parameter selection dari query() . Nilai tanda tanya (?) diganti dengan teks kueri.

Terakhir, Anda juga harus menyertakan android:includeInGlobalSearch dengan nilai "true". Berikut contohnya File 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>

Menangani istilah penelusuran

Segera setelah dialog penelusuran memiliki kata yang cocok dengan nilai di salah satu kolom aplikasi Anda, seperti yang dijelaskan di bagian Identifikasi kolom, sistem akan mengaktifkan ACTION_SEARCH. Aktivitas di aplikasi Anda yang menangani hal tersebut intent akan menelusuri repositori untuk kolom dengan kata yang diberikan dalam nilainya dan menampilkan daftar item konten dengan kolom tersebut. Di file AndroidManifest.xml, Anda menetapkan aktivitas yang menangani ACTION_SEARCH seperti yang ditunjukkan dalam contoh berikut:

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

Aktivitas ini juga harus mendeskripsikan konfigurasi yang dapat ditelusuri dengan referensi ke File searchable.xml. Untuk menggunakan dialog penelusuran global, manifes harus menjelaskan aktivitas mana yang akan menerima kueri penelusuran. Manifes juga harus menjelaskan <provider> , persis seperti yang dijelaskan dalam file searchable.xml.

Deep link ke aplikasi Anda di layar detail

Jika Anda telah menyiapkan konfigurasi penelusuran seperti yang dijelaskan di bagian Menangani penelusuran saran dan memetakan SUGGEST_COLUMN_TEXT_1, SUGGEST_COLUMN_PRODUCTION_YEAR, dan SUGGEST_COLUMN_DURATION seperti yang dijelaskan di bagian Identify columns, deep link ke tindakan menonton untuk konten Anda muncul di layar detail yang diluncurkan saat pengguna memilih hasil penelusuran:

Deep link dalam layar detail

Saat pengguna memilih tautan untuk aplikasi Anda, diidentifikasi dengan tombol **Available On** di layar detail, sistem akan meluncurkan aktivitas yang menangani ACTION_VIEW tetapkan sebagai android:searchSuggestIntentAction dengan nilai "android.intent.action.VIEW" dalam file searchable.xml.

Anda juga dapat menyiapkan intent kustom untuk meluncurkan aktivitas. Hal ini ditunjukkan dalam Aplikasi contoh Leanback kami. Perhatikan bahwa aplikasi contoh meluncurkan LeanbackDetailsFragment-nya sendiri untuk menampilkan detail untuk media yang dipilih; di aplikasi Anda, luncurkan aktivitas yang memutar media segera untuk menyimpan pengguna sekali lagi.

Perilaku penelusuran

Penelusuran tersedia di Android TV dari layar utama dan dari dalam aplikasi Anda. Hasil penelusuran berbeda untuk kedua kasus ini.

Penelusuran dari layar utama

Saat pengguna melakukan penelusuran dari layar utama, hasil pertama muncul di kartu entitas. Jika ada aplikasi yang dapat memutar konten tersebut, link ke masing-masing akan muncul di bagian bawah kartu:

Pemutaran Hasil Penelusuran TV

Anda tidak dapat menempatkan aplikasi secara terprogram ke dalam kartu entity. Untuk disertakan sebagai pilihan pemutaran, hasil penelusuran aplikasi harus sesuai dengan judul, tahun, dan durasi konten yang ditelusuri.

Hasil penelusuran lainnya mungkin tersedia di bawah kartu. Untuk melihatnya, pengguna harus menekan tombol remote dan scroll ke bawah. Hasil untuk setiap aplikasi muncul di baris terpisah. Anda tidak dapat mengontrol pengurutan baris. Aplikasi yang mendukung tindakan menonton dicantumkan terlebih dahulu.

Hasil Penelusuran TV

Penelusuran dari aplikasi Anda

Pengguna juga dapat memulai penelusuran dari dalam aplikasi Anda dengan memulai mikrofon dari remote atau pengontrol game pad. Hasil penelusuran ditampilkan dalam satu baris di atas konten aplikasi. Aplikasi Anda menyusun hasil penelusuran menggunakan penyedia penelusuran global-nya sendiri.

Hasil Penelusuran dalam Aplikasi TV

Pelajari lebih lanjut

Untuk mempelajari lebih lanjut cara menelusuri aplikasi TV, baca Integrasikan fitur penelusuran Android ke dalam aplikasi Anda dan Menambahkan fungsi penelusuran.

Untuk informasi selengkapnya tentang cara menyesuaikan pengalaman penelusuran dalam aplikasi dengan SearchFragment, baca Menelusuri di dalam aplikasi TV.