tv-provider: support update without loading existing rows

Previously, it was required to load the existing program/channel
first to update the properties.

Test: ./gradlew support-tv-provider:connectedCheck
Bug: 62022533
Change-Id: I3e919b714324e8a99d5f849afee68dd48869937f
diff --git a/tv-provider/src/android/support/media/tv/BasePreviewProgram.java b/tv-provider/src/android/support/media/tv/BasePreviewProgram.java
index 35d2a03..d9aaf82 100644
--- a/tv-provider/src/android/support/media/tv/BasePreviewProgram.java
+++ b/tv-provider/src/android/support/media/tv/BasePreviewProgram.java
@@ -22,6 +22,7 @@
 import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.support.annotation.RestrictTo;
 import android.support.media.tv.TvContractCompat.PreviewProgramColumns;
 import android.support.media.tv.TvContractCompat.PreviewProgramColumns.AspectRatio;
@@ -29,13 +30,10 @@
 import android.support.media.tv.TvContractCompat.PreviewProgramColumns.InteractionType;
 import android.support.media.tv.TvContractCompat.PreviewProgramColumns.Type;
 import android.support.media.tv.TvContractCompat.PreviewPrograms;
-import android.support.v4.os.BuildCompat;
-import android.text.TextUtils;
 
 import java.net.URISyntaxException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
-import java.util.Objects;
 import java.util.TimeZone;
 
 /**
@@ -55,51 +53,8 @@
     private static final int IS_LIVE = 1;
     private static final int IS_BROWSABLE = 1;
 
-    private final String mInternalProviderId;
-    private final Uri mPreviewVideoUri;
-    private final int mLastPlaybackPositionMillis;
-    private final int mDurationMillis;
-    private final Uri mIntentUri;
-    private final int mTransient;
-    private final int mType;
-    private final int mPosterArtAspectRatio;
-    private final int mThumbnailAspectRatio;
-    private final Uri mLogoUri;
-    private final int mAvailability;
-    private final String mStartingPrice;
-    private final String mOfferPrice;
-    private final String mReleaseDate;
-    private final int mItemCount;
-    private final int mLive;
-    private final int mInteractionType;
-    private final long mInteractionCount;
-    private final String mAuthor;
-    private final int mBrowsable;
-    private final String mContentId;
-
     BasePreviewProgram(Builder builder) {
         super(builder);
-        mInternalProviderId = builder.mExternalId;
-        mPreviewVideoUri = builder.mPreviewVideoUri;
-        mLastPlaybackPositionMillis = builder.mLastPlaybackPositionMillis;
-        mDurationMillis = builder.mDurationMillis;
-        mIntentUri = builder.mIntentUri;
-        mTransient = builder.mTransient;
-        mType = builder.mType;
-        mPosterArtAspectRatio = builder.mPosterArtAspectRatio;
-        mThumbnailAspectRatio = builder.mThumbnailAspectRatio;
-        mLogoUri = builder.mLogoUri;
-        mAvailability = builder.mAvailability;
-        mStartingPrice = builder.mStartingPrice;
-        mOfferPrice = builder.mOfferPrice;
-        mReleaseDate = builder.mReleaseDate;
-        mItemCount = builder.mItemCount;
-        mLive = builder.mLive;
-        mInteractionType = builder.mInteractionType;
-        mInteractionCount = builder.mInteractionCount;
-        mAuthor = builder.mAuthor;
-        mBrowsable = builder.mBrowsable;
-        mContentId = builder.mContentId;
     }
 
     /**
@@ -107,7 +62,7 @@
      * @see PreviewPrograms#COLUMN_INTERNAL_PROVIDER_ID
      */
     public String getInternalProviderId() {
-        return mInternalProviderId;
+        return mValues.getAsString(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID);
     }
 
     /**
@@ -115,7 +70,8 @@
      * @see PreviewPrograms#COLUMN_PREVIEW_VIDEO_URI
      */
     public Uri getPreviewVideoUri() {
-        return mPreviewVideoUri;
+        String uri = mValues.getAsString(PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI);
+        return uri == null ? null : Uri.parse(uri);
     }
 
     /**
@@ -123,7 +79,8 @@
      * @see PreviewPrograms#COLUMN_LAST_PLAYBACK_POSITION_MILLIS
      */
     public int getLastPlaybackPositionMillis() {
-        return mLastPlaybackPositionMillis;
+        Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
@@ -131,7 +88,8 @@
      * @see PreviewPrograms#COLUMN_DURATION_MILLIS
      */
     public int getDurationMillis() {
-        return mDurationMillis;
+        Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_DURATION_MILLIS);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
@@ -139,7 +97,8 @@
      * @see PreviewPrograms#COLUMN_INTENT_URI
      */
     public Uri getIntentUri() {
-        return mIntentUri;
+        String uri = mValues.getAsString(PreviewPrograms.COLUMN_INTENT_URI);
+        return uri == null ? null : Uri.parse(uri);
     }
 
     /**
@@ -147,7 +106,8 @@
      * @see PreviewPrograms#COLUMN_INTENT_URI
      */
     public Intent getIntent() throws URISyntaxException {
-        return Intent.parseUri(mIntentUri.toString(), Intent.URI_INTENT_SCHEME);
+        String uri = mValues.getAsString(PreviewPrograms.COLUMN_INTENT_URI);
+        return uri == null ? null : Intent.parseUri(uri, Intent.URI_INTENT_SCHEME);
     }
 
     /**
@@ -155,7 +115,8 @@
      * @see PreviewPrograms#COLUMN_TRANSIENT
      */
     public boolean isTransient() {
-        return mTransient == IS_TRANSIENT;
+        Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_TRANSIENT);
+        return i != null && i == IS_TRANSIENT;
     }
 
     /**
@@ -163,7 +124,8 @@
      * @see PreviewPrograms#COLUMN_TYPE
      */
     public @Type int getType() {
-        return mType;
+        Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_TYPE);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
@@ -172,7 +134,8 @@
      * @see PreviewPrograms#COLUMN_POSTER_ART_URI
      */
     public @AspectRatio int getPosterArtAspectRatio() {
-        return mPosterArtAspectRatio;
+        Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
@@ -181,7 +144,8 @@
      * @see PreviewPrograms#COLUMN_THUMBNAIL_URI
      */
     public @AspectRatio int getThumbnailAspectRatio() {
-        return mThumbnailAspectRatio;
+        Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
@@ -189,7 +153,8 @@
      * @see PreviewPrograms#COLUMN_LOGO_URI
      */
     public Uri getLogoUri() {
-        return mLogoUri;
+        String uri = mValues.getAsString(PreviewPrograms.COLUMN_LOGO_URI);
+        return uri == null ? null : Uri.parse(uri);
     }
 
     /**
@@ -197,7 +162,8 @@
      * @see PreviewPrograms#COLUMN_AVAILABILITY
      */
     public @Availability int getAvailability() {
-        return mAvailability;
+        Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_AVAILABILITY);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
@@ -205,7 +171,7 @@
      * @see PreviewPrograms#COLUMN_STARTING_PRICE
      */
     public String getStartingPrice() {
-        return mStartingPrice;
+        return mValues.getAsString(PreviewPrograms.COLUMN_STARTING_PRICE);
     }
 
     /**
@@ -213,7 +179,7 @@
      * @see PreviewPrograms#COLUMN_OFFER_PRICE
      */
     public String getOfferPrice() {
-        return mOfferPrice;
+        return mValues.getAsString(PreviewPrograms.COLUMN_OFFER_PRICE);
     }
 
     /**
@@ -221,7 +187,7 @@
      * @see PreviewPrograms#COLUMN_RELEASE_DATE
      */
     public String getReleaseDate() {
-        return mReleaseDate;
+        return mValues.getAsString(PreviewPrograms.COLUMN_RELEASE_DATE);
     }
 
     /**
@@ -229,7 +195,8 @@
      * @see PreviewPrograms#COLUMN_ITEM_COUNT
      */
     public int getItemCount() {
-        return mItemCount;
+        Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_ITEM_COUNT);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
@@ -237,7 +204,8 @@
      * @see PreviewPrograms#COLUMN_LIVE
      */
     public boolean isLive() {
-        return mLive == IS_LIVE;
+        Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_LIVE);
+        return i != null && i == IS_LIVE;
     }
 
     /**
@@ -245,7 +213,8 @@
      * @see PreviewPrograms#COLUMN_INTERACTION_TYPE
      */
     public @InteractionType int getInteractionType() {
-        return mInteractionType;
+        Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_INTERACTION_TYPE);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
@@ -253,7 +222,8 @@
      * @see PreviewPrograms#COLUMN_INTERACTION_COUNT
      */
     public long getInteractionCount() {
-        return mInteractionCount;
+        Long l = mValues.getAsLong(PreviewPrograms.COLUMN_INTERACTION_COUNT);
+        return l == null ? INVALID_LONG_VALUE : l;
     }
 
     /**
@@ -261,7 +231,7 @@
      * @see PreviewPrograms#COLUMN_AUTHOR
      */
     public String getAuthor() {
-        return mAuthor;
+        return mValues.getAsString(PreviewPrograms.COLUMN_AUTHOR);
     }
 
     /**
@@ -269,7 +239,8 @@
      * @see PreviewPrograms#COLUMN_BROWSABLE;
      */
     public boolean isBrowsable() {
-        return mBrowsable == IS_BROWSABLE;
+        Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_BROWSABLE);
+        return i != null && i == IS_BROWSABLE;
     }
 
     /**
@@ -277,7 +248,7 @@
      * @see PreviewPrograms#COLUMN_CONTENT_ID
      */
     public String getContentId() {
-        return mContentId;
+        return mValues.getAsString(PreviewPrograms.COLUMN_CONTENT_ID);
     }
 
     @Override
@@ -285,32 +256,7 @@
         if (!(other instanceof BasePreviewProgram)) {
             return false;
         }
-        if (!super.equals(other)) {
-            return false;
-        }
-        BasePreviewProgram program = (BasePreviewProgram) other;
-        return Objects.equals(mInternalProviderId, program.mInternalProviderId)
-                && Objects.equals(mPreviewVideoUri, program.mPreviewVideoUri)
-                && Objects.equals(mLastPlaybackPositionMillis,
-                program.mLastPlaybackPositionMillis)
-                && Objects.equals(mDurationMillis, program.mDurationMillis)
-                && Objects.equals(mIntentUri, program.mIntentUri)
-                && Objects.equals(mTransient, program.mTransient)
-                && Objects.equals(mType, program.mType)
-                && Objects.equals(mPosterArtAspectRatio, program.mPosterArtAspectRatio)
-                && Objects.equals(mThumbnailAspectRatio, program.mThumbnailAspectRatio)
-                && Objects.equals(mLogoUri, program.mLogoUri)
-                && Objects.equals(mAvailability, program.mAvailability)
-                && Objects.equals(mStartingPrice, program.mStartingPrice)
-                && Objects.equals(mOfferPrice, program.mOfferPrice)
-                && Objects.equals(mReleaseDate, program.mReleaseDate)
-                && Objects.equals(mItemCount, program.mItemCount)
-                && Objects.equals(mLive, program.mLive)
-                && Objects.equals(mInteractionType, program.mInteractionType)
-                && Objects.equals(mInteractionCount, program.mInteractionCount)
-                && Objects.equals(mAuthor, program.mAuthor)
-                && Objects.equals(mBrowsable, program.mBrowsable)
-                && Objects.equals(mContentId, program.mContentId);
+        return mValues.equals(((BasePreviewProgram) other).mValues);
     }
 
     /**
@@ -332,78 +278,29 @@
     @RestrictTo(LIBRARY_GROUP)
     public ContentValues toContentValues(boolean includeProtectedFields) {
         ContentValues values = super.toContentValues();
-        if (BuildCompat.isAtLeastO()) {
-            if (!TextUtils.isEmpty(mInternalProviderId)) {
-                values.put(PreviewProgramColumns.COLUMN_INTERNAL_PROVIDER_ID, mInternalProviderId);
-            }
-            if (mPreviewVideoUri != null) {
-                values.put(PreviewProgramColumns.COLUMN_PREVIEW_VIDEO_URI,
-                        mPreviewVideoUri.toString());
-            }
-            if (mLastPlaybackPositionMillis != INVALID_INT_VALUE) {
-                values.put(PreviewProgramColumns.COLUMN_LAST_PLAYBACK_POSITION_MILLIS,
-                        mLastPlaybackPositionMillis);
-            }
-            if (mDurationMillis != INVALID_INT_VALUE) {
-                values.put(PreviewProgramColumns.COLUMN_DURATION_MILLIS, mDurationMillis);
-            }
-            if (mIntentUri != null) {
-                values.put(PreviewProgramColumns.COLUMN_INTENT_URI, mIntentUri.toString());
-            }
-            if (mTransient != INVALID_INT_VALUE) {
-                values.put(PreviewProgramColumns.COLUMN_TRANSIENT, mTransient);
-            }
-            if (mType != INVALID_INT_VALUE) {
-                values.put(PreviewProgramColumns.COLUMN_TYPE, mType);
-            }
-            if (mPosterArtAspectRatio != INVALID_INT_VALUE) {
-                values.put(PreviewProgramColumns.COLUMN_POSTER_ART_ASPECT_RATIO,
-                        mPosterArtAspectRatio);
-            }
-            if (mThumbnailAspectRatio != INVALID_INT_VALUE) {
-                values.put(PreviewProgramColumns.COLUMN_THUMBNAIL_ASPECT_RATIO,
-                        mThumbnailAspectRatio);
-            }
-            if (mLogoUri != null) {
-                values.put(PreviewProgramColumns.COLUMN_LOGO_URI, mLogoUri.toString());
-            }
-            if (mAvailability != INVALID_INT_VALUE) {
-                values.put(PreviewProgramColumns.COLUMN_AVAILABILITY, mAvailability);
-            }
-            if (!TextUtils.isEmpty(mStartingPrice)) {
-                values.put(PreviewProgramColumns.COLUMN_STARTING_PRICE, mStartingPrice);
-            }
-            if (!TextUtils.isEmpty(mOfferPrice)) {
-                values.put(PreviewProgramColumns.COLUMN_OFFER_PRICE, mOfferPrice);
-            }
-            if (!TextUtils.isEmpty(mReleaseDate)) {
-                values.put(PreviewProgramColumns.COLUMN_RELEASE_DATE, mReleaseDate);
-            }
-            if (mItemCount != INVALID_INT_VALUE) {
-                values.put(PreviewProgramColumns.COLUMN_ITEM_COUNT, mItemCount);
-            }
-            if (mLive != INVALID_INT_VALUE) {
-                values.put(PreviewProgramColumns.COLUMN_LIVE, mLive);
-            }
-            if (mInteractionType != INVALID_INT_VALUE) {
-                values.put(PreviewProgramColumns.COLUMN_INTERACTION_TYPE, mInteractionType);
-            }
-            if (mInteractionCount != INVALID_LONG_VALUE) {
-                values.put(PreviewProgramColumns.COLUMN_INTERACTION_COUNT, mInteractionCount);
-            }
-            if (!TextUtils.isEmpty(mAuthor)) {
-                values.put(PreviewProgramColumns.COLUMN_AUTHOR, mAuthor);
-            }
-            if (!TextUtils.isEmpty(mContentId)) {
-                values.put(PreviewProgramColumns.COLUMN_CONTENT_ID, mContentId);
-            }
-            if (includeProtectedFields) {
-                if (BuildCompat.isAtLeastO()) {
-                    if (mBrowsable != INVALID_INT_VALUE) {
-                        values.put(PreviewProgramColumns.COLUMN_BROWSABLE, mBrowsable);
-                    }
-                }
-            }
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+            values.remove(PreviewProgramColumns.COLUMN_INTERNAL_PROVIDER_ID);
+            values.remove(PreviewProgramColumns.COLUMN_PREVIEW_VIDEO_URI);
+            values.remove(PreviewProgramColumns.COLUMN_LAST_PLAYBACK_POSITION_MILLIS);
+            values.remove(PreviewProgramColumns.COLUMN_DURATION_MILLIS);
+            values.remove(PreviewProgramColumns.COLUMN_INTENT_URI);
+            values.remove(PreviewProgramColumns.COLUMN_TRANSIENT);
+            values.remove(PreviewProgramColumns.COLUMN_TYPE);
+            values.remove(PreviewProgramColumns.COLUMN_POSTER_ART_ASPECT_RATIO);
+            values.remove(PreviewProgramColumns.COLUMN_THUMBNAIL_ASPECT_RATIO);
+            values.remove(PreviewProgramColumns.COLUMN_LOGO_URI);
+            values.remove(PreviewProgramColumns.COLUMN_AVAILABILITY);
+            values.remove(PreviewProgramColumns.COLUMN_STARTING_PRICE);
+            values.remove(PreviewProgramColumns.COLUMN_OFFER_PRICE);
+            values.remove(PreviewProgramColumns.COLUMN_RELEASE_DATE);
+            values.remove(PreviewProgramColumns.COLUMN_ITEM_COUNT);
+            values.remove(PreviewProgramColumns.COLUMN_LIVE);
+            values.remove(PreviewProgramColumns.COLUMN_INTERACTION_COUNT);
+            values.remove(PreviewProgramColumns.COLUMN_AUTHOR);
+            values.remove(PreviewProgramColumns.COLUMN_CONTENT_ID);
+        }
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !includeProtectedFields) {
+            values.remove(PreviewProgramColumns.COLUMN_BROWSABLE);
         }
         return values;
     }
@@ -418,7 +315,7 @@
         // TODO: Add additional API which does not use costly getColumnIndex().
         BaseProgram.setFieldsFromCursor(cursor, builder);
         int index;
-        if (BuildCompat.isAtLeastO()) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             if ((index =
                     cursor.getColumnIndex(PreviewProgramColumns.COLUMN_INTERNAL_PROVIDER_ID)) >= 0
                     && !cursor.isNull(index)) {
@@ -552,28 +449,6 @@
             sFormat.setTimeZone(TimeZone.getTimeZone("GMT-0"));
         }
 
-        private String mExternalId;
-        private Uri mPreviewVideoUri;
-        private int mLastPlaybackPositionMillis = INVALID_INT_VALUE;
-        private int mDurationMillis = INVALID_INT_VALUE;
-        private Uri mIntentUri;
-        private int mTransient = INVALID_INT_VALUE;
-        private int mType = INVALID_INT_VALUE;
-        private int mPosterArtAspectRatio = INVALID_INT_VALUE;
-        private int mThumbnailAspectRatio = INVALID_INT_VALUE;
-        private Uri mLogoUri;
-        private int mAvailability = INVALID_INT_VALUE;
-        private String mStartingPrice;
-        private String mOfferPrice;
-        private String mReleaseDate;
-        private int mItemCount = INVALID_INT_VALUE;
-        private int mLive = INVALID_INT_VALUE;
-        private int mInteractionType = INVALID_INT_VALUE;
-        private long mInteractionCount = INVALID_LONG_VALUE;
-        private String mAuthor;
-        private int mBrowsable = INVALID_INT_VALUE;
-        private String mContentId;
-
         /**
          * Creates a new Builder object.
          */
