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

テレビ入力は、セットアップ アクティビティで少なくとも 1 つのチャンネルの電子番組ガイド(EPG)データを提供する必要があります。また、更新のサイズとそれを処理する処理スレッドを考慮して、更新データを定期的に更新する必要があります。また、関連するコンテンツとアクティビティにユーザーを誘導するチャンネルのアプリリンクを指定することもできます。このレッスンでは、以下の点を念頭に置いて、システム データベースでチャンネル データと番組データを作成および更新する方法について説明します。

TV 入力サービスのサンプルアプリを試す。

権限を設定する

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

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

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

テレビ入力のためのチャンネル データのレコードは、Android TV システム データベースに保持されます。設定アクティビティでは、チャンネルごとに、TvContract.Channels クラスの次のフィールドにチャンネル データをマッピングする必要があります。

TV 入力フレームワークは汎用性が高く、従来のブロードキャスト コンテンツとオーバー ザ トップ(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 に示すように、システム TV アプリは、ユーザーがチャンネル間を移動する際に、チャンネルと番組の情報を表示します。チャンネルと番組の情報がシステム テレビアプリのチャンネルと番組情報のプレゼンターと連携するようにするには、以下のガイドラインに従ってください。

  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 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 スレッドをブロックしないようにする必要があります。AsyncTask を使用することは、更新を非同期で行う方法の 1 つです。たとえば、バックエンド サーバーからチャンネル情報を読み込む場合は、次のように 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. チャンネルのコンテンツが表示されているときに [Channels] 行に表示されているアプリリンクの例。

ユーザーがアプリリンクを選択すると、システムはチャンネル アプリで指定されたインテント 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)には「Open app-name」が使用されます。有効なアプリリンクのインテント URI が定義されていない場合は、「No link available」が使用されます。
  • アクセント カラー(COLUMN_APP_LINK_COLOR)には、デフォルトのアプリカラーが使用されます。
  • ポスター画像(COLUMN_APP_LINK_POSTER_ART_URI)には、アプリのホーム画面のバナーが使用されます。アプリでバナーを提供しない場合、システムはデフォルトのテレビアプリの画像を使用します。
  • バッジアイコン(COLUMN_APP_LINK_ICON_URI)には、アプリ名を示すバッジが使用されます。システムがアプリバナーまたはデフォルトのアプリ画像をポスター画像にも使用している場合、アプリバッジは表示されません。

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