Google berkomitmen untuk mendorong terwujudnya keadilan ras bagi komunitas Kulit Hitam. Lihat caranya.

Menjadikan aplikasi TV dapat ditelusuri

Android TV menggunakan antarmuka penelusuran Android untuk mengambil data konten dari aplikasi yang terinstal dan menampilkan hasil penelusuran kepada pengguna. Data konten aplikasi Anda dapat disertakan bersama hasil tersebut, untuk memberikan pengguna akses langsung ke konten di aplikasi Anda.

Aplikasi Anda harus menyediakan kolom data yang dapat digunakan oleh Android TV untuk menghasilkan hasil penelusuran yang disarankan saat pengguna memasukkan karakter dalam dialog penelusuran. Untuk melakukannya, aplikasi Anda harus mengimplementasikan Penyedia Konten yang menyajikan saran bersama dengan file konfigurasi searchable.xml yang mendeskripsikan penyedia konten dan informasi penting lainnya untuk Android TV. Anda juga memerlukan aktivitas yang menangani intent yang aktif saat pengguna memilih hasil penelusuran yang disarankan. Semua ini dijelaskan lebih detail di Menambahkan saran kustom. Berikut ini dideskripsikan poin utama untuk aplikasi Android TV.

Tutorial ini dibuat berdasarkan pengetahuan Anda dalam menggunakan penelusuran di Android untuk menunjukkan cara agar aplikasi dapat ditelusuri di Android TV. Pastikan Anda terbiasa dengan konsep yang dijelaskan dalam Panduan Search API sebelum mengikuti tutorial ini. Lihat juga pelatihan Menambahkan fungsionalitas penelusuran.

Diskusi ini mendeskripsikan beberapa kode dari aplikasi contoh Android Leanback di repositori GitHub Android TV.

Mengidentifikasi kolom

SearchManager menjelaskan kolom data yang diharapkan dengan merepresentasikannya sebagai kolom database lokal. Terlepas dari format data Anda, Anda harus memetakan kolom data Anda ke kolom tersebut, biasanya di class yang mengakses data konten Anda. Untuk informasi tentang membuat class yang memetakan data Anda yang ada ke kolom wajib diisi, lihat Membuat tabel saran.

Class SearchManager menyertakan beberapa kolom untuk Android TV. Beberapa kolom yang lebih penting dideskripsikan di bawah.

Nilai Deskripsi
SUGGEST_COLUMN_TEXT_1 Nama konten Anda (wajib)
SUGGEST_COLUMN_TEXT_2 Deskripsi teks dari konten Anda
SUGGEST_COLUMN_RESULT_CARD_IMAGE Gambar/poster/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 tersebut untuk konten Anda cocok dengan nilai untuk konten yang sama dari penyedia lain yang ditemukan oleh server Google, sistem akan menyediakan deep link ke aplikasi Anda dalam tampilan detail untuk konten, bersama dengan link ke aplikasi dari penyedia lain. Hal tersebut dibahas lebih lanjut dalam Deep link ke aplikasi Anda di layar detail, di bawah.

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 Anda membuat peta dari kolom SearchManager ke kolom data, Anda juga harus menentukan _ID untuk memberi ID unik ke setiap baris.

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

Pada contoh di atas, perhatikan pemetaan ke kolom SUGGEST_COLUMN_INTENT_DATA_ID. Ini adalah bagian dari URI yang menunjuk ke konten unik untuk data di baris ini, yaitu bagian terakhir dari URI yang mendeskripsikan tempat konten disimpan. Bagian pertama URI, jika umum untuk semua baris dalam tabel, ditetapkan dalam file searchable.xml sebagai atribut android:searchSuggestIntentData, seperti yang dijelaskan dalam Menangani saran penelusuran, di bawah.

Jika bagian pertama URI berbeda untuk setiap baris dalam tabel, Anda memetakan nilai tersebut bersama kolom SUGGEST_COLUMN_INTENT_DATA. Saat pengguna memilih konten ini, intent yang aktif memberikan data intent dari kombinasi SUGGEST_COLUMN_INTENT_DATA_ID dan atribut android:searchSuggestIntentData atau nilai kolom SUGGEST_COLUMN_INTENT_DATA.

Memberikan data saran penelusuran

Implementasikan Penyedia Konten untuk menampilkan saran istilah penelusuran ke dialog penelusuran Android TV. Sistem mengkueri penyedia konten Anda untuk saran dengan memanggil metode query() setiap kali huruf diketik. Dalam implementasi query(), penyedia konten akan menelusuri data saran Anda dan menampilkan Cursor yang menunjuk 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. Bukan diberi tag sebagai aktivitas, melainkan dideskripsikan sebagai <provider>. Penyedia menyertakan atribut android:authorities untuk memberi tahu sistem tentang ruang nama penyedia konten Anda. Selain itu, Anda harus menetapkan atribut android:exported ke "true" agar penelusuran global Android dapat menggunakan hasil yang ditampilkan darinya.

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