@@ -585,28 +460,7 @@
          * @param other The Program you're copying from.
          */
         public Builder(BasePreviewProgram other) {
-            super(other);
-            mExternalId = other.mInternalProviderId;
-            mPreviewVideoUri = other.mPreviewVideoUri;
-            mLastPlaybackPositionMillis = other.mLastPlaybackPositionMillis;
-            mDurationMillis = other.mDurationMillis;
-            mIntentUri = other.mIntentUri;
-            mTransient = other.mTransient;
-            mType = other.mType;
-            mPosterArtAspectRatio = other.mPosterArtAspectRatio;
-            mThumbnailAspectRatio = other.mThumbnailAspectRatio;
-            mLogoUri = other.mLogoUri;
-            mAvailability = other.mAvailability;
-            mStartingPrice = other.mStartingPrice;
-            mOfferPrice = other.mOfferPrice;
-            mReleaseDate = other.mReleaseDate;
-            mItemCount = other.mItemCount;
-            mLive = other.mLive;
-            mInteractionType = other.mInteractionType;
-            mInteractionCount  = other.mInteractionCount;
-            mAuthor = other.mAuthor;
-            mBrowsable = other.mBrowsable;
-            mContentId = other.mContentId;
+            mValues = new ContentValues(other.mValues);
         }
 
         /**
@@ -617,7 +471,7 @@
          * @see PreviewPrograms#COLUMN_INTERNAL_PROVIDER_ID
          */
         public T setInternalProviderId(String externalId) {
-            mExternalId = externalId;
+            mValues.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, externalId);
             return (T) this;
         }
 
@@ -629,7 +483,8 @@
          * @see PreviewPrograms#COLUMN_PREVIEW_VIDEO_URI
          */
         public T setPreviewVideoUri(Uri previewVideoUri) {
-            mPreviewVideoUri = previewVideoUri;
+            mValues.put(PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI,
+                    previewVideoUri == null ? null : previewVideoUri.toString());
             return (T) this;
         }
 
@@ -641,7 +496,7 @@
          * @see PreviewPrograms#COLUMN_LAST_PLAYBACK_POSITION_MILLIS
          */
         public T setLastPlaybackPositionMillis(int position) {
-            mLastPlaybackPositionMillis = position;
+            mValues.put(PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS, position);
             return (T) this;
         }
 
@@ -653,7 +508,7 @@
          * @see PreviewPrograms#COLUMN_DURATION_MILLIS
          */
         public T setDurationMillis(int duration) {
-            mDurationMillis = duration;
+            mValues.put(PreviewPrograms.COLUMN_DURATION_MILLIS, duration);
             return (T) this;
         }
 
@@ -665,7 +520,8 @@
          * @see PreviewPrograms#COLUMN_INTENT_URI
          */
         public T setIntentUri(Uri intentUri) {
-            mIntentUri = intentUri;
+            mValues.put(PreviewPrograms.COLUMN_INTENT_URI,
+                    intentUri == null ? null : intentUri.toString());
             return (T) this;
         }
 
@@ -687,7 +543,7 @@
          * @see PreviewPrograms#COLUMN_TRANSIENT
          */
         public T setTransient(boolean transientValue) {
-            mTransient = transientValue ? IS_TRANSIENT : 0;
+            mValues.put(PreviewPrograms.COLUMN_TRANSIENT, transientValue ? IS_TRANSIENT : 0);
             return (T) this;
         }
 
@@ -713,7 +569,7 @@
          * @see PreviewPrograms#COLUMN_TYPE
          */
         public T setType(@Type int type) {
-            mType = type;
+            mValues.put(PreviewPrograms.COLUMN_TYPE, type);
             return (T) this;
         }
 
@@ -733,7 +589,7 @@
          * @see PreviewPrograms#COLUMN_POSTER_ART_URI
          */
         public T setPosterArtAspectRatio(@AspectRatio int ratio) {
-            mPosterArtAspectRatio = ratio;
+            mValues.put(PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, ratio);
             return (T) this;
         }
 
@@ -752,7 +608,7 @@
          * @see PreviewPrograms#COLUMN_THUMBNAIL_ASPECT_RATIO
          */
         public T setThumbnailAspectRatio(@AspectRatio int ratio) {
-            mThumbnailAspectRatio = ratio;
+            mValues.put(PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO, ratio);
             return (T) this;
         }
 
@@ -764,7 +620,8 @@
          * @see PreviewPrograms#COLUMN_LOGO_URI
          */
         public T setLogoUri(Uri logoUri) {
-            mLogoUri = logoUri;
+            mValues.put(PreviewPrograms.COLUMN_LOGO_URI,
+                    logoUri == null ? null : logoUri.toString());
             return (T) this;
         }
 
@@ -781,7 +638,7 @@
          * @see PreviewPrograms#COLUMN_AVAILABILITY
          */
         public T setAvailability(@Availability int availability) {
-            mAvailability = availability;
+            mValues.put(PreviewPrograms.COLUMN_AVAILABILITY, availability);
             return (T) this;
         }
 
@@ -793,7 +650,7 @@
          * @see PreviewPrograms#COLUMN_STARTING_PRICE
          */
         public T setStartingPrice(String price) {
-            mStartingPrice = price;
+            mValues.put(PreviewPrograms.COLUMN_STARTING_PRICE, price);
             return (T) this;
         }
 
@@ -805,7 +662,7 @@
          * @see PreviewPrograms#COLUMN_OFFER_PRICE
          */
         public T setOfferPrice(String price) {
-            mOfferPrice = price;
+            mValues.put(PreviewPrograms.COLUMN_OFFER_PRICE, price);
             return (T) this;
         }
 
@@ -820,7 +677,7 @@
          * @see PreviewPrograms#COLUMN_RELEASE_DATE
          */
         public T setReleaseDate(String releaseDate) {
-            mReleaseDate = releaseDate;
+            mValues.put(PreviewPrograms.COLUMN_RELEASE_DATE, releaseDate);
             return (T) this;
         }
 
@@ -832,7 +689,7 @@
          * @see PreviewPrograms#COLUMN_RELEASE_DATE
          */
         public T setReleaseDate(Date releaseDate) {
-            mReleaseDate = sFormat.format(releaseDate);
+            mValues.put(PreviewPrograms.COLUMN_RELEASE_DATE, sFormat.format(releaseDate));
             return (T) this;
         }
 
@@ -844,7 +701,7 @@
          * @see PreviewPrograms#COLUMN_ITEM_COUNT
          */
         public T setItemCount(int itemCount) {
-            mItemCount = itemCount;
+            mValues.put(PreviewPrograms.COLUMN_ITEM_COUNT, itemCount);
             return (T) this;
         }
 
@@ -856,7 +713,7 @@
          * @see PreviewPrograms#COLUMN_LIVE
          */
         public T setLive(boolean live) {
-            mLive = live ? IS_LIVE : 0;
+            mValues.put(PreviewPrograms.COLUMN_LIVE, live ? IS_LIVE : 0);
             return (T) this;
         }
 
@@ -877,7 +734,7 @@
          * @see PreviewPrograms#COLUMN_INTERACTION_TYPE
          */
         public T setInteractionType(@InteractionType int interactionType) {
-            mInteractionType = interactionType;
+            mValues.put(PreviewPrograms.COLUMN_INTERACTION_TYPE, interactionType);
             return (T) this;
         }
 
@@ -889,7 +746,7 @@
          * @see PreviewPrograms#COLUMN_INTERACTION_COUNT
          */
         public T setInteractionCount(long interactionCount) {
-            mInteractionCount = interactionCount;
+            mValues.put(PreviewPrograms.COLUMN_INTERACTION_COUNT, interactionCount);
             return (T) this;
         }
 
@@ -901,7 +758,7 @@
          * @see PreviewPrograms#COLUMN_AUTHOR
          */
         public T setAuthor(String author) {
-            mAuthor = author;
+            mValues.put(PreviewPrograms.COLUMN_AUTHOR, author);
             return (T) this;
         }
 
@@ -915,7 +772,7 @@
          */
         @RestrictTo(LIBRARY_GROUP)
         public T setBrowsable(boolean browsable) {
-            mBrowsable = browsable ? IS_BROWSABLE : 0;
+            mValues.put(PreviewPrograms.COLUMN_BROWSABLE, browsable ? IS_BROWSABLE : 0);
             return (T) this;
         }
 
@@ -927,7 +784,7 @@
          * @see PreviewPrograms#COLUMN_CONTENT_ID
          */
         public T setContentId(String contentId) {
-            mContentId = contentId;
+            mValues.put(PreviewPrograms.COLUMN_CONTENT_ID, contentId);
             return (T) this;
         }
     }
diff --git a/tv-provider/src/android/support/media/tv/BaseProgram.java b/tv-provider/src/android/support/media/tv/BaseProgram.java
index 4b9223e..885b544 100644
--- a/tv-provider/src/android/support/media/tv/BaseProgram.java
+++ b/tv-provider/src/android/support/media/tv/BaseProgram.java
@@ -28,11 +28,6 @@
 import android.support.media.tv.TvContractCompat.ProgramColumns.ReviewRatingStyle;
 import android.support.media.tv.TvContractCompat.Programs;
 import android.support.media.tv.TvContractCompat.Programs.Genres.Genre;
