diff options
Diffstat (limited to 'tests/common/src/com/android/tv/testing/FakeTvProvider.java')
-rw-r--r-- | tests/common/src/com/android/tv/testing/FakeTvProvider.java | 2605 |
1 files changed, 2605 insertions, 0 deletions
diff --git a/tests/common/src/com/android/tv/testing/FakeTvProvider.java b/tests/common/src/com/android/tv/testing/FakeTvProvider.java new file mode 100644 index 00000000..24c26f39 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/FakeTvProvider.java @@ -0,0 +1,2605 @@ +/* + * 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; + +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.support.media.tv.TvContractCompat; +import android.support.media.tv.TvContractCompat.BaseTvColumns; +import android.support.media.tv.TvContractCompat.Channels; +import android.support.media.tv.TvContractCompat.PreviewPrograms; +import android.support.media.tv.TvContractCompat.Programs; +import android.support.media.tv.TvContractCompat.Programs.Genres; +import android.support.media.tv.TvContractCompat.RecordedPrograms; +import android.support.media.tv.TvContractCompat.WatchNextPrograms; +import android.text.TextUtils; +import android.util.Log; +import com.android.tv.util.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() {} + } +} |