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

package com.android.tv.testing.fakes;

import android.annotation.SuppressLint;
import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.tv.TvContract;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import androidx.tvprovider.media.tv.TvContractCompat;
import androidx.tvprovider.media.tv.TvContractCompat.BaseTvColumns;
import androidx.tvprovider.media.tv.TvContractCompat.Channels;
import androidx.tvprovider.media.tv.TvContractCompat.PreviewPrograms;
import androidx.tvprovider.media.tv.TvContractCompat.Programs;
import androidx.tvprovider.media.tv.TvContractCompat.Programs.Genres;
import androidx.tvprovider.media.tv.TvContractCompat.RecordedPrograms;
import androidx.tvprovider.media.tv.TvContractCompat.WatchNextPrograms;
import com.android.tv.common.util.sql.SqlParams;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Fake TV content provider suitable for unit tests. The contract between this provider and
 * applications is defined in {@link TvContractCompat}.
 */
// TODO(b/62143348): remove when error prone check fixed
@SuppressWarnings({"AndroidApiChecker", "TryWithResources"})
public class FakeTvProvider extends ContentProvider {
    // TODO either make this a shadow or move it to the support library

    private static final boolean DEBUG = false;
    private static final String TAG = "TvProvider";

    static final int DATABASE_VERSION = 34;
    static final String SHARED_PREF_BLOCKED_PACKAGES_KEY = "blocked_packages";
    static final String CHANNELS_TABLE = "channels";
    static final String PROGRAMS_TABLE = "programs";
    static final String RECORDED_PROGRAMS_TABLE = "recorded_programs";
    static final String PREVIEW_PROGRAMS_TABLE = "preview_programs";
    static final String WATCH_NEXT_PROGRAMS_TABLE = "watch_next_programs";
    static final String WATCHED_PROGRAMS_TABLE = "watched_programs";
    static final String PROGRAMS_TABLE_PACKAGE_NAME_INDEX = "programs_package_name_index";
    static final String PROGRAMS_TABLE_CHANNEL_ID_INDEX = "programs_channel_id_index";
    static final String PROGRAMS_TABLE_START_TIME_INDEX = "programs_start_time_index";
    static final String PROGRAMS_TABLE_END_TIME_INDEX = "programs_end_time_index";
    static final String WATCHED_PROGRAMS_TABLE_CHANNEL_ID_INDEX =
            "watched_programs_channel_id_index";
    // The internal column in the watched programs table to indicate whether the current log entry
    // is consolidated or not. Unconsolidated entries may have columns with missing data.
    static final String WATCHED_PROGRAMS_COLUMN_CONSOLIDATED = "consolidated";
    static final String CHANNELS_COLUMN_LOGO = "logo";
    private static final String DATABASE_NAME = "tv.db";
    private static final String DELETED_CHANNELS_TABLE = "deleted_channels"; // Deprecated
    private static final String DEFAULT_PROGRAMS_SORT_ORDER =
            Programs.COLUMN_START_TIME_UTC_MILLIS + " ASC";
    private static final String DEFAULT_WATCHED_PROGRAMS_SORT_ORDER =
            WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC";
    private static final String CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE =
            CHANNELS_TABLE
                    + " INNER JOIN "
                    + PROGRAMS_TABLE
                    + " ON ("
                    + CHANNELS_TABLE
                    + "."
                    + Channels._ID
                    + "="
                    + PROGRAMS_TABLE
                    + "."
                    + Programs.COLUMN_CHANNEL_ID
                    + ")";

    // Operation names for createSqlParams().
    private static final String OP_QUERY = "query";
    private static final String OP_UPDATE = "update";
    private static final String OP_DELETE = "delete";

    private static final UriMatcher sUriMatcher;
    private static final int MATCH_CHANNEL = 1;
    private static final int MATCH_CHANNEL_ID = 2;
    private static final int MATCH_CHANNEL_ID_LOGO = 3;
    private static final int MATCH_PASSTHROUGH_ID = 4;
    private static final int MATCH_PROGRAM = 5;
    private static final int MATCH_PROGRAM_ID = 6;
    private static final int MATCH_WATCHED_PROGRAM = 7;
    private static final int MATCH_WATCHED_PROGRAM_ID = 8;
    private static final int MATCH_RECORDED_PROGRAM = 9;
    private static final int MATCH_RECORDED_PROGRAM_ID = 10;
    private static final int MATCH_PREVIEW_PROGRAM = 11;
    private static final int MATCH_PREVIEW_PROGRAM_ID = 12;
    private static final int MATCH_WATCH_NEXT_PROGRAM = 13;
    private static final int MATCH_WATCH_NEXT_PROGRAM_ID = 14;

    private static final int MAX_LOGO_IMAGE_SIZE = 256;

    private static final String EMPTY_STRING = "";

    private static final Map<String, String> sChannelProjectionMap;
    private static final Map<String, String> sProgramProjectionMap;
    private static final Map<String, String> sWatchedProgramProjectionMap;
    private static final Map<String, String> sRecordedProgramProjectionMap;
    private static final Map<String, String> sPreviewProgramProjectionMap;
    private static final Map<String, String> sWatchNextProgramProjectionMap;

    // TvContract hidden
    private static final String PARAM_PACKAGE = "package";
    private static final String PARAM_PREVIEW = "preview";

    private static boolean sInitialized;

    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(TvContractCompat.AUTHORITY, "channel", MATCH_CHANNEL);
        sUriMatcher.addURI(TvContractCompat.AUTHORITY, "channel/#", MATCH_CHANNEL_ID);
        sUriMatcher.addURI(TvContractCompat.AUTHORITY, "channel/#/logo", MATCH_CHANNEL_ID_LOGO);
        sUriMatcher.addURI(TvContractCompat.AUTHORITY, "passthrough/*", MATCH_PASSTHROUGH_ID);
        sUriMatcher.addURI(TvContractCompat.AUTHORITY, "program", MATCH_PROGRAM);
        sUriMatcher.addURI(TvContractCompat.AUTHORITY, "program/#", MATCH_PROGRAM_ID);
        sUriMatcher.addURI(TvContractCompat.AUTHORITY, "watched_program", MATCH_WATCHED_PROGRAM);
        sUriMatcher.addURI(
                TvContractCompat.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID);
        sUriMatcher.addURI(TvContractCompat.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM);
        sUriMatcher.addURI(
                TvContractCompat.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID);
        sUriMatcher.addURI(TvContractCompat.AUTHORITY, "preview_program", MATCH_PREVIEW_PROGRAM);
        sUriMatcher.addURI(
                TvContractCompat.AUTHORITY, "preview_program/#", MATCH_PREVIEW_PROGRAM_ID);
        sUriMatcher.addURI(
                TvContractCompat.AUTHORITY, "watch_next_program", MATCH_WATCH_NEXT_PROGRAM);
        sUriMatcher.addURI(
                TvContractCompat.AUTHORITY, "watch_next_program/#", MATCH_WATCH_NEXT_PROGRAM_ID);

        sChannelProjectionMap = new HashMap<>();
        sChannelProjectionMap.put(Channels._ID, CHANNELS_TABLE + "." + Channels._ID);
        sChannelProjectionMap.put(
                Channels.COLUMN_PACKAGE_NAME, CHANNELS_TABLE + "." + Channels.COLUMN_PACKAGE_NAME);
        sChannelProjectionMap.put(
                Channels.COLUMN_INPUT_ID, CHANNELS_TABLE + "." + Channels.COLUMN_INPUT_ID);
        sChannelProjectionMap.put(
                Channels.COLUMN_TYPE, CHANNELS_TABLE + "." + Channels.COLUMN_TYPE);
        sChannelProjectionMap.put(
                Channels.COLUMN_SERVICE_TYPE, CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_TYPE);
        sChannelProjectionMap.put(
                Channels.COLUMN_ORIGINAL_NETWORK_ID,
                CHANNELS_TABLE + "." + Channels.COLUMN_ORIGINAL_NETWORK_ID);
        sChannelProjectionMap.put(
                Channels.COLUMN_TRANSPORT_STREAM_ID,
                CHANNELS_TABLE + "." + Channels.COLUMN_TRANSPORT_STREAM_ID);
        sChannelProjectionMap.put(
                Channels.COLUMN_SERVICE_ID, CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_ID);
        sChannelProjectionMap.put(
                Channels.COLUMN_DISPLAY_NUMBER,
                CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NUMBER);
        sChannelProjectionMap.put(
                Channels.COLUMN_DISPLAY_NAME, CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NAME);
        sChannelProjectionMap.put(
                Channels.COLUMN_NETWORK_AFFILIATION,
                CHANNELS_TABLE + "." + Channels.COLUMN_NETWORK_AFFILIATION);
        sChannelProjectionMap.put(
                Channels.COLUMN_DESCRIPTION, CHANNELS_TABLE + "." + Channels.COLUMN_DESCRIPTION);
        sChannelProjectionMap.put(
                Channels.COLUMN_VIDEO_FORMAT, CHANNELS_TABLE + "." + Channels.COLUMN_VIDEO_FORMAT);
        sChannelProjectionMap.put(
                Channels.COLUMN_BROWSABLE, CHANNELS_TABLE + "." + Channels.COLUMN_BROWSABLE);
        sChannelProjectionMap.put(
                Channels.COLUMN_SEARCHABLE, CHANNELS_TABLE + "." + Channels.COLUMN_SEARCHABLE);
        sChannelProjectionMap.put(
                Channels.COLUMN_LOCKED, CHANNELS_TABLE + "." + Channels.COLUMN_LOCKED);
        sChannelProjectionMap.put(
                Channels.COLUMN_APP_LINK_ICON_URI,
                CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_ICON_URI);
        sChannelProjectionMap.put(
                Channels.COLUMN_APP_LINK_POSTER_ART_URI,
                CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_POSTER_ART_URI);
        sChannelProjectionMap.put(
                Channels.COLUMN_APP_LINK_TEXT,
                CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_TEXT);
        sChannelProjectionMap.put(
                Channels.COLUMN_APP_LINK_COLOR,
                CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_COLOR);
        sChannelProjectionMap.put(
                Channels.COLUMN_APP_LINK_INTENT_URI,
                CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_INTENT_URI);
        sChannelProjectionMap.put(
                Channels.COLUMN_INTERNAL_PROVIDER_DATA,
                CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_DATA);
        sChannelProjectionMap.put(
                Channels.COLUMN_INTERNAL_PROVIDER_FLAG1,
                CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1);
        sChannelProjectionMap.put(
                Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
                CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2);
        sChannelProjectionMap.put(
                Channels.COLUMN_INTERNAL_PROVIDER_FLAG3,
                CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3);
        sChannelProjectionMap.put(
                Channels.COLUMN_INTERNAL_PROVIDER_FLAG4,
                CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4);
        sChannelProjectionMap.put(
                Channels.COLUMN_VERSION_NUMBER,
                CHANNELS_TABLE + "." + Channels.COLUMN_VERSION_NUMBER);
        sChannelProjectionMap.put(
                Channels.COLUMN_TRANSIENT, CHANNELS_TABLE + "." + Channels.COLUMN_TRANSIENT);
        sChannelProjectionMap.put(
                Channels.COLUMN_INTERNAL_PROVIDER_ID,
                CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_ID);