-import android.support.v4.os.BuildCompat;
-import android.text.TextUtils;
-
-import java.util.Arrays;
-import java.util.Objects;
 
 /**
  * Base class for derived classes that want to have common fields for programs defined in
@@ -49,57 +44,13 @@
     private static final int INVALID_INT_VALUE = -1;
     private static final int IS_SEARCHABLE = 1;
 
-    private final long mId;
-    private final String mPackageName;
-    private final String mTitle;
-    private final String mEpisodeTitle;
-    private final String mSeasonNumber;
-    private final String mEpisodeNumber;
-    private final String mDescription;
-    private final String mLongDescription;
-    private final int mVideoWidth;
-    private final int mVideoHeight;
-    private final Uri mPosterArtUri;
-    private final Uri mThumbnailUri;
-    private final String[] mCanonicalGenres;
-    private final TvContentRating[] mContentRatings;
-    private final byte[] mInternalProviderData;
-    private final String[] mAudioLanguages;
-    private final int mSearchable;
-    private final Long mInternalProviderFlag1;
-    private final Long mInternalProviderFlag2;
-    private final Long mInternalProviderFlag3;
-    private final Long mInternalProviderFlag4;
-    private final int mReviewRatingStyle;
-    private final String mReviewRating;
-    private final String mSeasonTitle;
+    /** @hide */
+    @RestrictTo(LIBRARY_GROUP)
+    protected ContentValues mValues;
 
     /* package-private */
     BaseProgram(Builder builder) {
-        mId = builder.mId;
-        mPackageName = builder.mPackageName;
-        mTitle = builder.mTitle;
-        mEpisodeTitle = builder.mEpisodeTitle;
-        mSeasonNumber = builder.mSeasonNumber;
-        mEpisodeNumber = builder.mEpisodeNumber;
-        mDescription = builder.mDescription;
-        mLongDescription = builder.mLongDescription;
-        mVideoWidth = builder.mVideoWidth;
-        mVideoHeight = builder.mVideoHeight;
-        mPosterArtUri = builder.mPosterArtUri;
-        mThumbnailUri = builder.mThumbnailUri;
-        mCanonicalGenres = builder.mCanonicalGenres;
-        mContentRatings = builder.mContentRatings;
-        mInternalProviderData = builder.mInternalProviderData;
-        mAudioLanguages = builder.mAudioLanguages;
-        mSearchable = builder.mSearchable;
-        mInternalProviderFlag1 = builder.mInternalProviderFlag1;
-        mInternalProviderFlag2 = builder.mInternalProviderFlag2;
-        mInternalProviderFlag3 = builder.mInternalProviderFlag3;
-        mInternalProviderFlag4 = builder.mInternalProviderFlag4;
-        mReviewRatingStyle = builder.mReviewRatingStyle;
-        mReviewRating = builder.mReviewRating;
-        mSeasonTitle = builder.mSeasonTitle;
+        mValues = builder.mValues;
     }
 
     /**
@@ -107,7 +58,8 @@
      * @see BaseTvColumns#_ID
      */
     public long getId() {
-        return mId;
+        Long l = mValues.getAsLong(BaseTvColumns._ID);
+        return l == null ? INVALID_LONG_VALUE : l;
     }
 
     /**
@@ -117,7 +69,7 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     public String getPackageName() {
-        return mPackageName;
+        return mValues.getAsString(BaseTvColumns.COLUMN_PACKAGE_NAME);
     }
 
     /**
@@ -125,7 +77,7 @@
      * @see Programs#COLUMN_TITLE
      */
     public String getTitle() {
-        return mTitle;
+        return mValues.getAsString(Programs.COLUMN_TITLE);
     }
 
     /**
@@ -133,7 +85,7 @@
      * @see Programs#COLUMN_EPISODE_TITLE
      */
     public String getEpisodeTitle() {
-        return mEpisodeTitle;
+        return mValues.getAsString(Programs.COLUMN_EPISODE_TITLE);
     }
 
     /**
@@ -141,7 +93,11 @@
      * @see Programs#COLUMN_SEASON_DISPLAY_NUMBER
      */
     public String getSeasonNumber() {
-        return mSeasonNumber;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            return mValues.getAsString(Programs.COLUMN_SEASON_DISPLAY_NUMBER);
+        } else {
+            return mValues.getAsString(Programs.COLUMN_SEASON_NUMBER);
+        }
     }
 
     /**
@@ -149,7 +105,11 @@
      * @see Programs#COLUMN_EPISODE_DISPLAY_NUMBER
      */
     public String getEpisodeNumber() {
-        return mEpisodeNumber;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            return mValues.getAsString(Programs.COLUMN_EPISODE_DISPLAY_NUMBER);
+        } else {
+            return mValues.getAsString(Programs.COLUMN_EPISODE_NUMBER);
+        }
     }
 
     /**
@@ -157,7 +117,7 @@
      * @see Programs#COLUMN_SHORT_DESCRIPTION
      */
     public String getDescription() {
-        return mDescription;
+        return mValues.getAsString(Programs.COLUMN_SHORT_DESCRIPTION);
     }
 
     /**
@@ -165,7 +125,7 @@
      * @see Programs#COLUMN_LONG_DESCRIPTION
      */
     public String getLongDescription() {
-        return mLongDescription;
+        return mValues.getAsString(Programs.COLUMN_LONG_DESCRIPTION);
     }
 
     /**
@@ -173,7 +133,8 @@
      * @see Programs#COLUMN_VIDEO_WIDTH
      */
     public int getVideoWidth() {
-        return mVideoWidth;
+        Integer i = mValues.getAsInteger(Programs.COLUMN_VIDEO_WIDTH);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
@@ -181,7 +142,8 @@
      * @see Programs#COLUMN_VIDEO_HEIGHT
      */
     public int getVideoHeight() {
-        return mVideoHeight;
+        Integer i = mValues.getAsInteger(Programs.COLUMN_VIDEO_HEIGHT);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
@@ -189,7 +151,7 @@
      * @see Programs#COLUMN_CANONICAL_GENRE
      */
     public @Genre String[] getCanonicalGenres() {
-        return mCanonicalGenres;
+        return Programs.Genres.decode(mValues.getAsString(Programs.COLUMN_CANONICAL_GENRE));
     }
 
     /**
@@ -197,7 +159,8 @@
      * @see Programs#COLUMN_CONTENT_RATING
      */
     public TvContentRating[] getContentRatings() {
-        return mContentRatings;
+        return TvContractUtils.stringToContentRatings(mValues.getAsString(
+                Programs.COLUMN_CONTENT_RATING));
     }
 
     /**
@@ -205,7 +168,8 @@
      * @see Programs#COLUMN_POSTER_ART_URI
      */
     public Uri getPosterArtUri() {
-        return mPosterArtUri;
+        String uri = mValues.getAsString(Programs.COLUMN_POSTER_ART_URI);
+        return uri == null ? null : Uri.parse(uri);
     }
 
     /**
@@ -213,7 +177,8 @@
      * @see Programs#COLUMN_THUMBNAIL_URI
      */
     public Uri getThumbnailUri() {
-        return mThumbnailUri;
+        String uri = mValues.getAsString(Programs.COLUMN_POSTER_ART_URI);
+        return uri == null ? null : Uri.parse(uri);
     }
 
     /**
@@ -221,7 +186,7 @@
      * @see Programs#COLUMN_INTERNAL_PROVIDER_DATA
      */
     public byte[] getInternalProviderDataByteArray() {
-        return mInternalProviderData;
+        return mValues.getAsByteArray(Programs.COLUMN_INTERNAL_PROVIDER_DATA);
     }
 
     /**
@@ -229,7 +194,8 @@
      * @see Programs#COLUMN_AUDIO_LANGUAGE
      */
     public String[] getAudioLanguages() {
-        return mAudioLanguages;
+        return TvContractUtils.stringToAudioLanguages(mValues.getAsString(
+                Programs.COLUMN_AUDIO_LANGUAGE));
     }
 
     /**
@@ -237,7 +203,8 @@
      * @see Programs#COLUMN_SEARCHABLE
      */
     public boolean isSearchable() {
-        return mSearchable == IS_SEARCHABLE || mSearchable == INVALID_INT_VALUE;
+        Integer i = mValues.getAsInteger(Programs.COLUMN_SEARCHABLE);
+        return i == null || i == IS_SEARCHABLE;
     }
 
     /**
@@ -245,7 +212,7 @@
      * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG1
      */
     public Long getInternalProviderFlag1() {
-        return mInternalProviderFlag1;
+        return mValues.getAsLong(Programs.COLUMN_INTERNAL_PROVIDER_FLAG1);
     }
 
     /**
@@ -253,7 +220,7 @@
      * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG2
      */
     public Long getInternalProviderFlag2() {
-        return mInternalProviderFlag2;
+        return mValues.getAsLong(Programs.COLUMN_INTERNAL_PROVIDER_FLAG2);
     }
 
     /**
@@ -261,7 +228,7 @@
      * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG3
      */
     public Long getInternalProviderFlag3() {
-        return mInternalProviderFlag3;
+        return mValues.getAsLong(Programs.COLUMN_INTERNAL_PROVIDER_FLAG3);
     }
 
     /**
@@ -269,7 +236,7 @@
      * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG4
      */
     public Long getInternalProviderFlag4() {
-        return mInternalProviderFlag4;
+        return mValues.getAsLong(Programs.COLUMN_INTERNAL_PROVIDER_FLAG4);
     }
 
     /**
@@ -277,7 +244,7 @@
      * @see Programs#COLUMN_SEASON_TITLE
      */
     public String getSeasonTitle() {
-        return mSeasonTitle;
+        return mValues.getAsString(Programs.COLUMN_SEASON_TITLE);
     }
 
     /**
@@ -285,7 +252,8 @@
      * @see Programs#COLUMN_REVIEW_RATING_STYLE
      */
     public @ReviewRatingStyle int getReviewRatingStyle() {
-        return mReviewRatingStyle;
+        Integer i = mValues.getAsInteger(Programs.COLUMN_REVIEW_RATING_STYLE);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
@@ -293,15 +261,12 @@
      * @see Programs#COLUMN_REVIEW_RATING
      */
     public String getReviewRating() {
-        return mReviewRating;
+        return mValues.getAsString(Programs.COLUMN_REVIEW_RATING);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(
-                mTitle, mEpisodeTitle, mDescription, mLongDescription, mVideoWidth, mVideoHeight,
-                mPosterArtUri, mThumbnailUri, Arrays.hashCode(mContentRatings),
-                Arrays.hashCode(mCanonicalGenres), mSeasonNumber, mEpisodeNumber);
+        return mValues.hashCode();
     }
 
     @Override
@@ -309,51 +274,12 @@
         if (!(other instanceof BaseProgram)) {
             return false;
         }
-        BaseProgram program = (BaseProgram) other;
-        return Objects.equals(mTitle, program.mTitle)
-                && Objects.equals(mEpisodeTitle, program.mEpisodeTitle)
-                && Objects.equals(mSeasonNumber, program.mSeasonNumber)
-                && Objects.equals(mEpisodeNumber, program.mEpisodeNumber)
-                && Objects.equals(mDescription, program.mDescription)
-                && Objects.equals(mLongDescription, program.mLongDescription)
-                && mVideoWidth == program.mVideoWidth
-                && mVideoHeight == program.mVideoHeight
-                && Objects.equals(mPosterArtUri, program.mPosterArtUri)
-                && Objects.equals(mThumbnailUri, program.mThumbnailUri)
-                && Arrays.equals(mInternalProviderData, program.mInternalProviderData)
-                && Arrays.equals(mCanonicalGenres, program.mCanonicalGenres)
-                && Arrays.equals(mContentRatings, program.mContentRatings)
-                && Arrays.equals(mAudioLanguages, program.mAudioLanguages)
-                && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
-                        || (Objects.equals(mSearchable, program.mSearchable)
-                        && Objects.equals(mInternalProviderFlag1, program.mInternalProviderFlag1)
-                        && Objects.equals(mInternalProviderFlag2, program.mInternalProviderFlag2)
-                        && Objects.equals(mInternalProviderFlag3, program.mInternalProviderFlag3)
-                        && Objects.equals(mInternalProviderFlag4, program.mInternalProviderFlag4)))
-                && (Build.VERSION.SDK_INT < Build.VERSION_CODES.N
-                        || Objects.equals(mSeasonTitle, program.mSeasonTitle))
-                && (!BuildCompat.isAtLeastO()
-                        || (Objects.equals(mReviewRatingStyle, program.mReviewRatingStyle)
-                        && Objects.equals(mReviewRating, program.mReviewRating)));
+        return mValues.equals(((BaseProgram) other).mValues);
     }
 
     @Override
     public String toString() {
-        return "BaseProgram{"
-                + "id=" + mId
-                + ", packageName=" + mPackageName
-                + ", title=" + mTitle
-                + ", episodeTitle=" + mEpisodeTitle
-                + ", seasonNumber=" + mSeasonNumber
-                + ", episodeNumber=" + mEpisodeNumber
-                + ", videoWidth=" + mVideoWidth
-                + ", videoHeight=" + mVideoHeight
-                + ", contentRatings=" + Arrays.toString(mContentRatings)
-                + ", posterArtUri=" + mPosterArtUri
-                + ", thumbnailUri=" + mThumbnailUri
-                + ", contentRatings=" + Arrays.toString(mContentRatings)
-                + ", genres=" + Arrays.toString(mCanonicalGenres)
-                + "}";
+        return "BaseProgram{" + mValues.toString() + "}";
     }
 
     /**
@@ -361,129 +287,20 @@
      * into the TV Input Framework database.
      */
     public ContentValues toContentValues() {
-        ContentValues values = new ContentValues();
-        if (mId != INVALID_LONG_VALUE) {
-            values.put(BaseTvColumns._ID, mId);
+        ContentValues values = new ContentValues(mValues);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+            values.remove(ProgramColumns.COLUMN_SEARCHABLE);
+            values.remove(ProgramColumns.COLUMN_INTERNAL_PROVIDER_FLAG1);
+            values.remove(ProgramColumns.COLUMN_INTERNAL_PROVIDER_FLAG2);
+            values.remove(ProgramColumns.COLUMN_INTERNAL_PROVIDER_FLAG3);
+            values.remove(ProgramColumns.COLUMN_INTERNAL_PROVIDER_FLAG4);
         }
-        if (!TextUtils.isEmpty(mPackageName)) {
-            values.put(BaseTvColumns.COLUMN_PACKAGE_NAME, mPackageName);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+            values.remove(ProgramColumns.COLUMN_SEASON_TITLE);
         }
-        if (!TextUtils.isEmpty(mTitle)) {
-            values.put(ProgramColumns.COLUMN_TITLE, mTitle);
-        } else {
-            values.putNull(ProgramColumns.COLUMN_TITLE);
-        }
-        if (!TextUtils.isEmpty(mEpisodeTitle)) {
-            values.put(ProgramColumns.COLUMN_EPISODE_TITLE, mEpisodeTitle);
-        } else {
-            values.putNull(ProgramColumns.COLUMN_EPISODE_TITLE);
-        }
-        if (!TextUtils.isEmpty(mSeasonNumber) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            values.put(ProgramColumns.COLUMN_SEASON_DISPLAY_NUMBER, mSeasonNumber);
-        } else if (!TextUtils.isEmpty(mSeasonNumber)
-                && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
-            values.put(Programs.COLUMN_SEASON_NUMBER,
-                    Integer.parseInt(mSeasonNumber));
-        } else {
-            values.putNull(TvContractCompat.Programs.COLUMN_SEASON_NUMBER);
-        }
-        if (!TextUtils.isEmpty(mEpisodeNumber) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            values.put(ProgramColumns.COLUMN_EPISODE_DISPLAY_NUMBER, mEpisodeNumber);
-        } else if (!TextUtils.isEmpty(mEpisodeNumber)
-                && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
-            values.put(Programs.COLUMN_EPISODE_NUMBER,
-                    Integer.parseInt(mEpisodeNumber));
-        } else {
-            values.putNull(Programs.COLUMN_EPISODE_NUMBER);
-        }
-        if (!TextUtils.isEmpty(mDescription)) {
-            values.put(ProgramColumns.COLUMN_SHORT_DESCRIPTION, mDescription);
-        } else {
-            values.putNull(ProgramColumns.COLUMN_SHORT_DESCRIPTION);
-        }
-        if (!TextUtils.isEmpty(mLongDescription)) {
-            values.put(ProgramColumns.COLUMN_LONG_DESCRIPTION, mLongDescription);
-        } else {
-            values.putNull(ProgramColumns.COLUMN_LONG_DESCRIPTION);
-        }
-        if (mPosterArtUri != null) {
-            values.put(ProgramColumns.COLUMN_POSTER_ART_URI, mPosterArtUri.toString());
-        } else {
-            values.putNull(ProgramColumns.COLUMN_POSTER_ART_URI);
-        }
-        if (mThumbnailUri != null) {
-            values.put(ProgramColumns.COLUMN_THUMBNAIL_URI, mThumbnailUri.toString());
-        } else {
-            values.putNull(ProgramColumns.COLUMN_THUMBNAIL_URI);
-        }
-        if (mAudioLanguages != null && mAudioLanguages.length > 0) {
-            values.put(ProgramColumns.COLUMN_AUDIO_LANGUAGE,
-                    TvContractUtils.audioLanguagesToString(mAudioLanguages));
-        } else {
-            values.putNull(ProgramColumns.COLUMN_AUDIO_LANGUAGE);
-        }
-        if (mCanonicalGenres != null && mCanonicalGenres.length > 0) {
-            values.put(ProgramColumns.COLUMN_CANONICAL_GENRE,
-                    Programs.Genres.encode(mCanonicalGenres));
-        } else {
-            values.putNull(ProgramColumns.COLUMN_CANONICAL_GENRE);
-        }
-        if (mContentRatings != null && mContentRatings.length > 0) {
-            values.put(ProgramColumns.COLUMN_CONTENT_RATING,
-                    TvContractUtils.contentRatingsToString(mContentRatings));
-        } else {
-            values.putNull(ProgramColumns.COLUMN_CONTENT_RATING);
-        }
-        if (mVideoWidth != INVALID_INT_VALUE) {
-            values.put(ProgramColumns.COLUMN_VIDEO_WIDTH, mVideoWidth);
-        } else {
-            values.putNull(ProgramColumns.COLUMN_VIDEO_WIDTH);
-        }
-        if (mVideoHeight != INVALID_INT_VALUE) {
-            values.put(ProgramColumns.COLUMN_VIDEO_HEIGHT, mVideoHeight);
-        } else {
-            values.putNull(ProgramColumns.COLUMN_VIDEO_HEIGHT);
-        }
-        if (mInternalProviderData != null && mInternalProviderData.length > 0) {
-            values.put(ProgramColumns.COLUMN_INTERNAL_PROVIDER_DATA,
-                    mInternalProviderData);
-        } else {
-            values.putNull(ProgramColumns.COLUMN_INTERNAL_PROVIDER_DATA);
-        }
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            if (mSearchable != INVALID_INT_VALUE) {
-                values.put(ProgramColumns.COLUMN_SEARCHABLE, mSearchable);
-            }
-            if (mInternalProviderFlag1 != null) {
-                values.put(ProgramColumns.COLUMN_INTERNAL_PROVIDER_FLAG1,
-                        mInternalProviderFlag1);
-            }
-            if (mInternalProviderFlag2 != null) {
-                values.put(ProgramColumns.COLUMN_INTERNAL_PROVIDER_FLAG2,
-                        mInternalProviderFlag2);
-            }
-            if (mInternalProviderFlag3 != null) {
-                values.put(ProgramColumns.COLUMN_INTERNAL_PROVIDER_FLAG3,
-                        mInternalProviderFlag3);
-            }
-            if (mInternalProviderFlag4 != null) {
-                values.put(ProgramColumns.COLUMN_INTERNAL_PROVIDER_FLAG4,
-                        mInternalProviderFlag4);
-            }
-        }
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            if (!TextUtils.isEmpty(mSeasonTitle)) {
-                values.put(ProgramColumns.COLUMN_SEASON_TITLE, mSeasonTitle);
-            }
-        }
-        if (BuildCompat.isAtLeastO()) {
-            if (mReviewRatingStyle != INVALID_INT_VALUE) {
-                values.put(ProgramColumns.COLUMN_REVIEW_RATING_STYLE,
-                        mReviewRatingStyle);
-            }
-            if (!TextUtils.isEmpty(mReviewRating)) {
-                values.put(ProgramColumns.COLUMN_REVIEW_RATING, mReviewRating);
-            }
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+            values.remove(ProgramColumns.COLUMN_REVIEW_RATING_STYLE);
+            values.remove(ProgramColumns.COLUMN_REVIEW_RATING);
         }
         return values;
     }
@@ -611,7 +428,7 @@
                 builder.setSeasonTitle(cursor.getString(index));
             }
         }
-        if (BuildCompat.isAtLeastO()) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             if ((index = cursor.getColumnIndex(
                     ProgramColumns.COLUMN_REVIEW_RATING_STYLE)) >= 0
                     && !cursor.isNull(index)) {
@@ -661,7 +478,7 @@
                 ProgramColumns.COLUMN_REVIEW_RATING,
                 ProgramColumns.COLUMN_REVIEW_RATING_STYLE,
         };
