Trabalhar com dados de canal

Sua entrada de TV precisa fornecer dados ao Guia da programação eletrônico (EPG, na sigla em inglês) para pelo menos um canal na atividade de configuração. Também é necessário atualizar esses dados periodicamente, considerando o tamanho da atualização e o thread de processamento relacionado. Além disso, você pode oferecer links de apps para canais que direcionam o usuário para conteúdos e atividades relacionados. Esta lição aborda a criação e a atualização de dados de canais e programas na base de dados do sistema, pensando nessas considerações.

Conheça o app de exemplo Serviço de entrada de TV (em inglês).

Conseguir permissão

Para que sua entrada de TV funcione com dados do EPG, é necessário declarar a permissão de gravação no arquivo de manifesto do Android da seguinte maneira:

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

Registrar canais no banco de dados

O banco de dados do sistema da Android TV mantém registros de dados de canais para entradas de TV. Na sua atividade de configuração, é necessário mapear os dados de cada canal para os seguintes campos da classe TvContract.Channels:

Embora o framework da entrada de TV seja genérico o suficiente para lidar tanto com a transmissão tradicional quanto com conteúdo over-the-top (OTT) sem distinções, você pode querer definir as seguintes colunas, além daquelas acima, para identificar melhor os canais de transmissão tradicionais:

Se você quiser fornecer detalhes de links de apps para seus canais, é necessário atualizar outros campos. Para mais informações sobre os campos de links de apps, consulte Adicionar informações de links de apps.

Para entradas de TV baseadas em streaming da Internet, atribua os próprios valores aos códigos acima, de modo que cada canal possa ser identificado de forma única.

Extraia os metadados do seu canal (em XML, JSON etc.) do servidor de back-end e, na atividade de configuração, mapeie os valores para o banco de dados do sistema, da seguinte maneira:

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

No exemplo acima, channel é um objeto que detém metadados de canal a partir do servidor de back-end.

Apresentar informações de canais e programas

O app de TV do sistema apresenta informações de canais e programas para os usuários conforme eles passam pelos canais, como mostrado na figura 1. Para garantir que as informações de canais e programas funcionem no apresentador do app de TV do sistema, siga as instruções abaixo.

  1. Número do canal (COLUMN_DISPLAY_NUMBER)
  2. Ícone (android:icon no manifesto da entrada de TV)
  3. Descrição do programa (COLUMN_SHORT_DESCRIPTION)
  4. Título do programa (COLUMN_TITLE)
  5. Logotipo do canal (TvContract.Channels.Logo)
    • Use a cor #EEEEEE para corresponder ao texto ao redor.
    • Não inclua padding.
  6. Arte de pôster (COLUMN_POSTER_ART_URI)
    • Use uma proporção entre 16:9 e 4:3.

Figura 1. Apresentador de informações de canais e programas do app de TV do sistema.

O app de TV do sistema oferece as mesmas informações no guia da programação, inclusive a arte do pôster, como mostrado na figura 2.

Figura 2. Guia da programação do app de TV do sistema.

Atualizar dados de canais

Ao atualizar dados de canais existentes, use o método update() em vez de excluir e adicionar novamente os dados. Identifique a versão atual dos dados usando Channels.COLUMN_VERSION_NUMBER e Programs.COLUMN_VERSION_NUMBER ao escolher os registros a serem atualizados.

Observação: a adição de dados de canais ao ContentProvider pode demorar. Adicione os programas atuais (aqueles dentro do intervalo de duas horas do horário atual) somente quando configurar seu EpgSyncJobService para atualizar o restante dos dados de canais em segundo plano. Consulte App de exemplo de TV ao vivo da Android TV (em inglês) para ver um exemplo.

Carregar dados de canais em lote

Ao atualizar o banco de dados do sistema com uma grande quantidade de dados de canais, use o método ContentResolverapplyBatch() ou bulkInsert(). Veja um exemplo com 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();
        }
    }
    

Processar dados de canais de forma assíncrona

A manipulação de dados, como a busca de um stream do servidor ou o acesso ao banco de dados, não deve bloquear a linha de execução da IU. O uso de uma AsyncTask é uma das maneiras de realizar atualizações de forma assíncrona. Por exemplo, ao carregar informações de canal de um servidor de back-end, você pode usar AsyncTask, da seguinte maneira:

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

Se você precisa atualizar os dados do EPG regularmente, use WorkManager para executar o processo de atualização durante o tempo inativo, por exemplo, todos os dias às 3h00.

