チャンネル データを利用する

テレビ入力では、セットアップ アクティビティで少なくとも 1 つのチャンネルの Electronic Program Guide(EPG)データを提供する必要があります。EPG データは定期的に更新する必要があります。更新にあたっては、更新のサイズと更新を処理するスレッドを考慮するようにします。また、チャンネルのアプリリンクを用意してユーザーに関連するコンテンツやアクティビティを案内することもできます。このレッスンでは、こうした事項を考慮してシステム データベースでチャンネルおよび番組のデータを作成、更新する方法を解説します。

テレビ入力サービスのサンプルアプリをお試しください。

権限を設定する

テレビ入力と EPG データを連携させるには、Android マニフェスト ファイルで以下のように書き込み権限を宣言する必要があります。

    <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
    

データベースにチャンネルを登録する

テレビ入力のためのチャンネル データのレコードは、Android TV システム データベースに保持されます。セットアップ アクティビティで、各チャンネルについて TvContract.Channels クラスの以下のフィールドにチャンネル データをマッピングする必要があります。

テレビ入力フレームワークは汎用性があるため従来の放送もオーバー ザ トップ(OTT)コンテンツも区別なく扱えますが、必要に応じて従来の放送チャンネルを特定しやすくするために上記の列に加えて以下の列を定義することもできます。

チャンネルのアプリリンクの詳細を追加する場合は、追加のフィールドを更新する必要があります。アプリリンク フィールドについて詳しくは、アプリリンク情報を追加するをご覧ください。

インターネット ストリーミング ベースのテレビ入力の場合は、各チャンネルを一意に識別できるように、上記に独自の値を割り当てます。

バックエンド サーバーからチャンネルのメタデータ(XML や JSON など任意の形式)を取得し、セットアップ アクティビティで以下のように値をシステム データベースにマッピングします。

Kotlin

    val values = ContentValues().apply {
        put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.number)
        put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.name)
        put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.originalNetworkId)
        put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.transportStreamId)
        put(TvContract.Channels.COLUMN_SERVICE_ID, channel.serviceId)
        put(TvContract.Channels.COLUMN_VIDEO_FORMAT, channel.videoFormat)
    }
    val uri = context.contentResolver.insert(TvContract.Channels.CONTENT_URI, values)
    

Java

    ContentValues values = new ContentValues();

    values.put(Channels.COLUMN_DISPLAY_NUMBER, channel.number);
    values.put(Channels.COLUMN_DISPLAY_NAME, channel.name);
    values.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.originalNetworkId);
    values.put(Channels.COLUMN_TRANSPORT_STREAM_ID, channel.transportStreamId);
    values.put(Channels.COLUMN_SERVICE_ID, channel.serviceId);
    values.put(Channels.COLUMN_VIDEO_FORMAT, channel.videoFormat);

    Uri uri = context.getContentResolver().insert(TvContract.Channels.CONTENT_URI, values);
    

上記の例で channel は、バックエンド サーバーから取得したチャンネル メタデータを保持するオブジェクトです。

チャンネルと番組の情報を表示する

システム テレビ アプリは、図 1 に示すように、ユーザーがチャンネル間を切り替えたときにチャンネルと番組の情報をユーザーに表示します。チャンネルと番組の情報をシステム テレビアプリのチャンネルおよび番組の情報表示画面と連携させるには、以下のガイドラインに沿って設定します。

  1. チャンネル番号COLUMN_DISPLAY_NUMBER
  2. アイコン(テレビ入力のマニフェストにある android:icon
  3. 番組説明COLUMN_SHORT_DESCRIPTION
  4. 番組タイトルCOLUMN_TITLE
  5. チャンネル ロゴTvContract.Channels.Logo
    • 周囲のテキストに合わせるには #EEEEEE の色を使用します。
    • パディングを入れないようにします。
  6. ポスターアートCOLUMN_POSTER_ART_URI
    • 16:9~4:3 のアスペクト比

図 1. システム テレビ アプリのチャンネルおよび番組の情報表示画面

システム テレビアプリは、図 2 に示すように、番組ガイドでも同じ情報(ポスターアートを含む)を提供します。

図 2. システム テレビ アプリの番組ガイド

チャンネル データを更新する

既存のチャンネル データを更新する場合は、データを削除して追加し直すのではなく update() メソッドを使用します。更新するレコードを選択する際に Channels.COLUMN_VERSION_NUMBERPrograms.COLUMN_VERSION_NUMBER を使用してデータの現在のバージョンを確認できます。

注: ContentProvider にチャンネル データを追加する際の処理に時間がかかることがあります。残りのチャンネル データがバックグラウンドで更新されるよう EpgSyncJobService を設定する場合は、現在の番組(現時点から 2 時間以内のもの)のみを追加するようにします。例として Android TV Live TV サンプル アプリをご覧ください。

チャンネル データを一括で読み込む

大量のチャンネル データでシステム データベースを更新する場合は、ContentResolver applyBatch() または bulkInsert() メソッドを使用します。以下に applyBatch() を使用した例を示します。

Kotlin

    val ops = ArrayList<ContentProviderOperation>()
    val programsCount = channelInfo.mPrograms.size
    channelInfo.mPrograms.forEachIndexed { index, program ->
        ops += ContentProviderOperation.newInsert(
                TvContract.Programs.CONTENT_URI).run {
            withValues(programs[index])
            withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, programStartSec * 1000)
            withValue(
                    TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
                    (programStartSec + program.durationSec) * 1000
            )
            build()
        }
        programStartSec += program.durationSec
        if (index % 100 == 99 || index == programsCount - 1) {
            try {
                contentResolver.applyBatch(TvContract.AUTHORITY, ops)
            } catch (e: RemoteException) {
                Log.e(TAG, "Failed to insert programs.", e)
                return
            } catch (e: OperationApplicationException) {
                Log.e(TAG, "Failed to insert programs.", e)
                return
            }
            ops.clear()
        }
    }
    