-        if (BuildCompat.isAtLeastO()) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             return CollectionUtils.concatAll(baseColumns, marshmallowColumns, nougatColumns,
                 oColumns);
         } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -679,35 +496,15 @@
      * @param <T> The Builder of the derived classe.
      */
     public abstract static class Builder<T extends Builder> {
-        private long mId = INVALID_LONG_VALUE;
-        private String mPackageName;
-        private String mTitle;
-        private String mEpisodeTitle;
-        private String mSeasonNumber;
-        private String mEpisodeNumber;
-        private String mDescription;
-        private String mLongDescription;
-        private int mVideoWidth = INVALID_INT_VALUE;
-        private int mVideoHeight = INVALID_INT_VALUE;
-        private Uri mPosterArtUri;
-        private Uri mThumbnailUri;
-        private String[] mCanonicalGenres;
-        private TvContentRating[] mContentRatings;
-        private byte[] mInternalProviderData;
-        private String[] mAudioLanguages;
-        private int mSearchable = INVALID_INT_VALUE;
-        private Long mInternalProviderFlag1;
-        private Long mInternalProviderFlag2;
-        private Long mInternalProviderFlag3;
-        private Long mInternalProviderFlag4;
-        private int mReviewRatingStyle = INVALID_INT_VALUE;
-        private String mReviewRating;
-        private String mSeasonTitle;
+        /** @hide */
+        @RestrictTo(LIBRARY_GROUP)
+        protected ContentValues mValues;
 
         /**
          * Creates a new Builder object.
          */
         public Builder() {
+            mValues = new ContentValues();
         }
 
         /**
@@ -715,30 +512,7 @@
          * @param other The Program you're copying from.
          */
         public Builder(BaseProgram other) {
-            mId = other.mId;
-            mPackageName = other.mPackageName;
-            mTitle = other.mTitle;
-            mEpisodeTitle = other.mEpisodeTitle;
-            mSeasonNumber = other.mSeasonNumber;
-            mEpisodeNumber = other.mEpisodeNumber;
-            mDescription = other.mDescription;
-            mLongDescription = other.mLongDescription;
-            mVideoWidth = other.mVideoWidth;
-            mVideoHeight = other.mVideoHeight;
-            mPosterArtUri = other.mPosterArtUri;
-            mThumbnailUri = other.mThumbnailUri;
-            mCanonicalGenres = other.mCanonicalGenres;
-            mContentRatings = other.mContentRatings;
-            mInternalProviderData = other.mInternalProviderData;
-            mAudioLanguages = other.mAudioLanguages;
-            mSearchable = other.mSearchable;
-            mInternalProviderFlag1 = other.mInternalProviderFlag1;
-            mInternalProviderFlag2 = other.mInternalProviderFlag2;
-            mInternalProviderFlag3 = other.mInternalProviderFlag3;
-            mInternalProviderFlag4 = other.mInternalProviderFlag4;
-            mReviewRatingStyle = other.mReviewRatingStyle;
-            mReviewRating = other.mReviewRating;
-            mSeasonTitle = other.mSeasonTitle;
+            mValues = new ContentValues(other.mValues);
         }
 
         /**
@@ -749,7 +523,7 @@
          * @see BaseTvColumns#_ID
          */
         public T setId(long programId) {
-            mId = programId;
+            mValues.put(BaseTvColumns._ID, programId);
             return (T) this;
         }
 
@@ -763,7 +537,7 @@
          */
         @RestrictTo(LIBRARY_GROUP)
         public T setPackageName(String packageName) {
-            mPackageName = packageName;
+            mValues.put(BaseTvColumns.COLUMN_PACKAGE_NAME, packageName);
             return (T) this;
         }
 
@@ -775,7 +549,7 @@
          * @see Programs#COLUMN_TITLE
          */
         public T setTitle(String title) {
-            mTitle = title;
+            mValues.put(Programs.COLUMN_TITLE, title);
             return (T) this;
         }
 
@@ -787,7 +561,7 @@
          * @see Programs#COLUMN_EPISODE_TITLE
          */
         public T setEpisodeTitle(String episodeTitle) {
-            mEpisodeTitle = episodeTitle;
+            mValues.put(Programs.COLUMN_EPISODE_TITLE, episodeTitle);
             return (T) this;
         }
 
@@ -799,7 +573,7 @@
          * @see Programs#COLUMN_SEASON_DISPLAY_NUMBER
          */
         public T setSeasonNumber(int seasonNumber) {
-            mSeasonNumber = String.valueOf(seasonNumber);
+            setSeasonNumber(String.valueOf(seasonNumber), seasonNumber);
             return (T) this;
         }
 
@@ -815,9 +589,9 @@
          */
         public T setSeasonNumber(String seasonNumber, int numericalSeasonNumber) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                mSeasonNumber = seasonNumber;
+                mValues.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, seasonNumber);
             } else {
-                mSeasonNumber = String.valueOf(numericalSeasonNumber);
+                mValues.put(Programs.COLUMN_SEASON_NUMBER, numericalSeasonNumber);
             }
             return (T) this;
         }
@@ -830,7 +604,7 @@
          * @see Programs#COLUMN_EPISODE_DISPLAY_NUMBER
          */
         public T setEpisodeNumber(int episodeNumber) {
-            mEpisodeNumber = String.valueOf(episodeNumber);
+            setEpisodeNumber(String.valueOf(episodeNumber), episodeNumber);
             return (T) this;
         }
 
@@ -846,9 +620,9 @@
          */
         public T setEpisodeNumber(String episodeNumber, int numericalEpisodeNumber) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                mEpisodeNumber = episodeNumber;
+                mValues.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER, episodeNumber);
             } else {
-                mEpisodeNumber = String.valueOf(numericalEpisodeNumber);
+                mValues.put(Programs.COLUMN_EPISODE_NUMBER, numericalEpisodeNumber);
             }
             return (T) this;
         }
@@ -862,7 +636,7 @@
          * @see Programs#COLUMN_SHORT_DESCRIPTION
          */
         public T setDescription(String description) {
-            mDescription = description;
+            mValues.put(Programs.COLUMN_SHORT_DESCRIPTION, description);
             return (T) this;
         }
 
@@ -874,7 +648,7 @@
          * @see Programs#COLUMN_LONG_DESCRIPTION
          */
         public T setLongDescription(String longDescription) {
-            mLongDescription = longDescription;
+            mValues.put(Programs.COLUMN_LONG_DESCRIPTION, longDescription);
             return (T) this;
         }
 
@@ -886,7 +660,7 @@
          * @see Programs#COLUMN_VIDEO_WIDTH
          */
         public T setVideoWidth(int width) {
-            mVideoWidth = width;
+            mValues.put(Programs.COLUMN_VIDEO_WIDTH, width);
             return (T) this;
         }
 
@@ -898,7 +672,7 @@
          * @see Programs#COLUMN_VIDEO_HEIGHT
          */
         public T setVideoHeight(int height) {
-            mVideoHeight = height;
+            mValues.put(Programs.COLUMN_VIDEO_HEIGHT, height);
             return (T) this;
         }
 
@@ -911,7 +685,8 @@
          * @see Programs#COLUMN_CONTENT_RATING
          */
         public T setContentRatings(TvContentRating[] contentRatings) {
-            mContentRatings = contentRatings;
+            mValues.put(Programs.COLUMN_CONTENT_RATING,
+                    TvContractUtils.contentRatingsToString(contentRatings));
             return (T) this;
         }
 
@@ -923,7 +698,8 @@
          * @see Programs#COLUMN_POSTER_ART_URI
          */
         public T setPosterArtUri(Uri posterArtUri) {
-            mPosterArtUri = posterArtUri;
+            mValues.put(Programs.COLUMN_POSTER_ART_URI,
+                    posterArtUri == null ? null : posterArtUri.toString());
             return (T) this;
         }
 
@@ -935,7 +711,8 @@
          * @see Programs#COLUMN_THUMBNAIL_URI
          */
         public T setThumbnailUri(Uri thumbnailUri) {
-            mThumbnailUri = thumbnailUri;
+            mValues.put(Programs.COLUMN_THUMBNAIL_URI,
+                    thumbnailUri == null ? null : thumbnailUri.toString());
             return (T) this;
         }
 
@@ -948,7 +725,7 @@
          * @see Programs#COLUMN_CANONICAL_GENRE
          */
         public T setCanonicalGenres(@Genre String[] genres) {
-            mCanonicalGenres = genres;
+            mValues.put(Programs.COLUMN_CANONICAL_GENRE, Programs.Genres.encode(genres));
             return (T) this;
         }
 
@@ -960,7 +737,7 @@
          * @see Programs#COLUMN_INTERNAL_PROVIDER_DATA
          */
         public T setInternalProviderData(byte[] data) {
-            mInternalProviderData = data;
+            mValues.put(ProgramColumns.COLUMN_INTERNAL_PROVIDER_DATA, data);
             return (T) this;
         }
 
@@ -972,7 +749,8 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public T setAudioLanguages(String[] audioLanguages) {
-            mAudioLanguages = audioLanguages;
+            mValues.put(ProgramColumns.COLUMN_AUDIO_LANGUAGE,
+                    TvContractUtils.audioLanguagesToString(audioLanguages));
             return (T) this;
         }
 
@@ -984,7 +762,7 @@
          * @see Programs#COLUMN_SEARCHABLE
          */
         public T setSearchable(boolean searchable) {
-            mSearchable = searchable ? IS_SEARCHABLE : 0;
+            mValues.put(Programs.COLUMN_SEARCHABLE, searchable ? IS_SEARCHABLE : 0);
             return (T) this;
         }
 
@@ -996,7 +774,7 @@
          * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG1
          */
         public T setInternalProviderFlag1(long flag) {
-            mInternalProviderFlag1 = flag;
+            mValues.put(ProgramColumns.COLUMN_INTERNAL_PROVIDER_FLAG1, flag);
             return (T) this;
         }
 
@@ -1008,7 +786,7 @@
          * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG2
          */
         public T setInternalProviderFlag2(long flag) {
-            mInternalProviderFlag2 = flag;
+            mValues.put(ProgramColumns.COLUMN_INTERNAL_PROVIDER_FLAG2, flag);
             return (T) this;
         }
 
@@ -1020,7 +798,7 @@
          * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG3
          */
         public T setInternalProviderFlag3(long flag) {
-            mInternalProviderFlag3 = flag;
+            mValues.put(ProgramColumns.COLUMN_INTERNAL_PROVIDER_FLAG3, flag);
             return (T) this;
         }
 
@@ -1032,7 +810,7 @@
          * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG4
          */
         public T setInternalProviderFlag4(long flag) {
-            mInternalProviderFlag4 = flag;
+            mValues.put(ProgramColumns.COLUMN_INTERNAL_PROVIDER_FLAG4, flag);
             return (T) this;
         }
 
@@ -1048,7 +826,7 @@
          * @see Programs#REVIEW_RATING_STYLE_PERCENTAGE
          */
         public T setReviewRatingStyle(@ReviewRatingStyle int reviewRatingStyle) {
-            mReviewRatingStyle = reviewRatingStyle;
+            mValues.put(ProgramColumns.COLUMN_REVIEW_RATING_STYLE, reviewRatingStyle);
             return (T) this;
         }
 
@@ -1072,7 +850,7 @@
          * @see Programs#REVIEW_RATING_STYLE_PERCENTAGE
          */
         public T setReviewRating(String reviewRating) {
-            mReviewRating = reviewRating;
+            mValues.put(ProgramColumns.COLUMN_REVIEW_RATING, reviewRating);
             return (T) this;
         }
 
@@ -1084,7 +862,7 @@
          * @see Programs#COLUMN_SEASON_TITLE
          */
         public T setSeasonTitle(String seasonTitle) {
-            mSeasonTitle = seasonTitle;
+            mValues.put(ProgramColumns.COLUMN_SEASON_TITLE, seasonTitle);
             return (T) this;
         }
     }
diff --git a/tv-provider/src/android/support/media/tv/Channel.java b/tv-provider/src/android/support/media/tv/Channel.java
index 2c12901..9b13e42 100644
--- a/tv-provider/src/android/support/media/tv/Channel.java
+++ b/tv-provider/src/android/support/media/tv/Channel.java
@@ -28,8 +28,6 @@
 import android.support.media.tv.TvContractCompat.Channels.ServiceType;
 import android.support.media.tv.TvContractCompat.Channels.Type;
 import android.support.media.tv.TvContractCompat.Channels.VideoFormat;
-import android.support.v4.os.BuildCompat;
-import android.text.TextUtils;
 
 import java.net.URISyntaxException;
 import java.nio.charset.Charset;
@@ -62,6 +60,21 @@
  *     }
  * }
  * </pre>