        sProgramProjectionMap = new HashMap<>();
        sProgramProjectionMap.put(Programs._ID, Programs._ID);
        sProgramProjectionMap.put(Programs.COLUMN_PACKAGE_NAME, Programs.COLUMN_PACKAGE_NAME);
        sProgramProjectionMap.put(Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_CHANNEL_ID);
        sProgramProjectionMap.put(Programs.COLUMN_TITLE, Programs.COLUMN_TITLE);
        // COLUMN_SEASON_NUMBER is deprecated. Return COLUMN_SEASON_DISPLAY_NUMBER instead.
        sProgramProjectionMap.put(
                Programs.COLUMN_SEASON_NUMBER,
                Programs.COLUMN_SEASON_DISPLAY_NUMBER + " AS " + Programs.COLUMN_SEASON_NUMBER);
        sProgramProjectionMap.put(
                Programs.COLUMN_SEASON_DISPLAY_NUMBER, Programs.COLUMN_SEASON_DISPLAY_NUMBER);
        sProgramProjectionMap.put(Programs.COLUMN_SEASON_TITLE, Programs.COLUMN_SEASON_TITLE);
        // COLUMN_EPISODE_NUMBER is deprecated. Return COLUMN_EPISODE_DISPLAY_NUMBER instead.
        sProgramProjectionMap.put(
                Programs.COLUMN_EPISODE_NUMBER,
                Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " AS " + Programs.COLUMN_EPISODE_NUMBER);
        sProgramProjectionMap.put(
                Programs.COLUMN_EPISODE_DISPLAY_NUMBER, Programs.COLUMN_EPISODE_DISPLAY_NUMBER);
        sProgramProjectionMap.put(Programs.COLUMN_EPISODE_TITLE, Programs.COLUMN_EPISODE_TITLE);
        sProgramProjectionMap.put(
                Programs.COLUMN_START_TIME_UTC_MILLIS, Programs.COLUMN_START_TIME_UTC_MILLIS);
        sProgramProjectionMap.put(
                Programs.COLUMN_END_TIME_UTC_MILLIS, Programs.COLUMN_END_TIME_UTC_MILLIS);
        sProgramProjectionMap.put(Programs.COLUMN_BROADCAST_GENRE, Programs.COLUMN_BROADCAST_GENRE);
        sProgramProjectionMap.put(Programs.COLUMN_CANONICAL_GENRE, Programs.COLUMN_CANONICAL_GENRE);
        sProgramProjectionMap.put(
                Programs.COLUMN_SHORT_DESCRIPTION, Programs.COLUMN_SHORT_DESCRIPTION);
        sProgramProjectionMap.put(
                Programs.COLUMN_LONG_DESCRIPTION, Programs.COLUMN_LONG_DESCRIPTION);
        sProgramProjectionMap.put(Programs.COLUMN_VIDEO_WIDTH, Programs.COLUMN_VIDEO_WIDTH);
        sProgramProjectionMap.put(Programs.COLUMN_VIDEO_HEIGHT, Programs.COLUMN_VIDEO_HEIGHT);
        sProgramProjectionMap.put(Programs.COLUMN_AUDIO_LANGUAGE, Programs.COLUMN_AUDIO_LANGUAGE);
        sProgramProjectionMap.put(Programs.COLUMN_CONTENT_RATING, Programs.COLUMN_CONTENT_RATING);
        sProgramProjectionMap.put(Programs.COLUMN_POSTER_ART_URI, Programs.COLUMN_POSTER_ART_URI);
        sProgramProjectionMap.put(Programs.COLUMN_THUMBNAIL_URI, Programs.COLUMN_THUMBNAIL_URI);
        sProgramProjectionMap.put(Programs.COLUMN_SEARCHABLE, Programs.COLUMN_SEARCHABLE);
        sProgramProjectionMap.put(
                Programs.COLUMN_RECORDING_PROHIBITED, Programs.COLUMN_RECORDING_PROHIBITED);
        sProgramProjectionMap.put(
                Programs.COLUMN_INTERNAL_PROVIDER_DATA, Programs.COLUMN_INTERNAL_PROVIDER_DATA);
        sProgramProjectionMap.put(
                Programs.COLUMN_INTERNAL_PROVIDER_FLAG1, Programs.COLUMN_INTERNAL_PROVIDER_FLAG1);
        sProgramProjectionMap.put(
                Programs.COLUMN_INTERNAL_PROVIDER_FLAG2, Programs.COLUMN_INTERNAL_PROVIDER_FLAG2);
        sProgramProjectionMap.put(
                Programs.COLUMN_INTERNAL_PROVIDER_FLAG3, Programs.COLUMN_INTERNAL_PROVIDER_FLAG3);
        sProgramProjectionMap.put(
                Programs.COLUMN_INTERNAL_PROVIDER_FLAG4, Programs.COLUMN_INTERNAL_PROVIDER_FLAG4);
        sProgramProjectionMap.put(Programs.COLUMN_VERSION_NUMBER, Programs.COLUMN_VERSION_NUMBER);
        sProgramProjectionMap.put(
                Programs.COLUMN_REVIEW_RATING_STYLE, Programs.COLUMN_REVIEW_RATING_STYLE);
        sProgramProjectionMap.put(Programs.COLUMN_REVIEW_RATING, Programs.COLUMN_REVIEW_RATING);

        sWatchedProgramProjectionMap = new HashMap<>();
        sWatchedProgramProjectionMap.put(WatchedPrograms._ID, WatchedPrograms._ID);
        sWatchedProgramProjectionMap.put(
                WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
                WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS);
        sWatchedProgramProjectionMap.put(
                WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
                WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS);
        sWatchedProgramProjectionMap.put(
                WatchedPrograms.COLUMN_CHANNEL_ID, WatchedPrograms.COLUMN_CHANNEL_ID);
        sWatchedProgramProjectionMap.put(
                WatchedPrograms.COLUMN_TITLE, WatchedPrograms.COLUMN_TITLE);
        sWatchedProgramProjectionMap.put(
                WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS,
                WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS);
        sWatchedProgramProjectionMap.put(
                WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS,
                WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS);
        sWatchedProgramProjectionMap.put(
                WatchedPrograms.COLUMN_DESCRIPTION, WatchedPrograms.COLUMN_DESCRIPTION);
        sWatchedProgramProjectionMap.put(
                WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS,
                WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS);
        sWatchedProgramProjectionMap.put(
                WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN,
                WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN);
        sWatchedProgramProjectionMap.put(
                WATCHED_PROGRAMS_COLUMN_CONSOLIDATED, WATCHED_PROGRAMS_COLUMN_CONSOLIDATED);