Java

    ArrayList<ContentProviderOperation> ops = new ArrayList<>();
    int programsCount = channelInfo.mPrograms.size();
    for (int j = 0; j < programsCount; ++j) {
        ProgramInfo program = channelInfo.mPrograms.get(j);
        ops.add(ContentProviderOperation.newInsert(
                TvContract.Programs.CONTENT_URI)
                .withValues(programs.get(j))
                .withValue(Programs.COLUMN_START_TIME_UTC_MILLIS,
                        programStartSec * 1000)
                .withValue(Programs.COLUMN_END_TIME_UTC_MILLIS,
                        (programStartSec + program.durationSec) * 1000)
                .build());
        programStartSec = programStartSec + program.durationSec;
        if (j % 100 == 99 || j == programsCount - 1) {
            try {
                getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
            } catch (RemoteException | OperationApplicationException e) {
                Log.e(TAG, "Failed to insert programs.", e);
                return;
            }
            ops.clear();
        }
    }
    

チャンネル データを非同期で処理する

サーバーからのストリームの取得やデータベースへのアクセスなどのデータ操作が UI スレッドをブロックすることがあってはなりません。更新を非同期で実行する方法の 1 つに、AsyncTask を使用する方法があります。たとえば、バックエンド サーバーからチャンネル情報を読み込む場合については以下のように AsyncTask を使用できます。

Kotlin

    private class LoadTvInputTask(val context: Context) : AsyncTask<Uri, Unit, Unit>() {

        override fun doInBackground(vararg uris: Uri) {
            try {
                fetchUri(uris[0])
            } catch (e: IOException) {
                Log.d("LoadTvInputTask", "fetchUri error")
            }
        }

        @Throws(IOException::class)
        private fun fetchUri(videoUri: Uri) {
            context.contentResolver.openInputStream(videoUri).use { inputStream ->
                Xml.newPullParser().also { parser ->
                    try {
                        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
                        parser.setInput(inputStream, null)
                        sTvInput = ChannelXMLParser.parseTvInput(parser)
                        sSampleChannels = ChannelXMLParser.parseChannelXML(parser)
                    } catch (e: XmlPullParserException) {
                        e.printStackTrace()
                    }
                }
            }
        }
    }
    

Java

    private static class LoadTvInputTask extends AsyncTask<Uri, Void, Void> {

        private Context mContext;

        public LoadTvInputTask(Context context) {
            mContext = context;
        }

        @Override
        protected Void doInBackground(Uri... uris) {
            try {
                fetchUri(uris[0]);
            } catch (IOException e) {
              Log.d("LoadTvInputTask", "fetchUri error");
            }
            return null;
        }

        private void fetchUri(Uri videoUri) throws IOException {
            InputStream inputStream = null;
            try {
                inputStream = mContext.getContentResolver().openInputStream(videoUri);
                XmlPullParser parser = Xml.newPullParser();
                try {
                    parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
                    parser.setInput(inputStream, null);
                    sTvInput = ChannelXMLParser.parseTvInput(parser);
                    sSampleChannels = ChannelXMLParser.parseChannelXML(parser);
                } catch (XmlPullParserException e) {
                    e.printStackTrace();
                }
            } finally {
                if (inputStream != null) {
                    inputStream.close();
                }
            }
        }
    }
    

EPG データを定期的に更新する必要がある場合は、WorkManager を使用して、アイドル時間(毎日午前 3 時など)に更新プロセスを実行するよう設定することを検討します。