+ *
+ * <p>Usage example when updating an existing channel:
+ * <pre>
+ * Channel updatedChannel = new Channel.Builder(channel)
+ *         .setDescription("New channel description")
+ *         .build();
+ * getContentResolver().update(TvContractCompat.buildChannelUri(updatedChannel.getId()),
+ *         updatedChannel.toContentValues(), null, null);
+ * </pre>
+ *
+ * <p>Usage example when deleting a channel:
+ * <pre>
+ * getContentResolver().delete(
+ *         TvContractCompat.buildChannelUri(existingChannel.getId()), null, null);
+ * </pre>
  */
 @TargetApi(21)
 public final class Channel {
@@ -72,213 +85,167 @@
     public static final String[] PROJECTION = getProjection();
 
     private static final long INVALID_CHANNEL_ID = -1;
-    private static final int INVALID_INTEGER_VALUE = -1;
+    private static final int INVALID_INT_VALUE = -1;
     private static final int IS_SEARCHABLE = 1;
     private static final int IS_TRANSIENT = 1;
     private static final int IS_BROWSABLE = 1;
     private static final int IS_SYSTEM_APPROVED = 1;
     private static final int IS_LOCKED = 1;
 
-    private final long mId;
-    private final String mPackageName;
-    private final String mInputId;
-    private final String mType;
-    private final String mDisplayNumber;
-    private final String mDisplayName;
-    private final String mDescription;
-    private final String mVideoFormat;
-    private final int mOriginalNetworkId;
-    private final int mTransportStreamId;
-    private final int mServiceId;
-    private final String mAppLinkText;
-    private final int mAppLinkColor;
-    private final Uri mAppLinkIconUri;
-    private final Uri mAppLinkPosterArtUri;
-    private final Uri mAppLinkIntentUri;
-    private final byte[] mInternalProviderData;
-    private final String mNetworkAffiliation;
-    private final int mSearchable;
-    private final String mServiceType;
-    private final Long mInternalProviderFlag1;
-    private final Long mInternalProviderFlag2;
-    private final Long mInternalProviderFlag3;
-    private final Long mInternalProviderFlag4;
-    private final String mInternalProviderId;
-    private final int mTransient;
-    private final int mBrowsable;
-    private final int mSystemApproved;
-    private final int mLocked;
+    private ContentValues mValues;
 
     private Channel(Builder builder) {
-        mId = builder.mId;
-        mPackageName = builder.mPackageName;
-        mInputId = builder.mInputId;
-        mType = builder.mType;
-        mDisplayNumber = builder.mDisplayNumber;
-        mDisplayName = builder.mDisplayName;
-        mDescription = builder.mDescription;
-        mVideoFormat = builder.mVideoFormat;
-        mOriginalNetworkId = builder.mOriginalNetworkId;
-        mTransportStreamId = builder.mTransportStreamId;
-        mServiceId = builder.mServiceId;
-        mAppLinkText = builder.mAppLinkText;
-        mAppLinkColor = builder.mAppLinkColor;
-        mAppLinkIconUri = builder.mAppLinkIconUri;
-        mAppLinkPosterArtUri = builder.mAppLinkPosterArtUri;
-        mAppLinkIntentUri = builder.mAppLinkIntentUri;
-        mInternalProviderData = builder.mInternalProviderData;
-        mNetworkAffiliation = builder.mNetworkAffiliation;
-        mSearchable = builder.mSearchable;
-        mServiceType = builder.mServiceType;
-        mInternalProviderFlag1 = builder.mInternalProviderFlag1;
-        mInternalProviderFlag2 = builder.mInternalProviderFlag2;
-        mInternalProviderFlag3 = builder.mInternalProviderFlag3;
-        mInternalProviderFlag4 = builder.mInternalProviderFlag4;
-        mInternalProviderId = builder.mInternalProviderId;
-        mTransient = builder.mTransient;
-        mBrowsable = builder.mBrowsable;
-        mSystemApproved = builder.mSystemApproved;
-        mLocked = builder.mLocked;
+        mValues = builder.mValues;
     }
 
     /**
      * @return The value of {@link Channels#_ID} for the channel.
      */
     public long getId() {
-        return mId;
+        Long l = mValues.getAsLong(Channels._ID);
+        return l == null ? INVALID_CHANNEL_ID : l;
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_PACKAGE_NAME} for the channel.
      */
     public String getPackageName() {
-        return mPackageName;
+        return mValues.getAsString(Channels.COLUMN_PACKAGE_NAME);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_INPUT_ID} for the channel.
      */
     public String getInputId() {
-        return mInputId;
+        return mValues.getAsString(Channels.COLUMN_INPUT_ID);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_TYPE} for the channel.
      */
     public @Type String getType() {
-        return mType;
+        return mValues.getAsString(Channels.COLUMN_TYPE);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_DISPLAY_NUMBER} for the channel.
      */
     public String getDisplayNumber() {
-        return mDisplayNumber;
+        return mValues.getAsString(Channels.COLUMN_DISPLAY_NUMBER);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_DISPLAY_NAME} for the channel.
      */
     public String getDisplayName() {
-        return mDisplayName;
+        return mValues.getAsString(Channels.COLUMN_DISPLAY_NAME);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_DESCRIPTION} for the channel.
      */
     public String getDescription() {
-        return mDescription;
+        return mValues.getAsString(Channels.COLUMN_DESCRIPTION);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_VIDEO_FORMAT} for the channel.
      */
     public @VideoFormat String getVideoFormat() {
-        return mVideoFormat;
+        return mValues.getAsString(Channels.COLUMN_VIDEO_FORMAT);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_ORIGINAL_NETWORK_ID} for the channel.
      */
     public int getOriginalNetworkId() {
-        return mOriginalNetworkId;
+        Integer i = mValues.getAsInteger(Channels.COLUMN_ORIGINAL_NETWORK_ID);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_TRANSPORT_STREAM_ID} for the channel.
      */
     public int getTransportStreamId() {
-        return mTransportStreamId;
+        Integer i = mValues.getAsInteger(Channels.COLUMN_TRANSPORT_STREAM_ID);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_SERVICE_ID} for the channel.
      */
     public int getServiceId() {
-        return mServiceId;
+        Integer i = mValues.getAsInteger(Channels.COLUMN_SERVICE_ID);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_APP_LINK_TEXT} for the channel.
      */
     public String getAppLinkText() {
-        return mAppLinkText;
+        return mValues.getAsString(Channels.COLUMN_APP_LINK_TEXT);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_APP_LINK_COLOR} for the channel.
      */
     public int getAppLinkColor() {
-        return mAppLinkColor;
+        Integer i = mValues.getAsInteger(Channels.COLUMN_APP_LINK_COLOR);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_APP_LINK_ICON_URI} for the channel.
      */
     public Uri getAppLinkIconUri() {
-        return mAppLinkIconUri;
+        String uri = mValues.getAsString(Channels.COLUMN_APP_LINK_ICON_URI);
+        return uri == null ? null : Uri.parse(uri);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_APP_LINK_POSTER_ART_URI} for the channel.
      */
     public Uri getAppLinkPosterArtUri() {
-        return mAppLinkPosterArtUri;
+        String uri = mValues.getAsString(Channels.COLUMN_APP_LINK_POSTER_ART_URI);
+        return uri == null ? null : Uri.parse(uri);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_APP_LINK_INTENT_URI} for the channel.
      */
     public Uri getAppLinkIntentUri() {
-        return mAppLinkIntentUri;
+        String uri = mValues.getAsString(Channels.COLUMN_APP_LINK_INTENT_URI);
+        return uri == null ? null : Uri.parse(uri);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_APP_LINK_INTENT_URI} for the program.
      */
     public Intent getAppLinkIntent() throws URISyntaxException {
-        return Intent.parseUri(mAppLinkIntentUri.toString(), Intent.URI_INTENT_SCHEME);
+        String uri = mValues.getAsString(Channels.COLUMN_APP_LINK_INTENT_URI);
+        return uri == null ? null : Intent.parseUri(uri.toString(), Intent.URI_INTENT_SCHEME);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_NETWORK_AFFILIATION} for the channel.
      */
     public String getNetworkAffiliation() {
-        return mNetworkAffiliation;
+        return mValues.getAsString(Channels.COLUMN_NETWORK_AFFILIATION);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_SEARCHABLE} for the channel.
      */
     public boolean isSearchable() {
-        return mSearchable == IS_SEARCHABLE;
+        Integer i = mValues.getAsInteger(Channels.COLUMN_SEARCHABLE);
+        return i == null || i == IS_SEARCHABLE;
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_DATA} for the channel.
      */
     public byte[] getInternalProviderDataByteArray() {
-        return mInternalProviderData;
+        return mValues.getAsByteArray(Channels.COLUMN_INTERNAL_PROVIDER_DATA);
     }
 
     /**
@@ -288,56 +255,58 @@
      * {@link Channels#SERVICE_TYPE_OTHER}.
      */
     public @ServiceType String getServiceType() {
-        return mServiceType;
+        return mValues.getAsString(Channels.COLUMN_SERVICE_TYPE);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG1} for the channel.
      */
     public Long getInternalProviderFlag1() {
-        return mInternalProviderFlag1;
+        return mValues.getAsLong(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG2} for the channel.
      */
     public Long getInternalProviderFlag2() {
-        return mInternalProviderFlag2;
+        return mValues.getAsLong(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG3} for the channel.
      */
     public Long getInternalProviderFlag3() {
-        return mInternalProviderFlag3;
+        return mValues.getAsLong(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG4} for the channel.
      */
     public Long getInternalProviderFlag4() {
-        return mInternalProviderFlag4;
+        return mValues.getAsLong(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_ID} for the channel.
      */
     public String getInternalProviderId() {
-        return mInternalProviderId;
+        return mValues.getAsString(Channels.COLUMN_INTERNAL_PROVIDER_ID);
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_TRANSIENT} for the channel.
      */
     public boolean isTransient() {
-        return mTransient == IS_TRANSIENT;
+        Integer i = mValues.getAsInteger(Channels.COLUMN_TRANSIENT);
+        return i != null && i == IS_TRANSIENT;
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_BROWSABLE} for the channel.
      */
     public boolean isBrowsable() {
-        return mBrowsable == IS_BROWSABLE;
+        Integer i = mValues.getAsInteger(Channels.COLUMN_BROWSABLE);
+        return i != null && i == IS_BROWSABLE;
     }
 
     /**
@@ -346,28 +315,33 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     public boolean isSystemApproved() {
-        return mSystemApproved == IS_SYSTEM_APPROVED;
+        Integer i = mValues.getAsInteger(Channels.COLUMN_SYSTEM_APPROVED);
+        return i != null && i == IS_SYSTEM_APPROVED;
     }
 
     /**
      * @return The value of {@link Channels#COLUMN_LOCKED} for the channel.
      */
     public boolean isLocked() {
-        return mLocked == IS_LOCKED;
+        Integer i = mValues.getAsInteger(Channels.COLUMN_LOCKED);
+        return i != null && i == IS_LOCKED;
     }
 
     @Override
+    public int hashCode() {
+        return mValues.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof Channel)) {
+            return false;
+        }
+        return mValues.equals(((Channel) other).mValues);
+    }
+    @Override
     public String toString() {
-        return "Channel{"
-                + "id=" + mId
-                + ", packageName=" + mPackageName
-                + ", inputId=" + mInputId
-                + ", originalNetworkId=" + mOriginalNetworkId
-                + ", type=" + mType
-                + ", displayNumber=" + mDisplayNumber
-                + ", displayName=" + mDisplayName
-                + ", description=" + mDescription
-                + ", videoFormat=" + mVideoFormat + "}";
+        return "Channel{" + mValues.toString() + "}";
     }
 
     /**
@@ -387,105 +361,29 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     public ContentValues toContentValues(boolean includeProtectedFields) {
-        ContentValues values = new ContentValues();
-        if (mId != INVALID_CHANNEL_ID) {
-            values.put(Channels._ID, mId);
+        ContentValues values = new ContentValues(mValues);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+            values.remove(Channels.COLUMN_APP_LINK_COLOR);
+            values.remove(Channels.COLUMN_APP_LINK_TEXT);
+            values.remove(Channels.COLUMN_APP_LINK_ICON_URI);
+            values.remove(Channels.COLUMN_APP_LINK_POSTER_ART_URI);
+            values.remove(Channels.COLUMN_APP_LINK_INTENT_URI);
+            values.remove(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1);
+            values.remove(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2);
+            values.remove(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3);
+            values.remove(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4);
         }
-        if (!TextUtils.isEmpty(mPackageName)) {
-            values.put(Channels.COLUMN_PACKAGE_NAME, mPackageName);
-        } else {
-            values.putNull(Channels.COLUMN_PACKAGE_NAME);
-        }
-        if (!TextUtils.isEmpty(mInputId)) {
-            values.put(Channels.COLUMN_INPUT_ID, mInputId);
-        } else {
-            values.putNull(Channels.COLUMN_INPUT_ID);
-        }
-        if (!TextUtils.isEmpty(mType)) {
-            values.put(Channels.COLUMN_TYPE, mType);
-        } else {
-            values.putNull(Channels.COLUMN_TYPE);
-        }
-        if (!TextUtils.isEmpty(mDisplayNumber)) {
-            values.put(Channels.COLUMN_DISPLAY_NUMBER, mDisplayNumber);
-        } else {
-            values.putNull(Channels.COLUMN_DISPLAY_NUMBER);
-        }
-        if (!TextUtils.isEmpty(mDisplayName)) {
-            values.put(Channels.COLUMN_DISPLAY_NAME, mDisplayName);
-        } else {
-            values.putNull(Channels.COLUMN_DISPLAY_NAME);
-        }
-        if (!TextUtils.isEmpty(mDescription)) {
-            values.put(Channels.COLUMN_DESCRIPTION, mDescription);
-        } else {
-            values.putNull(Channels.COLUMN_DESCRIPTION);
-        }
-        if (!TextUtils.isEmpty(mVideoFormat)) {
-            values.put(Channels.COLUMN_VIDEO_FORMAT, mVideoFormat);
-        } else {
-            values.putNull(Channels.COLUMN_VIDEO_FORMAT);
-        }
-        if (mInternalProviderData != null && mInternalProviderData.length > 0) {
-            values.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA,
-                    mInternalProviderData);
-        } else {
-            values.putNull(Channels.COLUMN_INTERNAL_PROVIDER_DATA);
-        }
-        values.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, mOriginalNetworkId);
-        values.put(Channels.COLUMN_TRANSPORT_STREAM_ID, mTransportStreamId);
-        values.put(Channels.COLUMN_SERVICE_ID, mServiceId);
-        values.put(Channels.COLUMN_NETWORK_AFFILIATION, mNetworkAffiliation);
-        values.put(Channels.COLUMN_SEARCHABLE, mSearchable);
-        values.put(Channels.COLUMN_SERVICE_TYPE, mServiceType);
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            values.put(Channels.COLUMN_APP_LINK_COLOR, mAppLinkColor);
-            if (!TextUtils.isEmpty(mAppLinkText)) {
-                values.put(Channels.COLUMN_APP_LINK_TEXT, mAppLinkText);
-            } else {
-                values.putNull(Channels.COLUMN_APP_LINK_TEXT);
-            }
-            if (mAppLinkIconUri != null) {
-                values.put(Channels.COLUMN_APP_LINK_ICON_URI, mAppLinkIconUri.toString());
-            } else {
-                values.putNull(Channels.COLUMN_APP_LINK_ICON_URI);
-            }
-            if (mAppLinkPosterArtUri != null) {
-                values.put(Channels.COLUMN_APP_LINK_POSTER_ART_URI,
-                        mAppLinkPosterArtUri.toString());
-            } else {
-                values.putNull(Channels.COLUMN_APP_LINK_POSTER_ART_URI);
-            }
-            if (mAppLinkIntentUri != null) {
-                values.put(Channels.COLUMN_APP_LINK_INTENT_URI, mAppLinkIntentUri.toString());
-            } else {
-                values.putNull(Channels.COLUMN_APP_LINK_INTENT_URI);
-            }
-            if (mInternalProviderFlag1 != null) {
-                values.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, mInternalProviderFlag1);
-            }
-            if (mInternalProviderFlag2 != null) {
-                values.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, mInternalProviderFlag2);
-            }
-            if (mInternalProviderFlag3 != null) {
-                values.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3, mInternalProviderFlag3);
-            }
-            if (mInternalProviderFlag4 != null) {
-                values.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4, mInternalProviderFlag4);
-            }
-        }
-        if (BuildCompat.isAtLeastO()) {
-            values.put(Channels.COLUMN_INTERNAL_PROVIDER_ID, mInternalProviderId);
-            values.put(Channels.COLUMN_TRANSIENT, mTransient);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+            values.remove(Channels.COLUMN_INTERNAL_PROVIDER_ID);
+            values.remove(Channels.COLUMN_TRANSIENT);
         }
 
-        if (includeProtectedFields) {
-            values.put(Channels.COLUMN_BROWSABLE, mBrowsable);
-            values.put(Channels.COLUMN_LOCKED, mLocked);
-            if (BuildCompat.isAtLeastO()) {
-                values.put(Channels.COLUMN_SYSTEM_APPROVED, mSystemApproved);
-            }
+        if (!includeProtectedFields) {
+            values.remove(Channels.COLUMN_BROWSABLE);
+            values.remove(Channels.COLUMN_LOCKED);
+        }
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !includeProtectedFields) {
+            values.remove(Channels.COLUMN_SYSTEM_APPROVED);
         }
         return values;
     }
@@ -604,7 +502,7 @@
                 builder.setInternalProviderFlag4(cursor.getLong(index));
             }
         }
-        if (BuildCompat.isAtLeastO()) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             if ((index = cursor.getColumnIndex(Channels.COLUMN_INTERNAL_PROVIDER_ID)) >= 0
                     && !cursor.isNull(index)) {
                 builder.setInternalProviderId(cursor.getString(index));
@@ -657,7 +555,7 @@
                 Channels.COLUMN_TRANSIENT,
                 Channels.COLUMN_SYSTEM_APPROVED,
         };
-        if (BuildCompat.isAtLeastO()) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             return CollectionUtils.concatAll(baseColumns, marshmallowColumns, oReleaseColumns);
         }
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -670,69 +568,14 @@
      * The builder class that makes it easy to chain setters to create a {@link Channel} object.
      */
     public static final class Builder {
-        private long mId = INVALID_CHANNEL_ID;
-        private String mPackageName;
-        private String mInputId;
-        private String mType;
-        private String mDisplayNumber;
-        private String mDisplayName;
-        private String mDescription;
-        private String mVideoFormat;
-        private int mOriginalNetworkId = INVALID_INTEGER_VALUE;
-        private int mTransportStreamId;
-        private int mServiceId;
-        private String mAppLinkText;
-        private int mAppLinkColor;
-        private Uri mAppLinkIconUri;
-        private Uri mAppLinkPosterArtUri;
-        private Uri mAppLinkIntentUri;
-        private byte[] mInternalProviderData;
-        private String mNetworkAffiliation;
-        private int mSearchable;
-        private String mServiceType = Channels.SERVICE_TYPE_AUDIO_VIDEO;
-        private Long mInternalProviderFlag1;
-        private Long mInternalProviderFlag2;
-        private Long mInternalProviderFlag3;
-        private Long mInternalProviderFlag4;
-        private String mInternalProviderId;
-        private int mTransient;
-        private int mBrowsable;
-        private int mSystemApproved;
-        private int mLocked;
+        private ContentValues mValues;
 
         public Builder() {
+            mValues = new ContentValues();
         }
 
         public Builder(Channel other) {
-            mId = other.mId;
-            mPackageName = other.mPackageName;
-            mInputId = other.mInputId;
-            mType = other.mType;
-            mDisplayNumber = other.mDisplayNumber;
-            mDisplayName = other.mDisplayName;
-            mDescription = other.mDescription;
-            mVideoFormat = other.mVideoFormat;
-            mOriginalNetworkId = other.mOriginalNetworkId;
-            mTransportStreamId = other.mTransportStreamId;
-            mServiceId = other.mServiceId;
-            mAppLinkText = other.mAppLinkText;
-            mAppLinkColor = other.mAppLinkColor;
-            mAppLinkIconUri = other.mAppLinkIconUri;
-            mAppLinkPosterArtUri = other.mAppLinkPosterArtUri;
-            mAppLinkIntentUri = other.mAppLinkIntentUri;
-            mInternalProviderData = other.mInternalProviderData;
-            mNetworkAffiliation = other.mNetworkAffiliation;
-            mSearchable = other.mSearchable;
-            mServiceType = other.mServiceType;
-            mInternalProviderFlag1 = other.mInternalProviderFlag1;
-            mInternalProviderFlag2 = other.mInternalProviderFlag2;
-            mInternalProviderFlag3 = other.mInternalProviderFlag3;
-            mInternalProviderFlag4 = other.mInternalProviderFlag4;
-            mInternalProviderId = other.mInternalProviderId;
-            mTransient = other.mTransient;
-            mBrowsable = other.mBrowsable;
-            mSystemApproved = other.mSystemApproved;
-            mLocked = other.mLocked;
+            mValues = new ContentValues(other.mValues);
         }
 
         /**
@@ -742,7 +585,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         private Builder setId(long id) {
-            mId = id;
+            mValues.put(Channels._ID, id);
             return this;
         }
 
@@ -755,7 +598,7 @@
          */
         @RestrictTo(LIBRARY_GROUP)
         Builder setPackageName(String packageName) {
-            mPackageName = packageName;
+            mValues.put(Channels.COLUMN_PACKAGE_NAME, packageName);
             return this;
         }
 
@@ -766,7 +609,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setInputId(String inputId) {
-            mInputId = inputId;
+            mValues.put(Channels.COLUMN_INPUT_ID, inputId);
             return this;
         }
 
@@ -777,7 +620,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setType(@Type String type) {
-            mType = type;
+            mValues.put(Channels.COLUMN_TYPE, type);
             return this;
         }
 
@@ -788,7 +631,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setDisplayNumber(String displayNumber) {
-            mDisplayNumber = displayNumber;
+            mValues.put(Channels.COLUMN_DISPLAY_NUMBER, displayNumber);
             return this;
         }
 
@@ -799,7 +642,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setDisplayName(String displayName) {
-            mDisplayName = displayName;
+            mValues.put(Channels.COLUMN_DISPLAY_NAME, displayName);
             return this;
         }
 
@@ -810,7 +653,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setDescription(String description) {
-            mDescription = description;
+            mValues.put(Channels.COLUMN_DESCRIPTION, description);
             return this;
         }
 
@@ -821,7 +664,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setVideoFormat(@VideoFormat String videoFormat) {
-            mVideoFormat = videoFormat;
+            mValues.put(Channels.COLUMN_VIDEO_FORMAT, videoFormat);
             return this;
         }
 
@@ -833,7 +676,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setOriginalNetworkId(int originalNetworkId) {
-            mOriginalNetworkId = originalNetworkId;
+            mValues.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, originalNetworkId);
             return this;
         }
 
@@ -845,7 +688,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setTransportStreamId(int transportStreamId) {
-            mTransportStreamId = transportStreamId;
+            mValues.put(Channels.COLUMN_TRANSPORT_STREAM_ID, transportStreamId);
             return this;
         }
 
@@ -856,7 +699,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setServiceId(int serviceId) {
-            mServiceId = serviceId;
+            mValues.put(Channels.COLUMN_SERVICE_ID, serviceId);
             return this;
         }
 
@@ -868,7 +711,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setInternalProviderData(byte[] internalProviderData) {
-            mInternalProviderData = internalProviderData;
+            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA, internalProviderData);
             return this;
         }
 
@@ -880,7 +723,8 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setInternalProviderData(String internalProviderData) {
-            mInternalProviderData = internalProviderData.getBytes(Charset.defaultCharset());
+            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA,
+                    internalProviderData.getBytes(Charset.defaultCharset()));
             return this;
         }
 
@@ -891,7 +735,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setAppLinkText(String appLinkText) {
-            mAppLinkText = appLinkText;
+            mValues.put(Channels.COLUMN_APP_LINK_TEXT, appLinkText);
             return this;
         }
 
@@ -902,7 +746,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setAppLinkColor(int appLinkColor) {
-            mAppLinkColor = appLinkColor;
+            mValues.put(Channels.COLUMN_APP_LINK_COLOR, appLinkColor);
             return this;
         }
 
@@ -914,7 +758,8 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setAppLinkIconUri(Uri appLinkIconUri) {
-            mAppLinkIconUri = appLinkIconUri;
+            mValues.put(Channels.COLUMN_APP_LINK_ICON_URI,
+                    appLinkIconUri == null ? null : appLinkIconUri.toString());
             return this;
         }
 
@@ -926,7 +771,8 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setAppLinkPosterArtUri(Uri appLinkPosterArtUri) {
-            mAppLinkPosterArtUri = appLinkPosterArtUri;
+            mValues.put(Channels.COLUMN_APP_LINK_POSTER_ART_URI,
+                    appLinkPosterArtUri == null ? null : appLinkPosterArtUri.toString());
             return this;
         }
 
@@ -950,7 +796,8 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setAppLinkIntentUri(Uri appLinkIntentUri) {
-            mAppLinkIntentUri = appLinkIntentUri;
+            mValues.put(Channels.COLUMN_APP_LINK_INTENT_URI,
+                    appLinkIntentUri == null ? null : appLinkIntentUri.toString());
             return this;
         }
 
@@ -962,7 +809,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setNetworkAffiliation(String networkAffiliation) {
-            mNetworkAffiliation = networkAffiliation;
+            mValues.put(Channels.COLUMN_NETWORK_AFFILIATION, networkAffiliation);
             return this;
         }
 
@@ -973,7 +820,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setSearchable(boolean searchable) {
-            mSearchable = searchable ? IS_SEARCHABLE : 0;
+            mValues.put(Channels.COLUMN_SEARCHABLE, searchable ? IS_SEARCHABLE : 0);
             return this;
         }
 
@@ -986,7 +833,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setServiceType(@ServiceType String serviceType) {
-            mServiceType = serviceType;
+            mValues.put(Channels.COLUMN_SERVICE_TYPE, serviceType);
             return this;
         }
 
@@ -997,7 +844,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setInternalProviderFlag1(long flag) {
-            mInternalProviderFlag1 = flag;
+            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, flag);
             return this;
         }
 
