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

テレビ入力では、セットアップ アクティビティで少なくとも 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 は、バックエンド サーバーから取得したチャンネル メタデータを保持するオブジェクトです。

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

システム TV アプリは、図 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.システム TV アプリのチャンネルおよび番組の情報表示画面

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

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

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

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

注: ContentProvider にチャンネル データを追加するには時間がかかることがあります。現在の番組(現在時刻から 2 時間以内の番組)は、EpgSyncJobService を構成して残りのチャンネル データをバックグラウンドで更新する場合にのみ追加してください。例として Android 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 スレッドをブロックすることがあってはなりません。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 クラスを使用する方法があります。あるいは LooperHandler クラスを使用して独自の実装を行うこともできます。詳しくは、 プロセスとスレッドをご覧ください。

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

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

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

図 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_INTENT_SCHEMEtoUri(int) を使用して URI を作成し、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)には、「app-nameを開く」と表示されます。アプリリンクの使用可能なインテント URI が定義されていない場合は、 「リンクはありません」と表示されます。
  • アクセント カラー(COLUMN_APP_LINK_COLOR)には、アプリのデフォルトの色が使用されます。
  • ポスター画像(COLUMN_APP_LINK_POSTER_ART_URI)には、アプリのホーム画面のバナーが使用されます。アプリにバナーが用意されていない場合は、テレビアプリのデフォルトの画像が使用されます。
  • バッジアイコン(COLUMN_APP_LINK_ICON_URI)には、アプリ名が表示されたバッジが使用されます。ポスター画像についてアプリバナーやデフォルトのアプリ画像が使用されている場合は、バッジは表示されません。

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