public class

PreviewChannelHelper

extends java.lang.Object

 java.lang.Object

↳androidx.tvprovider.media.tv.PreviewChannelHelper

Gradle dependencies

compile group: 'androidx.tvprovider', name: 'tvprovider', version: '1.1.0-alpha01'

  • groupId: androidx.tvprovider
  • artifactId: tvprovider
  • version: 1.1.0-alpha01

Artifact androidx.tvprovider:tvprovider:1.1.0-alpha01 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.tvprovider:tvprovider com.android.support:support-tv-provider

Androidx class mapping:

androidx.tvprovider.media.tv.PreviewChannelHelper android.support.media.tv.PreviewChannelHelper

Overview

From a user's perspective, the TV home screen has two types of channels: the single Live Channels row versus the App preview Channels. This class is concerned with App Channels; or more precisely: your app's preview Channels. In API 26+, all TV apps are allowed to create multiple channels and publish those Channels to the home screen.

This class provides convenience methods to help you publish, update and delete channels; add, update or remove programs in a channel. You do not need to know anything about Content Providers, Content Resolvers, Cursors or such to publish your channels. This class abstracts away all database interactions for you.

To make it easy for you to distinguish classes that help you build App Channels, the support library uses the prefix Preview- to denote the classes that pertain to app Channels. Hence, the classes PreviewChannel and PreviewProgram help your app add channels to the TV home page. All calls to methods in the class should be made on worker threads.

Summary

Constructors
publicPreviewChannelHelper(Context context)

publicPreviewChannelHelper(Context context, int urlConnectionTimeoutMillis, int urlReadTimeoutMillis)

Methods
public voiddeletePreviewChannel(long channelId)

Removes a preview channel from the system's content provider (aka TvProvider).

public voiddeletePreviewProgram(long programId)

Removes programs from a preview channel.

protected BitmapdownloadBitmap(Uri logoUri)

Downloads a Bitmap from a remote server.

public java.util.List<PreviewChannel>getAllChannels()

The TvProvider does not allow select queries.

public PreviewChannelgetPreviewChannel(long channelId)

Retrieves a single preview channel from the TvProvider.

public PreviewProgramgetPreviewProgram(long programId)

Retrieves a single preview program from the system content provider (aka TvProvider).

public WatchNextProgramgetWatchNextProgram(long programId)

Retrieves a single WatchNext program from the system content provider (aka TvProvider).

public longpublishChannel(PreviewChannel channel)

Publishing a channel to the TV home screen is a two step process: first, you add the channel to the TV content provider; second, you make the channel browsable (i.e.

public longpublishDefaultChannel(PreviewChannel channel)

This is a convenience method that simply publishes your first channel for you.

public longpublishPreviewProgram(PreviewProgram program)

Adds programs to a preview channel.

public longpublishWatchNextProgram(WatchNextProgram program)

Adds a program to the Watch Next channel

public voidupdatePreviewChannel(long channelId, PreviewChannel update)

To update a preview channel, you need to use the PreviewChannel.Builder to set the attributes you wish to change.

protected voidupdatePreviewChannelInternal(long channelId, PreviewChannel upgrade)

Inner methods that does the actual work of updating a Preview Channel.

public voidupdatePreviewProgram(long programId, PreviewProgram update)

Updates programs in a preview channel.

public voidupdateWatchNextProgram(WatchNextProgram upgrade, long programId)

Updates a WatchNext program.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public PreviewChannelHelper(Context context)

public PreviewChannelHelper(Context context, int urlConnectionTimeoutMillis, int urlReadTimeoutMillis)

Parameters:

urlConnectionTimeoutMillis: see setConnectTimeout
urlReadTimeoutMillis: see setReadTimeout

Methods

public long publishChannel(PreviewChannel channel)

Publishing a channel to the TV home screen is a two step process: first, you add the channel to the TV content provider; second, you make the channel browsable (i.e. visible). This method adds the channel to the TV content provider for you and returns a channelId. Next you must use the channelId to make the channel browsable.
There are two ways you can make a channel browsable:
a) For your first channel, simply ask the system to make the channel browsable: TvContractCompat.requestChannelBrowsable(context,channelId)
b) For any additional channel beyond the first channel, you must get permission from the user. So if this channel is not your first channel, you must request user permission through the following intent. So take the channelId returned by this method and do the following inside an Activity or Fragment:

 intent = new Intent(TvContractCompat.ACTION_REQUEST_CHANNEL_BROWSABLE);
 intent.putExtra(TvContractCompat.EXTRA_CHANNEL_ID, channelId);
 startActivityForResult(intent, REQUEST_CHANNEL_BROWSABLE);
 