@@ -1008,7 +855,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setInternalProviderFlag2(long flag) {
-            mInternalProviderFlag2 = flag;
+            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, flag);
             return this;
         }
 
@@ -1019,7 +866,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setInternalProviderFlag3(long flag) {
-            mInternalProviderFlag3 = flag;
+            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3, flag);
             return this;
         }
 
@@ -1030,7 +877,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setInternalProviderFlag4(long flag) {
-            mInternalProviderFlag4 = flag;
+            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4, flag);
             return this;
         }
 
@@ -1042,7 +889,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setInternalProviderId(String internalProviderId) {
-            mInternalProviderId = internalProviderId;
+            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_ID, internalProviderId);
             return this;
         }
 
@@ -1053,7 +900,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setTransient(boolean value) {
-            mTransient = value ? IS_TRANSIENT : 0;
+            mValues.put(Channels.COLUMN_TRANSIENT, value ? IS_TRANSIENT : 0);
             return this;
         }
 
@@ -1066,7 +913,7 @@
          */
         @RestrictTo(LIBRARY_GROUP)
         public Builder setBrowsable(boolean value) {
-            mBrowsable = value ? IS_BROWSABLE : 0;
+            mValues.put(Channels.COLUMN_BROWSABLE, value ? IS_BROWSABLE : 0);
             return this;
         }
 
@@ -1079,7 +926,7 @@
          */
         @RestrictTo(LIBRARY_GROUP)
         public Builder setSystemApproved(boolean value) {
-            mSystemApproved = value ? IS_SYSTEM_APPROVED : 0;
+            mValues.put(Channels.COLUMN_SYSTEM_APPROVED, value ? IS_SYSTEM_APPROVED : 0);
             return this;
         }
 
@@ -1092,7 +939,7 @@
          */
         @RestrictTo(LIBRARY_GROUP)
         public Builder setLocked(boolean value) {
-            mLocked = value ? IS_LOCKED : 0;
+            mValues.put(Channels.COLUMN_LOCKED, value ? IS_LOCKED : 0);
             return this;
         }
 
diff --git a/tv-provider/src/android/support/media/tv/PreviewProgram.java b/tv-provider/src/android/support/media/tv/PreviewProgram.java
index 4645a64..ad8e957 100644
--- a/tv-provider/src/android/support/media/tv/PreviewProgram.java
+++ b/tv-provider/src/android/support/media/tv/PreviewProgram.java
@@ -20,11 +20,10 @@
 import android.annotation.TargetApi;
 import android.content.ContentValues;
 import android.database.Cursor;
+import android.os.Build;
 import android.support.annotation.RestrictTo;
 import android.support.media.tv.TvContractCompat.PreviewPrograms;
 