その他にデータ更新タスクを UI スレッドから分離する手法としては、HandlerThread クラスを使用する方法があります。あるいは Looper クラスと Handler クラスを使用して独自の実装を行うこともできます。詳しくは、プロセスとスレッドをご覧ください。

チャンネルでアプリリンクを使用して、ユーザーがチャンネルのコンテンツを視聴しているときに関連アクティビティを簡単に起動できるようにすることができます。チャンネル アプリでアプリリンクを使用して、関連情報や追加コンテンツを表示するアクティビティを起動することで、ユーザー エンゲージメントを拡大できます。たとえば、アプリリンクを使用して以下のことを行えます。

  • ユーザーが関連コンテンツを見つけて購入できる場所を案内する
  • 現在再生中のコンテンツに関する追加情報を提供する
  • エピソード コンテンツを表示しているときに、シリーズの次のエピソードを表示する
  • コンテンツの再生を中断することなく、ユーザーがコンテンツの評価やレビューを入力できるようにする

アプリリンクは、ユーザーがチャンネルのコンテンツを視聴中に [選択] を押してテレビメニューを表示したときに表示されます。

図 1. チャンネルのコンテンツが表示されているときに [チャンネル] 行に表示されるアプリリンクの例

ユーザーがアプリリンクを選択すると、チャンネル アプリで指定されたインテント URI を使用してアクティビティが開始されます。アプリリンクのアクティビティがアクティブになっている間も引き続きチャンネルのコンテンツは再生されます。[戻る] を押すと、チャンネルのコンテンツに戻ることができます。

アプリリンクにチャンネル データを提供する

Android TV では、チャンネル データからの情報を使用して、各チャンネルのアプリリンクが自動的に作成されます。アプリリンクに情報を提供するには、TvContract.Channels フィールドに以下の項目を指定します。

  • COLUMN_APP_LINK_COLOR - このチャンネルのアプリリンクのアクセント カラー。アクセント カラーの例は、図 2 の吹き出し 3 をご覧ください。
  • COLUMN_APP_LINK_ICON_URI - このチャンネルのアプリリンクのアプリバッジ アイコンの URI。アプリのバッジ アイコンの例は、図 2 の吹き出し 2 をご覧ください。
  • COLUMN_APP_LINK_INTENT_URI - このチャンネルのアプリリンクのインテント URI。URI は、toUri(int)URI_INTENT_SCHEME とともに使用して、parseUri() で URI を元のインテントに変換し直して作成できます。
  • COLUMN_APP_LINK_POSTER_ART_URI - このチャンネルのアプリリンクのバックグラウンドとして使用されるポスター アートの URI。ポスター画像の例は、図 2 の吹き出し 1 をご覧ください。
  • COLUMN_APP_LINK_TEXT - このチャンネルのアプリリンクの説明リンクテキスト。アプリリンクの説明の例は、図 2 の吹き出し 3 をご覧ください。

図 2. アプリリンクの詳細項目

チャンネル データにアプリリンク情報が指定されていない場合、デフォルトのアプリリンクが作成されます。詳細項目はデフォルトでは以下のように選択されます。

  • インテント URI(COLUMN_APP_LINK_INTENT_URI)には、CATEGORY_LEANBACK_LAUNCHER カテゴリの ACTION_MAIN アクティビティが使用されます(通常アプリのマニフェストで定義されています)。 このアクティビティが定義されていない場合、機能しないアプリリンク(クリックしても何も起こらないリンク)が表示されます。
  • 説明文(COLUMN_APP_LINK_TEXT)には、「アプリ名を開く」と表示されます。アプリリンクの使用可能なインテント URI が定義されていない場合は、「リンクはありません」と表示されます。
  • アクセント カラー(COLUMN_APP_LINK_COLOR)には、アプリのデフォルトの色が使用されます。
  • ポスター画像(COLUMN_APP_LINK_POSTER_ART_URI)には、アプリのホーム画面のバナーが使用されます。アプリにバナーが用意されていない場合は、テレビアプリのデフォルトの画像が使用されます。
  • バッジアイコン(COLUMN_APP_LINK_ICON_URI)には、アプリ名が表示されたバッジが使用されます。ポスター画像についてアプリバナーやデフォルトのアプリ画像が使用されている場合は、バッジは表示されません。

アプリのセットアップ アクティビティでチャンネルのアプリリンクの詳細事項を指定します。この詳細事項はいつでも更新できます。アプリリンクをチャンネルの変更に合わせる必要がある場合は、必要に応じてアプリリンクの詳細事項を更新して ContentResolver.update() を呼び出します。チャンネル データの更新について詳しくは、チャンネル データを更新するをご覧ください。