Creating a PreviewChannel, you may pass to the builder a url as your logo. In such case, PreviewChannelHelper.updatePreviewChannel(long, PreviewChannel) will load the logo over the network. To use your own networking code, override PreviewChannelHelper.downloadBitmap(Uri).

Returns:

channelId or -1 if insertion fails. This is the id the system assigns to your published channel. You can use it later to get a reference to this published PreviewChannel.

public long publishDefaultChannel(PreviewChannel channel)

This is a convenience method that simply publishes your first channel for you. After calling PreviewChannelHelper.publishChannel(PreviewChannel) to add the channel to the TvProvider, it calls TvContractCompat.requestChannelBrowsable(Context, long) to make the channel visible.

Only use this method to publish your first channel as you do not need user permission to make your first channel browsable (i.e. visible on home screen). For additional channels, see the documentations for PreviewChannelHelper.publishChannel(PreviewChannel).

Creating a PreviewChannel, you may pass to the builder a url as your logo. In such case, PreviewChannelHelper.updatePreviewChannel(long, PreviewChannel) will load the logo over the network. To use your own networking code, override PreviewChannelHelper.downloadBitmap(Uri).

Returns:

channelId: This is the id the system assigns to your published channel. You can use it later to get a reference to this published PreviewChannel.

public java.util.List<PreviewChannel> getAllChannels()

The TvProvider does not allow select queries. Hence, unless you are querying for a single PreviewChannel by id, you must get all of your channels at once and then use the returned list as necessary.

public PreviewChannel getPreviewChannel(long channelId)

Retrieves a single preview channel from the TvProvider. When you publish a preview channel, the TvProvider assigns an ID to it. That's the channelId to use here.

Parameters:

channelId: ID of preview channel in TvProvider

Returns:

PreviewChannel or null if not found

public void updatePreviewChannel(long channelId, PreviewChannel update)

To update a preview channel, you need to use the PreviewChannel.Builder to set the attributes you wish to change. Then simply pass in the built channel and the channelId of the preview channel. (The channelId is the ID you received when you originally published the preview channel.)

Creating a PreviewChannel, you may pass to the builder a url as your logo. In such case, PreviewChannelHelper.updatePreviewChannel(long, PreviewChannel) will load the logo over the network. To use your own networking code, override PreviewChannelHelper.downloadBitmap(Uri).

protected void updatePreviewChannelInternal(long channelId, PreviewChannel upgrade)

Inner methods that does the actual work of updating a Preview Channel. The method is extracted to make PreviewChannelHelper.updatePreviewChannel(long, PreviewChannel) testable.

protected Bitmap downloadBitmap(Uri logoUri)

Downloads a Bitmap from a remote server. It is declared protected to allow you to override it to use your own networking implementation if you so wish.

public void deletePreviewChannel(long channelId)

Removes a preview channel from the system's content provider (aka TvProvider).

public long publishPreviewProgram(PreviewProgram program)

Adds programs to a preview channel.

public PreviewProgram getPreviewProgram(long programId)

Retrieves a single preview program from the system content provider (aka TvProvider).

public void updatePreviewProgram(long programId, PreviewProgram update)

Updates programs in a preview channel.

public void deletePreviewProgram(long programId)

Removes programs from a preview channel.

public long publishWatchNextProgram(WatchNextProgram program)

Adds a program to the Watch Next channel

public WatchNextProgram getWatchNextProgram(long programId)

Retrieves a single WatchNext program from the system content provider (aka TvProvider).

public void updateWatchNextProgram(WatchNextProgram upgrade, long programId)

Updates a WatchNext program.

Source

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.tvprovider.media.tv;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;