Menangani saran penelusuran

Aplikasi Anda harus menyertakan file res/xml/searchable.xml untuk mengonfigurasi setelan saran penelusuran. Ini termasuk atribut android:searchSuggestAuthority untuk memberi tahu sistem tentang ruang nama penyedia konten Anda. Ini harus cocok dengan nilai string yang Anda tentukan di atribut android:authorities dari elemen <provider> dalam file AndroidManifest.xml Anda.

Aplikasi Anda harus menyertakan 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" untuk menentukan tindakan intent guna memberikan saran kustom. Ini berbeda dengan tindakan intent untuk memberikan istilah penelusuran yang dijelaskan di bawah. Lihat juga, Mendeklarasikan tindakan intent untuk cara lain dalam mendeklarasikan tindakan intent untuk saran.

Seiring dengan tindakan intent, aplikasi Anda harus menyediakan data intent, yang Anda tentukan dengan atribut android:searchSuggestIntentData. Ini adalah bagian pertama dari URI yang menunjuk ke konten. Bagian tersebut menjelaskan bagian URI yang umum bagi semua baris dalam tabel pemetaan untuk konten itu. Bagian URI yang unik untuk setiap baris ditetapkan dengan kolom SUGGEST_COLUMN_INTENT_DATA_ID, seperti dideskripsikan di atas dalam Mengidentifikasi kolom. Lihat juga, Mendeklarasikan data intent untuk mengetahui cara lain untuk mendeklarasikan data intent untuk saran.

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

Terakhir, Anda juga harus menyertakan atribut android:includeInGlobalSearch dengan nilai "true". Berikut adalah contoh 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 (dideskripsikan dalam Mengidentifikasi kolom, di atas), sistem akan mengaktifkan intent ACTION_SEARCH. Aktivitas di aplikasi Anda yang menangani intent tersebut menelusuri repositori untuk kolom dengan kata yang diberikan dalam nilainya, dan menampilkan daftar item konten dengan kolom tersebut. Dalam file AndroidManifest.xml, Anda menetapkan aktivitas yang menangani intent ACTION_SEARCH seperti ini:

    ...
      <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 tersebut juga harus mendeskripsikan konfigurasi yang dapat ditelusuri dengan referensi ke file searchable.xml. Untuk menggunakan dialog penelusuran global, manifes harus mendeskripsikan aktivitas mana yang harus menerima kueri penelusuran. Manifes tersebut juga harus mendeskripsikan elemen <provider> , persis seperti yang dideskripsikan dalam file searchable.xml.

Deep link ke aplikasi Anda di layar detail

Jika Anda telah menyiapkan konfigurasi penelusuran seperti yang dideskripsikan di Menangani saran penelusuran dan memetakan kolom SUGGEST_COLUMN_TEXT_1, SUGGEST_COLUMN_PRODUCTION_YEAR, dan SUGGEST_COLUMN_DURATION seperti yang dideskripsikan di Mengidentifikasi kolom, deep link ke tindakan menonton untuk konten Anda akan muncul di layar detail yang terbuka saat pengguna memilih hasil penelusuran, seperti yang ditunjukkan pada gambar 1.

Deep link dalam layar detail

Gambar 1. Layar detail menampilkan deep link untuk contoh aplikasi Videos by Google (Leanback). Sintel: © hak cipta Blender Foundation, www.sintel.org.

Saat pengguna memilih link untuk aplikasi Anda, yang diidentifikasi oleh tombol "Tersedia di" pada 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 Anda, dan tindakan ini ditunjukkan dalam aplikasi contoh Android Leanback di repositori GitHub Android TV. Perhatikan bahwa aplikasi contoh diluncurkan dengan sendirinya LeanbackDetailsFragment untuk menampilkan detail media yang dipilih, tetapi Anda harus segera meluncurkan aktivitas yang langsung memutar media untuk memudahkan pengguna.

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 Anda menelusuri dari layar utama, hasil pertama muncul di kartu entitas. Jika ada aplikasi yang dapat memutar konten tersebut, link ke masing-masing aplikasi akan muncul di bagian bawah kartu.

Pemutaran Hasil Penelusuran TV

Anda tidak dapat menempatkan secara terprogram aplikasi ke dalam kartu entitas. Agar disertakan sebagai opsi pemutaran, hasil penelusuran aplikasi harus sesuai dengan judul, tahun, dan durasi konten.

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

Hasil Penelusuran TV

Penelusuran dari aplikasi Anda

Pengguna 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

Selengkapnya

Untuk mempelajari lebih lanjut cara menelusuri aplikasi TV, baca Ringkasan penelusuran dan Menambahkan fungsionalitas penelusuran.

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