Cómo trabajar con datos del canal

La entrada de TV debe proporcionar los datos de la Guía electrónica de programas (EPG) para al menos un canal en la actividad de configuración. Además, deberás actualizar periódicamente los datos teniendo en cuenta el tamaño de la actualización y el subproceso que la controla. Además, puedes proporcionar vínculos de app para los canales que guían al usuario al contenido y las actividades relacionadas. En esta lección, se analiza la creación y la actualización de datos de canales y programas en la base de datos del sistema con estas consideraciones en mente.

Prueba la app de ejemplo TV Input Service.

Obtén el permiso

Para que la entrada de TV funcione con los datos de la EPG, debe declarar el permiso de escritura en el archivo del manifiesto de Android, de la siguiente manera:

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

Registra los canales en la base de datos

La base de datos del sistema de Android TV guarda registros de los datos de los canales para las entradas de TV. En la actividad de configuración, para cada uno de los canales, debes asignar los datos del canal a los siguientes campos de la clase TvContract.Channels:

Aunque el marco de entrada de TV es genérico y permite controlar el contenido de emisión tradicional y de transmisión libre (OTT) de la misma manera, puedes definir las siguientes columnas, además de las que se muestran arriba, con el fin de identificar mejor los canales de emisión tradicional:

Si quieres proporcionar detalles del vínculo de la app para tus canales, debes actualizar algunos campos adicionales. Para obtener más información sobre los campos del vínculo de app, consulta Cómo agregar información de vínculo de app.

Para la transmisión de Internet basada en entradas de TV, asigna tus propios valores según corresponda para que se pueda identificar cada canal de forma exclusiva.

Toma los metadatos del canal (en XML, JSON u otro) desde el servidor de backend y, en la actividad de configuración, asigna los valores a la base de datos del sistema de la siguiente manera:

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

En el ejemplo de arriba, channel es un objeto que conserva los metadatos del canal desde el servidor de backend.

Presenta la información de los canales y los programas

La app de TV del sistema presenta la información de los canales y los programas a los usuarios mientras pasan de un canal a otro, como se muestra en la Figura 1. Para asegurarte de que la información de los canales y los programas funcionen con el presentador de información de canales y programas de la app del TV del sistema, sigue estos lineamientos.

  1. Número de canal (COLUMN_DISPLAY_NUMBER)
  2. Ícono (android:icon en el manifiesto de la entrada de TV)
  3. Descripción del programa (COLUMN_SHORT_DESCRIPTION)
  4. Título del programa (COLUMN_TITLE)
  5. Logotipo del canal (TvContract.Channels.Logo)
    • Usa el color #EEEEEE para que coincida con el texto adyacente.
    • No incluyas relleno.
  6. Material gráfico de pósters (COLUMN_POSTER_ART_URI)
    • La proporción debe ser entre 16:9 y 4.3.

Figura 1: El presentador de la información de programas y canales de la app para TV del sistema

La app para TV del sistema proporciona la misma información mediante la guía de programas, incluido el material gráfico de pósteres, como se muestra en la Figura 2.

Figura 2: La guía de programas de la app para TV del sistema

Actualiza los datos de los canales

Cuando actualices los datos de los canales existentes, usa el método update(), en lugar de borrar y volver a agregar los datos. Puedes identificar la versión actual de los datos usando Channels.COLUMN_VERSION_NUMBER y Programs.COLUMN_VERSION_NUMBER a la hora de seleccionar los registros que se deben actualizar.

Nota: Es posible que agregar datos de canales a ContentProvider demore varios minutos. Agrega los programas actuales (los que se emitan dentro del rango de dos horas a partir de la hora actual) solo cuando configures EpgSyncJobService para actualizar el resto de los datos de canales en segundo plano. Consulta la app de ejemplo de TV en directo de Android TV.

Carga los datos de los canales en lote

Si actualizas la base de datos del sistema con una gran cantidad de datos de canales, usa el método ContentResolverapplyBatch() o bulkInsert(). A continuación se muestra un ejemplo con el uso de 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();
        }
    }
    

Procesa los datos de los canales de forma simultánea

La manipulación de datos, como la obtención de una transmisión desde el servidor o el acceso a la base de datos, no debe bloquear el procesamiento de IU. Usar AsyncTask es una manera de realizar actualizaciones de forma asíncrona. Por ejemplo, cuando cargas información de canales desde un servidor de backend, puedes usar AsyncTask de la siguiente manera:

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

Si necesitas actualizar los datos de EPG de forma regular, considera usar WorkManager para ejecutar el proceso de actualización durante el tiempo de inactividad, por ejemplo, todos los días a las 3:00 a.m.