Outras técnicas para separar as tarefas de atualização de dados da linha de execução da IU incluem o uso da classe HandlerThread. Você também pode implementar a própria técnica usando as classes Looper e Handler. Consulte Visão geral dos processos e threads para saber mais.

Os canais podem usar links de apps para permitir que os usuários iniciem facilmente uma atividade relacionada enquanto estiverem assistindo o conteúdo do canal. Apps de canais usam links de apps para estender o envolvimento do usuário iniciando atividades que mostram informações relacionadas ou outros conteúdos. Por exemplo, você pode usar links de apps para fazer o seguinte:

  • Orientar o usuário para encontrar e adquirir conteúdo relacionado.
  • Oferecer mais informações sobre o conteúdo em exibição no momento.
  • Durante a visualização de conteúdo em episódios, iniciar a visualização do próximo episódio da série.
  • Permitir que o usuário interaja com o conteúdo, por exemplo, classificando ou avaliando o conteúdo, sem interromper a exibição.

Links de apps são exibidos quando o usuário pressiona Select para mostrar o menu da TV enquanto assiste o conteúdo do canal.

Figura 1. Exemplo de link de app exibido na linha Channels enquanto o conteúdo do canal é apresentado.

Quando o usuário seleciona o link do app, o sistema inicia uma atividade usando um URI de intent especificado pelo app do canal. O conteúdo do canal continua sendo exibido enquanto a atividade do link do app está ativa. O usuário pode retornar ao conteúdo do canal pressionando Back.

Fornecer dados de canal do link do app

A Android TV cria automaticamente um link de app para cada canal, usando informações dos dados do canal. Para disponibilizar informações de links de apps, especifique os detalhes a seguir nos campos de TvContract.Channels:

  • COLUMN_APP_LINK_COLOR: cor de destaque do link de app desse canal. Para ver um exemplo de cor de destaque, consulte a figura 2, destaque 3.
  • COLUMN_APP_LINK_ICON_URI: URI para o ícone do selo do app correspondente ao link de app desse canal. Para ver um exemplo de ícone de selo de app, consulte a figura 2, destaque 2.
  • COLUMN_APP_LINK_INTENT_URI: URI de intent do link de app desse canal. É possível criar o URI usando toUri(int) com URI_INTENT_SCHEME e converter o URI novamente para a intent original com parseUri().
  • COLUMN_APP_LINK_POSTER_ART_URI: URI para a arte do pôster usada como segundo plano do link de app desse canal. Para ver um exemplo de imagem de pôster, consulte a figura 2, destaque 1.
  • COLUMN_APP_LINK_TEXT: texto descritivo do link do app para o canal. Para ver um exemplo de descrição de link de app, consulte o texto na figura 2, destaque 3.

Figura 2. Detalhes do link de app.

Se os dados do canal não especificam as informações do link de app, o sistema cria um link de app padrão. O sistema escolhe os detalhes padrão da seguinte maneira:

  • Para o URI de intent (COLUMN_APP_LINK_INTENT_URI), o sistema usa a atividade ACTION_MAIN para a categoria CATEGORY_LEANBACK_LAUNCHER, normalmente definida no manifesto do app. Se essa atividade não for definida, um link de app inativo será exibido. Se o usuário clicar nele, nada acontecerá.
  • Para o texto descritivo, (COLUMN_APP_LINK_TEXT), o sistema usa "Open nome-do-app". Se nenhum URI de intent viável para o link de app for definido, o sistema usará "No link available".
  • Para a cor de destaque (COLUMN_APP_LINK_COLOR), o sistema usa a cor de app padrão.
  • Para a imagem do pôster (COLUMN_APP_LINK_POSTER_ART_URI), o sistema usa o banner da tela inicial do app. Se o app não oferecer um banner, o sistema usará a imagem padrão do app de TV.
  • Para o ícone de selo (COLUMN_APP_LINK_ICON_URI), o sistema usa um selo que mostra o nome do app. Se o sistema também estiver usando o banner ou imagem padrão do app como imagem de pôster, nenhum selo será exibido.

Especifique detalhes do link do app para seus canais na atividade de configuração do app. É possível atualizar esses detalhes do link do app a qualquer momento. Assim, se um link de app precisa corresponder a alterações de canal, atualize os detalhes do link do app e chame ContentResolver.update() conforme necessário. Para mais detalhes sobre como atualizar dados de canal, consulte Atualizar dados de canal.