        sRecordedProgramProjectionMap = new HashMap<>();
        sRecordedProgramProjectionMap.put(RecordedPrograms._ID, RecordedPrograms._ID);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_PACKAGE_NAME, RecordedPrograms.COLUMN_PACKAGE_NAME);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_INPUT_ID, RecordedPrograms.COLUMN_INPUT_ID);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_CHANNEL_ID, RecordedPrograms.COLUMN_CHANNEL_ID);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_TITLE, RecordedPrograms.COLUMN_TITLE);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
                RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_SEASON_TITLE, RecordedPrograms.COLUMN_SEASON_TITLE);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
                RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_EPISODE_TITLE, RecordedPrograms.COLUMN_EPISODE_TITLE);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
                RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS,
                RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_BROADCAST_GENRE, RecordedPrograms.COLUMN_BROADCAST_GENRE);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_CANONICAL_GENRE, RecordedPrograms.COLUMN_CANONICAL_GENRE);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_SHORT_DESCRIPTION,
                RecordedPrograms.COLUMN_SHORT_DESCRIPTION);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_LONG_DESCRIPTION, RecordedPrograms.COLUMN_LONG_DESCRIPTION);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_VIDEO_WIDTH, RecordedPrograms.COLUMN_VIDEO_WIDTH);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_VIDEO_HEIGHT, RecordedPrograms.COLUMN_VIDEO_HEIGHT);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_AUDIO_LANGUAGE, RecordedPrograms.COLUMN_AUDIO_LANGUAGE);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_CONTENT_RATING, RecordedPrograms.COLUMN_CONTENT_RATING);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_POSTER_ART_URI, RecordedPrograms.COLUMN_POSTER_ART_URI);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_THUMBNAIL_URI, RecordedPrograms.COLUMN_THUMBNAIL_URI);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_SEARCHABLE, RecordedPrograms.COLUMN_SEARCHABLE);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_RECORDING_DATA_URI,
                RecordedPrograms.COLUMN_RECORDING_DATA_URI);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_RECORDING_DATA_BYTES,
                RecordedPrograms.COLUMN_RECORDING_DATA_BYTES);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
                RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
                RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_VERSION_NUMBER, RecordedPrograms.COLUMN_VERSION_NUMBER);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_REVIEW_RATING_STYLE,
                RecordedPrograms.COLUMN_REVIEW_RATING_STYLE);
        sRecordedProgramProjectionMap.put(
                RecordedPrograms.COLUMN_REVIEW_RATING, RecordedPrograms.COLUMN_REVIEW_RATING);

        sPreviewProgramProjectionMap = new HashMap<>();
        sPreviewProgramProjectionMap.put(PreviewPrograms._ID, PreviewPrograms._ID);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_PACKAGE_NAME, PreviewPrograms.COLUMN_PACKAGE_NAME);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_CHANNEL_ID, PreviewPrograms.COLUMN_CHANNEL_ID);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_TITLE, PreviewPrograms.COLUMN_TITLE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
                PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_SEASON_TITLE, PreviewPrograms.COLUMN_SEASON_TITLE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
                PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_EPISODE_TITLE, PreviewPrograms.COLUMN_EPISODE_TITLE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_CANONICAL_GENRE, PreviewPrograms.COLUMN_CANONICAL_GENRE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_SHORT_DESCRIPTION, PreviewPrograms.COLUMN_SHORT_DESCRIPTION);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_LONG_DESCRIPTION, PreviewPrograms.COLUMN_LONG_DESCRIPTION);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_VIDEO_WIDTH, PreviewPrograms.COLUMN_VIDEO_WIDTH);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_VIDEO_HEIGHT, PreviewPrograms.COLUMN_VIDEO_HEIGHT);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_AUDIO_LANGUAGE, PreviewPrograms.COLUMN_AUDIO_LANGUAGE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_CONTENT_RATING, PreviewPrograms.COLUMN_CONTENT_RATING);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_POSTER_ART_URI, PreviewPrograms.COLUMN_POSTER_ART_URI);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_THUMBNAIL_URI, PreviewPrograms.COLUMN_THUMBNAIL_URI);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_SEARCHABLE, PreviewPrograms.COLUMN_SEARCHABLE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
                PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
                PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
                PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
                PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
                PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_VERSION_NUMBER, PreviewPrograms.COLUMN_VERSION_NUMBER);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID,
                PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI, PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS,
                PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_DURATION_MILLIS, PreviewPrograms.COLUMN_DURATION_MILLIS);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_INTENT_URI, PreviewPrograms.COLUMN_INTENT_URI);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_WEIGHT, PreviewPrograms.COLUMN_WEIGHT);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_TRANSIENT, PreviewPrograms.COLUMN_TRANSIENT);
        sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_TYPE, PreviewPrograms.COLUMN_TYPE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO,
                PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO,
                PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_LOGO_URI, PreviewPrograms.COLUMN_LOGO_URI);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_AVAILABILITY, PreviewPrograms.COLUMN_AVAILABILITY);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_STARTING_PRICE, PreviewPrograms.COLUMN_STARTING_PRICE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_OFFER_PRICE, PreviewPrograms.COLUMN_OFFER_PRICE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_RELEASE_DATE, PreviewPrograms.COLUMN_RELEASE_DATE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_ITEM_COUNT, PreviewPrograms.COLUMN_ITEM_COUNT);
        sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LIVE, PreviewPrograms.COLUMN_LIVE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_INTERACTION_TYPE, PreviewPrograms.COLUMN_INTERACTION_TYPE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_INTERACTION_COUNT, PreviewPrograms.COLUMN_INTERACTION_COUNT);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_AUTHOR, PreviewPrograms.COLUMN_AUTHOR);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_REVIEW_RATING_STYLE,
                PreviewPrograms.COLUMN_REVIEW_RATING_STYLE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_REVIEW_RATING, PreviewPrograms.COLUMN_REVIEW_RATING);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_BROWSABLE, PreviewPrograms.COLUMN_BROWSABLE);
        sPreviewProgramProjectionMap.put(
                PreviewPrograms.COLUMN_CONTENT_ID, PreviewPrograms.COLUMN_CONTENT_ID);

        sWatchNextProgramProjectionMap = new HashMap<>();
        sWatchNextProgramProjectionMap.put(WatchNextPrograms._ID, WatchNextPrograms._ID);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_PACKAGE_NAME, WatchNextPrograms.COLUMN_PACKAGE_NAME);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_TITLE, WatchNextPrograms.COLUMN_TITLE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
                WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_SEASON_TITLE, WatchNextPrograms.COLUMN_SEASON_TITLE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
                WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_EPISODE_TITLE, WatchNextPrograms.COLUMN_EPISODE_TITLE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_CANONICAL_GENRE, WatchNextPrograms.COLUMN_CANONICAL_GENRE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_SHORT_DESCRIPTION,
                WatchNextPrograms.COLUMN_SHORT_DESCRIPTION);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_LONG_DESCRIPTION,
                WatchNextPrograms.COLUMN_LONG_DESCRIPTION);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_VIDEO_WIDTH, WatchNextPrograms.COLUMN_VIDEO_WIDTH);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_VIDEO_HEIGHT, WatchNextPrograms.COLUMN_VIDEO_HEIGHT);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_AUDIO_LANGUAGE, WatchNextPrograms.COLUMN_AUDIO_LANGUAGE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_CONTENT_RATING, WatchNextPrograms.COLUMN_CONTENT_RATING);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_POSTER_ART_URI, WatchNextPrograms.COLUMN_POSTER_ART_URI);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_THUMBNAIL_URI, WatchNextPrograms.COLUMN_THUMBNAIL_URI);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_SEARCHABLE, WatchNextPrograms.COLUMN_SEARCHABLE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_VERSION_NUMBER, WatchNextPrograms.COLUMN_VERSION_NUMBER);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID,
                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI,
                WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS,
                WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_DURATION_MILLIS, WatchNextPrograms.COLUMN_DURATION_MILLIS);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_INTENT_URI, WatchNextPrograms.COLUMN_INTENT_URI);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_TRANSIENT, WatchNextPrograms.COLUMN_TRANSIENT);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_TYPE, WatchNextPrograms.COLUMN_TYPE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE, WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO,
                WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO,
                WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_LOGO_URI, WatchNextPrograms.COLUMN_LOGO_URI);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_AVAILABILITY, WatchNextPrograms.COLUMN_AVAILABILITY);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_STARTING_PRICE, WatchNextPrograms.COLUMN_STARTING_PRICE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_OFFER_PRICE, WatchNextPrograms.COLUMN_OFFER_PRICE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_RELEASE_DATE, WatchNextPrograms.COLUMN_RELEASE_DATE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_ITEM_COUNT, WatchNextPrograms.COLUMN_ITEM_COUNT);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_LIVE, WatchNextPrograms.COLUMN_LIVE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_INTERACTION_TYPE,
                WatchNextPrograms.COLUMN_INTERACTION_TYPE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_INTERACTION_COUNT,
                WatchNextPrograms.COLUMN_INTERACTION_COUNT);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_AUTHOR, WatchNextPrograms.COLUMN_AUTHOR);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE,
                WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_REVIEW_RATING, WatchNextPrograms.COLUMN_REVIEW_RATING);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_BROWSABLE, WatchNextPrograms.COLUMN_BROWSABLE);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_CONTENT_ID, WatchNextPrograms.COLUMN_CONTENT_ID);
        sWatchNextProgramProjectionMap.put(
                WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS,
                WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS);
    }

    // Mapping from broadcast genre to canonical genre.
    private static Map<String, String> sGenreMap;

    private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";

    private static final String PERMISSION_ACCESS_ALL_EPG_DATA =
            "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA";

    private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS =
            "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS";

    private static final String CREATE_RECORDED_PROGRAMS_TABLE_SQL =
            "CREATE TABLE "
                    + RECORDED_PROGRAMS_TABLE
                    + " ("
                    + RecordedPrograms._ID
                    + " INTEGER PRIMARY KEY AUTOINCREMENT,"
                    + RecordedPrograms.COLUMN_PACKAGE_NAME
                    + " TEXT NOT NULL,"
                    + RecordedPrograms.COLUMN_INPUT_ID
                    + " TEXT NOT NULL,"
                    + RecordedPrograms.COLUMN_CHANNEL_ID
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_TITLE
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_SEASON_TITLE
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_EPISODE_TITLE
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_BROADCAST_GENRE
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_CANONICAL_GENRE
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_SHORT_DESCRIPTION
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_LONG_DESCRIPTION
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_VIDEO_WIDTH
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_VIDEO_HEIGHT
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_AUDIO_LANGUAGE
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_CONTENT_RATING
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_POSTER_ART_URI
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_THUMBNAIL_URI
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_SEARCHABLE
                    + " INTEGER NOT NULL DEFAULT 1,"
                    + RecordedPrograms.COLUMN_RECORDING_DATA_URI
                    + " TEXT,"
                    + RecordedPrograms.COLUMN_RECORDING_DATA_BYTES
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA
                    + " BLOB,"
                    + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_VERSION_NUMBER
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_REVIEW_RATING_STYLE
                    + " INTEGER,"
                    + RecordedPrograms.COLUMN_REVIEW_RATING
                    + " TEXT,"
                    + "FOREIGN KEY("
                    + RecordedPrograms.COLUMN_CHANNEL_ID
                    + ") "
                    + "REFERENCES "
                    + CHANNELS_TABLE
                    + "("
                    + Channels._ID
                    + ") "
                    + "ON UPDATE CASCADE ON DELETE SET NULL);";

    private static final String CREATE_PREVIEW_PROGRAMS_TABLE_SQL =
            "CREATE TABLE "
                    + PREVIEW_PROGRAMS_TABLE
                    + " ("
                    + PreviewPrograms._ID
                    + " INTEGER PRIMARY KEY AUTOINCREMENT,"
                    + PreviewPrograms.COLUMN_PACKAGE_NAME
                    + " TEXT NOT NULL,"
                    + PreviewPrograms.COLUMN_CHANNEL_ID
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_TITLE
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_SEASON_TITLE
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_EPISODE_TITLE
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_CANONICAL_GENRE
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_SHORT_DESCRIPTION
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_LONG_DESCRIPTION
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_VIDEO_WIDTH
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_VIDEO_HEIGHT
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_AUDIO_LANGUAGE
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_CONTENT_RATING
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_POSTER_ART_URI
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_THUMBNAIL_URI
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_SEARCHABLE
                    + " INTEGER NOT NULL DEFAULT 1,"
                    + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA
                    + " BLOB,"
                    + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_VERSION_NUMBER
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_DURATION_MILLIS
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_INTENT_URI
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_WEIGHT
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_TRANSIENT
                    + " INTEGER NOT NULL DEFAULT 0,"
                    + PreviewPrograms.COLUMN_TYPE
                    + " INTEGER NOT NULL,"
                    + PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_LOGO_URI
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_AVAILABILITY
                    + " INTERGER,"
                    + PreviewPrograms.COLUMN_STARTING_PRICE
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_OFFER_PRICE
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_RELEASE_DATE
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_ITEM_COUNT
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_LIVE
                    + " INTEGER NOT NULL DEFAULT 0,"
                    + PreviewPrograms.COLUMN_INTERACTION_TYPE
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_INTERACTION_COUNT
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_AUTHOR
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_REVIEW_RATING_STYLE
                    + " INTEGER,"
                    + PreviewPrograms.COLUMN_REVIEW_RATING
                    + " TEXT,"
                    + PreviewPrograms.COLUMN_BROWSABLE
                    + " INTEGER NOT NULL DEFAULT 1,"
                    + PreviewPrograms.COLUMN_CONTENT_ID
                    + " TEXT,"
                    + "FOREIGN KEY("
                    + PreviewPrograms.COLUMN_CHANNEL_ID
                    + ","
                    + PreviewPrograms.COLUMN_PACKAGE_NAME
                    + ") REFERENCES "
                    + CHANNELS_TABLE
                    + "("
                    + Channels._ID
                    + ","
                    + Channels.COLUMN_PACKAGE_NAME
                    + ") ON UPDATE CASCADE ON DELETE CASCADE"
                    + ");";
    private static final String CREATE_PREVIEW_PROGRAMS_PACKAGE_NAME_INDEX_SQL =
            "CREATE INDEX preview_programs_package_name_index ON "
                    + PREVIEW_PROGRAMS_TABLE
                    + "("
                    + PreviewPrograms.COLUMN_PACKAGE_NAME
                    + ");";
    private static final String CREATE_PREVIEW_PROGRAMS_CHANNEL_ID_INDEX_SQL =
            "CREATE INDEX preview_programs_id_index ON "
                    + PREVIEW_PROGRAMS_TABLE
                    + "("
                    + PreviewPrograms.COLUMN_CHANNEL_ID
                    + ");";
    private static final String CREATE_WATCH_NEXT_PROGRAMS_TABLE_SQL =
            "CREATE TABLE "
                    + WATCH_NEXT_PROGRAMS_TABLE
                    + " ("
                    + WatchNextPrograms._ID
                    + " INTEGER PRIMARY KEY AUTOINCREMENT,"
                    + WatchNextPrograms.COLUMN_PACKAGE_NAME
                    + " TEXT NOT NULL,"
                    + WatchNextPrograms.COLUMN_TITLE
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_SEASON_TITLE
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_EPISODE_TITLE
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_CANONICAL_GENRE
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_SHORT_DESCRIPTION
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_LONG_DESCRIPTION
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_VIDEO_WIDTH
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_VIDEO_HEIGHT
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_AUDIO_LANGUAGE
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_CONTENT_RATING
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_POSTER_ART_URI
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_THUMBNAIL_URI
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_SEARCHABLE
                    + " INTEGER NOT NULL DEFAULT 1,"
                    + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA
                    + " BLOB,"
                    + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_VERSION_NUMBER
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_DURATION_MILLIS
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_INTENT_URI
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_TRANSIENT
                    + " INTEGER NOT NULL DEFAULT 0,"
                    + WatchNextPrograms.COLUMN_TYPE
                    + " INTEGER NOT NULL,"
                    + WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_LOGO_URI
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_AVAILABILITY
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_STARTING_PRICE
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_OFFER_PRICE
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_RELEASE_DATE
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_ITEM_COUNT
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_LIVE
                    + " INTEGER NOT NULL DEFAULT 0,"
                    + WatchNextPrograms.COLUMN_INTERACTION_TYPE
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_INTERACTION_COUNT
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_AUTHOR
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE
                    + " INTEGER,"
                    + WatchNextPrograms.COLUMN_REVIEW_RATING
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_BROWSABLE
                    + " INTEGER NOT NULL DEFAULT 1,"
                    + WatchNextPrograms.COLUMN_CONTENT_ID
                    + " TEXT,"
                    + WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS
                    + " INTEGER"
                    + ");";
    private static final String CREATE_WATCH_NEXT_PROGRAMS_PACKAGE_NAME_INDEX_SQL =
            "CREATE INDEX watch_next_programs_package_name_index ON "
                    + WATCH_NEXT_PROGRAMS_TABLE
                    + "("
                    + WatchNextPrograms.COLUMN_PACKAGE_NAME
                    + ");";

    private String mCallingPackage = "com.android.tv";

    static class DatabaseHelper extends SQLiteOpenHelper {
        private Context mContext;

        public static synchronized DatabaseHelper createInstance(Context context) {
            return new DatabaseHelper(context);
        }

        private DatabaseHelper(Context context) {
            this(context, DATABASE_NAME, DATABASE_VERSION);
        }

        @VisibleForTesting
        DatabaseHelper(Context context, String databaseName, int databaseVersion) {
            super(context, databaseName, null, databaseVersion);
            mContext = context;
        }

        @Override
        public void onConfigure(SQLiteDatabase db) {
            db.setForeignKeyConstraintsEnabled(true);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            if (DEBUG) {
                Log.d(TAG, "Creating database");
            }
            // Set up the database schema.
            db.execSQL(
                    "CREATE TABLE "
                            + CHANNELS_TABLE
                            + " ("
                            + Channels._ID
                            + " INTEGER PRIMARY KEY AUTOINCREMENT,"
                            + Channels.COLUMN_PACKAGE_NAME
                            + " TEXT NOT NULL,"
                            + Channels.COLUMN_INPUT_ID
                            + " TEXT NOT NULL,"
                            + Channels.COLUMN_TYPE
                            + " TEXT NOT NULL DEFAULT '"
                            + Channels.TYPE_OTHER
                            + "',"
                            + Channels.COLUMN_SERVICE_TYPE
                            + " TEXT NOT NULL DEFAULT '"
                            + Channels.SERVICE_TYPE_AUDIO_VIDEO
                            + "',"
                            + Channels.COLUMN_ORIGINAL_NETWORK_ID
                            + " INTEGER NOT NULL DEFAULT 0,"
                            + Channels.COLUMN_TRANSPORT_STREAM_ID
                            + " INTEGER NOT NULL DEFAULT 0,"
                            + Channels.COLUMN_SERVICE_ID
                            + " INTEGER NOT NULL DEFAULT 0,"
                            + Channels.COLUMN_DISPLAY_NUMBER
                            + " TEXT,"
                            + Channels.COLUMN_DISPLAY_NAME
                            + " TEXT,"
                            + Channels.COLUMN_NETWORK_AFFILIATION
                            + " TEXT,"
                            + Channels.COLUMN_DESCRIPTION
                            + " TEXT,"
                            + Channels.COLUMN_VIDEO_FORMAT
                            + " TEXT,"
                            + Channels.COLUMN_BROWSABLE
                            + " INTEGER NOT NULL DEFAULT 0,"
                            + Channels.COLUMN_SEARCHABLE
                            + " INTEGER NOT NULL DEFAULT 1,"
                            + Channels.COLUMN_LOCKED
                            + " INTEGER NOT NULL DEFAULT 0,"
                            + Channels.COLUMN_APP_LINK_ICON_URI
                            + " TEXT,"
                            + Channels.COLUMN_APP_LINK_POSTER_ART_URI
                            + " TEXT,"
                            + Channels.COLUMN_APP_LINK_TEXT
                            + " TEXT,"
                            + Channels.COLUMN_APP_LINK_COLOR
                            + " INTEGER,"
                            + Channels.COLUMN_APP_LINK_INTENT_URI
                            + " TEXT,"
                            + Channels.COLUMN_INTERNAL_PROVIDER_DATA
                            + " BLOB,"
                            + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1
                            + " INTEGER,"
                            + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2
                            + " INTEGER,"
                            + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3
                            + " INTEGER,"
                            + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4
                            + " INTEGER,"
                            + CHANNELS_COLUMN_LOGO
                            + " BLOB,"
                            + Channels.COLUMN_VERSION_NUMBER
                            + " INTEGER,"
                            + Channels.COLUMN_TRANSIENT
                            + " INTEGER NOT NULL DEFAULT 0,"
                            + Channels.COLUMN_INTERNAL_PROVIDER_ID
                            + " TEXT,"
                            // Needed for foreign keys in other tables.
                            + "UNIQUE("
                            + Channels._ID
                            + ","
                            + Channels.COLUMN_PACKAGE_NAME
                            + ")"
                            + ");");
            db.execSQL(
                    "CREATE TABLE "
                            + PROGRAMS_TABLE
                            + " ("
                            + Programs._ID
                            + " INTEGER PRIMARY KEY AUTOINCREMENT,"
                            + Programs.COLUMN_PACKAGE_NAME
                            + " TEXT NOT NULL,"
                            + Programs.COLUMN_CHANNEL_ID
                            + " INTEGER,"
                            + Programs.COLUMN_TITLE
                            + " TEXT,"
                            + Programs.COLUMN_SEASON_DISPLAY_NUMBER
                            + " TEXT,"
                            + Programs.COLUMN_SEASON_TITLE
                            + " TEXT,"
                            + Programs.COLUMN_EPISODE_DISPLAY_NUMBER
                            + " TEXT,"
                            + Programs.COLUMN_EPISODE_TITLE
                            + " TEXT,"
                            + Programs.COLUMN_START_TIME_UTC_MILLIS
                            + " INTEGER,"
                            + Programs.COLUMN_END_TIME_UTC_MILLIS
                            + " INTEGER,"
                            + Programs.COLUMN_BROADCAST_GENRE
                            + " TEXT,"
                            + Programs.COLUMN_CANONICAL_GENRE
                            + " TEXT,"
                            + Programs.COLUMN_SHORT_DESCRIPTION
                            + " TEXT,"
                            + Programs.COLUMN_LONG_DESCRIPTION
                            + " TEXT,"
                            + Programs.COLUMN_VIDEO_WIDTH
                            + " INTEGER,"
                            + Programs.COLUMN_VIDEO_HEIGHT
                            + " INTEGER,"
                            + Programs.COLUMN_AUDIO_LANGUAGE
                            + " TEXT,"
                            + Programs.COLUMN_CONTENT_RATING
                            + " TEXT,"
                            + Programs.COLUMN_POSTER_ART_URI
                            + " TEXT,"
                            + Programs.COLUMN_THUMBNAIL_URI
                            + " TEXT,"
                            + Programs.COLUMN_SEARCHABLE
                            + " INTEGER NOT NULL DEFAULT 1,"
                            + Programs.COLUMN_RECORDING_PROHIBITED
                            + " INTEGER NOT NULL DEFAULT 0,"
                            + Programs.COLUMN_INTERNAL_PROVIDER_DATA
                            + " BLOB,"
                            + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1
                            + " INTEGER,"
                            + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2
                            + " INTEGER,"
                            + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3
                            + " INTEGER,"
                            + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4
                            + " INTEGER,"
                            + Programs.COLUMN_REVIEW_RATING_STYLE
                            + " INTEGER,"
                            + Programs.COLUMN_REVIEW_RATING
                            + " TEXT,"
                            + Programs.COLUMN_VERSION_NUMBER
                            + " INTEGER,"
                            + "FOREIGN KEY("
                            + Programs.COLUMN_CHANNEL_ID
                            + ","
                            + Programs.COLUMN_PACKAGE_NAME
                            + ") REFERENCES "
                            + CHANNELS_TABLE
                            + "("
                            + Channels._ID
                            + ","
                            + Channels.COLUMN_PACKAGE_NAME
                            + ") ON UPDATE CASCADE ON DELETE CASCADE"
                            + ");");
            db.execSQL(
                    "CREATE INDEX "
                            + PROGRAMS_TABLE_PACKAGE_NAME_INDEX
                            + " ON "
                            + PROGRAMS_TABLE
                            + "("
                            + Programs.COLUMN_PACKAGE_NAME
                            + ");");
            db.execSQL(
                    "CREATE INDEX "
                            + PROGRAMS_TABLE_CHANNEL_ID_INDEX
                            + " ON "
                            + PROGRAMS_TABLE
                            + "("
                            + Programs.COLUMN_CHANNEL_ID
                            + ");");
            db.execSQL(
                    "CREATE INDEX "
                            + PROGRAMS_TABLE_START_TIME_INDEX
                            + " ON "
                            + PROGRAMS_TABLE
                            + "("
                            + Programs.COLUMN_START_TIME_UTC_MILLIS
                            + ");");
            db.execSQL(
                    "CREATE INDEX "
                            + PROGRAMS_TABLE_END_TIME_INDEX
                            + " ON "
                            + PROGRAMS_TABLE
                            + "("
                            + Programs.COLUMN_END_TIME_UTC_MILLIS
                            + ");");
            db.execSQL(
                    "CREATE TABLE "
                            + WATCHED_PROGRAMS_TABLE
                            + " ("
                            + WatchedPrograms._ID
                            + " INTEGER PRIMARY KEY AUTOINCREMENT,"
                            + WatchedPrograms.COLUMN_PACKAGE_NAME
                            + " TEXT NOT NULL,"
                            + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS
                            + " INTEGER NOT NULL DEFAULT 0,"
                            + WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS
                            + " INTEGER NOT NULL DEFAULT 0,"
                            + WatchedPrograms.COLUMN_CHANNEL_ID
                            + " INTEGER,"
                            + WatchedPrograms.COLUMN_TITLE
                            + " TEXT,"
                            + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS
                            + " INTEGER,"
                            + WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS
                            + " INTEGER,"
                            + WatchedPrograms.COLUMN_DESCRIPTION
                            + " TEXT,"
                            + WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS
                            + " TEXT,"
                            + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN
                            + " TEXT NOT NULL,"
                            + WATCHED_PROGRAMS_COLUMN_CONSOLIDATED
                            + " INTEGER NOT NULL DEFAULT 0,"
                            + "FOREIGN KEY("
                            + WatchedPrograms.COLUMN_CHANNEL_ID
                            + ","
                            + WatchedPrograms.COLUMN_PACKAGE_NAME
                            + ") REFERENCES "
                            + CHANNELS_TABLE
                            + "("
                            + Channels._ID
                            + ","
                            + Channels.COLUMN_PACKAGE_NAME
                            + ") ON UPDATE CASCADE ON DELETE CASCADE"
                            + ");");
            db.execSQL(
                    "CREATE INDEX "
                            + WATCHED_PROGRAMS_TABLE_CHANNEL_ID_INDEX
                            + " ON "
                            + WATCHED_PROGRAMS_TABLE
                            + "("
                            + WatchedPrograms.COLUMN_CHANNEL_ID
                            + ");");
            db.execSQL(CREATE_RECORDED_PROGRAMS_TABLE_SQL);
            db.execSQL(CREATE_PREVIEW_PROGRAMS_TABLE_SQL);
            db.execSQL(CREATE_PREVIEW_PROGRAMS_PACKAGE_NAME_INDEX_SQL);
            db.execSQL(CREATE_PREVIEW_PROGRAMS_CHANNEL_ID_INDEX_SQL);
            db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_TABLE_SQL);
            db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_PACKAGE_NAME_INDEX_SQL);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            if (oldVersion < 23) {
                Log.i(
                        TAG,
                        "Upgrading from version "
                                + oldVersion
                                + " to "
                                + newVersion
                                + ", data will be lost!");
                db.execSQL("DROP TABLE IF EXISTS " + DELETED_CHANNELS_TABLE);
                db.execSQL("DROP TABLE IF EXISTS " + WATCHED_PROGRAMS_TABLE);
                db.execSQL("DROP TABLE IF EXISTS " + PROGRAMS_TABLE);
                db.execSQL("DROP TABLE IF EXISTS " + CHANNELS_TABLE);

                onCreate(db);
                return;
            }

            Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion + ".");
            if (oldVersion <= 23) {
                db.execSQL(
                        "ALTER TABLE "
                                + CHANNELS_TABLE
                                + " ADD "
                                + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1
                                + " INTEGER;");
                db.execSQL(
                        "ALTER TABLE "
                                + CHANNELS_TABLE
                                + " ADD "
                                + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2
                                + " INTEGER;");
                db.execSQL(
                        "ALTER TABLE "
                                + CHANNELS_TABLE
                                + " ADD "
                                + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3
                                + " INTEGER;");
                db.execSQL(
                        "ALTER TABLE "
                                + CHANNELS_TABLE
                                + " ADD "
                                + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4
                                + " INTEGER;");
            }
            if (oldVersion <= 24) {
                db.execSQL(
                        "ALTER TABLE "
                                + PROGRAMS_TABLE
                                + " ADD "
                                + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1
                                + " INTEGER;");
                db.execSQL(
                        "ALTER TABLE "
                                + PROGRAMS_TABLE
                                + " ADD "
                                + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2
                                + " INTEGER;");
                db.execSQL(
                        "ALTER TABLE "
                                + PROGRAMS_TABLE
                                + " ADD "
                                + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3
                                + " INTEGER;");
                db.execSQL(
                        "ALTER TABLE "
                                + PROGRAMS_TABLE
                                + " ADD "
                                + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4
                                + " INTEGER;");
            }
            if (oldVersion <= 25) {
                db.execSQL(
                        "ALTER TABLE "
                                + CHANNELS_TABLE
                                + " ADD "
                                + Channels.COLUMN_APP_LINK_ICON_URI
                                + " TEXT;");
                db.execSQL(
                        "ALTER TABLE "
                                + CHANNELS_TABLE
                                + " ADD "
                                + Channels.COLUMN_APP_LINK_POSTER_ART_URI
                                + " TEXT;");
                db.execSQL(
                        "ALTER TABLE "
                                + CHANNELS_TABLE
                                + " ADD "
                                + Channels.COLUMN_APP_LINK_TEXT
                                + " TEXT;");
                db.execSQL(
                        "ALTER TABLE "
                                + CHANNELS_TABLE
                                + " ADD "
                                + Channels.COLUMN_APP_LINK_COLOR
                                + " INTEGER;");
                db.execSQL(
                        "ALTER TABLE "
                                + CHANNELS_TABLE
                                + " ADD "
                                + Channels.COLUMN_APP_LINK_INTENT_URI
                                + " TEXT;");
                db.execSQL(
                        "ALTER TABLE "
                                + PROGRAMS_TABLE
                                + " ADD "
                                + Programs.COLUMN_SEARCHABLE
                                + " INTEGER NOT NULL DEFAULT 1;");
            }
            if (oldVersion <= 28) {
                db.execSQL(
                        "ALTER TABLE "
                                + PROGRAMS_TABLE
                                + " ADD "
                                + Programs.COLUMN_SEASON_TITLE
                                + " TEXT;");
                migrateIntegerColumnToTextColumn(
                        db,
                        PROGRAMS_TABLE,
                        Programs.COLUMN_SEASON_NUMBER,
                        Programs.COLUMN_SEASON_DISPLAY_NUMBER);
                migrateIntegerColumnToTextColumn(
                        db,
                        PROGRAMS_TABLE,
                        Programs.COLUMN_EPISODE_NUMBER,
                        Programs.COLUMN_EPISODE_DISPLAY_NUMBER);
            }
            if (oldVersion <= 29) {
                db.execSQL("DROP TABLE IF EXISTS " + RECORDED_PROGRAMS_TABLE);
                db.execSQL(CREATE_RECORDED_PROGRAMS_TABLE_SQL);
            }
            if (oldVersion <= 30) {
                db.execSQL(
                        "ALTER TABLE "
                                + PROGRAMS_TABLE
                                + " ADD "
                                + Programs.COLUMN_RECORDING_PROHIBITED
                                + " INTEGER NOT NULL DEFAULT 0;");
            }
            if (oldVersion <= 32) {
                db.execSQL(
                        "ALTER TABLE "
                                + CHANNELS_TABLE
                                + " ADD "
                                + Channels.COLUMN_TRANSIENT
                                + " INTEGER NOT NULL DEFAULT 0;");
                db.execSQL(
                        "ALTER TABLE "
                                + CHANNELS_TABLE
                                + " ADD "
                                + Channels.COLUMN_INTERNAL_PROVIDER_ID
                                + " TEXT;");
                db.execSQL(
                        "ALTER TABLE "
                                + PROGRAMS_TABLE
                                + " ADD "
                                + Programs.COLUMN_REVIEW_RATING_STYLE
                                + " INTEGER;");
                db.execSQL(
                        "ALTER TABLE "
                                + PROGRAMS_TABLE
                                + " ADD "
                                + Programs.COLUMN_REVIEW_RATING
                                + " TEXT;");
                if (oldVersion > 29) {
                    db.execSQL(
                            "ALTER TABLE "
                                    + RECORDED_PROGRAMS_TABLE
                                    + " ADD "
                                    + RecordedPrograms.COLUMN_REVIEW_RATING_STYLE
                                    + " INTEGER;");
                    db.execSQL(
                            "ALTER TABLE "
                                    + RECORDED_PROGRAMS_TABLE
                                    + " ADD "
                                    + RecordedPrograms.COLUMN_REVIEW_RATING
                                    + " TEXT;");
                }
            }
            if (oldVersion <= 33) {
                db.execSQL("DROP TABLE IF EXISTS " + PREVIEW_PROGRAMS_TABLE);
                db.execSQL("DROP TABLE IF EXISTS " + WATCH_NEXT_PROGRAMS_TABLE);
                db.execSQL(CREATE_PREVIEW_PROGRAMS_TABLE_SQL);
                db.execSQL(CREATE_PREVIEW_PROGRAMS_PACKAGE_NAME_INDEX_SQL);
                db.execSQL(CREATE_PREVIEW_PROGRAMS_CHANNEL_ID_INDEX_SQL);
                db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_TABLE_SQL);
                db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_PACKAGE_NAME_INDEX_SQL);
            }
            Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion + " is done.");
        }

        @Override
        public void onOpen(SQLiteDatabase db) {
            // Call a static method on the TvProvider because changes to sInitialized must
            // be guarded by a lock on the class.
            initOnOpenIfNeeded(mContext, db);
        }

        private static void migrateIntegerColumnToTextColumn(
                SQLiteDatabase db, String table, String integerColumn, String textColumn) {
            db.execSQL("ALTER TABLE " + table + " ADD " + textColumn + " TEXT;");
            db.execSQL(
                    "UPDATE "
                            + table
                            + " SET "
                            + textColumn
                            + " = CAST("
                            + integerColumn
                            + " AS TEXT);");
        }
    }

    private DatabaseHelper mOpenHelper;
    private static SharedPreferences sBlockedPackagesSharedPreference;
    private static Map<String, Boolean> sBlockedPackages;

    @Override
    public boolean onCreate() {
        if (DEBUG) {
            Log.d(TAG, "Creating TvProvider");
        }
        mOpenHelper = DatabaseHelper.createInstance(getContext());
        return true;
    }

    @VisibleForTesting
    String getCallingPackage_() {
        return mCallingPackage;
    }

    public void setCallingPackage(String packageName) {
        mCallingPackage = packageName;
    }

    void setOpenHelper(DatabaseHelper helper) {
        mOpenHelper = helper;
    }

    @Override
    public String getType(Uri uri) {
        switch (sUriMatcher.match(uri)) {
            case MATCH_CHANNEL:
                return Channels.CONTENT_TYPE;
            case MATCH_CHANNEL_ID:
                return Channels.CONTENT_ITEM_TYPE;
            case MATCH_CHANNEL_ID_LOGO:
                return "image/png";
            case MATCH_PASSTHROUGH_ID:
                return Channels.CONTENT_ITEM_TYPE;
            case MATCH_PROGRAM:
                return Programs.CONTENT_TYPE;
            case MATCH_PROGRAM_ID:
                return Programs.CONTENT_ITEM_TYPE;
            case MATCH_WATCHED_PROGRAM:
                return WatchedPrograms.CONTENT_TYPE;
            case MATCH_WATCHED_PROGRAM_ID:
                return WatchedPrograms.CONTENT_ITEM_TYPE;
            case MATCH_RECORDED_PROGRAM:
                return RecordedPrograms.CONTENT_TYPE;
            case MATCH_RECORDED_PROGRAM_ID:
                return RecordedPrograms.CONTENT_ITEM_TYPE;
            case MATCH_PREVIEW_PROGRAM:
                return PreviewPrograms.CONTENT_TYPE;
            case MATCH_PREVIEW_PROGRAM_ID:
                return PreviewPrograms.CONTENT_ITEM_TYPE;
            case MATCH_WATCH_NEXT_PROGRAM:
                return WatchNextPrograms.CONTENT_TYPE;
            case MATCH_WATCH_NEXT_PROGRAM_ID:
                return WatchNextPrograms.CONTENT_ITEM_TYPE;
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }
    }

    @Override
    public Bundle call(String method, String arg, Bundle extras) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Cursor query(
            Uri uri,
            String[] projection,
            String selection,
            String[] selectionArgs,
            String sortOrder) {
        ensureInitialized();
        boolean needsToValidateSortOrder = !callerHasAccessAllEpgDataPermission();
        SqlParams params = createSqlParams(OP_QUERY, uri, selection, selectionArgs);

        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
        queryBuilder.setStrict(needsToValidateSortOrder);
        queryBuilder.setTables(params.getTables());
        String orderBy = null;
        Map<String, String> projectionMap;
        switch (params.getTables()) {
            case PROGRAMS_TABLE:
                projectionMap = sProgramProjectionMap;
                orderBy = DEFAULT_PROGRAMS_SORT_ORDER;
                break;
            case WATCHED_PROGRAMS_TABLE:
                projectionMap = sWatchedProgramProjectionMap;
                orderBy = DEFAULT_WATCHED_PROGRAMS_SORT_ORDER;
                break;
            case RECORDED_PROGRAMS_TABLE:
                projectionMap = sRecordedProgramProjectionMap;
                break;
            case PREVIEW_PROGRAMS_TABLE:
                projectionMap = sPreviewProgramProjectionMap;
                break;
            case WATCH_NEXT_PROGRAMS_TABLE:
                projectionMap = sWatchNextProgramProjectionMap;
                break;
            default:
                projectionMap = sChannelProjectionMap;
                break;
        }
        queryBuilder.setProjectionMap(createProjectionMapForQuery(projection, projectionMap));
        if (needsToValidateSortOrder) {
            validateSortOrder(sortOrder, projectionMap.keySet());
        }

        // Use the default sort order only if no sort order is specified.
        if (!TextUtils.isEmpty(sortOrder)) {
            orderBy = sortOrder;
        }

        // Get the database and run the query.
        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
        Cursor c =
                queryBuilder.query(
                        db,
                        projection,
                        params.getSelection(),
                        params.getSelectionArgs(),
                        null,
                        null,
                        orderBy);

        // Tell the cursor what URI to watch, so it knows when its source data changes.
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        ensureInitialized();
        switch (sUriMatcher.match(uri)) {
            case MATCH_CHANNEL:
                // Preview channels are not necessarily associated with TV input service.
                // Therefore, we fill a fake ID to meet not null restriction for preview channels.
                if (values.get(Channels.COLUMN_INPUT_ID) == null
                        && TvContractCompat.PARAM_CHANNEL.equals(
                                values.get(Channels.COLUMN_TYPE))) {
                    values.put(Channels.COLUMN_INPUT_ID, EMPTY_STRING);
                }
                filterContentValues(values, sChannelProjectionMap);
                return insertChannel(uri, values);
            case MATCH_PROGRAM:
                filterContentValues(values, sProgramProjectionMap);
                return insertProgram(uri, values);
            case MATCH_WATCHED_PROGRAM:
                return insertWatchedProgram(uri, values);
            case MATCH_RECORDED_PROGRAM:
                filterContentValues(values, sRecordedProgramProjectionMap);
                return insertRecordedProgram(uri, values);
            case MATCH_PREVIEW_PROGRAM:
                filterContentValues(values, sPreviewProgramProjectionMap);
                return insertPreviewProgram(uri, values);
            case MATCH_WATCH_NEXT_PROGRAM:
                filterContentValues(values, sWatchNextProgramProjectionMap);
                return insertWatchNextProgram(uri, values);
            case MATCH_CHANNEL_ID:
            case MATCH_CHANNEL_ID_LOGO:
            case MATCH_PASSTHROUGH_ID:
            case MATCH_PROGRAM_ID:
            case MATCH_WATCHED_PROGRAM_ID:
            case MATCH_RECORDED_PROGRAM_ID:
            case MATCH_PREVIEW_PROGRAM_ID:
                throw new UnsupportedOperationException("Cannot insert into that URI: " + uri);
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }
    }

    private Uri insertChannel(Uri uri, ContentValues values) {
        if (TextUtils.equals(
                values.getAsString(Channels.COLUMN_TYPE), TvContractCompat.Channels.TYPE_PREVIEW)) {
            blockIllegalAccessFromBlockedPackage();
        }
        // Mark the owner package of this channel.
        values.put(Channels.COLUMN_PACKAGE_NAME, getCallingPackage_());
        blockIllegalAccessToChannelsSystemColumns(values);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        long rowId = db.insert(CHANNELS_TABLE, null, values);
        if (rowId > 0) {
            Uri channelUri = TvContractCompat.buildChannelUri(rowId);
            notifyChange(channelUri);
            return channelUri;
        }

        throw new SQLException("Failed to insert row into " + uri);
    }

    private Uri insertProgram(Uri uri, ContentValues values) {
        if (!callerHasAccessAllEpgDataPermission()
                || !values.containsKey(Programs.COLUMN_PACKAGE_NAME)) {
            // Mark the owner package of this program. System app with a proper permission may
            // change the owner of the program.
            values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
        }

        checkAndConvertGenre(values);
        checkAndConvertDeprecatedColumns(values);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        long rowId = db.insert(PROGRAMS_TABLE, null, values);
        if (rowId > 0) {
            Uri programUri = TvContractCompat.buildProgramUri(rowId);
            notifyChange(programUri);
            return programUri;
        }

        throw new SQLException("Failed to insert row into " + uri);
    }

    private Uri insertWatchedProgram(Uri uri, ContentValues values) {
        if (DEBUG) {
            Log.d(TAG, "insertWatchedProgram(uri=" + uri + ", values={" + values + "})");
        }
        Long watchStartTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS);
        Long watchEndTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS);
        // The system sends only two kinds of watch events:
        // 1. The user tunes to a new channel. (COLUMN_WATCH_START_TIME_UTC_MILLIS)
        // 2. The user stops watching. (COLUMN_WATCH_END_TIME_UTC_MILLIS)
        if (watchStartTime != null && watchEndTime == null) {
            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
            long rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values);
            if (rowId > 0) {
                return ContentUris.withAppendedId(WatchedPrograms.CONTENT_URI, rowId);
            }
            Log.w(TAG, "Failed to insert row for " + values + ". Channel does not exist.");
            return null;
        } else if (watchStartTime == null && watchEndTime != null) {
            return null;
        }
        // All the other cases are invalid.
        throw new IllegalArgumentException(
                "Only one of COLUMN_WATCH_START_TIME_UTC_MILLIS and"
                        + " COLUMN_WATCH_END_TIME_UTC_MILLIS should be specified");
    }

    private Uri insertRecordedProgram(Uri uri, ContentValues values) {
        // Mark the owner package of this program.
        values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());

        checkAndConvertGenre(values);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        long rowId = db.insert(RECORDED_PROGRAMS_TABLE, null, values);
        if (rowId > 0) {
            Uri recordedProgramUri = TvContractCompat.buildRecordedProgramUri(rowId);
            notifyChange(recordedProgramUri);
            return recordedProgramUri;
        }

        throw new SQLException("Failed to insert row into " + uri);
    }

    private Uri insertPreviewProgram(Uri uri, ContentValues values) {
        if (!values.containsKey(PreviewPrograms.COLUMN_TYPE)) {
            throw new IllegalArgumentException(
                    "Missing the required column: " + PreviewPrograms.COLUMN_TYPE);
        }
        blockIllegalAccessFromBlockedPackage();
        // Mark the owner package of this program.
        values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
        blockIllegalAccessToPreviewProgramsSystemColumns(values);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        long rowId = db.insert(PREVIEW_PROGRAMS_TABLE, null, values);
        if (rowId > 0) {
            Uri previewProgramUri = TvContractCompat.buildPreviewProgramUri(rowId);
            notifyChange(previewProgramUri);
            return previewProgramUri;
        }

        throw new SQLException("Failed to insert row into " + uri);
    }

    private Uri insertWatchNextProgram(Uri uri, ContentValues values) {
        if (!values.containsKey(WatchNextPrograms.COLUMN_TYPE)) {
            throw new IllegalArgumentException(
                    "Missing the required column: " + WatchNextPrograms.COLUMN_TYPE);
        }
        blockIllegalAccessFromBlockedPackage();
        if (!callerHasAccessAllEpgDataPermission()
                || !values.containsKey(Programs.COLUMN_PACKAGE_NAME)) {
            // Mark the owner package of this program. System app with a proper permission may
            // change the owner of the program.
            values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
        }
        blockIllegalAccessToPreviewProgramsSystemColumns(values);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        long rowId = db.insert(WATCH_NEXT_PROGRAMS_TABLE, null, values);
        if (rowId > 0) {
            Uri watchNextProgramUri = TvContractCompat.buildWatchNextProgramUri(rowId);
            notifyChange(watchNextProgramUri);
            return watchNextProgramUri;
        }

        throw new SQLException("Failed to insert row into " + uri);
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        SqlParams params = createSqlParams(OP_DELETE, uri, selection, selectionArgs);
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count;
        switch (sUriMatcher.match(uri)) {
            case MATCH_CHANNEL_ID_LOGO:
                ContentValues values = new ContentValues();
                values.putNull(CHANNELS_COLUMN_LOGO);
                count =
                        db.update(
                                params.getTables(),
                                values,
                                params.getSelection(),
                                params.getSelectionArgs());
                break;
            case MATCH_CHANNEL:
            case MATCH_PROGRAM:
            case MATCH_WATCHED_PROGRAM:
            case MATCH_RECORDED_PROGRAM:
            case MATCH_PREVIEW_PROGRAM:
            case MATCH_WATCH_NEXT_PROGRAM:
            case MATCH_CHANNEL_ID:
            case MATCH_PASSTHROUGH_ID:
            case MATCH_PROGRAM_ID:
            case MATCH_WATCHED_PROGRAM_ID:
            case MATCH_RECORDED_PROGRAM_ID:
            case MATCH_PREVIEW_PROGRAM_ID:
            case MATCH_WATCH_NEXT_PROGRAM_ID:
                count =
                        db.delete(
                                params.getTables(),
                                params.getSelection(),
                                params.getSelectionArgs());
                break;
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }
        if (count > 0) {
            notifyChange(uri);
        }
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        SqlParams params = createSqlParams(OP_UPDATE, uri, selection, selectionArgs);
        blockIllegalAccessToIdAndPackageName(uri, values);
        boolean containImmutableColumn = false;
        if (params.getTables().equals(CHANNELS_TABLE)) {
            filterContentValues(values, sChannelProjectionMap);
            containImmutableColumn = disallowModifyChannelType(values, params);
            if (containImmutableColumn && sUriMatcher.match(uri) != MATCH_CHANNEL_ID) {
                Log.i(TAG, "Updating failed. Attempt to change immutable column for channels.");
                return 0;
            }
            blockIllegalAccessToChannelsSystemColumns(values);
        } else if (params.getTables().equals(PROGRAMS_TABLE)) {
            filterContentValues(values, sProgramProjectionMap);
            checkAndConvertGenre(values);
            checkAndConvertDeprecatedColumns(values);
        } else if (params.getTables().equals(RECORDED_PROGRAMS_TABLE)) {
            filterContentValues(values, sRecordedProgramProjectionMap);
            checkAndConvertGenre(values);
        } else if (params.getTables().equals(PREVIEW_PROGRAMS_TABLE)) {
            filterContentValues(values, sPreviewProgramProjectionMap);
            containImmutableColumn = disallowModifyChannelId(values, params);
            if (containImmutableColumn && PreviewPrograms.CONTENT_URI.equals(uri)) {
                Log.i(
                        TAG,
                        "Updating failed. Attempt to change unmodifiable column for "
                                + "preview programs.");
                return 0;
            }
            blockIllegalAccessToPreviewProgramsSystemColumns(values);
        } else if (params.getTables().equals(WATCH_NEXT_PROGRAMS_TABLE)) {
            filterContentValues(values, sWatchNextProgramProjectionMap);
            blockIllegalAccessToPreviewProgramsSystemColumns(values);
        }
        if (values.size() == 0) {
            // All values may be filtered out, no need to update
            return 0;
        }
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count =
                db.update(
                        params.getTables(),
                        values,
                        params.getSelection(),
                        params.getSelectionArgs());
        if (count > 0) {
            notifyChange(uri);
        } else if (containImmutableColumn) {
            Log.i(
                    TAG,
                    "Updating failed. The item may not exist or attempt to change "
                            + "immutable column.");
        }
        return count;
    }

    private synchronized void ensureInitialized() {
        if (!sInitialized) {
            // Database is not accessed before and the projection maps and the blocked package list
            // are not updated yet. Gets database here to make it initialized.
            mOpenHelper.getReadableDatabase();
        }
    }

    private static synchronized void initOnOpenIfNeeded(Context context, SQLiteDatabase db) {
        if (!sInitialized) {
            updateProjectionMap(db, CHANNELS_TABLE, sChannelProjectionMap);
            updateProjectionMap(db, PROGRAMS_TABLE, sProgramProjectionMap);
            updateProjectionMap(db, WATCHED_PROGRAMS_TABLE, sWatchedProgramProjectionMap);
            updateProjectionMap(db, RECORDED_PROGRAMS_TABLE, sRecordedProgramProjectionMap);
            updateProjectionMap(db, PREVIEW_PROGRAMS_TABLE, sPreviewProgramProjectionMap);
            updateProjectionMap(db, WATCH_NEXT_PROGRAMS_TABLE, sWatchNextProgramProjectionMap);
            sBlockedPackagesSharedPreference =
                    PreferenceManager.getDefaultSharedPreferences(context);
            sBlockedPackages = new ConcurrentHashMap<>();
            for (String packageName :
                    sBlockedPackagesSharedPreference.getStringSet(
                            SHARED_PREF_BLOCKED_PACKAGES_KEY, new HashSet<>())) {
                sBlockedPackages.put(packageName, true);
            }
            sInitialized = true;
        }
    }

    private static void updateProjectionMap(
            SQLiteDatabase db, String tableName, Map<String, String> projectionMap) {
        try (Cursor cursor = db.rawQuery("SELECT * FROM " + tableName + " LIMIT 0", null)) {
            for (String columnName : cursor.getColumnNames()) {
                if (!projectionMap.containsKey(columnName)) {
                    projectionMap.put(columnName, tableName + '.' + columnName);
                }
            }
        }
    }

    private Map<String, String> createProjectionMapForQuery(
            String[] projection, Map<String, String> projectionMap) {
        if (projection == null) {
            return projectionMap;
        }
        Map<String, String> columnProjectionMap = new HashMap<>();
        for (String columnName : projection) {
            // Value NULL will be provided if the requested column does not exist in the database.
            columnProjectionMap.put(
                    columnName, projectionMap.getOrDefault(columnName, "NULL as " + columnName));
        }
        return columnProjectionMap;
    }

    private void filterContentValues(ContentValues values, Map<String, String> projectionMap) {
        Iterator<String> iter = values.keySet().iterator();
        while (iter.hasNext()) {
            String columnName = iter.next();
            if (!projectionMap.containsKey(columnName)) {
                iter.remove();
            }
        }
    }

    private SqlParams createSqlParams(
            String operation, Uri uri, String selection, String[] selectionArgs) {
        int match = sUriMatcher.match(uri);
        SqlParams params = new SqlParams(null, selection, selectionArgs);

        // Control access to EPG data (excluding watched programs) when the caller doesn't have all
        // access.
        String prefix = match == MATCH_CHANNEL ? CHANNELS_TABLE + "." : "";
        if (!callerHasAccessAllEpgDataPermission()
                && match != MATCH_WATCHED_PROGRAM
                && match != MATCH_WATCHED_PROGRAM_ID) {
            if (!TextUtils.isEmpty(selection)) {
                throw new SecurityException("Selection not allowed for " + uri);
            }
            // Limit the operation only to the data that the calling package owns except for when
            // the caller tries to read TV listings and has the appropriate permission.
            if (operation.equals(OP_QUERY) && callerHasReadTvListingsPermission()) {
                params.setWhere(
                        prefix
                                + BaseTvColumns.COLUMN_PACKAGE_NAME
                                + "=? OR "
                                + Channels.COLUMN_SEARCHABLE
                                + "=?",
                        getCallingPackage_(),
                        "1");
            } else {
                params.setWhere(
                        prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=?", getCallingPackage_());
            }
        }
        String packageName = uri.getQueryParameter(PARAM_PACKAGE);
        if (packageName != null) {
            params.appendWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=?", packageName);
        }

        switch (match) {
            case MATCH_CHANNEL:
                String genre = uri.getQueryParameter(TvContractCompat.PARAM_CANONICAL_GENRE);
                if (genre == null) {
                    params.setTables(CHANNELS_TABLE);
                } else {
                    if (!operation.equals(OP_QUERY)) {
                        throw new SecurityException(
                                capitalize(operation) + " not allowed for " + uri);
                    }
                    if (!Genres.isCanonical(genre)) {
                        throw new IllegalArgumentException("Not a canonical genre : " + genre);
                    }
                    params.setTables(CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE);
                    String curTime = String.valueOf(System.currentTimeMillis());
                    params.appendWhere(
                            "LIKE(?, "
                                    + Programs.COLUMN_CANONICAL_GENRE
                                    + ") AND "
                                    + Programs.COLUMN_START_TIME_UTC_MILLIS
                                    + "<=? AND "
                                    + Programs.COLUMN_END_TIME_UTC_MILLIS
                                    + ">=?",
                            "%" + genre + "%",
                            curTime,
                            curTime);
                }
                String inputId = uri.getQueryParameter(TvContractCompat.PARAM_INPUT);
                if (inputId != null) {
                    params.appendWhere(Channels.COLUMN_INPUT_ID + "=?", inputId);
                }
                boolean browsableOnly =
                        uri.getBooleanQueryParameter(TvContractCompat.PARAM_BROWSABLE_ONLY, false);
                if (browsableOnly) {
                    params.appendWhere(Channels.COLUMN_BROWSABLE + "=1");
                }
                String preview = uri.getQueryParameter(PARAM_PREVIEW);
                if (preview != null) {
                    String previewSelection =
                            Channels.COLUMN_TYPE
                                    + (preview.equals(String.valueOf(true)) ? "=?" : "!=?");
                    params.appendWhere(previewSelection, Channels.TYPE_PREVIEW);
                }
                break;
            case MATCH_CHANNEL_ID:
                params.setTables(CHANNELS_TABLE);
                params.appendWhere(Channels._ID + "=?", uri.getLastPathSegment());
                break;
            case MATCH_PROGRAM:
                params.setTables(PROGRAMS_TABLE);
                String paramChannelId = uri.getQueryParameter(TvContractCompat.PARAM_CHANNEL);
                if (paramChannelId != null) {
                    String channelId = String.valueOf(Long.parseLong(paramChannelId));
                    params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId);
                }
                String paramStartTime = uri.getQueryParameter(TvContractCompat.PARAM_START_TIME);
                String paramEndTime = uri.getQueryParameter(TvContractCompat.PARAM_END_TIME);
                if (paramStartTime != null && paramEndTime != null) {
                    String startTime = String.valueOf(Long.parseLong(paramStartTime));
                    String endTime = String.valueOf(Long.parseLong(paramEndTime));
                    params.appendWhere(
                            Programs.COLUMN_START_TIME_UTC_MILLIS
                                    + "<=? AND "
                                    + Programs.COLUMN_END_TIME_UTC_MILLIS
                                    + ">=? AND ?<=?",
                            endTime,
                            startTime,
                            startTime,
                            endTime);
                }
                break;
            case MATCH_PROGRAM_ID:
                params.setTables(PROGRAMS_TABLE);
                params.appendWhere(Programs._ID + "=?", uri.getLastPathSegment());
                break;
            case MATCH_WATCHED_PROGRAM:
                if (!callerHasAccessWatchedProgramsPermission()) {
                    throw new SecurityException("Access not allowed for " + uri);
                }
                params.setTables(WATCHED_PROGRAMS_TABLE);
                params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1");
                break;
            case MATCH_WATCHED_PROGRAM_ID:
                if (!callerHasAccessWatchedProgramsPermission()) {
                    throw new SecurityException("Access not allowed for " + uri);
                }
                params.setTables(WATCHED_PROGRAMS_TABLE);
                params.appendWhere(WatchedPrograms._ID + "=?", uri.getLastPathSegment());
                params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1");
                break;
            case MATCH_RECORDED_PROGRAM_ID:
                params.appendWhere(RecordedPrograms._ID + "=?", uri.getLastPathSegment());
                // fall-through
            case MATCH_RECORDED_PROGRAM:
                params.setTables(RECORDED_PROGRAMS_TABLE);
                paramChannelId = uri.getQueryParameter(TvContractCompat.PARAM_CHANNEL);
                if (paramChannelId != null) {
                    String channelId = String.valueOf(Long.parseLong(paramChannelId));
                    params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId);
                }
                break;
            case MATCH_PREVIEW_PROGRAM_ID:
                params.appendWhere(PreviewPrograms._ID + "=?", uri.getLastPathSegment());
                // fall-through
            case MATCH_PREVIEW_PROGRAM:
                params.setTables(PREVIEW_PROGRAMS_TABLE);
                paramChannelId = uri.getQueryParameter(TvContractCompat.PARAM_CHANNEL);
                if (paramChannelId != null) {
                    String channelId = String.valueOf(Long.parseLong(paramChannelId));
                    params.appendWhere(PreviewPrograms.COLUMN_CHANNEL_ID + "=?", channelId);
                }
                break;
            case MATCH_WATCH_NEXT_PROGRAM_ID:
                params.appendWhere(WatchNextPrograms._ID + "=?", uri.getLastPathSegment());
                // fall-through
            case MATCH_WATCH_NEXT_PROGRAM:
                params.setTables(WATCH_NEXT_PROGRAMS_TABLE);
                break;
            case MATCH_CHANNEL_ID_LOGO:
                if (operation.equals(OP_DELETE)) {
                    params.setTables(CHANNELS_TABLE);
                    params.appendWhere(Channels._ID + "=?", uri.getPathSegments().get(1));
                    break;
                }
                // fall-through
            case MATCH_PASSTHROUGH_ID:
                throw new UnsupportedOperationException(operation + " not permmitted on " + uri);
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }
        return params;
    }

    private static String capitalize(String str) {
        return Character.toUpperCase(str.charAt(0)) + str.substring(1);
    }

    @SuppressLint("DefaultLocale")
    private void checkAndConvertGenre(ContentValues values) {
        String canonicalGenres = values.getAsString(Programs.COLUMN_CANONICAL_GENRE);

        if (!TextUtils.isEmpty(canonicalGenres)) {
            // Check if the canonical genres are valid. If not, clear them.
            String[] genres = Genres.decode(canonicalGenres);
            for (String genre : genres) {
                if (!Genres.isCanonical(genre)) {
                    values.putNull(Programs.COLUMN_CANONICAL_GENRE);
                    canonicalGenres = null;
                    break;
                }
            }
        }

        if (TextUtils.isEmpty(canonicalGenres)) {
            // If the canonical genre is not set, try to map the broadcast genre to the canonical
            // genre.
            String broadcastGenres = values.getAsString(Programs.COLUMN_BROADCAST_GENRE);
            if (!TextUtils.isEmpty(broadcastGenres)) {
                Set<String> genreSet = new HashSet<>();
                String[] genres = Genres.decode(broadcastGenres);
                for (String genre : genres) {
                    String canonicalGenre = sGenreMap.get(genre.toUpperCase());
                    if (Genres.isCanonical(canonicalGenre)) {
                        genreSet.add(canonicalGenre);
                    }
                }
                if (genreSet.size() > 0) {
                    values.put(
                            Programs.COLUMN_CANONICAL_GENRE,
                            Genres.encode(genreSet.toArray(new String[genreSet.size()])));
                }
            }
        }
    }

    private void checkAndConvertDeprecatedColumns(ContentValues values) {
        if (values.containsKey(Programs.COLUMN_SEASON_NUMBER)) {
            if (!values.containsKey(Programs.COLUMN_SEASON_DISPLAY_NUMBER)) {
                values.put(
                        Programs.COLUMN_SEASON_DISPLAY_NUMBER,
                        values.getAsInteger(Programs.COLUMN_SEASON_NUMBER));
            }
            values.remove(Programs.COLUMN_SEASON_NUMBER);
        }
        if (values.containsKey(Programs.COLUMN_EPISODE_NUMBER)) {
            if (!values.containsKey(Programs.COLUMN_EPISODE_DISPLAY_NUMBER)) {
                values.put(
                        Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
                        values.getAsInteger(Programs.COLUMN_EPISODE_NUMBER));
            }
            values.remove(Programs.COLUMN_EPISODE_NUMBER);
        }
    }

    // We might have more than one thread trying to make its way through applyBatch() so the
    // notification coalescing needs to be thread-local to work correctly.
    private final ThreadLocal<Set<Uri>> mTLBatchNotifications = new ThreadLocal<>();

    private Set<Uri> getBatchNotificationsSet() {
        return mTLBatchNotifications.get();
    }

    private void setBatchNotificationsSet(Set<Uri> batchNotifications) {
        mTLBatchNotifications.set(batchNotifications);
    }

    @Override
    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
            throws OperationApplicationException {
        setBatchNotificationsSet(new HashSet<Uri>());
        Context context = getContext();
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        db.beginTransaction();
        try {
            ContentProviderResult[] results = super.applyBatch(operations);
            db.setTransactionSuccessful();
            return results;
        } finally {
            db.endTransaction();
            final Set<Uri> notifications = getBatchNotificationsSet();
            setBatchNotificationsSet(null);
            for (final Uri uri : notifications) {
                context.getContentResolver().notifyChange(uri, null);
            }
        }
    }

    @Override
    public int bulkInsert(Uri uri, ContentValues[] values) {
        setBatchNotificationsSet(new HashSet<Uri>());
        Context context = getContext();
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        db.beginTransaction();
        try {
            int result = super.bulkInsert(uri, values);
            db.setTransactionSuccessful();
            return result;
        } finally {
            db.endTransaction();
            final Set<Uri> notifications = getBatchNotificationsSet();
            setBatchNotificationsSet(null);
            for (final Uri notificationUri : notifications) {
                context.getContentResolver().notifyChange(notificationUri, null);
            }
        }
    }

    private void notifyChange(Uri uri) {
        final Set<Uri> batchNotifications = getBatchNotificationsSet();
        if (batchNotifications != null) {
            batchNotifications.add(uri);
        } else {
            getContext().getContentResolver().notifyChange(uri, null);
        }
    }

    private boolean callerHasReadTvListingsPermission() {
        return getContext().checkCallingOrSelfPermission(PERMISSION_READ_TV_LISTINGS)
                == PackageManager.PERMISSION_GRANTED;
    }

    private boolean callerHasAccessAllEpgDataPermission() {
        return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_ALL_EPG_DATA)
                == PackageManager.PERMISSION_GRANTED;
    }

    private boolean callerHasAccessWatchedProgramsPermission() {
        return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_WATCHED_PROGRAMS)
                == PackageManager.PERMISSION_GRANTED;
    }

    private boolean callerHasModifyParentalControlsPermission() {
        return getContext()
                        .checkCallingOrSelfPermission(
                                android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
                == PackageManager.PERMISSION_GRANTED;
    }

    private void blockIllegalAccessToIdAndPackageName(Uri uri, ContentValues values) {
        if (values.containsKey(BaseColumns._ID)) {
            int match = sUriMatcher.match(uri);
            switch (match) {
                case MATCH_CHANNEL_ID:
                case MATCH_PROGRAM_ID:
                case MATCH_PREVIEW_PROGRAM_ID:
                case MATCH_RECORDED_PROGRAM_ID:
                case MATCH_WATCH_NEXT_PROGRAM_ID:
                case MATCH_WATCHED_PROGRAM_ID:
                    if (TextUtils.equals(
                            values.getAsString(BaseColumns._ID), uri.getLastPathSegment())) {
                        break;
                    }
                    // fall through
                default:
                    throw new IllegalArgumentException("Not allowed to change ID.");
            }
        }
        if (values.containsKey(BaseTvColumns.COLUMN_PACKAGE_NAME)
                && !callerHasAccessAllEpgDataPermission()
                && !TextUtils.equals(
                        values.getAsString(BaseTvColumns.COLUMN_PACKAGE_NAME),
                        getCallingPackage_())) {
            throw new SecurityException("Not allowed to change package name.");
        }
    }

    private void blockIllegalAccessToChannelsSystemColumns(ContentValues values) {
        if (values.containsKey(Channels.COLUMN_LOCKED)
                && !callerHasModifyParentalControlsPermission()) {
            throw new SecurityException("Not allowed to access Channels.COLUMN_LOCKED");
        }
        Boolean hasAccessAllEpgDataPermission = null;
        if (values.containsKey(Channels.COLUMN_BROWSABLE)) {
            hasAccessAllEpgDataPermission = callerHasAccessAllEpgDataPermission();
            if (!hasAccessAllEpgDataPermission) {
                throw new SecurityException("Not allowed to access Channels.COLUMN_BROWSABLE");
            }
        }
    }

    private void blockIllegalAccessToPreviewProgramsSystemColumns(ContentValues values) {
        if (values.containsKey(PreviewPrograms.COLUMN_BROWSABLE)
                && !callerHasAccessAllEpgDataPermission()) {
            throw new SecurityException("Not allowed to access Programs.COLUMN_BROWSABLE");
        }
    }

    private void blockIllegalAccessFromBlockedPackage() {
        String callingPackageName = getCallingPackage_();
        if (sBlockedPackages.containsKey(callingPackageName)) {
            throw new SecurityException(
                    "Not allowed to access "
                            + TvContractCompat.AUTHORITY
                            + ", "
                            + callingPackageName
                            + " is blocked");
        }
    }

    private boolean disallowModifyChannelType(ContentValues values, SqlParams params) {
        if (values.containsKey(Channels.COLUMN_TYPE)) {
            params.appendWhere(
                    Channels.COLUMN_TYPE + "=?", values.getAsString(Channels.COLUMN_TYPE));
            return true;
        }
        return false;
    }

    private boolean disallowModifyChannelId(ContentValues values, SqlParams params) {
        if (values.containsKey(PreviewPrograms.COLUMN_CHANNEL_ID)) {
            params.appendWhere(
                    PreviewPrograms.COLUMN_CHANNEL_ID + "=?",
                    values.getAsString(PreviewPrograms.COLUMN_CHANNEL_ID));
            return true;
        }
        return false;
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        switch (sUriMatcher.match(uri)) {
            case MATCH_CHANNEL_ID_LOGO:
                return openLogoFile(uri, mode);
            default:
                throw new FileNotFoundException(uri.toString());
        }
    }

    private ParcelFileDescriptor openLogoFile(Uri uri, String mode) throws FileNotFoundException {
        long channelId = Long.parseLong(uri.getPathSegments().get(1));

        SqlParams params =
                new SqlParams(CHANNELS_TABLE, Channels._ID + "=?", String.valueOf(channelId));
        if (!callerHasAccessAllEpgDataPermission()) {
            if (callerHasReadTvListingsPermission()) {
                params.appendWhere(
                        Channels.COLUMN_PACKAGE_NAME + "=? OR " + Channels.COLUMN_SEARCHABLE + "=?",
                        getCallingPackage_(),
                        "1");
            } else {
                params.appendWhere(Channels.COLUMN_PACKAGE_NAME + "=?", getCallingPackage_());
            }
        }

        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
        queryBuilder.setTables(params.getTables());

        // We don't write the database here.
        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
        if (mode.equals("r")) {
            String sql =
                    queryBuilder.buildQuery(
                            new String[] {CHANNELS_COLUMN_LOGO},
                            params.getSelection(),
                            null,
                            null,
                            null,
                            null);
            ParcelFileDescriptor fd =
                    DatabaseUtils.blobFileDescriptorForQuery(db, sql, params.getSelectionArgs());
            if (fd == null) {
                throw new FileNotFoundException(uri.toString());
            }
            return fd;
        } else {
            try (Cursor cursor =
                    queryBuilder.query(
                            db,
                            new String[] {Channels._ID},
                            params.getSelection(),
                            params.getSelectionArgs(),
                            null,
                            null,
                            null)) {
                if (cursor.getCount() < 1) {
                    // Fails early if corresponding channel does not exist.
                    // PipeMonitor may still fail to update DB later.
                    throw new FileNotFoundException(uri.toString());
                }
            }

            try {
                ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe();
                PipeMonitor pipeMonitor = new PipeMonitor(pipeFds[0], channelId, params);
                pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                return pipeFds[1];
            } catch (IOException ioe) {
                FileNotFoundException fne = new FileNotFoundException(uri.toString());
                fne.initCause(ioe);
                throw fne;
            }
        }
    }

    /**
     * Validates the sort order based on the given field set.
     *
     * @throws IllegalArgumentException if there is any unknown field.
     */
    @SuppressLint("DefaultLocale")
    private static void validateSortOrder(String sortOrder, Set<String> possibleFields) {
        if (TextUtils.isEmpty(sortOrder) || possibleFields.isEmpty()) {
            return;
        }
        String[] orders = sortOrder.split(",");
        for (String order : orders) {
            String field =
                    order.replaceAll("\\s+", " ")
                            .trim()
                            .toLowerCase()
                            .replace(" asc", "")
                            .replace(" desc", "");
            if (!possibleFields.contains(field)) {
                throw new IllegalArgumentException("Illegal field in sort order " + order);
            }
        }
    }

    private class PipeMonitor extends AsyncTask<Void, Void, Void> {
        private final ParcelFileDescriptor mPfd;
        private final long mChannelId;
        private final SqlParams mParams;

        private PipeMonitor(ParcelFileDescriptor pfd, long channelId, SqlParams params) {
            mPfd = pfd;
            mChannelId = channelId;
            mParams = params;
        }

        @Override
        protected Void doInBackground(Void... params) {
            int count = 0;
            try (AutoCloseInputStream is = new AutoCloseInputStream(mPfd);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                Bitmap bitmap = BitmapFactory.decodeStream(is);
                if (bitmap == null) {
                    Log.e(TAG, "Failed to decode logo image for channel ID " + mChannelId);
                    return null;
                }

                float scaleFactor =
                        Math.min(
                                1f,
                                ((float) MAX_LOGO_IMAGE_SIZE)
                                        / Math.max(bitmap.getWidth(), bitmap.getHeight()));
                if (scaleFactor < 1f) {
                    bitmap =
                            Bitmap.createScaledBitmap(
                                    bitmap,
                                    (int) (bitmap.getWidth() * scaleFactor),
                                    (int) (bitmap.getHeight() * scaleFactor),
                                    false);
                }
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
                byte[] bytes = baos.toByteArray();

                ContentValues values = new ContentValues();
                values.put(CHANNELS_COLUMN_LOGO, bytes);
                SQLiteDatabase db = mOpenHelper.getWritableDatabase();
                count =
                        db.update(
                                mParams.getTables(),
                                values,
                                mParams.getSelection(),
                                mParams.getSelectionArgs());
                if (count > 0) {
                    Uri uri = TvContractCompat.buildChannelLogoUri(mChannelId);
                    notifyChange(uri);
                }
            } catch (IOException e) {
                Log.e(TAG, "Failed to write logo for channel ID " + mChannelId, e);

            } finally {
                if (count == 0) {
                    try {
                        mPfd.closeWithError("Failed to write logo for channel ID " + mChannelId);
                    } catch (IOException ioe) {
                        Log.e(TAG, "Failed to close pipe", ioe);
                    }
                }
            }
            return null;
        }
    }

    /**
     * Column definitions for the TV programs that the user watched. Applications do not have access
     * to this table.
     *
     * <p>
     *
     * <p>By default, the query results will be sorted by {@link
     * WatchedPrograms#COLUMN_WATCH_START_TIME_UTC_MILLIS} in descending order.
     *
     * @hide
     */
    public static final class WatchedPrograms implements BaseTvColumns {

        /** The content:// style URI for this table. */
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + TvContract.AUTHORITY + "/watched_program");

        /** The MIME type of a directory of watched programs. */
        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/watched_program";

        /** The MIME type of a single item in this table. */
        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/watched_program";

        /**
         * The UTC time that the user started watching this TV program, in milliseconds since the
         * epoch.
         *
         * <p>
         *
         * <p>Type: INTEGER (long)
         */
        public static final String COLUMN_WATCH_START_TIME_UTC_MILLIS =
                "watch_start_time_utc_millis";

        /**
         * The UTC time that the user stopped watching this TV program, in milliseconds since the
         * epoch.
         *
         * <p>
         *
         * <p>Type: INTEGER (long)
         */
        public static final String COLUMN_WATCH_END_TIME_UTC_MILLIS = "watch_end_time_utc_millis";

        /**
         * The ID of the TV channel that provides this TV program.
         *
         * <p>
         *
         * <p>This is a required field.
         *
         * <p>
         *
         * <p>Type: INTEGER (long)
         */
        public static final String COLUMN_CHANNEL_ID = "channel_id";

        /**
         * The title of this TV program.
         *
         * <p>
         *
         * <p>Type: TEXT
         */
        public static final String COLUMN_TITLE = "title";

        /**
         * The start time of this TV program, in milliseconds since the epoch.
         *
         * <p>
         *
         * <p>Type: INTEGER (long)
         */
        public static final String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis";

        /**
         * The end time of this TV program, in milliseconds since the epoch.
         *
         * <p>
         *
         * <p>Type: INTEGER (long)
         */
        public static final String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";

        /**
         * The description of this TV program.
         *
         * <p>
         *
         * <p>Type: TEXT
         */
        public static final String COLUMN_DESCRIPTION = "description";

        /**
         * Extra parameters given to {@link TvInputService.Session#tune(Uri, android.os.Bundle)
         * TvInputService.Session.tune(Uri, android.os.Bundle)} when tuning to the channel that
         * provides this TV program. (Used internally.)
         *
         * <p>
         *
         * <p>This column contains an encoded string that represents comma-separated key-value pairs
         * of the tune parameters. (Ex. "[key1]=[value1], [key2]=[value2]"). '%' is used as an
         * escape character for '%', '=', and ','.
         *
         * <p>
         *
         * <p>Type: TEXT
         */
        public static final String COLUMN_INTERNAL_TUNE_PARAMS = "tune_params";

        /**
         * The session token of this TV program. (Used internally.)
         *
         * <p>
         *
         * <p>This contains a String representation of {@link IBinder} for {@link
         * TvInputService.Session} that provides the current TV program. It is used internally to
         * distinguish watched programs entries from different TV input sessions.
         *
         * <p>
         *
         * <p>Type: TEXT
         */
        public static final String COLUMN_INTERNAL_SESSION_TOKEN = "session_token";

        private WatchedPrograms() {}
    }
}