Otras técnicas para separar las tareas de actualización de datos desde el procesamiento de IU son usar la clase HandlerThread o implementar la tuya con las clases Looper y Handler. Consulta Procesos y subprocesos para obtener más información.

Los canales pueden usar vínculos de app para que los usuarios puedan iniciar con facilidad una actividad relacionada mientras miran el contenido de un canal. Las apps de canales usan vínculos de app para ampliar la participación de los usuarios mediante el lanzamiento de actividades que muestran información relacionada o contenido adicional. Por ejemplo, puedes usar vínculos de app para lo siguiente:

  • Guiar al usuario para que descubra y compre contenido relacionado
  • Proporcionar información adicional acerca del contenido de reproducción actual
  • Mientras se visualiza el contenido de los episodios, iniciar la visualización del episodio siguiente de una serie
  • Permitir al usuario interactuar con el contenido, por ejemplo, calificar o dejar una opinión sobre el contenido, sin interrumpir la reproducción

Los vínculos de app se muestran cuando el usuario presiona Seleccionar para mostrar el menú de TV mientras se reproduce el contenido del canal.

Figura 1: Ejemplo de vínculo de app que se muestra en la fila Canales mientras se visualiza el contenido del canal

Cuando el usuario selecciona el vínculo de app, el sistema inicia una actividad con la IU de intent especificada por la app del canal. El contenido del canal se sigue reproduciendo mientras la actividad del vínculo de la app está activa. Para regresar al contenido del canal, el usuario puede presionar Atrás.

Proporciona datos de canales del vínculo de app

Android TV crea automáticamente un vínculo de app para cada canal, con la información de los datos de canales. Para proporcionar información de vínculo de app, especifica los siguientes detalles en los campos de TvContract.Channels:

  • COLUMN_APP_LINK_COLOR: Es el color de los elementos destacados del vínculo de la app para este canal. Para ver un ejemplo de color de elementos destacados, consulta la Figura 2, leyenda 3.
  • COLUMN_APP_LINK_ICON_URI: Es la URI para el ícono de insignia del vínculo de app de este canal. Para ver un ejemplo de ícono de insignia de app, consulta la Figura 2, leyenda 2.
  • COLUMN_APP_LINK_INTENT_URI: Es la URI de intent del vínculo de app de este canal. Puedes crear la URI usando toUri(int) con URI_INTENT_SCHEME, y convertir la URI de nuevo al intent original parseUri().
  • COLUMN_APP_LINK_POSTER_ART_URI: Es la URI del material gráfico de póster usado como fondo del vínculo de app de este canal. Para ver un ejemplo de imagen de póster, consulta la Figura 2, leyenda 1.
  • COLUMN_APP_LINK_TEXT: Es el texto descriptivo del vínculo de la app de este canal. Para ver un ejemplo de descripción de vínculo de app, consulta el texto de la Figura 2, leyenda 3.

Figura 2: Detalles de vínculo de app

Si los datos del canal no especifican la información de vínculo de la app, el sistema creará un vínculo de app predeterminado. El sistema elige los detalles predeterminados de la siguiente manera:

  • Para la URI de intent (COLUMN_APP_LINK_INTENT_URI), el sistema usa la actividad ACTION_MAIN en la categoría CATEGORY_LEANBACK_LAUNCHER, en general, definida en el manifiesto de la app. Si no se define esta actividad, se muestra un vínculo de app que no funciona, y no se lleva a cabo ninguna acción cuando el usuario hace clic en el vínculo.
  • Para el texto descriptivo (COLUMN_APP_LINK_TEXT), el sistema usa "Abrir app-name". Si no se define ninguna URI de intent de app viable, el sistema usa "No hay ningún vínculo disponible".
  • Para el color de los elementos destacados (COLUMN_APP_LINK_COLOR), el sistema usa el color de app predeterminado.
  • Para la imagen de póster (COLUMN_APP_LINK_POSTER_ART_URI), el sistema usa el banner de la pantalla principal de la app. Si la app no proporciona un banner, el sistema usa la imagen de app de TV predeterminada.
  • Para el ícono de insignia (COLUMN_APP_LINK_ICON_URI), el sistema usa una insignia que muestra el nombre de la app. Si el sistema también usa el banner de app o la imagen de app predeterminada para la imagen de póster, no se muestra ninguna insignia de app.

Los detalles del vínculo de la app para los canales se especifican en la actividad de configuración de la app. Puedes actualizar estos detalles de vínculo de la app en cualquier momento, de modo que si un vínculo de app debe coincidir con los cambios de canal, actualiza los detalles de vínculo de la app y llama a ContentResolver.update() según corresponda. Para obtener más información sobre los datos de canales, consulta Actualiza los datos de los canales.