-import java.util.Objects;
-
 /**
  * A convenience class to access {@link PreviewPrograms} entries in the system content
  * provider.
@@ -56,6 +55,21 @@
  *     }
  * }
  * </pre>
+ *
+ * <p>Usage example when updating an existing preview program:
+ * <pre>
+ * PreviewProgram updatedProgram = new PreviewProgram.Builder(previewProgram)
+ *         .setWeight(20)
+ *         .build();
+ * getContentResolver().update(TvContractCompat.buildPreviewProgramUri(updatedProgram.getId()),
+ *         updatedProgram.toContentValues(), null, null);
+ * </pre>
+ *
+ * <p>Usage example when deleting a preview program:
+ * <pre>
+ * getContentResolver().delete(TvContractCompat.buildPreviewProgramUri(existingProgram.getId()),
+ *         null, null);
+ * </pre>
  */
 @TargetApi(26)
 public final class PreviewProgram extends BasePreviewProgram {
@@ -68,27 +82,24 @@
     private static final long INVALID_LONG_VALUE = -1;
     private static final int INVALID_INT_VALUE = -1;
 
-    private final long mChannelId;
-    private final int mWeight;
-
     private PreviewProgram(Builder builder) {
         super(builder);
-        mChannelId = builder.mChannelId;
-        mWeight = builder.mWeight;
     }
 
     /**
      * @return The value of {@link PreviewPrograms#COLUMN_CHANNEL_ID} for the program.
      */
     public long getChannelId() {
-        return mChannelId;
+        Long l = mValues.getAsLong(PreviewPrograms.COLUMN_CHANNEL_ID);
+        return l == null ? INVALID_LONG_VALUE : l;
     }
 
     /**
      * @return The value of {@link PreviewPrograms#COLUMN_WEIGHT} for the program.
      */
     public int getWeight() {
-        return mWeight;
+        Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_WEIGHT);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     @Override
@@ -96,19 +107,12 @@
         if (!(other instanceof PreviewProgram)) {
             return false;
         }
-        if (!super.equals(other)) {
-            return false;
-        }
-        PreviewProgram program = (PreviewProgram) other;
-        return mChannelId == program.mChannelId && Objects.equals(mWeight, program.mWeight);
+        return mValues.equals(((PreviewProgram) other).mValues);
     }
 
     @Override
     public String toString() {
-        return "Program{"
-                + ", channelId=" + mChannelId
-                + ", weight=" + mWeight
-                + "}";
+        return "PreviewProgram{" + mValues.toString() + "}";
     }
 
     /**
@@ -131,13 +135,9 @@
     @Override
     public ContentValues toContentValues(boolean includeProtectedFields) {
         ContentValues values = super.toContentValues(includeProtectedFields);
-        if (mChannelId != INVALID_LONG_VALUE) {
-            values.put(PreviewPrograms.COLUMN_CHANNEL_ID, mChannelId);
-        } else {
-            values.putNull(PreviewPrograms.COLUMN_CHANNEL_ID);
-        }
-        if (mWeight != INVALID_INT_VALUE) {
-            values.put(PreviewPrograms.COLUMN_WEIGHT, mWeight);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+            values.remove(PreviewPrograms.COLUMN_CHANNEL_ID);
+            values.remove(PreviewPrograms.COLUMN_WEIGHT);
         }
         return values;
     }
@@ -177,8 +177,6 @@
      * This Builder class simplifies the creation of a {@link PreviewProgram} object.
      */
     public static final class Builder extends BasePreviewProgram.Builder<Builder> {
-        private long mChannelId = INVALID_LONG_VALUE;
-        private int mWeight = INVALID_INT_VALUE;
 
         /**
          * Creates a new Builder object.
@@ -191,9 +189,7 @@
          * @param other The Program you're copying from.
          */
         public Builder(PreviewProgram other) {
-            super(other);
-            mChannelId = other.mChannelId;
-            mWeight = other.mWeight;
+            mValues = new ContentValues(other.mValues);
         }
 
         /**
@@ -203,7 +199,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setChannelId(long channelId) {
-            mChannelId = channelId;
+            mValues.put(PreviewPrograms.COLUMN_CHANNEL_ID, channelId);
             return this;
         }
 
@@ -214,7 +210,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setWeight(int weight) {
-            mWeight = weight;
+            mValues.put(PreviewPrograms.COLUMN_WEIGHT, weight);
             return this;
         }
 
diff --git a/tv-provider/src/android/support/media/tv/Program.java b/tv-provider/src/android/support/media/tv/Program.java
index b82555f..8dda52d 100644
--- a/tv-provider/src/android/support/media/tv/Program.java
+++ b/tv-provider/src/android/support/media/tv/Program.java
@@ -25,9 +25,6 @@
 import android.support.annotation.RestrictTo;
 import android.support.media.tv.TvContractCompat.Programs;
 
-import java.util.Arrays;
-import java.util.Objects;
-
 /**
  * A convenience class to access {@link TvContractCompat.Programs} entries in the system content
  * provider.
@@ -57,6 +54,21 @@
  *     }
  * }
  * </pre>
+ *
+ * <p>Usage example when updating an existing program:
+ * <pre>
+ * Program updatedProgram = new Program.Builder(program)
+ *         .setEndTimeUtcMillis(newProgramEndTime)
+ *         .build();
+ * getContentResolver().update(TvContractCompat.buildProgramUri(updatedProgram.getId()),
+ *         updatedProgram.toContentValues(), null, null);
+ * </pre>
+ *
+ * <p>Usage example when deleting a program:
+ * <pre>
+ * getContentResolver().delete(TvContractCompat.buildProgramUri(existingProgram.getId()),
+ *         null, null);
+ * </pre>
  */
 @TargetApi(21)
 public final class Program extends BaseProgram implements Comparable<Program> {
@@ -67,62 +79,54 @@
     public static final String[] PROJECTION = getProjection();
 
     private static final long INVALID_LONG_VALUE = -1;
-    private static final int INVALID_INT_VALUE = -1;
     private static final int IS_RECORDING_PROHIBITED = 1;
 
-    private final long mChannelId;
-    private final long mStartTimeUtcMillis;
-    private final long mEndTimeUtcMillis;
-    private final String[] mBroadcastGenres;
-    private final int mRecordingProhibited;
-
     private Program(Builder builder) {
         super(builder);
-        mChannelId = builder.mChannelId;
-        mStartTimeUtcMillis = builder.mStartTimeUtcMillis;
-        mEndTimeUtcMillis = builder.mEndTimeUtcMillis;
-        mBroadcastGenres = builder.mBroadcastGenres;
-        mRecordingProhibited = builder.mRecordingProhibited;
     }
 
     /**
      * @return The value of {@link Programs#COLUMN_CHANNEL_ID} for the program.
      */
     public long getChannelId() {
-        return mChannelId;
+        Long l = mValues.getAsLong(Programs.COLUMN_CHANNEL_ID);
+        return l == null ? INVALID_LONG_VALUE : l;
     }
 
     /**
      * @return The value of {@link Programs#COLUMN_START_TIME_UTC_MILLIS} for the program.
      */
     public long getStartTimeUtcMillis() {
-        return mStartTimeUtcMillis;
+        Long l = mValues.getAsLong(Programs.COLUMN_START_TIME_UTC_MILLIS);
+        return l == null ? INVALID_LONG_VALUE : l;
     }
 
     /**
      * @return The value of {@link Programs#COLUMN_END_TIME_UTC_MILLIS} for the program.
      */
     public long getEndTimeUtcMillis() {
-        return mEndTimeUtcMillis;
+        Long l = mValues.getAsLong(Programs.COLUMN_END_TIME_UTC_MILLIS);
+        return l == null ? INVALID_LONG_VALUE : l;
     }
 
     /**
      * @return The value of {@link Programs#COLUMN_BROADCAST_GENRE} for the program.
      */
     public String[] getBroadcastGenres() {
-        return mBroadcastGenres;
+        return Programs.Genres.decode(mValues.getAsString(Programs.COLUMN_BROADCAST_GENRE));
     }
 
     /**
      * @return The value of {@link Programs#COLUMN_RECORDING_PROHIBITED} for the program.
      */
     public boolean isRecordingProhibited() {
-        return mRecordingProhibited == IS_RECORDING_PROHIBITED;
+        Integer i = mValues.getAsInteger(Programs.COLUMN_RECORDING_PROHIBITED);
+        return i != null && i == IS_RECORDING_PROHIBITED;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(super.hashCode(), mChannelId, mStartTimeUtcMillis, mEndTimeUtcMillis);
+        return mValues.hashCode();
     }
 
     @Override
@@ -130,16 +134,7 @@
         if (!(other instanceof Program)) {
             return false;
         }
-        if (!super.equals(other)) {
-            return false;
-        }
-        Program program = (Program) other;
-        return mChannelId == program.mChannelId
-                && mStartTimeUtcMillis == program.mStartTimeUtcMillis
-                && mEndTimeUtcMillis == program.mEndTimeUtcMillis
-                && Arrays.equals(mBroadcastGenres, program.mBroadcastGenres)
-                && (Build.VERSION.SDK_INT < Build.VERSION_CODES.N
-                        || Objects.equals(mRecordingProhibited, program.mRecordingProhibited));
+        return mValues.equals(((Program) other).mValues);
     }
 
     /**
@@ -148,16 +143,13 @@
      */
     @Override
     public int compareTo(@NonNull Program other) {
-        return Long.compare(mStartTimeUtcMillis, other.mStartTimeUtcMillis);
+        return Long.compare(mValues.getAsLong(Programs.COLUMN_START_TIME_UTC_MILLIS),
+                other.mValues.getAsLong(Programs.COLUMN_START_TIME_UTC_MILLIS));
     }
 
     @Override
     public String toString() {
-        return "Program{"
-                + "channelId=" + mChannelId
-                + ", startTimeUtcSec=" + mStartTimeUtcMillis
-                + ", endTimeUtcSec=" + mEndTimeUtcMillis
-                + "}";
+        return "Program{" + mValues.toString() + "}";
     }
 
     /**
@@ -167,31 +159,8 @@
     @Override
     public ContentValues toContentValues() {
         ContentValues values = super.toContentValues();
-        if (mChannelId != INVALID_LONG_VALUE) {
-            values.put(Programs.COLUMN_CHANNEL_ID, mChannelId);
-        } else {
-            values.putNull(Programs.COLUMN_CHANNEL_ID);
-        }
-        if (mBroadcastGenres != null && mBroadcastGenres.length > 0) {
-            values.put(Programs.COLUMN_BROADCAST_GENRE,
-                    Programs.Genres.encode(mBroadcastGenres));
-        } else {
-            values.putNull(Programs.COLUMN_BROADCAST_GENRE);
-        }
-        if (mStartTimeUtcMillis != INVALID_LONG_VALUE) {
-            values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, mStartTimeUtcMillis);
-        } else {
-            values.putNull(Programs.COLUMN_START_TIME_UTC_MILLIS);
-        }
-        if (mEndTimeUtcMillis != INVALID_LONG_VALUE) {
-            values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, mEndTimeUtcMillis);
-        } else {
-            values.putNull(Programs.COLUMN_END_TIME_UTC_MILLIS);
-        }
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            if (mRecordingProhibited != INVALID_INT_VALUE) {
-                values.put(Programs.COLUMN_RECORDING_PROHIBITED, mRecordingProhibited);
-            }
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+            values.remove(Programs.COLUMN_RECORDING_PROHIBITED);
         }
         return values;
     }
@@ -254,11 +223,6 @@
      * This Builder class simplifies the creation of a {@link Program} object.
      */
     public static class Builder extends BaseProgram.Builder<Builder> {
-        private long mChannelId = INVALID_LONG_VALUE;
-        private long mStartTimeUtcMillis = INVALID_LONG_VALUE;
-        private long mEndTimeUtcMillis = INVALID_LONG_VALUE;
-        private String[] mBroadcastGenres;
-        private int mRecordingProhibited = INVALID_INT_VALUE;
 
         /**
          * Creates a new Builder object.
@@ -271,12 +235,7 @@
          * @param other The Program you're copying from.
          */
         public Builder(Program other) {
-            super(other);
-            mChannelId = other.mChannelId;
-            mStartTimeUtcMillis = other.mStartTimeUtcMillis;
-            mEndTimeUtcMillis = other.mEndTimeUtcMillis;
-            mBroadcastGenres = other.mBroadcastGenres;
-            mRecordingProhibited = other.mRecordingProhibited;
+            mValues = new ContentValues(other.mValues);
         }
 
         /**
@@ -286,7 +245,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setChannelId(long channelId) {
-            mChannelId = channelId;
+            mValues.put(Programs.COLUMN_CHANNEL_ID, channelId);
             return this;
         }
 
@@ -298,7 +257,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setStartTimeUtcMillis(long startTimeUtcMillis) {
-            mStartTimeUtcMillis = startTimeUtcMillis;
+            mValues.put(Programs.COLUMN_START_TIME_UTC_MILLIS, startTimeUtcMillis);
             return this;
         }
 
@@ -310,7 +269,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setEndTimeUtcMillis(long endTimeUtcMillis) {
-            mEndTimeUtcMillis = endTimeUtcMillis;
+            mValues.put(Programs.COLUMN_END_TIME_UTC_MILLIS, endTimeUtcMillis);
             return this;
         }
 
@@ -323,7 +282,7 @@
          * @see Programs#COLUMN_BROADCAST_GENRE
          */
         public Builder setBroadcastGenres(String[] genres) {
-            mBroadcastGenres = genres;
+            mValues.put(Programs.COLUMN_BROADCAST_GENRE, Programs.Genres.encode(genres));
             return this;
         }
 
@@ -335,7 +294,8 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setRecordingProhibited(boolean prohibited) {
-            mRecordingProhibited = prohibited ? IS_RECORDING_PROHIBITED : 0;
+            mValues.put(Programs.COLUMN_RECORDING_PROHIBITED,
+                    prohibited ? IS_RECORDING_PROHIBITED : 0);
             return this;
         }
 
diff --git a/tv-provider/src/android/support/media/tv/TvContractCompat.java b/tv-provider/src/android/support/media/tv/TvContractCompat.java
index 81a6b34..d108ade 100644
--- a/tv-provider/src/android/support/media/tv/TvContractCompat.java
+++ b/tv-provider/src/android/support/media/tv/TvContractCompat.java
@@ -27,15 +27,16 @@
 import android.media.tv.TvContentRating;
 import android.media.tv.TvContract;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.provider.BaseColumns;
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
 import android.support.annotation.RestrictTo;
 import android.support.annotation.StringDef;
 import android.support.media.tv.TvContractCompat.Programs.Genres;
-import android.support.v4.os.BuildCompat;
 import android.text.TextUtils;
 
 import java.lang.annotation.Retention;
@@ -567,8 +568,9 @@
      * @param channelId The channel ID to be browsable.
      * @see Channels#COLUMN_BROWSABLE
      */
+    @RequiresApi(api = 26)
     public static void requestChannelBrowsable(Context context, long channelId) {
-        if (BuildCompat.isAtLeastO()) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             TvContract.requestChannelBrowsable(context, channelId);
         }
     }
diff --git a/tv-provider/src/android/support/media/tv/WatchNextProgram.java b/tv-provider/src/android/support/media/tv/WatchNextProgram.java
index b7c9666..b0b6b1c 100644
--- a/tv-provider/src/android/support/media/tv/WatchNextProgram.java
+++ b/tv-provider/src/android/support/media/tv/WatchNextProgram.java
@@ -20,12 +20,11 @@
 import android.annotation.TargetApi;
 import android.content.ContentValues;
 import android.database.Cursor;
+import android.os.Build;
 import android.support.annotation.RestrictTo;
 import android.support.media.tv.TvContractCompat.WatchNextPrograms;
 import android.support.media.tv.TvContractCompat.WatchNextPrograms.WatchNextType;
 
-import java.util.Objects;
-
 /**
  * A convenience class to access {@link WatchNextPrograms} entries in the system content
  * provider.
@@ -57,6 +56,21 @@
  *     }
  * }
  * </pre>
+ *
+ * <p>Usage example when updating an existing "watch next" program:
+ * <pre>
+ * WatchNextProgram updatedProgram = new WatchNextProgram.Builder(watchNextProgram)
+ *         .setLastEngagementTimeUtcMillis(System.currentTimeMillis())
+ *         .build();
+ * getContentResolver().update(TvContractCompat.buildWatchNextProgramUri(updatedProgram.getId()),
+ *         updatedProgram.toContentValues(), null, null);
+ * </pre>
+ *
+ * <p>Usage example when deleting a "watch next" program:
+ * <pre>
+ * getContentResolver().delete(TvContractCompat.buildWatchNextProgramUri(existingProgram.getId()),
+ *         null, null);
+ * </pre>
  */
 @TargetApi(26)
 public final class WatchNextProgram extends BasePreviewProgram {
@@ -69,20 +83,16 @@
     private static final long INVALID_LONG_VALUE = -1;
     private static final int INVALID_INT_VALUE = -1;
 
-    private final int mWatchNextType;
-    private final long mLastEngagementTimeUtcMillis;
-
     private WatchNextProgram(Builder builder) {
         super(builder);
-        mWatchNextType = builder.mWatchNextType;
-        mLastEngagementTimeUtcMillis = builder.mLastEngagementTimeUtcMillis;
     }
 
     /**
      * @return The value of {@link WatchNextPrograms#COLUMN_WATCH_NEXT_TYPE} for the program.
      */
     public @WatchNextType int getWatchNextType() {
-        return mWatchNextType;
+        Integer i = mValues.getAsInteger(WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE);
+        return i == null ? INVALID_INT_VALUE : i;
     }
 
     /**
@@ -90,7 +100,8 @@
      * program.
      */
     public long getLastEngagementTimeUtcMillis() {
-        return mLastEngagementTimeUtcMillis;
+        Long l = mValues.getAsLong(WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS);
+        return l == null ? INVALID_LONG_VALUE : l;
     }
 
     @Override
@@ -98,20 +109,12 @@
         if (!(other instanceof WatchNextProgram)) {
             return false;
         }
-        if (!super.equals(other)) {
-            return false;
-        }
-        WatchNextProgram program = (WatchNextProgram) other;
-        return Objects.equals(mWatchNextType, program.mWatchNextType)
-                && mLastEngagementTimeUtcMillis == program.mLastEngagementTimeUtcMillis;
+        return mValues.equals(((WatchNextProgram) other).mValues);
     }
 
     @Override
     public String toString() {
-        return "Program{"
-                + ", watchNextType=" + mWatchNextType
-                + ", lastEngagementTimeUtcMillis=" + mLastEngagementTimeUtcMillis
-                + "}";
+        return "WatchNextProgram{" + mValues.toString() + "}";
     }
 
     /**
@@ -134,12 +137,9 @@
     @Override
     public ContentValues toContentValues(boolean includeProtectedFields) {
         ContentValues values = super.toContentValues(includeProtectedFields);
-        if (mWatchNextType != INVALID_INT_VALUE) {
-            values.put(WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE, mWatchNextType);
-        }
-        if (mLastEngagementTimeUtcMillis != INVALID_LONG_VALUE) {
-            values.put(WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS,
-                    mLastEngagementTimeUtcMillis);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+            values.remove(WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE);
+            values.remove(WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS);
         }
         return values;
     }
@@ -180,8 +180,6 @@
      * This Builder class simplifies the creation of a {@link WatchNextProgram} object.
      */
     public static final class Builder extends BasePreviewProgram.Builder<Builder> {
-        private int mWatchNextType = INVALID_INT_VALUE;
-        private long mLastEngagementTimeUtcMillis = INVALID_LONG_VALUE;
 
         /**
          * Creates a new Builder object.
@@ -194,9 +192,7 @@
          * @param other The Program you're copying from.
          */
         public Builder(WatchNextProgram other) {
-            super(other);
-            mWatchNextType = other.mWatchNextType;
-            mLastEngagementTimeUtcMillis = other.mLastEngagementTimeUtcMillis;
+            mValues = new ContentValues(other.mValues);
         }
 
         /**
@@ -212,7 +208,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setWatchNextType(@WatchNextType int watchNextType) {
-            mWatchNextType = watchNextType;
+            mValues.put(WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE, watchNextType);
             return this;
         }
 
@@ -224,7 +220,8 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          */
         public Builder setLastEngagementTimeUtcMillis(long lastEngagementTimeUtcMillis) {
-            mLastEngagementTimeUtcMillis = lastEngagementTimeUtcMillis;
+            mValues.put(WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS,
+                    lastEngagementTimeUtcMillis);
             return this;
         }
 
diff --git a/tv-provider/tests/src/android/support/media/tv/ChannelTest.java b/tv-provider/tests/src/android/support/media/tv/ChannelTest.java
index 88dc405..82d3a61 100644
--- a/tv-provider/tests/src/android/support/media/tv/ChannelTest.java
+++ b/tv-provider/tests/src/android/support/media/tv/ChannelTest.java
@@ -26,7 +26,6 @@
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SdkSuppress;
 import android.support.test.filters.SmallTest;
-import android.support.v4.os.BuildCompat;
 
 import junit.framework.TestCase;
 
@@ -102,14 +101,65 @@
         Uri channelUri = resolver.insert(Channels.CONTENT_URI, contentValues);
         assertNotNull(channelUri);
 
-        Channel channelFromSystemDb;
-        try (Cursor cursor = resolver.query(channelUri, Channel.PROJECTION, null, null, null)) {
+        Channel channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri);
+        compareChannel(fullyPopulatedChannel, channelFromSystemDb, false);
+    }
+
+    @Test
+    public void testChannelUpdateWithContentProvider() {
+        if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+            return;
+        }
+
+        Channel fullyPopulatedChannel = createFullyPopulatedChannel();
+        ContentValues contentValues = fullyPopulatedChannel.toContentValues();
+        ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+        Uri channelUri = resolver.insert(Channels.CONTENT_URI, contentValues);
+        assertNotNull(channelUri);
+
+        Channel channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri);
+        compareChannel(fullyPopulatedChannel, channelFromSystemDb, false);
+
+        // Update a field from a fully loaded channel.
+        Channel updatedChannel = new Channel.Builder(channelFromSystemDb)
+                .setDescription("new description").build();
+        assertEquals(1, resolver.update(channelUri, updatedChannel.toContentValues(), null, null));
+        channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri);
+        compareChannel(updatedChannel, channelFromSystemDb, false);
+
+        // Update a field with null from a fully loaded channel.
+        updatedChannel = new Channel.Builder(updatedChannel)
+                .setAppLinkText(null).build();
+        assertEquals(1, resolver.update(
+                channelUri, updatedChannel.toContentValues(), null, null));
+        channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri);
+        compareChannel(updatedChannel, channelFromSystemDb, false);
+
+        // Update a field without referencing fully channel.
+        ContentValues values = new Channel.Builder().setDisplayName("abc").build()
+                .toContentValues();
+        assertEquals(1, values.size());
+        assertEquals(1, resolver.update(channelUri, values, null, null));
+        channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri);
+        Channel expectedChannel = new Channel.Builder(channelFromSystemDb)
+                .setDisplayName("abc").build();
+        compareChannel(expectedChannel, channelFromSystemDb, false);
+    }
+
+    @Test
+    public void testChannelEquals() {
+        assertEquals(createFullyPopulatedChannel(), createFullyPopulatedChannel());
+    }
+
+
+    private static Channel loadChannelFromContentProvider(
+            ContentResolver resolver, Uri channelUri) {
+        try (Cursor cursor = resolver.query(channelUri, null, null, null, null)) {
             assertNotNull(cursor);
             assertEquals(1, cursor.getCount());
             cursor.moveToNext();
-            channelFromSystemDb = Channel.fromCursor(cursor);
+            return Channel.fromCursor(cursor);
         }
-        compareChannel(fullyPopulatedChannel, channelFromSystemDb, false);
     }
 
     private static Channel createFullyPopulatedChannel() {
@@ -166,7 +216,7 @@
             assertEquals(channelA.getAppLinkPosterArtUri(), channelB.getAppLinkPosterArtUri());
             assertEquals(channelA.getAppLinkText(), channelB.getAppLinkText());
         }
-        if (BuildCompat.isAtLeastO()) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             assertEquals(channelA.getInternalProviderId(), channelB.getInternalProviderId());
             assertEquals(channelA.isTransient(), channelB.isTransient());
         }
@@ -177,11 +227,10 @@
             // protected fields since they only can be modified by system apps.
             assertEquals(channelA.isBrowsable(), channelB.isBrowsable());
             assertEquals(channelA.isLocked(), channelB.isLocked());
-            if (BuildCompat.isAtLeastO()) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                 assertEquals(channelA.isSystemApproved(), channelB.isSystemApproved());
             }
             assertEquals(channelA.toContentValues(), channelB.toContentValues());
-            assertEquals(channelA.toString(), channelB.toString());
         }
     }
 
diff --git a/tv-provider/tests/src/android/support/media/tv/PreviewProgramTest.java b/tv-provider/tests/src/android/support/media/tv/PreviewProgramTest.java
index e8a7bdd..e222e40 100644
--- a/tv-provider/tests/src/android/support/media/tv/PreviewProgramTest.java
+++ b/tv-provider/tests/src/android/support/media/tv/PreviewProgramTest.java
@@ -15,6 +15,7 @@
  */
 package android.support.media.tv;
 
+import android.annotation.TargetApi;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -26,8 +27,8 @@
 import android.support.media.tv.TvContractCompat.Channels;
 import android.support.media.tv.TvContractCompat.PreviewPrograms;
 import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
 import android.support.test.filters.SmallTest;
-import android.support.v4.os.BuildCompat;
 
 import junit.framework.TestCase;
 
@@ -42,14 +43,12 @@
  * values from them.
  */
 @SmallTest