import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.text.format.DateUtils;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.WorkerThread;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * From a user's perspective, the TV home screen has two types of channels: the single Live
 * Channels row versus the App preview Channels. This class is concerned with App Channels; or more
 * precisely: <i>your</i> app's preview Channels. In API 26+, all TV apps are allowed to create
 * multiple channels and publish those Channels to the home screen.
 * <p>
 * This class provides convenience methods to help you publish, update and delete channels; add,
 * update or remove programs in a channel. You do not need to know anything about Content
 * Providers, Content Resolvers, Cursors or such to publish your channels. This class abstracts
 * away all database interactions for you.
 * <p>
 * To make it easy for you to distinguish classes that help you build App Channels, the support
 * library uses the prefix Preview- to denote the classes that pertain to app Channels. Hence,
 * the classes {@link PreviewChannel} and {@link PreviewProgram} help your app add channels to the
 * TV home page.
 *
 * All calls to methods in the class should be made on worker threads.
 */

@WorkerThread
public class PreviewChannelHelper {

    private static final String TAG = "PreviewChannelHelper";
    private static final int DEFAULT_URL_CONNNECTION_TIMEOUT_MILLIS =
            (int) (3 * DateUtils.SECOND_IN_MILLIS);
    private static final int DEFAULT_READ_TIMEOUT_MILLIS = (int) (10 * DateUtils.SECOND_IN_MILLIS);
    private static final int INVALID_CONTENT_ID = -1;
    private final int mUrlConnectionTimeoutMillis;
    private final int mUrlReadTimeoutMillis;
    private final Context mContext;

    public PreviewChannelHelper(Context context) {
        this(context, DEFAULT_URL_CONNNECTION_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS);
    }

    /**
     * @param urlConnectionTimeoutMillis see {@link URLConnection#setConnectTimeout(int)}
     * @param urlReadTimeoutMillis       see {@link URLConnection#setReadTimeout(int)}
     */
    public PreviewChannelHelper(Context context, int urlConnectionTimeoutMillis,
            int urlReadTimeoutMillis) {
        mContext = context;
        mUrlConnectionTimeoutMillis = urlConnectionTimeoutMillis;
        mUrlReadTimeoutMillis = urlReadTimeoutMillis;
    }

    /**
     * Publishing a channel to the TV home screen is a two step process: first, you add the
     * channel to the TV content provider; second, you make the channel browsable (i.e. visible).
     * {@link #publishChannel(PreviewChannel) This method} adds the channel to the
     * TV content provider for you and returns a channelId. Next you must use the channelId
     * to make the channel browsable.
     * </br>
     * There are two ways you can make a channel browsable:
     * </br>
     * a) For your first channel, simply ask the system to make the channel browsable:
     * TvContractCompat.requestChannelBrowsable(context,channelId)
     * </br>
     * b) For any additional channel beyond the first channel, you must get permission
     * from the user. So if this channel is not your first channel, you must request user
     * permission through the following intent. So take the channelId returned by
     * {@link #publishChannel(PreviewChannel) this method} and do the following
     * inside an Activity or Fragment:
     * </br>
     * <pre>
     * intent = new Intent(TvContractCompat.ACTION_REQUEST_CHANNEL_BROWSABLE);
     * intent.putExtra(TvContractCompat.EXTRA_CHANNEL_ID, channelId);
     * startActivityForResult(intent, REQUEST_CHANNEL_BROWSABLE);
     * </pre>
     *
     * <p>
     * Creating a PreviewChannel, you may pass to the builder a
     * {@link PreviewChannel.Builder#setLogo(Uri) url as your logo}. In such case,
     * {@link #updatePreviewChannel(long, PreviewChannel)} will load the logo over the network. To
     * use your own networking code, override {@link #downloadBitmap(Uri)}.
     *
     * @return channelId or -1 if insertion fails. This is the id the system assigns to your
     * published channel. You can use it later to get a reference to this published PreviewChannel.
     */
    public long publishChannel(@NonNull PreviewChannel channel) throws IOException {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return INVALID_CONTENT_ID;
        }

