Trabalhar com dados do canal

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

Teste o app de exemplo TV Input Service (link em inglês).

Conseguir permissão

Para que a entrada de TV funcione com dados de EPG, ela precisa 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 atividade de configuração, é preciso mapear os dados dos canais para os seguintes campos da classe TvContract.Channels:

Embora o framework de entrada de TV seja genérico o suficiente para processar a transmissão tradicional e o conteúdo over-the-top (OTT) sem distinções, é recomendável 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, será necessário atualizar alguns 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 de Internet, atribua seus próprios valores aos valores acima para que cada canal possa ser identificado de maneira exclusiva.

Extraia os metadados do seu canal (em XML, JSON ou qualquer outro) 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 contém metadados de canal 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 aos usuários à medida que eles alternam entre os canais, conforme mostrado na Figura 1. Para garantir que as informações de canais e programas funcionem com o apresentador de informações de canais e programas do app de TV do sistema, siga as diretrizes 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 fornece as mesmas informações no guia da programação, incluindo pôsteres, conforme 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 levar algum tempo. Adicione os programas atuais (aqueles dentro de duas horas do horário atual) somente quando configurar o EpgSyncJobService para atualizar o restante dos dados do canal em segundo plano. Consulte o app de exemplo de TV ao vivo do Android TV (link em inglês) para conferir um exemplo.

Carregar dados de canais em lote

Ao atualizar o banco de dados do sistema com uma grande quantidade de dados do canal, use o método ContentResolver applyBatch() 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 buscar um stream do servidor ou acessar o banco de dados, não pode bloquear a linha de execução de IU. O uso de um AsyncTask é unidirecional de fazer 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ê precisar atualizar os dados do EPG regularmente, use WorkManager para executar o processo de atualização durante o período inativo, por exemplo, todos os dias às 3h.

Outras técnicas para separar as tarefas de atualização de dados da linha de execução de interface incluem o uso da classe HandlerThread. Você também pode implementar uma técnica própria usando as classes Looper e Handler. Consulte Processos e linhas de execução 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. Os apps de canais usam links de apps para estender o engajamento 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, comece a assistir o próximo episódio de uma série.
  • Permita que o usuário interaja com o conteúdo, por exemplo, classificando ou analisando o conteúdo, sem interromper a reprodução.

Os links do app são mostrados 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 é mostrado.

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

Fornecer dados de canal do link do app

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

  • COLUMN_APP_LINK_COLOR: a cor de destaque do link do app para esse canal. Para conferir um exemplo de cor de destaque, consulte a Figura 2, destaque 3.
  • COLUMN_APP_LINK_ICON_URI: o URI do ícone de selo do app referente ao link de app desse canal. Para ver um exemplo de ícone de selo do app, consulte a Figura 2, frase de destaque 2.
  • COLUMN_APP_LINK_INTENT_URI: URI da intent do link de app desse canal. É possível criar o URI usando toUri(int) com URI_INTENT_SCHEME e converter o URI de volta para a intent original com parseUri().
  • COLUMN_APP_LINK_POSTER_ART_URI: o URI da arte do pôster usado como plano de fundo do link de app deste 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 esse canal. Para ver um exemplo de descrição de link de app, consulte o texto na Figura 2, frase de destaque 3.

Figura 2. Detalhes do link de app.

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

  • Para o URI da 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 app-name". Se nenhum URI de intent viável do link do app for definido, o sistema vai usar "No link disponível".
  • Para a cor de destaque (COLUMN_APP_LINK_COLOR), o sistema usa a cor padrão do app.
  • 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 vai usar uma 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 a imagem padrão do app como imagem do pôster, nenhum selo de app será exibido.

Especifique os detalhes do link do app para seus canais na atividade de configuração do app. Você pode atualizar esses detalhes do link do app a qualquer momento. Assim, se um link de app precisar corresponder a mudanças de canal, atualize os detalhes do link do app e chame ContentResolver.update() conforme necessário. Para mais detalhes sobre como atualizar dados do canal, consulte Atualizar dados do canal.