+@SdkSuppress(minSdkVersion = 26)
+@TargetApi(26)
 public class PreviewProgramTest extends TestCase {
 
     @Override
     protected void tearDown() {
-        // TODO: Use @SdkSuppress once Build.VERSION_CODES.O has a right value.
-        if (!BuildCompat.isAtLeastO()) {
-            return;
-        }
         if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
             return;
         }
@@ -59,9 +58,6 @@
 
     @Test
     public void testEmptyPreviewProgram() {
-        if (!BuildCompat.isAtLeastO()) {
-            return;
-        }
         PreviewProgram emptyProgram = new PreviewProgram.Builder().build();
         ContentValues contentValues = emptyProgram.toContentValues();
         compareProgram(emptyProgram,
@@ -71,9 +67,6 @@
 
     @Test
     public void testSampleProgram() {
-        if (!BuildCompat.isAtLeastO()) {
-            return;
-        }
         PreviewProgram sampleProgram = new PreviewProgram.Builder()
                 .setPackageName("My package")
                 .setTitle("Program Title")
@@ -94,10 +87,7 @@
     }
 
     @Test
-    public void testFullyPopulatedProgram() {
-        if (!BuildCompat.isAtLeastO()) {
-            return;
-        }
+    public void testFullyPopulatedPreviewProgram() {
         PreviewProgram fullyPopulatedProgram = createFullyPopulatedPreviewProgram(3);
         ContentValues contentValues = fullyPopulatedProgram.toContentValues(true);
         compareProgram(fullyPopulatedProgram,
@@ -110,10 +100,7 @@
     }
 
     @Test
-    public void testChannelWithSystemContentProvider() {
-        if (!BuildCompat.isAtLeastO()) {
-            return;
-        }
+    public void testPreviewProgramWithSystemContentProvider() {
         if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
             return;
         }
@@ -130,21 +117,77 @@
         Uri previewProgramUri = resolver.insert(PreviewPrograms.CONTENT_URI,
                 fullyPopulatedProgram.toContentValues());
 
-        PreviewProgram programFromSystemDb;
-        try (Cursor cursor = resolver.query(previewProgramUri, null, null, null, null)) {
-            assertNotNull(cursor);
-            assertEquals(1, cursor.getCount());
-            cursor.moveToNext();
-            programFromSystemDb = PreviewProgram.fromCursor(cursor);
-        }
+        PreviewProgram programFromSystemDb =
+                loadPreviewProgramFromContentProvider(resolver, previewProgramUri);
         compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
     }
 
     @Test
-    public void testPreviewProgramWithPartialData() {
-        if (!BuildCompat.isAtLeastO()) {
+    public void testPreviewProgramUpdateWithContentProvider() {
+        if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
             return;
         }
+        Channel channel = new Channel.Builder()
+                .setInputId("TestInputService")
+                .setType(TvContractCompat.Channels.TYPE_PREVIEW)
+                .build();
+        ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+        Uri channelUri = resolver.insert(Channels.CONTENT_URI, channel.toContentValues());
+        assertNotNull(channelUri);
+
+        PreviewProgram fullyPopulatedProgram = createFullyPopulatedPreviewProgram(
+                ContentUris.parseId(channelUri));
+        Uri previewProgramUri = resolver.insert(PreviewPrograms.CONTENT_URI,
+                fullyPopulatedProgram.toContentValues());
+
+        PreviewProgram programFromSystemDb = loadPreviewProgramFromContentProvider(
+                resolver, previewProgramUri);
+        compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
+
+        // Update a field from a fully loaded preview program.
+        PreviewProgram updatedProgram = new PreviewProgram.Builder(programFromSystemDb)
+                .setInteractionCount(programFromSystemDb.getInteractionCount() + 1).build();
+        assertEquals(1, resolver.update(
+                previewProgramUri, updatedProgram.toContentValues(), null, null));
+        programFromSystemDb = loadPreviewProgramFromContentProvider(resolver, previewProgramUri);
+        compareProgram(updatedProgram, programFromSystemDb, false);
+
+        // Update a field with null from a fully loaded preview program.
+        updatedProgram = new PreviewProgram.Builder(updatedProgram)
+                .setLongDescription(null).build();
+        assertEquals(1, resolver.update(
+                previewProgramUri, updatedProgram.toContentValues(), null, null));
+        programFromSystemDb = loadPreviewProgramFromContentProvider(resolver, previewProgramUri);
+        compareProgram(updatedProgram, programFromSystemDb, false);
+
+        // Update a field without referencing fully loaded preview program.
+        ContentValues values = new PreviewProgram.Builder().setInteractionCount(1).build()
+                .toContentValues();
+        assertEquals(1, values.size());
+        assertEquals(1, resolver.update(previewProgramUri, values, null, null));
+        programFromSystemDb = loadPreviewProgramFromContentProvider(resolver, previewProgramUri);
+        PreviewProgram expectedProgram = new PreviewProgram.Builder(programFromSystemDb)
+                .setInteractionCount(1).build();
+        compareProgram(expectedProgram, programFromSystemDb, false);
+    }
+
+    @Test
+    public void testPreviewProgramEquals() {
+        assertEquals(createFullyPopulatedPreviewProgram(1), createFullyPopulatedPreviewProgram(1));
+    }
+
+    private static PreviewProgram loadPreviewProgramFromContentProvider(
+            ContentResolver resolver, Uri previewProgramUri) {
+        try (Cursor cursor = resolver.query(previewProgramUri, null, null, null, null)) {
+            assertNotNull(cursor);
+            assertEquals(1, cursor.getCount());
+            cursor.moveToNext();
+            return PreviewProgram.fromCursor(cursor);
+        }
+    }
+
+    @Test
+    public void testPreviewProgramWithPartialData() {
         PreviewProgram previewProgram = new PreviewProgram.Builder()
                 .setChannelId(3)
                 .setWeight(100)
@@ -314,7 +357,6 @@
         assertEquals(programA.getReviewRatingStyle(), programB.getReviewRatingStyle());
         assertEquals(programA.getReviewRating(), programB.getReviewRating());
         assertEquals(programA.getContentId(), programB.getContentId());
-        assertEquals(programA.toString(), programB.toString());
         if (includeIdAndProtectedFields) {
             // Skip row ID since the one from system DB has the valid ID while the other does not.
             assertEquals(programA.getId(), programB.getId());
diff --git a/tv-provider/tests/src/android/support/media/tv/ProgramTest.java b/tv-provider/tests/src/android/support/media/tv/ProgramTest.java
index 927b34f..e809253 100644
--- a/tv-provider/tests/src/android/support/media/tv/ProgramTest.java
+++ b/tv-provider/tests/src/android/support/media/tv/ProgramTest.java
@@ -28,7 +28,6 @@
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SdkSuppress;
 import android.support.test.filters.SmallTest;
-import android.support.v4.os.BuildCompat;
 
 import junit.framework.TestCase;
 
@@ -113,14 +112,71 @@
         Uri programUri = resolver.insert(Programs.CONTENT_URI,
                 fullyPopulatedProgram.toContentValues());
 
-        Program programFromSystemDb;
+        Program programFromSystemDb = loadProgramFromContentProvider(resolver, programUri);
+        compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
+    }
+
+    @Test
+    public void testProgramUpdateWithContentProvider() {
+        if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+            return;
+        }
+        Channel channel = new Channel.Builder()
+                .setInputId("TestInputService")
+                .setType(TvContractCompat.Channels.TYPE_OTHER)
+                .build();
+        ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+        Uri channelUri = resolver.insert(Channels.CONTENT_URI, channel.toContentValues());
+        assertNotNull(channelUri);
+
+        Program fullyPopulatedProgram =
+                createFullyPopulatedProgram(ContentUris.parseId(channelUri));
+        Uri programUri = resolver.insert(Programs.CONTENT_URI,
+                fullyPopulatedProgram.toContentValues());
+
+        Program programFromSystemDb = loadProgramFromContentProvider(resolver, programUri);
+        compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
+
+        // Update a field from a fully loaded program.
+        Program updatedProgram = new Program.Builder(programFromSystemDb)
+                .setDescription("description1").build();
+        assertEquals(1, resolver.update(
+                programUri, updatedProgram.toContentValues(), null, null));
+        programFromSystemDb = loadProgramFromContentProvider(resolver, programUri);
+        compareProgram(updatedProgram, programFromSystemDb, false);
+
+        // Update a field with null from a fully loaded program.
+        updatedProgram = new Program.Builder(updatedProgram)
+                .setLongDescription(null).build();
+        assertEquals(1, resolver.update(
+                programUri, updatedProgram.toContentValues(), null, null));
+        programFromSystemDb = loadProgramFromContentProvider(resolver, programUri);
+        compareProgram(updatedProgram, programFromSystemDb, false);
+
+        // Update a field without referencing fully loaded program.
+        ContentValues values = new Program.Builder().setDescription("description2").build()
+                .toContentValues();
+        assertEquals(1, values.size());
+        assertEquals(1, resolver.update(programUri, values, null, null));
+        programFromSystemDb = loadProgramFromContentProvider(resolver, programUri);
+        Program expectedProgram = new Program.Builder(programFromSystemDb)
+                .setDescription("description2").build();
+        compareProgram(expectedProgram, programFromSystemDb, false);
+    }
+
+    @Test
+    public void testProgramEquals() {
+        assertEquals(createFullyPopulatedProgram(1), createFullyPopulatedProgram(1));
+    }
+
+    private static Program loadProgramFromContentProvider(
+            ContentResolver resolver, Uri programUri) {
         try (Cursor cursor = resolver.query(programUri, null, null, null, null)) {
             assertNotNull(cursor);
             assertEquals(1, cursor.getCount());
             cursor.moveToNext();
-            programFromSystemDb = Program.fromCursor(cursor);
+            return Program.fromCursor(cursor);
         }
-        compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
     }
 
     private static Program createFullyPopulatedProgram(long channelId) {
@@ -186,17 +242,15 @@
             assertTrue(Objects.equals(programA.isRecordingProhibited(),
                     programB.isRecordingProhibited()));
         }
-        if (BuildCompat.isAtLeastO()) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             assertEquals(programA.getReviewRatingStyle(), programB.getReviewRatingStyle());
             assertEquals(programA.getReviewRating(), programB.getReviewRating());
         }
-        assertEquals(programA.toString(), programB.toString());
         if (includeIdAndProtectedFields) {
             // Skip row ID since the one from system DB has the valid ID while the other does not.
             assertEquals(programA.getId(), programB.getId());
             assertEquals(programA.getPackageName(), programB.getPackageName());
             assertEquals(programA.toContentValues(), programB.toContentValues());
-            assertEquals(programA, programB);
         }
     }
 
diff --git a/tv-provider/tests/src/android/support/media/tv/WatchNextProgramTest.java b/tv-provider/tests/src/android/support/media/tv/WatchNextProgramTest.java
index 79048aa..b1a1041 100644
--- a/tv-provider/tests/src/android/support/media/tv/WatchNextProgramTest.java
+++ b/tv-provider/tests/src/android/support/media/tv/WatchNextProgramTest.java
@@ -16,6 +16,7 @@
 
 package android.support.media.tv;
 
+import android.annotation.TargetApi;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Intent;
@@ -25,8 +26,8 @@
 import android.net.Uri;
 import android.support.media.tv.TvContractCompat.WatchNextPrograms;
 import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
 import android.support.test.filters.SmallTest;
-import android.support.v4.os.BuildCompat;
 
 import junit.framework.TestCase;
 
@@ -41,14 +42,12 @@
  * values from them.
  */
 @SmallTest
+@SdkSuppress(minSdkVersion = 26)
+@TargetApi(26)
 public class WatchNextProgramTest extends TestCase {
 
     @Override
     protected void tearDown() {
-        // TODO: Use @SdkSuppress once Build.VERSION_CODES.O has a right value.
-        if (!BuildCompat.isAtLeastO()) {
-            return;
-        }
         if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
             return;
         }
@@ -58,9 +57,6 @@
 
     @Test
     public void testEmptyPreviewProgram() {
-        if (!BuildCompat.isAtLeastO()) {
-            return;
-        }
         WatchNextProgram emptyProgram = new WatchNextProgram.Builder().build();
         ContentValues contentValues = emptyProgram.toContentValues(true);
         compareProgram(emptyProgram,
@@ -70,9 +66,6 @@
 
     @Test
     public void testSampleProgram() {
-        if (!BuildCompat.isAtLeastO()) {
-            return;
-        }
         WatchNextProgram sampleProgram = new WatchNextProgram.Builder()
                 .setTitle("Program Title")
                 .setDescription("This is a sample program")
@@ -91,9 +84,6 @@
 
     @Test
     public void testFullyPopulatedProgram() {
-        if (!BuildCompat.isAtLeastO()) {
-            return;
-        }
         WatchNextProgram fullyPopulatedProgram = createFullyPopulatedWatchNextProgram();
         ContentValues contentValues = fullyPopulatedProgram.toContentValues(true);
         compareProgram(fullyPopulatedProgram,
@@ -107,9 +97,6 @@
 
     @Test
     public void testChannelWithSystemContentProvider() {
-        if (!BuildCompat.isAtLeastO()) {
-            return;
-        }
         if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
             return;
         }
@@ -118,21 +105,74 @@
         Uri watchNextProgramUri = resolver.insert(WatchNextPrograms.CONTENT_URI,
                 fullyPopulatedProgram.toContentValues());
 
-        WatchNextProgram programFromSystemDb;
-        try (Cursor cursor = resolver.query(watchNextProgramUri, null, null, null, null)) {
-            assertNotNull(cursor);
-            assertEquals(1, cursor.getCount());
-            cursor.moveToNext();
-            programFromSystemDb = WatchNextProgram.fromCursor(cursor);
-        }
+        WatchNextProgram programFromSystemDb =
+                loadWatchNextProgramFromContentProvider(resolver, watchNextProgramUri);
         compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
     }
 
     @Test
-    public void testWatchNextProgramWithPartialData() {
-        if (!BuildCompat.isAtLeastO()) {
+    public void testWatchNextProgramUpdateWithContentProvider() {
+        if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
             return;
         }
+
+        WatchNextProgram fullyPopulatedProgram = createFullyPopulatedWatchNextProgram();
+        ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+        Uri watchNextProgramUri = resolver.insert(WatchNextPrograms.CONTENT_URI,
+                fullyPopulatedProgram.toContentValues());
+
+        WatchNextProgram programFromSystemDb =
+                loadWatchNextProgramFromContentProvider(resolver, watchNextProgramUri);
+        compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
+
+        // Update a field from a fully loaded watch-next program.
+        WatchNextProgram updatedProgram = new WatchNextProgram.Builder(programFromSystemDb)
+                .setInteractionCount(programFromSystemDb.getInteractionCount() + 1).build();
+        assertEquals(1, resolver.update(
+                watchNextProgramUri, updatedProgram.toContentValues(), null, null));
+        programFromSystemDb =
+                loadWatchNextProgramFromContentProvider(resolver, watchNextProgramUri);
+        compareProgram(updatedProgram, programFromSystemDb, false);
+
+        // Update a field with null from a fully loaded watch-next program.
+        updatedProgram = new WatchNextProgram.Builder(updatedProgram)
+                .setPreviewVideoUri(null).build();
+        assertEquals(1, resolver.update(
+                watchNextProgramUri, updatedProgram.toContentValues(), null, null));
+        programFromSystemDb = loadWatchNextProgramFromContentProvider(
+                resolver, watchNextProgramUri);
+        compareProgram(updatedProgram, programFromSystemDb, false);
+
+        // Update a field without referencing fully watch-next program.
+        ContentValues values = new PreviewProgram.Builder().setInteractionCount(1).build()
+                .toContentValues();
+        assertEquals(1, values.size());
+        assertEquals(1, resolver.update(watchNextProgramUri, values, null, null));
+        programFromSystemDb = loadWatchNextProgramFromContentProvider(
+                resolver, watchNextProgramUri);
+        WatchNextProgram expectedProgram = new WatchNextProgram.Builder(programFromSystemDb)
+                .setInteractionCount(1).build();
+        compareProgram(expectedProgram, programFromSystemDb, false);
+    }
+
+    @Test
+    public void testWatchNextProgramEquals() {
+        assertEquals(createFullyPopulatedWatchNextProgram(),
+                createFullyPopulatedWatchNextProgram());
+    }
+
+    private static WatchNextProgram loadWatchNextProgramFromContentProvider(
+            ContentResolver resolver, Uri watchNextProgramUri) {
+        try (Cursor cursor = resolver.query(watchNextProgramUri, null, null, null, null)) {
+            assertNotNull(cursor);
+            assertEquals(1, cursor.getCount());
+            cursor.moveToNext();
+            return WatchNextProgram.fromCursor(cursor);
+        }
+    }
+
+    @Test
+    public void testWatchNextProgramWithPartialData() {
         WatchNextProgram previewProgram = new WatchNextProgram.Builder()
                 .setInternalProviderId("ID-4321")
                 .setPreviewVideoUri(Uri.parse("http://example.com/preview-video.mpg"))
@@ -296,7 +336,6 @@
         assertEquals(programA.getReviewRatingStyle(), programB.getReviewRatingStyle());
         assertEquals(programA.getReviewRating(), programB.getReviewRating());
         assertEquals(programA.getContentId(), programB.getContentId());
-        assertEquals(programA.toString(), programB.toString());
         if (includeIdAndProtectedFields) {
             // Skip row ID since the one from system DB has the valid ID while the other does not.
             assertEquals(programA.getId(), programB.getId());