        try {
            Uri channelUri = mContext.getContentResolver().insert(
                    TvContractCompat.Channels.CONTENT_URI,
                    channel.toContentValues());
            if (null == channelUri || channelUri.equals(Uri.EMPTY)) {
                throw new NullPointerException("Channel insertion failed");
            }
            long channelId = ContentUris.parseId(channelUri);
            boolean logoAdded = addChannelLogo(channelId, channel);
            // Rollback channel insertion if logo could not be added.
            if (!logoAdded) {
                deletePreviewChannel(channelId);
                throw new IOException("Failed to add logo, so channel (ID="
                        + channelId + ") was not created");
            }
            return channelId;
        } catch (SecurityException e) {
            Log.e(TAG, "Your app's ability to insert data into the TvProvider"
                    + " may have been revoked.", e);
        }
        return INVALID_CONTENT_ID;
    }

    /**
     * This is a convenience method that simply publishes your first channel for you. After calling
     * {@link #publishChannel(PreviewChannel)} to add the channel to the TvProvider, it
     * calls {@link TvContractCompat#requestChannelBrowsable(Context, long)} to make the channel
     * visible.
     * <p>
     * Only use this method to publish your first channel as you do not need user permission to
     * make your first channel browsable (i.e. visible on home screen). For additional channels,
     * see the documentations for {@link #publishChannel(PreviewChannel)}.
     *
     * <p>
     * Creating a PreviewChannel, you may pass to the builder a
     * {@link PreviewChannel.Builder#setLogo(Uri) url as your logo}. In such case,
     * {@link #updatePreviewChannel(long, PreviewChannel)} will load the logo over the network. To
     * use your own networking code, override {@link #downloadBitmap(Uri)}.
     *
     * @return channelId: This is the id the system assigns to your published channel. You can
     * use it later to get a reference to this published PreviewChannel.
     */
    public long publishDefaultChannel(@NonNull PreviewChannel channel) throws IOException {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return INVALID_CONTENT_ID;
        }

        long channelId = publishChannel(channel);
        TvContractCompat.requestChannelBrowsable(mContext, channelId);
        return channelId;
    }

    /**
     * The TvProvider does not allow select queries. Hence, unless you are querying for a
     * {@link #getPreviewChannel(long) single PreviewChannel by id}, you must get all of
     * your channels at once and then use the returned list as necessary.
     */
    public List<PreviewChannel> getAllChannels() {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return Collections.emptyList();
        }

        Cursor cursor = mContext.getContentResolver()
                .query(
                        TvContractCompat.Channels.CONTENT_URI,
                        PreviewChannel.Columns.PROJECTION,
                        null,
                        null,
                        null);

        List<PreviewChannel> channels = new ArrayList<>();
        if (cursor != null && cursor.moveToFirst()) {
            do {
                channels.add(PreviewChannel.fromCursor(cursor));
            } while (cursor.moveToNext());
        }
        return channels;
    }

    /**
     * Retrieves a single preview channel from the TvProvider. When you publish a preview channel,
     * the TvProvider assigns an ID to it. That's the channelId to use here.
     *
     * @param channelId ID of preview channel in TvProvider
     * @return PreviewChannel or null if not found
     */
    @Nullable
    public PreviewChannel getPreviewChannel(long channelId) {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return null;
        }

        PreviewChannel channel = null;
        Uri channelUri = TvContractCompat.buildChannelUri(channelId);
        Cursor cursor = mContext.getContentResolver()
                .query(channelUri, PreviewChannel.Columns.PROJECTION, null, null, null);
        if (cursor != null && cursor.moveToFirst()) {
            channel = PreviewChannel.fromCursor(cursor);
        }
        return channel;
    }

    /**
     * To update a preview channel, you need to use the {@link PreviewChannel.Builder} to set the
     * attributes you wish to change. Then simply pass in the built channel and the channelId of the
     * preview channel. (The channelId is the ID you received when you originally
     * {@link #publishChannel(PreviewChannel) published} the preview channel.)
     * <p>
     * Creating a PreviewChannel, you may pass to the builder a
     * {@link PreviewChannel.Builder#setLogo(Uri) url as your logo}. In such case,
     * {@link #updatePreviewChannel(long, PreviewChannel)} will load the logo over the network. To
     * use your own networking code, override {@link #downloadBitmap(Uri)}.
     */
    public void updatePreviewChannel(long channelId,
            @NonNull PreviewChannel update) throws IOException {
        // To avoid possibly expensive no-op updates, first check that the current content that's
        // in the database is different from the new content to be added.
        PreviewChannel curr = getPreviewChannel(channelId);
        if (curr != null && curr.hasAnyUpdatedValues(update)) {
            updatePreviewChannelInternal(channelId, update);
        }
        if (update.isLogoChanged()) {
            boolean logoAdded = addChannelLogo(channelId, update);
            if (!logoAdded) {
                throw new IOException("Fail to update channel (ID=" + channelId + ") logo.");
            }
        }
    }

    /**
     * Inner methods that does the actual work of updating a Preview Channel. The method is
     * extracted to make {@link #updatePreviewChannel(long, PreviewChannel)} testable.
     *
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    protected void updatePreviewChannelInternal(long channelId, @NonNull PreviewChannel upgrade) {
        mContext.getContentResolver().update(
                TvContractCompat.buildChannelUri(channelId),
                upgrade.toContentValues(),
                null,
                null);
    }

    /**
     * Internally, a logo is added to a channel after the channel has been added to the TvProvider.
     * This private method is called by one of the publish methods, to add a logo to the TvProvider
     * and associate the logo to the given channel identified by channelId. Because each channel
     * must have a logo, a NullPointerException is thrown if the channel being published has no
     * associated logo to publish with it.
     */
    @WorkerThread
    @SuppressLint("WrongThread") // TODO https://issuetracker.google.com/issues/116776070
    private boolean addChannelLogo(long channelId, @NonNull PreviewChannel channel) {
        boolean result = false;
        if (!channel.isLogoChanged()) {
            return result;
        }
        Bitmap logo = channel.getLogo(mContext);
        if (logo == null) {
            logo = getLogoFromUri(channel.getLogoUri());
        }
        Uri logoUri = TvContractCompat.buildChannelLogoUri(channelId);
        try (OutputStream outputStream = mContext.getContentResolver().openOutputStream(
                logoUri)) {
            result = logo.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
            outputStream.flush();
        } catch (SQLiteException | IOException | NullPointerException e) {
            Log.i(TAG, "Failed to add logo to the published channel (ID= " + channelId + ")", e);
        }
        return result;
    }

    /**
     * Handles the case where the Bitmap must be fetched from a known uri. First the
     * method checks if the Uri is local. If not, the method makes a connection to fetch the Bitmap
     * data from its remote location. To use your own networking implementation, simply override
     * {@link #downloadBitmap(Uri)}
     */
    private Bitmap getLogoFromUri(@NonNull Uri logoUri) {
        String scheme = logoUri.normalizeScheme().getScheme();
        InputStream inputStream = null;
        Bitmap logoImage = null;

        try {
            if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
                    || ContentResolver.SCHEME_FILE.equals(scheme)
                    || ContentResolver.SCHEME_CONTENT.equals(scheme)) {
                // for local resource
                inputStream = mContext.getContentResolver().openInputStream(logoUri);
                logoImage = BitmapFactory.decodeStream(inputStream);
            } else {
                logoImage = downloadBitmap(logoUri);
            }

        } catch (IOException e) {
            Log.e(TAG, "Failed to get logo from the URI: " + logoUri, e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // Do nothing
                }
            }
        }
        return logoImage;
    }

    /**
     * Downloads a Bitmap from a remote server. It is declared protected to allow you
     * to override it to use your own networking implementation if you so wish.
     */
    protected Bitmap downloadBitmap(@NonNull Uri logoUri) throws IOException {
        URLConnection urlConnection = null;
        InputStream inputStream = null;
        Bitmap logoImage = null;
        try {
            // for remote resource
            urlConnection = new URL(logoUri.toString()).openConnection();
            urlConnection.setConnectTimeout(mUrlConnectionTimeoutMillis);
            urlConnection.setReadTimeout(mUrlReadTimeoutMillis);
            inputStream = urlConnection.getInputStream();
            logoImage = BitmapFactory.decodeStream(inputStream);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // Do nothing
                }
            }
            if (urlConnection instanceof HttpURLConnection) {
                ((HttpURLConnection) urlConnection).disconnect();
            }
        }
        return logoImage;
    }

    /**
     * Removes a preview channel from the system's content provider (aka TvProvider).
     */
    public void deletePreviewChannel(long channelId) {
        mContext.getContentResolver().delete(
                TvContractCompat.buildChannelUri(channelId),
                null,
                null);
    }

    /**
     * Adds programs to a preview channel.
     */
    public long publishPreviewProgram(@NonNull PreviewProgram program) {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return INVALID_CONTENT_ID;
        }

        try {
            Uri programUri = mContext.getContentResolver().insert(
                    TvContractCompat.PreviewPrograms.CONTENT_URI,
                    program.toContentValues());
            long programId = ContentUris.parseId(programUri);
            return programId;
        } catch (SecurityException e) {
            Log.e(TAG, "Your app's ability to insert data into the TvProvider"
                    + " may have been revoked.", e);
        }
        return INVALID_CONTENT_ID;
    }

    /**
     * Retrieves a single preview program from the system content provider (aka TvProvider).
     */
    @Nullable
    public PreviewProgram getPreviewProgram(long programId) {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return null;
        }

        PreviewProgram program = null;
        Uri programUri = TvContractCompat.buildPreviewProgramUri(programId);
        Cursor cursor = mContext.getContentResolver().query(programUri, null, null, null, null);
        if (cursor != null && cursor.moveToFirst()) {
            program = PreviewProgram.fromCursor(cursor);
        }
        return program;
    }

    /**
     * Updates programs in a preview channel.
     */
    public void updatePreviewProgram(long programId, @NonNull PreviewProgram update) {
        // To avoid possibly expensive no-op updates, first check that the current content that's
        // in the database is different from the new content to be added.
        PreviewProgram curr = getPreviewProgram(programId);
        if (curr != null && curr.hasAnyUpdatedValues(update)) {
            updatePreviewProgramInternal(programId, update);
        }
    }

    /**
     * Inner methods that does the actual work of updating a Preview Program. The method is
     * extracted to make {@link #updatePreviewProgram(long, PreviewProgram)} testable.
     *
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    void updatePreviewProgramInternal(long programId, @NonNull PreviewProgram upgrade) {
        mContext.getContentResolver().update(
                TvContractCompat.buildPreviewProgramUri(programId),
                upgrade.toContentValues(), null, null);
    }

    /**
     * Removes programs from a preview channel.
     */
    public void deletePreviewProgram(long programId) {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return;
        }

        mContext.getContentResolver().delete(
                TvContractCompat.buildPreviewProgramUri(programId), null, null);
    }

    /**
     * Adds a program to the Watch Next channel
     */
    public long publishWatchNextProgram(@NonNull WatchNextProgram program) {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return INVALID_CONTENT_ID;
        }

        try {
            Uri programUri = mContext.getContentResolver().insert(
                    TvContractCompat.WatchNextPrograms.CONTENT_URI, program.toContentValues());
            return ContentUris.parseId(programUri);
        } catch (SecurityException e) {
            Log.e(TAG, "Your app's ability to insert data into the TvProvider"
                    + " may have been revoked.", e);
        }
        return INVALID_CONTENT_ID;
    }

    /**
     * Retrieves a single WatchNext program from the system content provider (aka TvProvider).
     */
    @Nullable
    public WatchNextProgram getWatchNextProgram(long programId) {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return null;
        }

        WatchNextProgram program = null;
        Uri programUri = TvContractCompat.buildWatchNextProgramUri(programId);
        Cursor cursor = mContext.getContentResolver().query(programUri, null, null, null, null);
        if (cursor != null && cursor.moveToFirst()) {
            program = WatchNextProgram.fromCursor(cursor);
        }
        return program;
    }

    /**
     * Updates a WatchNext program.
     */
    public void updateWatchNextProgram(@NonNull WatchNextProgram upgrade, long programId) {
        // To avoid possibly expensive no-op updates, first check that the current content that's in
        // the database is different from the new content to be added.
        WatchNextProgram curr = getWatchNextProgram(programId);
        if (curr != null && curr.hasAnyUpdatedValues(upgrade)) {
            updateWatchNextProgram(programId, upgrade);
        }
    }

    /**
     * Inner methods that does the actual work of updating a Watch Next Program. The method is
     * extracted to make {@link #updateWatchNextProgram(WatchNextProgram, long)} testable.
     *
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    void updateWatchNextProgram(long programId, @NonNull WatchNextProgram upgrade) {
        mContext.getContentResolver().update(
                TvContractCompat.buildWatchNextProgramUri(programId),
                upgrade.toContentValues(), null, null);
    }
}