• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.tv;
18 
19 import android.annotation.SuppressLint;
20 import android.app.AlarmManager;
21 import android.app.PendingIntent;
22 import android.content.ContentProvider;
23 import android.content.ContentProviderOperation;
24 import android.content.ContentProviderResult;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.OperationApplicationException;
29 import android.content.SharedPreferences;
30 import android.content.UriMatcher;
31 import android.content.pm.PackageManager;
32 import android.database.Cursor;
33 import android.database.DatabaseUtils;
34 import android.database.SQLException;
35 import android.database.sqlite.SQLiteDatabase;
36 import android.database.sqlite.SQLiteOpenHelper;
37 import android.database.sqlite.SQLiteQueryBuilder;
38 import android.graphics.Bitmap;
39 import android.graphics.BitmapFactory;
40 import android.media.tv.TvContract;
41 import android.media.tv.TvContract.BaseTvColumns;
42 import android.media.tv.TvContract.Channels;
43 import android.media.tv.TvContract.PreviewPrograms;
44 import android.media.tv.TvContract.Programs;
45 import android.media.tv.TvContract.Programs.Genres;
46 import android.media.tv.TvContract.RecordedPrograms;
47 import android.media.tv.TvContract.WatchedPrograms;
48 import android.media.tv.TvContract.WatchNextPrograms;
49 import android.net.Uri;
50 import android.os.AsyncTask;
51 import android.os.Bundle;
52 import android.os.Handler;
53 import android.os.Message;
54 import android.os.ParcelFileDescriptor;
55 import android.os.ParcelFileDescriptor.AutoCloseInputStream;
56 import android.preference.PreferenceManager;
57 import android.provider.BaseColumns;
58 import android.text.TextUtils;
59 import android.text.format.DateUtils;
60 import android.util.Log;
61 
62 import com.android.internal.annotations.VisibleForTesting;
63 import com.android.internal.os.SomeArgs;
64 import com.android.providers.tv.util.SqlParams;
65 
66 import com.android.providers.tv.util.SqliteTokenFinder;
67 import java.util.Locale;
68 import libcore.io.IoUtils;
69 
70 import java.io.ByteArrayOutputStream;
71 import java.io.FileNotFoundException;
72 import java.io.IOException;
73 import java.util.ArrayList;
74 import java.util.HashMap;
75 import java.util.HashSet;
76 import java.util.Iterator;
77 import java.util.Map;
78 import java.util.Set;
79 import java.util.concurrent.ConcurrentHashMap;
80 
81 /**
82  * TV content provider. The contract between this provider and applications is defined in
83  * {@link android.media.tv.TvContract}.
84  */
85 public class TvProvider extends ContentProvider {
86     private static final boolean DEBUG = false;
87     private static final String TAG = "TvProvider";
88 
89     static final int DATABASE_VERSION = 34;
90     static final String SHARED_PREF_BLOCKED_PACKAGES_KEY = "blocked_packages";
91     static final String CHANNELS_TABLE = "channels";
92     static final String PROGRAMS_TABLE = "programs";
93     static final String RECORDED_PROGRAMS_TABLE = "recorded_programs";
94     static final String PREVIEW_PROGRAMS_TABLE = "preview_programs";
95     static final String WATCH_NEXT_PROGRAMS_TABLE = "watch_next_programs";
96     static final String WATCHED_PROGRAMS_TABLE = "watched_programs";
97     static final String PROGRAMS_TABLE_PACKAGE_NAME_INDEX = "programs_package_name_index";
98     static final String PROGRAMS_TABLE_CHANNEL_ID_INDEX = "programs_channel_id_index";
99     static final String PROGRAMS_TABLE_START_TIME_INDEX = "programs_start_time_index";
100     static final String PROGRAMS_TABLE_END_TIME_INDEX = "programs_end_time_index";
101     static final String WATCHED_PROGRAMS_TABLE_CHANNEL_ID_INDEX =
102             "watched_programs_channel_id_index";
103     // The internal column in the watched programs table to indicate whether the current log entry
104     // is consolidated or not. Unconsolidated entries may have columns with missing data.
105     static final String WATCHED_PROGRAMS_COLUMN_CONSOLIDATED = "consolidated";
106     static final String CHANNELS_COLUMN_LOGO = "logo";
107     private static final String DATABASE_NAME = "tv.db";
108     private static final String DELETED_CHANNELS_TABLE = "deleted_channels";  // Deprecated
109     private static final String DEFAULT_PROGRAMS_SORT_ORDER = Programs.COLUMN_START_TIME_UTC_MILLIS
110             + " ASC";
111     private static final String DEFAULT_WATCHED_PROGRAMS_SORT_ORDER =
112             WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC";
113     private static final String CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE = CHANNELS_TABLE
114             + " INNER JOIN " + PROGRAMS_TABLE
115             + " ON (" + CHANNELS_TABLE + "." + Channels._ID + "="
116             + PROGRAMS_TABLE + "." + Programs.COLUMN_CHANNEL_ID + ")";
117 
118     // Operation names for createSqlParams().
119     private static final String OP_QUERY = "query";
120     private static final String OP_UPDATE = "update";
121     private static final String OP_DELETE = "delete";
122 
123     private static final UriMatcher sUriMatcher;
124     private static final int MATCH_CHANNEL = 1;
125     private static final int MATCH_CHANNEL_ID = 2;
126     private static final int MATCH_CHANNEL_ID_LOGO = 3;
127     private static final int MATCH_PASSTHROUGH_ID = 4;
128     private static final int MATCH_PROGRAM = 5;
129     private static final int MATCH_PROGRAM_ID = 6;
130     private static final int MATCH_WATCHED_PROGRAM = 7;
131     private static final int MATCH_WATCHED_PROGRAM_ID = 8;
132     private static final int MATCH_RECORDED_PROGRAM = 9;
133     private static final int MATCH_RECORDED_PROGRAM_ID = 10;
134     private static final int MATCH_PREVIEW_PROGRAM = 11;
135     private static final int MATCH_PREVIEW_PROGRAM_ID = 12;
136     private static final int MATCH_WATCH_NEXT_PROGRAM = 13;
137     private static final int MATCH_WATCH_NEXT_PROGRAM_ID = 14;
138 
139     private static final int MAX_LOGO_IMAGE_SIZE = 256;
140 
141     private static final String EMPTY_STRING = "";
142 
143     private static final long MAX_PROGRAM_DATA_DELAY_IN_MILLIS = 10 * 1000; // 10 seconds
144 
145     private static final Map<String, String> sChannelProjectionMap;
146     private static final Map<String, String> sProgramProjectionMap;
147     private static final Map<String, String> sWatchedProgramProjectionMap;
148     private static final Map<String, String> sRecordedProgramProjectionMap;
149     private static final Map<String, String> sPreviewProgramProjectionMap;
150     private static final Map<String, String> sWatchNextProgramProjectionMap;
151     private static boolean sInitialized;
152 
153     static {
154         sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL)155         sUriMatcher.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL);
sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID)156         sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID);
sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#/logo", MATCH_CHANNEL_ID_LOGO)157         sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#/logo", MATCH_CHANNEL_ID_LOGO);
sUriMatcher.addURI(TvContract.AUTHORITY, "passthrough/*", MATCH_PASSTHROUGH_ID)158         sUriMatcher.addURI(TvContract.AUTHORITY, "passthrough/*", MATCH_PASSTHROUGH_ID);
sUriMatcher.addURI(TvContract.AUTHORITY, "program", MATCH_PROGRAM)159         sUriMatcher.addURI(TvContract.AUTHORITY, "program", MATCH_PROGRAM);
sUriMatcher.addURI(TvContract.AUTHORITY, "program/#", MATCH_PROGRAM_ID)160         sUriMatcher.addURI(TvContract.AUTHORITY, "program/#", MATCH_PROGRAM_ID);
sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program", MATCH_WATCHED_PROGRAM)161         sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program", MATCH_WATCHED_PROGRAM);
sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID)162         sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID);
sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM)163         sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM);
sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID)164         sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID);
sUriMatcher.addURI(TvContract.AUTHORITY, "preview_program", MATCH_PREVIEW_PROGRAM)165         sUriMatcher.addURI(TvContract.AUTHORITY, "preview_program", MATCH_PREVIEW_PROGRAM);
sUriMatcher.addURI(TvContract.AUTHORITY, "preview_program/#", MATCH_PREVIEW_PROGRAM_ID)166         sUriMatcher.addURI(TvContract.AUTHORITY, "preview_program/#", MATCH_PREVIEW_PROGRAM_ID);
sUriMatcher.addURI(TvContract.AUTHORITY, "watch_next_program", MATCH_WATCH_NEXT_PROGRAM)167         sUriMatcher.addURI(TvContract.AUTHORITY, "watch_next_program", MATCH_WATCH_NEXT_PROGRAM);
sUriMatcher.addURI(TvContract.AUTHORITY, "watch_next_program/#", MATCH_WATCH_NEXT_PROGRAM_ID)168         sUriMatcher.addURI(TvContract.AUTHORITY, "watch_next_program/#",
169                 MATCH_WATCH_NEXT_PROGRAM_ID);
170 
171         sChannelProjectionMap = new HashMap<>();
sChannelProjectionMap.put(Channels._ID, CHANNELS_TABLE + "." + Channels._ID)172         sChannelProjectionMap.put(Channels._ID, CHANNELS_TABLE + "." + Channels._ID);
sChannelProjectionMap.put(Channels.COLUMN_PACKAGE_NAME, CHANNELS_TABLE + "." + Channels.COLUMN_PACKAGE_NAME)173         sChannelProjectionMap.put(Channels.COLUMN_PACKAGE_NAME,
174                 CHANNELS_TABLE + "." + Channels.COLUMN_PACKAGE_NAME);
sChannelProjectionMap.put(Channels.COLUMN_INPUT_ID, CHANNELS_TABLE + "." + Channels.COLUMN_INPUT_ID)175         sChannelProjectionMap.put(Channels.COLUMN_INPUT_ID,
176                 CHANNELS_TABLE + "." + Channels.COLUMN_INPUT_ID);
sChannelProjectionMap.put(Channels.COLUMN_TYPE, CHANNELS_TABLE + "." + Channels.COLUMN_TYPE)177         sChannelProjectionMap.put(Channels.COLUMN_TYPE,
178                 CHANNELS_TABLE + "." + Channels.COLUMN_TYPE);
sChannelProjectionMap.put(Channels.COLUMN_SERVICE_TYPE, CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_TYPE)179         sChannelProjectionMap.put(Channels.COLUMN_SERVICE_TYPE,
180                 CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_TYPE);
sChannelProjectionMap.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, CHANNELS_TABLE + "." + Channels.COLUMN_ORIGINAL_NETWORK_ID)181         sChannelProjectionMap.put(Channels.COLUMN_ORIGINAL_NETWORK_ID,
182                 CHANNELS_TABLE + "." + Channels.COLUMN_ORIGINAL_NETWORK_ID);
sChannelProjectionMap.put(Channels.COLUMN_TRANSPORT_STREAM_ID, CHANNELS_TABLE + "." + Channels.COLUMN_TRANSPORT_STREAM_ID)183         sChannelProjectionMap.put(Channels.COLUMN_TRANSPORT_STREAM_ID,
184                 CHANNELS_TABLE + "." + Channels.COLUMN_TRANSPORT_STREAM_ID);
sChannelProjectionMap.put(Channels.COLUMN_SERVICE_ID, CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_ID)185         sChannelProjectionMap.put(Channels.COLUMN_SERVICE_ID,
186                 CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_ID);
sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NUMBER, CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NUMBER)187         sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NUMBER,
188                 CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NUMBER);
sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NAME, CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NAME)189         sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NAME,
190                 CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NAME);
sChannelProjectionMap.put(Channels.COLUMN_NETWORK_AFFILIATION, CHANNELS_TABLE + "." + Channels.COLUMN_NETWORK_AFFILIATION)191         sChannelProjectionMap.put(Channels.COLUMN_NETWORK_AFFILIATION,
192                 CHANNELS_TABLE + "." + Channels.COLUMN_NETWORK_AFFILIATION);
sChannelProjectionMap.put(Channels.COLUMN_DESCRIPTION, CHANNELS_TABLE + "." + Channels.COLUMN_DESCRIPTION)193         sChannelProjectionMap.put(Channels.COLUMN_DESCRIPTION,
194                 CHANNELS_TABLE + "." + Channels.COLUMN_DESCRIPTION);
sChannelProjectionMap.put(Channels.COLUMN_VIDEO_FORMAT, CHANNELS_TABLE + "." + Channels.COLUMN_VIDEO_FORMAT)195         sChannelProjectionMap.put(Channels.COLUMN_VIDEO_FORMAT,
196                 CHANNELS_TABLE + "." + Channels.COLUMN_VIDEO_FORMAT);
sChannelProjectionMap.put(Channels.COLUMN_BROWSABLE, CHANNELS_TABLE + "." + Channels.COLUMN_BROWSABLE)197         sChannelProjectionMap.put(Channels.COLUMN_BROWSABLE,
198                 CHANNELS_TABLE + "." + Channels.COLUMN_BROWSABLE);
sChannelProjectionMap.put(Channels.COLUMN_SEARCHABLE, CHANNELS_TABLE + "." + Channels.COLUMN_SEARCHABLE)199         sChannelProjectionMap.put(Channels.COLUMN_SEARCHABLE,
200                 CHANNELS_TABLE + "." + Channels.COLUMN_SEARCHABLE);
sChannelProjectionMap.put(Channels.COLUMN_LOCKED, CHANNELS_TABLE + "." + Channels.COLUMN_LOCKED)201         sChannelProjectionMap.put(Channels.COLUMN_LOCKED,
202                 CHANNELS_TABLE + "." + Channels.COLUMN_LOCKED);
sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_ICON_URI, CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_ICON_URI)203         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_ICON_URI,
204                 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)205         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_POSTER_ART_URI,
206                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_POSTER_ART_URI);
sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_TEXT, CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_TEXT)207         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_TEXT,
208                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_TEXT);
sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_COLOR, CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_COLOR)209         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_COLOR,
210                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_COLOR);
sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_INTENT_URI, CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_INTENT_URI)211         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_INTENT_URI,
212                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_INTENT_URI);
sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA, CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_DATA)213         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA,
214                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_DATA);
sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1)215         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1,
216                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1);
sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2)217         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
218                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2);
sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3, CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3)219         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3,
220                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3);
sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4, CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4)221         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4,
222                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4);
sChannelProjectionMap.put(Channels.COLUMN_VERSION_NUMBER, CHANNELS_TABLE + "." + Channels.COLUMN_VERSION_NUMBER)223         sChannelProjectionMap.put(Channels.COLUMN_VERSION_NUMBER,
224                 CHANNELS_TABLE + "." + Channels.COLUMN_VERSION_NUMBER);
sChannelProjectionMap.put(Channels.COLUMN_TRANSIENT, CHANNELS_TABLE + "." + Channels.COLUMN_TRANSIENT)225         sChannelProjectionMap.put(Channels.COLUMN_TRANSIENT,
226                 CHANNELS_TABLE + "." + Channels.COLUMN_TRANSIENT);
sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_ID, CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_ID)227         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_ID,
228                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_ID);
229 
230         sProgramProjectionMap = new HashMap<>();
sProgramProjectionMap.put(Programs._ID, Programs._ID)231         sProgramProjectionMap.put(Programs._ID, Programs._ID);
sProgramProjectionMap.put(Programs.COLUMN_PACKAGE_NAME, Programs.COLUMN_PACKAGE_NAME)232         sProgramProjectionMap.put(Programs.COLUMN_PACKAGE_NAME, Programs.COLUMN_PACKAGE_NAME);
sProgramProjectionMap.put(Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_CHANNEL_ID)233         sProgramProjectionMap.put(Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_CHANNEL_ID);
sProgramProjectionMap.put(Programs.COLUMN_TITLE, Programs.COLUMN_TITLE)234         sProgramProjectionMap.put(Programs.COLUMN_TITLE, Programs.COLUMN_TITLE);
235         // 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)236         sProgramProjectionMap.put(Programs.COLUMN_SEASON_NUMBER,
237                 Programs.COLUMN_SEASON_DISPLAY_NUMBER + " AS " + Programs.COLUMN_SEASON_NUMBER);
sProgramProjectionMap.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, Programs.COLUMN_SEASON_DISPLAY_NUMBER)238         sProgramProjectionMap.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER,
239                 Programs.COLUMN_SEASON_DISPLAY_NUMBER);
sProgramProjectionMap.put(Programs.COLUMN_SEASON_TITLE, Programs.COLUMN_SEASON_TITLE)240         sProgramProjectionMap.put(Programs.COLUMN_SEASON_TITLE, Programs.COLUMN_SEASON_TITLE);
241         // 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)242         sProgramProjectionMap.put(Programs.COLUMN_EPISODE_NUMBER,
243                 Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " AS " + Programs.COLUMN_EPISODE_NUMBER);
sProgramProjectionMap.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER, Programs.COLUMN_EPISODE_DISPLAY_NUMBER)244         sProgramProjectionMap.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
245                 Programs.COLUMN_EPISODE_DISPLAY_NUMBER);
sProgramProjectionMap.put(Programs.COLUMN_EPISODE_TITLE, Programs.COLUMN_EPISODE_TITLE)246         sProgramProjectionMap.put(Programs.COLUMN_EPISODE_TITLE, Programs.COLUMN_EPISODE_TITLE);
sProgramProjectionMap.put(Programs.COLUMN_START_TIME_UTC_MILLIS, Programs.COLUMN_START_TIME_UTC_MILLIS)247         sProgramProjectionMap.put(Programs.COLUMN_START_TIME_UTC_MILLIS,
248                 Programs.COLUMN_START_TIME_UTC_MILLIS);
sProgramProjectionMap.put(Programs.COLUMN_END_TIME_UTC_MILLIS, Programs.COLUMN_END_TIME_UTC_MILLIS)249         sProgramProjectionMap.put(Programs.COLUMN_END_TIME_UTC_MILLIS,
250                 Programs.COLUMN_END_TIME_UTC_MILLIS);
sProgramProjectionMap.put(Programs.COLUMN_BROADCAST_GENRE, Programs.COLUMN_BROADCAST_GENRE)251         sProgramProjectionMap.put(Programs.COLUMN_BROADCAST_GENRE, Programs.COLUMN_BROADCAST_GENRE);
sProgramProjectionMap.put(Programs.COLUMN_CANONICAL_GENRE, Programs.COLUMN_CANONICAL_GENRE)252         sProgramProjectionMap.put(Programs.COLUMN_CANONICAL_GENRE, Programs.COLUMN_CANONICAL_GENRE);
sProgramProjectionMap.put(Programs.COLUMN_SHORT_DESCRIPTION, Programs.COLUMN_SHORT_DESCRIPTION)253         sProgramProjectionMap.put(Programs.COLUMN_SHORT_DESCRIPTION,
254                 Programs.COLUMN_SHORT_DESCRIPTION);
sProgramProjectionMap.put(Programs.COLUMN_LONG_DESCRIPTION, Programs.COLUMN_LONG_DESCRIPTION)255         sProgramProjectionMap.put(Programs.COLUMN_LONG_DESCRIPTION,
256                 Programs.COLUMN_LONG_DESCRIPTION);
sProgramProjectionMap.put(Programs.COLUMN_VIDEO_WIDTH, Programs.COLUMN_VIDEO_WIDTH)257         sProgramProjectionMap.put(Programs.COLUMN_VIDEO_WIDTH, Programs.COLUMN_VIDEO_WIDTH);
sProgramProjectionMap.put(Programs.COLUMN_VIDEO_HEIGHT, Programs.COLUMN_VIDEO_HEIGHT)258         sProgramProjectionMap.put(Programs.COLUMN_VIDEO_HEIGHT, Programs.COLUMN_VIDEO_HEIGHT);
sProgramProjectionMap.put(Programs.COLUMN_AUDIO_LANGUAGE, Programs.COLUMN_AUDIO_LANGUAGE)259         sProgramProjectionMap.put(Programs.COLUMN_AUDIO_LANGUAGE, Programs.COLUMN_AUDIO_LANGUAGE);
sProgramProjectionMap.put(Programs.COLUMN_CONTENT_RATING, Programs.COLUMN_CONTENT_RATING)260         sProgramProjectionMap.put(Programs.COLUMN_CONTENT_RATING, Programs.COLUMN_CONTENT_RATING);
sProgramProjectionMap.put(Programs.COLUMN_POSTER_ART_URI, Programs.COLUMN_POSTER_ART_URI)261         sProgramProjectionMap.put(Programs.COLUMN_POSTER_ART_URI, Programs.COLUMN_POSTER_ART_URI);
sProgramProjectionMap.put(Programs.COLUMN_THUMBNAIL_URI, Programs.COLUMN_THUMBNAIL_URI)262         sProgramProjectionMap.put(Programs.COLUMN_THUMBNAIL_URI, Programs.COLUMN_THUMBNAIL_URI);
sProgramProjectionMap.put(Programs.COLUMN_SEARCHABLE, Programs.COLUMN_SEARCHABLE)263         sProgramProjectionMap.put(Programs.COLUMN_SEARCHABLE, Programs.COLUMN_SEARCHABLE);
sProgramProjectionMap.put(Programs.COLUMN_RECORDING_PROHIBITED, Programs.COLUMN_RECORDING_PROHIBITED)264         sProgramProjectionMap.put(Programs.COLUMN_RECORDING_PROHIBITED,
265                 Programs.COLUMN_RECORDING_PROHIBITED);
sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_DATA, Programs.COLUMN_INTERNAL_PROVIDER_DATA)266         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_DATA,
267                 Programs.COLUMN_INTERNAL_PROVIDER_DATA);
sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG1, Programs.COLUMN_INTERNAL_PROVIDER_FLAG1)268         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG1,
269                 Programs.COLUMN_INTERNAL_PROVIDER_FLAG1);
sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG2, Programs.COLUMN_INTERNAL_PROVIDER_FLAG2)270         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG2,
271                 Programs.COLUMN_INTERNAL_PROVIDER_FLAG2);
sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG3, Programs.COLUMN_INTERNAL_PROVIDER_FLAG3)272         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG3,
273                 Programs.COLUMN_INTERNAL_PROVIDER_FLAG3);
sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG4, Programs.COLUMN_INTERNAL_PROVIDER_FLAG4)274         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG4,
275                 Programs.COLUMN_INTERNAL_PROVIDER_FLAG4);
sProgramProjectionMap.put(Programs.COLUMN_VERSION_NUMBER, Programs.COLUMN_VERSION_NUMBER)276         sProgramProjectionMap.put(Programs.COLUMN_VERSION_NUMBER, Programs.COLUMN_VERSION_NUMBER);
sProgramProjectionMap.put(Programs.COLUMN_REVIEW_RATING_STYLE, Programs.COLUMN_REVIEW_RATING_STYLE)277         sProgramProjectionMap.put(Programs.COLUMN_REVIEW_RATING_STYLE,
278                 Programs.COLUMN_REVIEW_RATING_STYLE);
sProgramProjectionMap.put(Programs.COLUMN_REVIEW_RATING, Programs.COLUMN_REVIEW_RATING)279         sProgramProjectionMap.put(Programs.COLUMN_REVIEW_RATING,
280                 Programs.COLUMN_REVIEW_RATING);
281 
282         sWatchedProgramProjectionMap = new HashMap<>();
sWatchedProgramProjectionMap.put(WatchedPrograms._ID, WatchedPrograms._ID)283         sWatchedProgramProjectionMap.put(WatchedPrograms._ID, WatchedPrograms._ID);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS)284         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
285                 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS)286         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
287                 WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_CHANNEL_ID, WatchedPrograms.COLUMN_CHANNEL_ID)288         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_CHANNEL_ID,
289                 WatchedPrograms.COLUMN_CHANNEL_ID);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_TITLE, WatchedPrograms.COLUMN_TITLE)290         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_TITLE,
291                 WatchedPrograms.COLUMN_TITLE);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS)292         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS,
293                 WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS)294         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS,
295                 WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_DESCRIPTION, WatchedPrograms.COLUMN_DESCRIPTION)296         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_DESCRIPTION,
297                 WatchedPrograms.COLUMN_DESCRIPTION);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS, WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS)298         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS,
299                 WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN)300         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN,
301                 WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN);
sWatchedProgramProjectionMap.put(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED, WATCHED_PROGRAMS_COLUMN_CONSOLIDATED)302         sWatchedProgramProjectionMap.put(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED,
303                 WATCHED_PROGRAMS_COLUMN_CONSOLIDATED);
304 
305         sRecordedProgramProjectionMap = new HashMap<>();
sRecordedProgramProjectionMap.put(RecordedPrograms._ID, RecordedPrograms._ID)306         sRecordedProgramProjectionMap.put(RecordedPrograms._ID, RecordedPrograms._ID);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_PACKAGE_NAME, RecordedPrograms.COLUMN_PACKAGE_NAME)307         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_PACKAGE_NAME,
308                 RecordedPrograms.COLUMN_PACKAGE_NAME);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INPUT_ID, RecordedPrograms.COLUMN_INPUT_ID)309         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INPUT_ID,
310                 RecordedPrograms.COLUMN_INPUT_ID);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CHANNEL_ID, RecordedPrograms.COLUMN_CHANNEL_ID)311         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CHANNEL_ID,
312                 RecordedPrograms.COLUMN_CHANNEL_ID);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_TITLE, RecordedPrograms.COLUMN_TITLE)313         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_TITLE,
314                 RecordedPrograms.COLUMN_TITLE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER)315         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
316                 RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_TITLE, RecordedPrograms.COLUMN_SEASON_TITLE)317         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_TITLE,
318                 RecordedPrograms.COLUMN_SEASON_TITLE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER)319         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
320                 RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_TITLE, RecordedPrograms.COLUMN_EPISODE_TITLE)321         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_TITLE,
322                 RecordedPrograms.COLUMN_EPISODE_TITLE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS)323         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
324                 RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS)325         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS,
326                 RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_BROADCAST_GENRE, RecordedPrograms.COLUMN_BROADCAST_GENRE)327         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_BROADCAST_GENRE,
328                 RecordedPrograms.COLUMN_BROADCAST_GENRE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CANONICAL_GENRE, RecordedPrograms.COLUMN_CANONICAL_GENRE)329         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CANONICAL_GENRE,
330                 RecordedPrograms.COLUMN_CANONICAL_GENRE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, RecordedPrograms.COLUMN_SHORT_DESCRIPTION)331         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION,
332                 RecordedPrograms.COLUMN_SHORT_DESCRIPTION);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, RecordedPrograms.COLUMN_LONG_DESCRIPTION)333         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION,
334                 RecordedPrograms.COLUMN_LONG_DESCRIPTION);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, RecordedPrograms.COLUMN_VIDEO_WIDTH)335         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_WIDTH,
336                 RecordedPrograms.COLUMN_VIDEO_WIDTH);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, RecordedPrograms.COLUMN_VIDEO_HEIGHT)337         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT,
338                 RecordedPrograms.COLUMN_VIDEO_HEIGHT);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, RecordedPrograms.COLUMN_AUDIO_LANGUAGE)339         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE,
340                 RecordedPrograms.COLUMN_AUDIO_LANGUAGE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CONTENT_RATING, RecordedPrograms.COLUMN_CONTENT_RATING)341         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CONTENT_RATING,
342                 RecordedPrograms.COLUMN_CONTENT_RATING);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_POSTER_ART_URI, RecordedPrograms.COLUMN_POSTER_ART_URI)343         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_POSTER_ART_URI,
344                 RecordedPrograms.COLUMN_POSTER_ART_URI);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, RecordedPrograms.COLUMN_THUMBNAIL_URI)345         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_THUMBNAIL_URI,
346                 RecordedPrograms.COLUMN_THUMBNAIL_URI);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEARCHABLE, RecordedPrograms.COLUMN_SEARCHABLE)347         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEARCHABLE,
348                 RecordedPrograms.COLUMN_SEARCHABLE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI, RecordedPrograms.COLUMN_RECORDING_DATA_URI)349         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI,
350                 RecordedPrograms.COLUMN_RECORDING_DATA_URI);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, RecordedPrograms.COLUMN_RECORDING_DATA_BYTES)351         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES,
352                 RecordedPrograms.COLUMN_RECORDING_DATA_BYTES);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS)353         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
354                 RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS)355         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
356                 RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA)357         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
358                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1)359         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
360                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2)361         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
362                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3)363         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
364                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4)365         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
366                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VERSION_NUMBER, RecordedPrograms.COLUMN_VERSION_NUMBER)367         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VERSION_NUMBER,
368                 RecordedPrograms.COLUMN_VERSION_NUMBER);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_REVIEW_RATING_STYLE, RecordedPrograms.COLUMN_REVIEW_RATING_STYLE)369         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_REVIEW_RATING_STYLE,
370                 RecordedPrograms.COLUMN_REVIEW_RATING_STYLE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_REVIEW_RATING, RecordedPrograms.COLUMN_REVIEW_RATING)371         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_REVIEW_RATING,
372                 RecordedPrograms.COLUMN_REVIEW_RATING);
373 
374         sPreviewProgramProjectionMap = new HashMap<>();
sPreviewProgramProjectionMap.put(PreviewPrograms._ID, PreviewPrograms._ID)375         sPreviewProgramProjectionMap.put(PreviewPrograms._ID, PreviewPrograms._ID);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_PACKAGE_NAME, PreviewPrograms.COLUMN_PACKAGE_NAME)376         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_PACKAGE_NAME,
377                 PreviewPrograms.COLUMN_PACKAGE_NAME);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_CHANNEL_ID, PreviewPrograms.COLUMN_CHANNEL_ID)378         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_CHANNEL_ID,
379                 PreviewPrograms.COLUMN_CHANNEL_ID);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_TITLE, PreviewPrograms.COLUMN_TITLE)380         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_TITLE,
381                 PreviewPrograms.COLUMN_TITLE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER, PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER)382         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
383                 PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_SEASON_TITLE, PreviewPrograms.COLUMN_SEASON_TITLE)384         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_SEASON_TITLE,
385                 PreviewPrograms.COLUMN_SEASON_TITLE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER)386         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
387                 PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_EPISODE_TITLE, PreviewPrograms.COLUMN_EPISODE_TITLE)388         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_EPISODE_TITLE,
389                 PreviewPrograms.COLUMN_EPISODE_TITLE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_CANONICAL_GENRE, PreviewPrograms.COLUMN_CANONICAL_GENRE)390         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_CANONICAL_GENRE,
391                 PreviewPrograms.COLUMN_CANONICAL_GENRE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_SHORT_DESCRIPTION, PreviewPrograms.COLUMN_SHORT_DESCRIPTION)392         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_SHORT_DESCRIPTION,
393                 PreviewPrograms.COLUMN_SHORT_DESCRIPTION);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LONG_DESCRIPTION, PreviewPrograms.COLUMN_LONG_DESCRIPTION)394         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LONG_DESCRIPTION,
395                 PreviewPrograms.COLUMN_LONG_DESCRIPTION);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_VIDEO_WIDTH, PreviewPrograms.COLUMN_VIDEO_WIDTH)396         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_VIDEO_WIDTH,
397                 PreviewPrograms.COLUMN_VIDEO_WIDTH);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_VIDEO_HEIGHT, PreviewPrograms.COLUMN_VIDEO_HEIGHT)398         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_VIDEO_HEIGHT,
399                 PreviewPrograms.COLUMN_VIDEO_HEIGHT);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_AUDIO_LANGUAGE, PreviewPrograms.COLUMN_AUDIO_LANGUAGE)400         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_AUDIO_LANGUAGE,
401                 PreviewPrograms.COLUMN_AUDIO_LANGUAGE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_CONTENT_RATING, PreviewPrograms.COLUMN_CONTENT_RATING)402         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_CONTENT_RATING,
403                 PreviewPrograms.COLUMN_CONTENT_RATING);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_POSTER_ART_URI, PreviewPrograms.COLUMN_POSTER_ART_URI)404         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_POSTER_ART_URI,
405                 PreviewPrograms.COLUMN_POSTER_ART_URI);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_THUMBNAIL_URI, PreviewPrograms.COLUMN_THUMBNAIL_URI)406         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_THUMBNAIL_URI,
407                 PreviewPrograms.COLUMN_THUMBNAIL_URI);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_SEARCHABLE, PreviewPrograms.COLUMN_SEARCHABLE)408         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_SEARCHABLE,
409                 PreviewPrograms.COLUMN_SEARCHABLE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA, PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA)410         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
411                 PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1)412         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
413                 PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2)414         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
415                 PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3)416         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
417                 PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4)418         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
419                 PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_VERSION_NUMBER, PreviewPrograms.COLUMN_VERSION_NUMBER)420         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_VERSION_NUMBER,
421                 PreviewPrograms.COLUMN_VERSION_NUMBER);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID)422         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID,
423                 PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI, PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI)424         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI,
425                 PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS, PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS)426         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS,
427                 PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_DURATION_MILLIS, PreviewPrograms.COLUMN_DURATION_MILLIS)428         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_DURATION_MILLIS,
429                 PreviewPrograms.COLUMN_DURATION_MILLIS);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTENT_URI, PreviewPrograms.COLUMN_INTENT_URI)430         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTENT_URI,
431                 PreviewPrograms.COLUMN_INTENT_URI);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_WEIGHT, PreviewPrograms.COLUMN_WEIGHT)432         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_WEIGHT,
433                 PreviewPrograms.COLUMN_WEIGHT);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_TRANSIENT, PreviewPrograms.COLUMN_TRANSIENT)434         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_TRANSIENT,
435                 PreviewPrograms.COLUMN_TRANSIENT);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_TYPE, PreviewPrograms.COLUMN_TYPE)436         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_TYPE, PreviewPrograms.COLUMN_TYPE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO)437         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO,
438                 PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO, PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO)439         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO,
440                 PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LOGO_URI, PreviewPrograms.COLUMN_LOGO_URI)441         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LOGO_URI,
442                 PreviewPrograms.COLUMN_LOGO_URI);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_AVAILABILITY, PreviewPrograms.COLUMN_AVAILABILITY)443         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_AVAILABILITY,
444                 PreviewPrograms.COLUMN_AVAILABILITY);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_STARTING_PRICE, PreviewPrograms.COLUMN_STARTING_PRICE)445         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_STARTING_PRICE,
446                 PreviewPrograms.COLUMN_STARTING_PRICE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_OFFER_PRICE, PreviewPrograms.COLUMN_OFFER_PRICE)447         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_OFFER_PRICE,
448                 PreviewPrograms.COLUMN_OFFER_PRICE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_RELEASE_DATE, PreviewPrograms.COLUMN_RELEASE_DATE)449         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_RELEASE_DATE,
450                 PreviewPrograms.COLUMN_RELEASE_DATE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_ITEM_COUNT, PreviewPrograms.COLUMN_ITEM_COUNT)451         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_ITEM_COUNT,
452                 PreviewPrograms.COLUMN_ITEM_COUNT);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LIVE, PreviewPrograms.COLUMN_LIVE)453         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LIVE, PreviewPrograms.COLUMN_LIVE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERACTION_TYPE, PreviewPrograms.COLUMN_INTERACTION_TYPE)454         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERACTION_TYPE,
455                 PreviewPrograms.COLUMN_INTERACTION_TYPE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERACTION_COUNT, PreviewPrograms.COLUMN_INTERACTION_COUNT)456         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERACTION_COUNT,
457                 PreviewPrograms.COLUMN_INTERACTION_COUNT);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_AUTHOR, PreviewPrograms.COLUMN_AUTHOR)458         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_AUTHOR,
459                 PreviewPrograms.COLUMN_AUTHOR);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_REVIEW_RATING_STYLE, PreviewPrograms.COLUMN_REVIEW_RATING_STYLE)460         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_REVIEW_RATING_STYLE,
461                 PreviewPrograms.COLUMN_REVIEW_RATING_STYLE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_REVIEW_RATING, PreviewPrograms.COLUMN_REVIEW_RATING)462         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_REVIEW_RATING,
463                 PreviewPrograms.COLUMN_REVIEW_RATING);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_BROWSABLE, PreviewPrograms.COLUMN_BROWSABLE)464         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_BROWSABLE,
465                 PreviewPrograms.COLUMN_BROWSABLE);
sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_CONTENT_ID, PreviewPrograms.COLUMN_CONTENT_ID)466         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_CONTENT_ID,
467                 PreviewPrograms.COLUMN_CONTENT_ID);
468 
469         sWatchNextProgramProjectionMap = new HashMap<>();
sWatchNextProgramProjectionMap.put(WatchNextPrograms._ID, WatchNextPrograms._ID)470         sWatchNextProgramProjectionMap.put(WatchNextPrograms._ID, WatchNextPrograms._ID);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_PACKAGE_NAME, WatchNextPrograms.COLUMN_PACKAGE_NAME)471         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_PACKAGE_NAME,
472                 WatchNextPrograms.COLUMN_PACKAGE_NAME);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_TITLE, WatchNextPrograms.COLUMN_TITLE)473         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_TITLE,
474                 WatchNextPrograms.COLUMN_TITLE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER, WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER)475         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
476                 WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_SEASON_TITLE, WatchNextPrograms.COLUMN_SEASON_TITLE)477         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_SEASON_TITLE,
478                 WatchNextPrograms.COLUMN_SEASON_TITLE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER)479         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
480                 WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_EPISODE_TITLE, WatchNextPrograms.COLUMN_EPISODE_TITLE)481         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_EPISODE_TITLE,
482                 WatchNextPrograms.COLUMN_EPISODE_TITLE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_CANONICAL_GENRE, WatchNextPrograms.COLUMN_CANONICAL_GENRE)483         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_CANONICAL_GENRE,
484                 WatchNextPrograms.COLUMN_CANONICAL_GENRE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_SHORT_DESCRIPTION, WatchNextPrograms.COLUMN_SHORT_DESCRIPTION)485         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_SHORT_DESCRIPTION,
486                 WatchNextPrograms.COLUMN_SHORT_DESCRIPTION);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LONG_DESCRIPTION, WatchNextPrograms.COLUMN_LONG_DESCRIPTION)487         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LONG_DESCRIPTION,
488                 WatchNextPrograms.COLUMN_LONG_DESCRIPTION);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_VIDEO_WIDTH, WatchNextPrograms.COLUMN_VIDEO_WIDTH)489         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_VIDEO_WIDTH,
490                 WatchNextPrograms.COLUMN_VIDEO_WIDTH);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_VIDEO_HEIGHT, WatchNextPrograms.COLUMN_VIDEO_HEIGHT)491         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_VIDEO_HEIGHT,
492                 WatchNextPrograms.COLUMN_VIDEO_HEIGHT);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_AUDIO_LANGUAGE, WatchNextPrograms.COLUMN_AUDIO_LANGUAGE)493         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_AUDIO_LANGUAGE,
494                 WatchNextPrograms.COLUMN_AUDIO_LANGUAGE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_CONTENT_RATING, WatchNextPrograms.COLUMN_CONTENT_RATING)495         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_CONTENT_RATING,
496                 WatchNextPrograms.COLUMN_CONTENT_RATING);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_POSTER_ART_URI, WatchNextPrograms.COLUMN_POSTER_ART_URI)497         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_POSTER_ART_URI,
498                 WatchNextPrograms.COLUMN_POSTER_ART_URI);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_THUMBNAIL_URI, WatchNextPrograms.COLUMN_THUMBNAIL_URI)499         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_THUMBNAIL_URI,
500                 WatchNextPrograms.COLUMN_THUMBNAIL_URI);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_SEARCHABLE, WatchNextPrograms.COLUMN_SEARCHABLE)501         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_SEARCHABLE,
502                 WatchNextPrograms.COLUMN_SEARCHABLE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA, WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA)503         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
504                 WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1)505         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
506                 WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2)507         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
508                 WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3)509         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
510                 WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4)511         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
512                 WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_VERSION_NUMBER, WatchNextPrograms.COLUMN_VERSION_NUMBER)513         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_VERSION_NUMBER,
514                 WatchNextPrograms.COLUMN_VERSION_NUMBER);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID, WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID)515         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID,
516                 WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI, WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI)517         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI,
518                 WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS, WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS)519         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS,
520                 WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_DURATION_MILLIS, WatchNextPrograms.COLUMN_DURATION_MILLIS)521         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_DURATION_MILLIS,
522                 WatchNextPrograms.COLUMN_DURATION_MILLIS);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTENT_URI, WatchNextPrograms.COLUMN_INTENT_URI)523         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTENT_URI,
524                 WatchNextPrograms.COLUMN_INTENT_URI);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_TRANSIENT, WatchNextPrograms.COLUMN_TRANSIENT)525         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_TRANSIENT,
526                 WatchNextPrograms.COLUMN_TRANSIENT);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_TYPE, WatchNextPrograms.COLUMN_TYPE)527         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_TYPE,
528                 WatchNextPrograms.COLUMN_TYPE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE, WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE)529         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE,
530                 WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO)531         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO,
532                 WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO, WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO)533         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO,
534                 WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LOGO_URI, WatchNextPrograms.COLUMN_LOGO_URI)535         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LOGO_URI,
536                 WatchNextPrograms.COLUMN_LOGO_URI);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_AVAILABILITY, WatchNextPrograms.COLUMN_AVAILABILITY)537         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_AVAILABILITY,
538                 WatchNextPrograms.COLUMN_AVAILABILITY);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_STARTING_PRICE, WatchNextPrograms.COLUMN_STARTING_PRICE)539         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_STARTING_PRICE,
540                 WatchNextPrograms.COLUMN_STARTING_PRICE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_OFFER_PRICE, WatchNextPrograms.COLUMN_OFFER_PRICE)541         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_OFFER_PRICE,
542                 WatchNextPrograms.COLUMN_OFFER_PRICE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_RELEASE_DATE, WatchNextPrograms.COLUMN_RELEASE_DATE)543         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_RELEASE_DATE,
544                 WatchNextPrograms.COLUMN_RELEASE_DATE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_ITEM_COUNT, WatchNextPrograms.COLUMN_ITEM_COUNT)545         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_ITEM_COUNT,
546                 WatchNextPrograms.COLUMN_ITEM_COUNT);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LIVE, WatchNextPrograms.COLUMN_LIVE)547         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LIVE,
548                 WatchNextPrograms.COLUMN_LIVE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERACTION_TYPE, WatchNextPrograms.COLUMN_INTERACTION_TYPE)549         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERACTION_TYPE,
550                 WatchNextPrograms.COLUMN_INTERACTION_TYPE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERACTION_COUNT, WatchNextPrograms.COLUMN_INTERACTION_COUNT)551         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERACTION_COUNT,
552                 WatchNextPrograms.COLUMN_INTERACTION_COUNT);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_AUTHOR, WatchNextPrograms.COLUMN_AUTHOR)553         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_AUTHOR,
554                 WatchNextPrograms.COLUMN_AUTHOR);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE, WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE)555         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE,
556                 WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_REVIEW_RATING, WatchNextPrograms.COLUMN_REVIEW_RATING)557         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_REVIEW_RATING,
558                 WatchNextPrograms.COLUMN_REVIEW_RATING);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_BROWSABLE, WatchNextPrograms.COLUMN_BROWSABLE)559         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_BROWSABLE,
560                 WatchNextPrograms.COLUMN_BROWSABLE);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_CONTENT_ID, WatchNextPrograms.COLUMN_CONTENT_ID)561         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_CONTENT_ID,
562                 WatchNextPrograms.COLUMN_CONTENT_ID);
sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS, WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS)563         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS,
564                 WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS);
565     }
566 
567     // Mapping from broadcast genre to canonical genre.
568     private static Map<String, String> sGenreMap;
569 
570     private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
571 
572     private static final String PERMISSION_ACCESS_ALL_EPG_DATA =
573             "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA";
574 
575     private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS =
576             "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS";
577 
578     private static final String CREATE_RECORDED_PROGRAMS_TABLE_SQL =
579             "CREATE TABLE " + RECORDED_PROGRAMS_TABLE + " ("
580             + RecordedPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
581             + RecordedPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
582             + RecordedPrograms.COLUMN_INPUT_ID + " TEXT NOT NULL,"
583             + RecordedPrograms.COLUMN_CHANNEL_ID + " INTEGER,"
584             + RecordedPrograms.COLUMN_TITLE + " TEXT,"
585             + RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER + " TEXT,"
586             + RecordedPrograms.COLUMN_SEASON_TITLE + " TEXT,"
587             + RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER + " TEXT,"
588             + RecordedPrograms.COLUMN_EPISODE_TITLE + " TEXT,"
589             + RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS + " INTEGER,"
590             + RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS + " INTEGER,"
591             + RecordedPrograms.COLUMN_BROADCAST_GENRE + " TEXT,"
592             + RecordedPrograms.COLUMN_CANONICAL_GENRE + " TEXT,"
593             + RecordedPrograms.COLUMN_SHORT_DESCRIPTION + " TEXT,"
594             + RecordedPrograms.COLUMN_LONG_DESCRIPTION + " TEXT,"
595             + RecordedPrograms.COLUMN_VIDEO_WIDTH + " INTEGER,"
596             + RecordedPrograms.COLUMN_VIDEO_HEIGHT + " INTEGER,"
597             + RecordedPrograms.COLUMN_AUDIO_LANGUAGE + " TEXT,"
598             + RecordedPrograms.COLUMN_CONTENT_RATING + " TEXT,"
599             + RecordedPrograms.COLUMN_POSTER_ART_URI + " TEXT,"
600             + RecordedPrograms.COLUMN_THUMBNAIL_URI + " TEXT,"
601             + RecordedPrograms.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
602             + RecordedPrograms.COLUMN_RECORDING_DATA_URI + " TEXT,"
603             + RecordedPrograms.COLUMN_RECORDING_DATA_BYTES + " INTEGER,"
604             + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS + " INTEGER,"
605             + RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS + " INTEGER,"
606             + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
607             + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
608             + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
609             + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
610             + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
611             + RecordedPrograms.COLUMN_VERSION_NUMBER + " INTEGER,"
612             + RecordedPrograms.COLUMN_REVIEW_RATING_STYLE + " INTEGER,"
613             + RecordedPrograms.COLUMN_REVIEW_RATING + " TEXT,"
614             + "FOREIGN KEY(" + RecordedPrograms.COLUMN_CHANNEL_ID + ") "
615                     + "REFERENCES " + CHANNELS_TABLE + "(" + Channels._ID + ") "
616                     + "ON UPDATE CASCADE ON DELETE SET NULL);";
617 
618     private static final String CREATE_PREVIEW_PROGRAMS_TABLE_SQL =
619             "CREATE TABLE " + PREVIEW_PROGRAMS_TABLE + " ("
620             + PreviewPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
621             + PreviewPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
622             + PreviewPrograms.COLUMN_CHANNEL_ID + " INTEGER,"
623             + PreviewPrograms.COLUMN_TITLE + " TEXT,"
624             + PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER + " TEXT,"
625             + PreviewPrograms.COLUMN_SEASON_TITLE + " TEXT,"
626             + PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER + " TEXT,"
627             + PreviewPrograms.COLUMN_EPISODE_TITLE + " TEXT,"
628             + PreviewPrograms.COLUMN_CANONICAL_GENRE + " TEXT,"
629             + PreviewPrograms.COLUMN_SHORT_DESCRIPTION + " TEXT,"
630             + PreviewPrograms.COLUMN_LONG_DESCRIPTION + " TEXT,"
631             + PreviewPrograms.COLUMN_VIDEO_WIDTH + " INTEGER,"
632             + PreviewPrograms.COLUMN_VIDEO_HEIGHT + " INTEGER,"
633             + PreviewPrograms.COLUMN_AUDIO_LANGUAGE + " TEXT,"
634             + PreviewPrograms.COLUMN_CONTENT_RATING + " TEXT,"
635             + PreviewPrograms.COLUMN_POSTER_ART_URI + " TEXT,"
636             + PreviewPrograms.COLUMN_THUMBNAIL_URI + " TEXT,"
637             + PreviewPrograms.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
638             + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
639             + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
640             + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
641             + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
642             + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
643             + PreviewPrograms.COLUMN_VERSION_NUMBER + " INTEGER,"
644             + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID + " TEXT,"
645             + PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI + " TEXT,"
646             + PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS + " INTEGER,"
647             + PreviewPrograms.COLUMN_DURATION_MILLIS + " INTEGER,"
648             + PreviewPrograms.COLUMN_INTENT_URI + " TEXT,"
649             + PreviewPrograms.COLUMN_WEIGHT + " INTEGER,"
650             + PreviewPrograms.COLUMN_TRANSIENT + " INTEGER NOT NULL DEFAULT 0,"
651             + PreviewPrograms.COLUMN_TYPE + " INTEGER NOT NULL,"
652             + PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO + " INTEGER,"
653             + PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO + " INTEGER,"
654             + PreviewPrograms.COLUMN_LOGO_URI + " TEXT,"
655             + PreviewPrograms.COLUMN_AVAILABILITY + " INTERGER,"
656             + PreviewPrograms.COLUMN_STARTING_PRICE + " TEXT,"
657             + PreviewPrograms.COLUMN_OFFER_PRICE + " TEXT,"
658             + PreviewPrograms.COLUMN_RELEASE_DATE + " TEXT,"
659             + PreviewPrograms.COLUMN_ITEM_COUNT + " INTEGER,"
660             + PreviewPrograms.COLUMN_LIVE + " INTEGER NOT NULL DEFAULT 0,"
661             + PreviewPrograms.COLUMN_INTERACTION_TYPE + " INTEGER,"
662             + PreviewPrograms.COLUMN_INTERACTION_COUNT + " INTEGER,"
663             + PreviewPrograms.COLUMN_AUTHOR + " TEXT,"
664             + PreviewPrograms.COLUMN_REVIEW_RATING_STYLE + " INTEGER,"
665             + PreviewPrograms.COLUMN_REVIEW_RATING + " TEXT,"
666             + PreviewPrograms.COLUMN_BROWSABLE + " INTEGER NOT NULL DEFAULT 1,"
667             + PreviewPrograms.COLUMN_CONTENT_ID + " TEXT,"
668             + "FOREIGN KEY("
669                     + PreviewPrograms.COLUMN_CHANNEL_ID + "," + PreviewPrograms.COLUMN_PACKAGE_NAME
670                     + ") REFERENCES " + CHANNELS_TABLE + "("
671                     + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME
672                     + ") ON UPDATE CASCADE ON DELETE CASCADE"
673                     + ");";
674     private static final String CREATE_PREVIEW_PROGRAMS_PACKAGE_NAME_INDEX_SQL =
675             "CREATE INDEX preview_programs_package_name_index ON " + PREVIEW_PROGRAMS_TABLE
676             + "(" + PreviewPrograms.COLUMN_PACKAGE_NAME + ");";
677     private static final String CREATE_PREVIEW_PROGRAMS_CHANNEL_ID_INDEX_SQL =
678             "CREATE INDEX preview_programs_id_index ON " + PREVIEW_PROGRAMS_TABLE
679             + "(" + PreviewPrograms.COLUMN_CHANNEL_ID + ");";
680     private static final String CREATE_WATCH_NEXT_PROGRAMS_TABLE_SQL =
681             "CREATE TABLE " + WATCH_NEXT_PROGRAMS_TABLE + " ("
682             + WatchNextPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
683             + WatchNextPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
684             + WatchNextPrograms.COLUMN_TITLE + " TEXT,"
685             + WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER + " TEXT,"
686             + WatchNextPrograms.COLUMN_SEASON_TITLE + " TEXT,"
687             + WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER + " TEXT,"
688             + WatchNextPrograms.COLUMN_EPISODE_TITLE + " TEXT,"
689             + WatchNextPrograms.COLUMN_CANONICAL_GENRE + " TEXT,"
690             + WatchNextPrograms.COLUMN_SHORT_DESCRIPTION + " TEXT,"
691             + WatchNextPrograms.COLUMN_LONG_DESCRIPTION + " TEXT,"
692             + WatchNextPrograms.COLUMN_VIDEO_WIDTH + " INTEGER,"
693             + WatchNextPrograms.COLUMN_VIDEO_HEIGHT + " INTEGER,"
694             + WatchNextPrograms.COLUMN_AUDIO_LANGUAGE + " TEXT,"
695             + WatchNextPrograms.COLUMN_CONTENT_RATING + " TEXT,"
696             + WatchNextPrograms.COLUMN_POSTER_ART_URI + " TEXT,"
697             + WatchNextPrograms.COLUMN_THUMBNAIL_URI + " TEXT,"
698             + WatchNextPrograms.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
699             + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
700             + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
701             + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
702             + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
703             + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
704             + WatchNextPrograms.COLUMN_VERSION_NUMBER + " INTEGER,"
705             + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID + " TEXT,"
706             + WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI + " TEXT,"
707             + WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS + " INTEGER,"
708             + WatchNextPrograms.COLUMN_DURATION_MILLIS + " INTEGER,"
709             + WatchNextPrograms.COLUMN_INTENT_URI + " TEXT,"
710             + WatchNextPrograms.COLUMN_TRANSIENT + " INTEGER NOT NULL DEFAULT 0,"
711             + WatchNextPrograms.COLUMN_TYPE + " INTEGER NOT NULL,"
712             + WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE + " INTEGER,"
713             + WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO + " INTEGER,"
714             + WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO + " INTEGER,"
715             + WatchNextPrograms.COLUMN_LOGO_URI + " TEXT,"
716             + WatchNextPrograms.COLUMN_AVAILABILITY + " INTEGER,"
717             + WatchNextPrograms.COLUMN_STARTING_PRICE + " TEXT,"
718             + WatchNextPrograms.COLUMN_OFFER_PRICE + " TEXT,"
719             + WatchNextPrograms.COLUMN_RELEASE_DATE + " TEXT,"
720             + WatchNextPrograms.COLUMN_ITEM_COUNT + " INTEGER,"
721             + WatchNextPrograms.COLUMN_LIVE + " INTEGER NOT NULL DEFAULT 0,"
722             + WatchNextPrograms.COLUMN_INTERACTION_TYPE + " INTEGER,"
723             + WatchNextPrograms.COLUMN_INTERACTION_COUNT + " INTEGER,"
724             + WatchNextPrograms.COLUMN_AUTHOR + " TEXT,"
725             + WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE + " INTEGER,"
726             + WatchNextPrograms.COLUMN_REVIEW_RATING + " TEXT,"
727             + WatchNextPrograms.COLUMN_BROWSABLE + " INTEGER NOT NULL DEFAULT 1,"
728             + WatchNextPrograms.COLUMN_CONTENT_ID + " TEXT,"
729             + WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS + " INTEGER"
730             + ");";
731     private static final String CREATE_WATCH_NEXT_PROGRAMS_PACKAGE_NAME_INDEX_SQL =
732             "CREATE INDEX watch_next_programs_package_name_index ON " + WATCH_NEXT_PROGRAMS_TABLE
733             + "(" + WatchNextPrograms.COLUMN_PACKAGE_NAME + ");";
734 
735     static class DatabaseHelper extends SQLiteOpenHelper {
736         private static DatabaseHelper sSingleton = null;
737         private static Context mContext;
738 
getInstance(Context context)739         public static synchronized DatabaseHelper getInstance(Context context) {
740             if (sSingleton == null) {
741                 sSingleton = new DatabaseHelper(context);
742             }
743             return sSingleton;
744         }
745 
DatabaseHelper(Context context)746         private DatabaseHelper(Context context) {
747             this(context, DATABASE_NAME, DATABASE_VERSION);
748         }
749 
750         @VisibleForTesting
DatabaseHelper(Context context, String databaseName, int databaseVersion)751         DatabaseHelper(Context context, String databaseName, int databaseVersion) {
752             super(context, databaseName, null, databaseVersion);
753             mContext = context;
754         }
755 
756         @Override
onConfigure(SQLiteDatabase db)757         public void onConfigure(SQLiteDatabase db) {
758             db.setForeignKeyConstraintsEnabled(true);
759         }
760 
761         @Override
onCreate(SQLiteDatabase db)762         public void onCreate(SQLiteDatabase db) {
763             if (DEBUG) {
764                 Log.d(TAG, "Creating database");
765             }
766             // Set up the database schema.
767             db.execSQL("CREATE TABLE " + CHANNELS_TABLE + " ("
768                     + Channels._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
769                     + Channels.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
770                     + Channels.COLUMN_INPUT_ID + " TEXT NOT NULL,"
771                     + Channels.COLUMN_TYPE + " TEXT NOT NULL DEFAULT '" + Channels.TYPE_OTHER + "',"
772                     + Channels.COLUMN_SERVICE_TYPE + " TEXT NOT NULL DEFAULT '"
773                     + Channels.SERVICE_TYPE_AUDIO_VIDEO + "',"
774                     + Channels.COLUMN_ORIGINAL_NETWORK_ID + " INTEGER NOT NULL DEFAULT 0,"
775                     + Channels.COLUMN_TRANSPORT_STREAM_ID + " INTEGER NOT NULL DEFAULT 0,"
776                     + Channels.COLUMN_SERVICE_ID + " INTEGER NOT NULL DEFAULT 0,"
777                     + Channels.COLUMN_DISPLAY_NUMBER + " TEXT,"
778                     + Channels.COLUMN_DISPLAY_NAME + " TEXT,"
779                     + Channels.COLUMN_NETWORK_AFFILIATION + " TEXT,"
780                     + Channels.COLUMN_DESCRIPTION + " TEXT,"
781                     + Channels.COLUMN_VIDEO_FORMAT + " TEXT,"
782                     + Channels.COLUMN_BROWSABLE + " INTEGER NOT NULL DEFAULT 0,"
783                     + Channels.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
784                     + Channels.COLUMN_LOCKED + " INTEGER NOT NULL DEFAULT 0,"
785                     + Channels.COLUMN_APP_LINK_ICON_URI + " TEXT,"
786                     + Channels.COLUMN_APP_LINK_POSTER_ART_URI + " TEXT,"
787                     + Channels.COLUMN_APP_LINK_TEXT + " TEXT,"
788                     + Channels.COLUMN_APP_LINK_COLOR + " INTEGER,"
789                     + Channels.COLUMN_APP_LINK_INTENT_URI + " TEXT,"
790                     + Channels.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
791                     + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
792                     + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
793                     + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
794                     + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
795                     + CHANNELS_COLUMN_LOGO + " BLOB,"
796                     + Channels.COLUMN_VERSION_NUMBER + " INTEGER,"
797                     + Channels.COLUMN_TRANSIENT + " INTEGER NOT NULL DEFAULT 0,"
798                     + Channels.COLUMN_INTERNAL_PROVIDER_ID + " TEXT,"
799                     // Needed for foreign keys in other tables.
800                     + "UNIQUE(" + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME + ")"
801                     + ");");
802             db.execSQL("CREATE TABLE " + PROGRAMS_TABLE + " ("
803                     + Programs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
804                     + Programs.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
805                     + Programs.COLUMN_CHANNEL_ID + " INTEGER,"
806                     + Programs.COLUMN_TITLE + " TEXT,"
807                     + Programs.COLUMN_SEASON_DISPLAY_NUMBER + " TEXT,"
808                     + Programs.COLUMN_SEASON_TITLE + " TEXT,"
809                     + Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " TEXT,"
810                     + Programs.COLUMN_EPISODE_TITLE + " TEXT,"
811                     + Programs.COLUMN_START_TIME_UTC_MILLIS + " INTEGER,"
812                     + Programs.COLUMN_END_TIME_UTC_MILLIS + " INTEGER,"
813                     + Programs.COLUMN_BROADCAST_GENRE + " TEXT,"
814                     + Programs.COLUMN_CANONICAL_GENRE + " TEXT,"
815                     + Programs.COLUMN_SHORT_DESCRIPTION + " TEXT,"
816                     + Programs.COLUMN_LONG_DESCRIPTION + " TEXT,"
817                     + Programs.COLUMN_VIDEO_WIDTH + " INTEGER,"
818                     + Programs.COLUMN_VIDEO_HEIGHT + " INTEGER,"
819                     + Programs.COLUMN_AUDIO_LANGUAGE + " TEXT,"
820                     + Programs.COLUMN_CONTENT_RATING + " TEXT,"
821                     + Programs.COLUMN_POSTER_ART_URI + " TEXT,"
822                     + Programs.COLUMN_THUMBNAIL_URI + " TEXT,"
823                     + Programs.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
824                     + Programs.COLUMN_RECORDING_PROHIBITED + " INTEGER NOT NULL DEFAULT 0,"
825                     + Programs.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
826                     + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
827                     + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
828                     + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
829                     + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
830                     + Programs.COLUMN_REVIEW_RATING_STYLE + " INTEGER,"
831                     + Programs.COLUMN_REVIEW_RATING + " TEXT,"
832                     + Programs.COLUMN_VERSION_NUMBER + " INTEGER,"
833                     + "FOREIGN KEY("
834                             + Programs.COLUMN_CHANNEL_ID + "," + Programs.COLUMN_PACKAGE_NAME
835                             + ") REFERENCES " + CHANNELS_TABLE + "("
836                             + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME
837                             + ") ON UPDATE CASCADE ON DELETE CASCADE"
838                     + ");");
839             db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_PACKAGE_NAME_INDEX + " ON " + PROGRAMS_TABLE
840                     + "(" + Programs.COLUMN_PACKAGE_NAME + ");");
841             db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_CHANNEL_ID_INDEX + " ON " + PROGRAMS_TABLE
842                     + "(" + Programs.COLUMN_CHANNEL_ID + ");");
843             db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_START_TIME_INDEX + " ON " + PROGRAMS_TABLE
844                     + "(" + Programs.COLUMN_START_TIME_UTC_MILLIS + ");");
845             db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_END_TIME_INDEX + " ON " + PROGRAMS_TABLE
846                     + "(" + Programs.COLUMN_END_TIME_UTC_MILLIS + ");");
847             db.execSQL("CREATE TABLE " + WATCHED_PROGRAMS_TABLE + " ("
848                     + WatchedPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
849                     + WatchedPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
850                     + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS
851                     + " INTEGER NOT NULL DEFAULT 0,"
852                     + WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS
853                     + " INTEGER NOT NULL DEFAULT 0,"
854                     + WatchedPrograms.COLUMN_CHANNEL_ID + " INTEGER,"
855                     + WatchedPrograms.COLUMN_TITLE + " TEXT,"
856                     + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS + " INTEGER,"
857                     + WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS + " INTEGER,"
858                     + WatchedPrograms.COLUMN_DESCRIPTION + " TEXT,"
859                     + WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS + " TEXT,"
860                     + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + " TEXT NOT NULL,"
861                     + WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + " INTEGER NOT NULL DEFAULT 0,"
862                     + "FOREIGN KEY("
863                             + WatchedPrograms.COLUMN_CHANNEL_ID + ","
864                             + WatchedPrograms.COLUMN_PACKAGE_NAME
865                             + ") REFERENCES " + CHANNELS_TABLE + "("
866                             + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME
867                             + ") ON UPDATE CASCADE ON DELETE CASCADE"
868                     + ");");
869             db.execSQL("CREATE INDEX " + WATCHED_PROGRAMS_TABLE_CHANNEL_ID_INDEX + " ON "
870                     + WATCHED_PROGRAMS_TABLE + "(" + WatchedPrograms.COLUMN_CHANNEL_ID + ");");
871             db.execSQL(CREATE_RECORDED_PROGRAMS_TABLE_SQL);
872             db.execSQL(CREATE_PREVIEW_PROGRAMS_TABLE_SQL);
873             db.execSQL(CREATE_PREVIEW_PROGRAMS_PACKAGE_NAME_INDEX_SQL);
874             db.execSQL(CREATE_PREVIEW_PROGRAMS_CHANNEL_ID_INDEX_SQL);
875             db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_TABLE_SQL);
876             db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_PACKAGE_NAME_INDEX_SQL);
877         }
878 
879         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)880         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
881             if (oldVersion < 23) {
882                 Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion
883                         + ", data will be lost!");
884                 db.execSQL("DROP TABLE IF EXISTS " + DELETED_CHANNELS_TABLE);
885                 db.execSQL("DROP TABLE IF EXISTS " + WATCHED_PROGRAMS_TABLE);
886                 db.execSQL("DROP TABLE IF EXISTS " + PROGRAMS_TABLE);
887                 db.execSQL("DROP TABLE IF EXISTS " + CHANNELS_TABLE);
888 
889                 onCreate(db);
890                 return;
891             }
892 
893             Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion + ".");
894             if (oldVersion <= 23) {
895                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
896                         + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER;");
897                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
898                         + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER;");
899                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
900                         + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER;");
901                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
902                         + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER;");
903             }
904             if (oldVersion <= 24) {
905                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
906                         + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER;");
907                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
908                         + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER;");
909                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
910                         + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER;");
911                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
912                         + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER;");
913             }
914             if (oldVersion <= 25) {
915                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
916                         + Channels.COLUMN_APP_LINK_ICON_URI + " TEXT;");
917                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
918                         + Channels.COLUMN_APP_LINK_POSTER_ART_URI + " TEXT;");
919                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
920                         + Channels.COLUMN_APP_LINK_TEXT + " TEXT;");
921                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
922                         + Channels.COLUMN_APP_LINK_COLOR + " INTEGER;");
923                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
924                         + Channels.COLUMN_APP_LINK_INTENT_URI + " TEXT;");
925                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
926                         + Programs.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1;");
927             }
928             if (oldVersion <= 28) {
929                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
930                         + Programs.COLUMN_SEASON_TITLE + " TEXT;");
931                 migrateIntegerColumnToTextColumn(db, PROGRAMS_TABLE, Programs.COLUMN_SEASON_NUMBER,
932                         Programs.COLUMN_SEASON_DISPLAY_NUMBER);
933                 migrateIntegerColumnToTextColumn(db, PROGRAMS_TABLE, Programs.COLUMN_EPISODE_NUMBER,
934                         Programs.COLUMN_EPISODE_DISPLAY_NUMBER);
935             }
936             if (oldVersion <= 29) {
937                 db.execSQL("DROP TABLE IF EXISTS " + RECORDED_PROGRAMS_TABLE);
938                 db.execSQL(CREATE_RECORDED_PROGRAMS_TABLE_SQL);
939             }
940             if (oldVersion <= 30) {
941                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
942                         + Programs.COLUMN_RECORDING_PROHIBITED + " INTEGER NOT NULL DEFAULT 0;");
943             }
944             if (oldVersion <= 32) {
945                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
946                         + Channels.COLUMN_TRANSIENT + " INTEGER NOT NULL DEFAULT 0;");
947                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
948                         + Channels.COLUMN_INTERNAL_PROVIDER_ID + " TEXT;");
949                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
950                         + Programs.COLUMN_REVIEW_RATING_STYLE + " INTEGER;");
951                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
952                         + Programs.COLUMN_REVIEW_RATING + " TEXT;");
953                 if (oldVersion > 29) {
954                     db.execSQL("ALTER TABLE " + RECORDED_PROGRAMS_TABLE + " ADD "
955                             + RecordedPrograms.COLUMN_REVIEW_RATING_STYLE + " INTEGER;");
956                     db.execSQL("ALTER TABLE " + RECORDED_PROGRAMS_TABLE + " ADD "
957                             + RecordedPrograms.COLUMN_REVIEW_RATING + " TEXT;");
958                 }
959             }
960             if (oldVersion <= 33) {
961                 db.execSQL("DROP TABLE IF EXISTS " + PREVIEW_PROGRAMS_TABLE);
962                 db.execSQL("DROP TABLE IF EXISTS " + WATCH_NEXT_PROGRAMS_TABLE);
963                 db.execSQL(CREATE_PREVIEW_PROGRAMS_TABLE_SQL);
964                 db.execSQL(CREATE_PREVIEW_PROGRAMS_PACKAGE_NAME_INDEX_SQL);
965                 db.execSQL(CREATE_PREVIEW_PROGRAMS_CHANNEL_ID_INDEX_SQL);
966                 db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_TABLE_SQL);
967                 db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_PACKAGE_NAME_INDEX_SQL);
968             }
969             Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion + " is done.");
970         }
971 
972         @Override
onOpen(SQLiteDatabase db)973         public void onOpen(SQLiteDatabase db) {
974             // Call a static method on the TvProvider because changes to sInitialized must
975             // be guarded by a lock on the class.
976             initOnOpenIfNeeded(mContext, db);
977         }
978 
migrateIntegerColumnToTextColumn(SQLiteDatabase db, String table, String integerColumn, String textColumn)979         private static void migrateIntegerColumnToTextColumn(SQLiteDatabase db, String table,
980                 String integerColumn, String textColumn) {
981             db.execSQL("ALTER TABLE " + table + " ADD " + textColumn + " TEXT;");
982             db.execSQL("UPDATE " + table + " SET " + textColumn + " = CAST(" + integerColumn
983                     + " AS TEXT);");
984         }
985     }
986 
987     private DatabaseHelper mOpenHelper;
988     private static SharedPreferences sBlockedPackagesSharedPreference;
989     private static Map<String, Boolean> sBlockedPackages;
990     @VisibleForTesting
991     protected TransientRowHelper mTransientRowHelper;
992 
993     private final Handler mLogHandler = new WatchLogHandler();
994 
995     @Override
onCreate()996     public boolean onCreate() {
997         if (DEBUG) {
998             Log.d(TAG, "Creating TvProvider");
999         }
1000         if (mOpenHelper == null) {
1001             mOpenHelper = DatabaseHelper.getInstance(getContext());
1002         }
1003         mTransientRowHelper = TransientRowHelper.getInstance(getContext());
1004         scheduleEpgDataCleanup();
1005         buildGenreMap();
1006 
1007         // DB operation, which may trigger upgrade, should not happen in onCreate.
1008         new AsyncTask<Void, Void, Void>() {
1009             @Override
1010             protected Void doInBackground(Void... params) {
1011                 deleteUnconsolidatedWatchedProgramsRows();
1012                 return null;
1013             }
1014         }.execute();
1015         return true;
1016     }
1017 
1018     @VisibleForTesting
scheduleEpgDataCleanup()1019     void scheduleEpgDataCleanup() {
1020         Intent intent = new Intent(EpgDataCleanupService.ACTION_CLEAN_UP_EPG_DATA);
1021         intent.setClass(getContext(), EpgDataCleanupService.class);
1022         PendingIntent pendingIntent = PendingIntent.getService(
1023                 getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
1024         AlarmManager alarmManager =
1025                 (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
1026         alarmManager.setInexactRepeating(AlarmManager.RTC, System.currentTimeMillis(),
1027                 AlarmManager.INTERVAL_HALF_DAY, pendingIntent);
1028     }
1029 
buildGenreMap()1030     private void buildGenreMap() {
1031         if (sGenreMap != null) {
1032             return;
1033         }
1034 
1035         sGenreMap = new HashMap<>();
1036         buildGenreMap(R.array.genre_mapping_atsc);
1037         buildGenreMap(R.array.genre_mapping_dvb);
1038         buildGenreMap(R.array.genre_mapping_isdb);
1039         buildGenreMap(R.array.genre_mapping_isdb_br);
1040     }
1041 
1042     @SuppressLint("DefaultLocale")
buildGenreMap(int id)1043     private void buildGenreMap(int id) {
1044         String[] maps = getContext().getResources().getStringArray(id);
1045         for (String map : maps) {
1046             String[] arr = map.split("\\|");
1047             if (arr.length != 2) {
1048                 throw new IllegalArgumentException("Invalid genre mapping : " + map);
1049             }
1050             sGenreMap.put(arr[0].toUpperCase(), arr[1]);
1051         }
1052     }
1053 
1054     @VisibleForTesting
getCallingPackage_()1055     String getCallingPackage_() {
1056         return getCallingPackage();
1057     }
1058 
1059     @VisibleForTesting
setOpenHelper(DatabaseHelper helper)1060     void setOpenHelper(DatabaseHelper helper) {
1061         mOpenHelper = helper;
1062     }
1063 
1064     @Override
getType(Uri uri)1065     public String getType(Uri uri) {
1066         switch (sUriMatcher.match(uri)) {
1067             case MATCH_CHANNEL:
1068                 return Channels.CONTENT_TYPE;
1069             case MATCH_CHANNEL_ID:
1070                 return Channels.CONTENT_ITEM_TYPE;
1071             case MATCH_CHANNEL_ID_LOGO:
1072                 return "image/png";
1073             case MATCH_PASSTHROUGH_ID:
1074                 return Channels.CONTENT_ITEM_TYPE;
1075             case MATCH_PROGRAM:
1076                 return Programs.CONTENT_TYPE;
1077             case MATCH_PROGRAM_ID:
1078                 return Programs.CONTENT_ITEM_TYPE;
1079             case MATCH_WATCHED_PROGRAM:
1080                 return WatchedPrograms.CONTENT_TYPE;
1081             case MATCH_WATCHED_PROGRAM_ID:
1082                 return WatchedPrograms.CONTENT_ITEM_TYPE;
1083             case MATCH_RECORDED_PROGRAM:
1084                 return RecordedPrograms.CONTENT_TYPE;
1085             case MATCH_RECORDED_PROGRAM_ID:
1086                 return RecordedPrograms.CONTENT_ITEM_TYPE;
1087             case MATCH_PREVIEW_PROGRAM:
1088                 return PreviewPrograms.CONTENT_TYPE;
1089             case MATCH_PREVIEW_PROGRAM_ID:
1090                 return PreviewPrograms.CONTENT_ITEM_TYPE;
1091             case MATCH_WATCH_NEXT_PROGRAM:
1092                 return WatchNextPrograms.CONTENT_TYPE;
1093             case MATCH_WATCH_NEXT_PROGRAM_ID:
1094                 return WatchNextPrograms.CONTENT_ITEM_TYPE;
1095             default:
1096                 throw new IllegalArgumentException("Unknown URI " + uri);
1097         }
1098     }
1099 
1100     @Override
call(String method, String arg, Bundle extras)1101     public Bundle call(String method, String arg, Bundle extras) {
1102         if (!callerHasAccessAllEpgDataPermission()) {
1103             return null;
1104         }
1105         ensureInitialized();
1106         Map<String, String> projectionMap;
1107         switch (method) {
1108             case TvContract.METHOD_GET_COLUMNS:
1109                 switch (sUriMatcher.match(Uri.parse(arg))) {
1110                     case MATCH_CHANNEL:
1111                         projectionMap = sChannelProjectionMap;
1112                         break;
1113                     case MATCH_PROGRAM:
1114                         projectionMap = sProgramProjectionMap;
1115                         break;
1116                     case MATCH_PREVIEW_PROGRAM:
1117                         projectionMap = sPreviewProgramProjectionMap;
1118                         break;
1119                     case MATCH_WATCH_NEXT_PROGRAM:
1120                         projectionMap = sWatchNextProgramProjectionMap;
1121                         break;
1122                     case MATCH_RECORDED_PROGRAM:
1123                         projectionMap = sRecordedProgramProjectionMap;
1124                         break;
1125                     default:
1126                         return null;
1127                 }
1128                 Bundle result = new Bundle();
1129                 result.putStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES,
1130                         projectionMap.keySet().toArray(new String[projectionMap.size()]));
1131                 return result;
1132             case TvContract.METHOD_ADD_COLUMN:
1133                 CharSequence columnName = extras.getCharSequence(TvContract.EXTRA_COLUMN_NAME);
1134                 CharSequence dataType = extras.getCharSequence(TvContract.EXTRA_DATA_TYPE);
1135                 if (TextUtils.isEmpty(columnName) || TextUtils.isEmpty(dataType)) {
1136                     return null;
1137                 }
1138                 CharSequence defaultValue = extras.getCharSequence(TvContract.EXTRA_DEFAULT_VALUE);
1139                 try {
1140                     defaultValue = TextUtils.isEmpty(defaultValue) ? "" : generateDefaultClause(
1141                             dataType.toString(), defaultValue.toString());
1142                 } catch (IllegalArgumentException e) {
1143                     return null;
1144                 }
1145                 String tableName;
1146                 switch (sUriMatcher.match(Uri.parse(arg))) {
1147                     case MATCH_CHANNEL:
1148                         tableName = CHANNELS_TABLE;
1149                         projectionMap = sChannelProjectionMap;
1150                         break;
1151                     case MATCH_PROGRAM:
1152                         tableName = PROGRAMS_TABLE;
1153                         projectionMap = sProgramProjectionMap;
1154                         break;
1155                     case MATCH_PREVIEW_PROGRAM:
1156                         tableName = PREVIEW_PROGRAMS_TABLE;
1157                         projectionMap = sPreviewProgramProjectionMap;
1158                         break;
1159                     case MATCH_WATCH_NEXT_PROGRAM:
1160                         tableName = WATCH_NEXT_PROGRAMS_TABLE;
1161                         projectionMap = sWatchNextProgramProjectionMap;
1162                         break;
1163                     case MATCH_RECORDED_PROGRAM:
1164                         tableName = RECORDED_PROGRAMS_TABLE;
1165                         projectionMap = sRecordedProgramProjectionMap;
1166                         break;
1167                     default:
1168                         return null;
1169                 }
1170                 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1171                 try {
1172                     db.execSQL("ALTER TABLE " + tableName + " ADD "
1173                             + columnName + " " + dataType + defaultValue + ";");
1174                     projectionMap.put(columnName.toString(), tableName + '.' + columnName);
1175                     Bundle returnValue = new Bundle();
1176                     returnValue.putStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES,
1177                             projectionMap.keySet().toArray(new String[projectionMap.size()]));
1178                     return returnValue;
1179                 } catch (SQLException e) {
1180                     return null;
1181                 }
1182             case TvContract.METHOD_GET_BLOCKED_PACKAGES:
1183                 Bundle allBlockedPackages = new Bundle();
1184                 allBlockedPackages.putStringArray(TvContract.EXTRA_BLOCKED_PACKAGES,
1185                         sBlockedPackages.keySet().toArray(new String[sBlockedPackages.size()]));
1186                 return allBlockedPackages;
1187             case TvContract.METHOD_BLOCK_PACKAGE:
1188                 String packageNameToBlock = arg;
1189                 Bundle blockPackageResult = new Bundle();
1190                 if (!TextUtils.isEmpty(packageNameToBlock)) {
1191                     sBlockedPackages.put(packageNameToBlock, true);
1192                     if (sBlockedPackagesSharedPreference.edit().putStringSet(
1193                             SHARED_PREF_BLOCKED_PACKAGES_KEY, sBlockedPackages.keySet()).commit()) {
1194                         String[] channelSelectionArgs = new String[] {
1195                                 packageNameToBlock, Channels.TYPE_PREVIEW };
1196                         delete(TvContract.Channels.CONTENT_URI,
1197                                 Channels.COLUMN_PACKAGE_NAME + "=? AND "
1198                                         + Channels.COLUMN_TYPE + "=?",
1199                                 channelSelectionArgs);
1200                         String[] programsSelectionArgs = new String[] {
1201                                 packageNameToBlock };
1202                         delete(TvContract.PreviewPrograms.CONTENT_URI,
1203                                 PreviewPrograms.COLUMN_PACKAGE_NAME + "=?", programsSelectionArgs);
1204                         delete(TvContract.WatchNextPrograms.CONTENT_URI,
1205                                 WatchNextPrograms.COLUMN_PACKAGE_NAME + "=?",
1206                                 programsSelectionArgs);
1207                         blockPackageResult.putInt(
1208                                 TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_OK);
1209                     } else {
1210                         Log.e(TAG, "Blocking package " + packageNameToBlock + " failed");
1211                         sBlockedPackages.remove(packageNameToBlock);
1212                         blockPackageResult.putInt(TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_ERROR_IO);
1213                     }
1214                 } else {
1215                     blockPackageResult.putInt(
1216                             TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_ERROR_INVALID_ARGUMENT);
1217                 }
1218                 return blockPackageResult;
1219             case TvContract.METHOD_UNBLOCK_PACKAGE:
1220                 String packageNameToUnblock = arg;
1221                 Bundle unblockPackageResult = new Bundle();
1222                 if (!TextUtils.isEmpty(packageNameToUnblock)) {
1223                     sBlockedPackages.remove(packageNameToUnblock);
1224                     if (sBlockedPackagesSharedPreference.edit().putStringSet(
1225                             SHARED_PREF_BLOCKED_PACKAGES_KEY, sBlockedPackages.keySet()).commit()) {
1226                         unblockPackageResult.putInt(
1227                                 TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_OK);
1228                     } else {
1229                         Log.e(TAG, "Unblocking package " + packageNameToUnblock + " failed");
1230                         sBlockedPackages.put(packageNameToUnblock, true);
1231                         unblockPackageResult.putInt(
1232                                 TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_ERROR_IO);
1233                     }
1234                 } else {
1235                     unblockPackageResult.putInt(
1236                             TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_ERROR_INVALID_ARGUMENT);
1237                 }
1238                 return unblockPackageResult;
1239         }
1240         return null;
1241     }
1242 
1243     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)1244     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
1245             String sortOrder) {
1246         ensureInitialized();
1247         mTransientRowHelper.ensureOldTransientRowsDeleted();
1248         boolean needsToValidateSortOrder = !callerHasAccessAllEpgDataPermission();
1249         SqlParams params = createSqlParams(OP_QUERY, uri, selection, selectionArgs);
1250 
1251         SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
1252         queryBuilder.setStrict(needsToValidateSortOrder);
1253         queryBuilder.setTables(params.getTables());
1254         String orderBy = null;
1255         Map<String, String> projectionMap;
1256         switch (params.getTables()) {
1257             case PROGRAMS_TABLE:
1258                 projectionMap = sProgramProjectionMap;
1259                 orderBy = DEFAULT_PROGRAMS_SORT_ORDER;
1260                 break;
1261             case WATCHED_PROGRAMS_TABLE:
1262                 projectionMap = sWatchedProgramProjectionMap;
1263                 orderBy = DEFAULT_WATCHED_PROGRAMS_SORT_ORDER;
1264                 break;
1265             case RECORDED_PROGRAMS_TABLE:
1266                 projectionMap = sRecordedProgramProjectionMap;
1267                 break;
1268             case PREVIEW_PROGRAMS_TABLE:
1269                 projectionMap = sPreviewProgramProjectionMap;
1270                 break;
1271             case WATCH_NEXT_PROGRAMS_TABLE:
1272                 projectionMap = sWatchNextProgramProjectionMap;
1273                 break;
1274             default:
1275                 projectionMap = sChannelProjectionMap;
1276                 break;
1277         }
1278         queryBuilder.setProjectionMap(createProjectionMapForQuery(projection, projectionMap));
1279         if (needsToValidateSortOrder) {
1280             validateSortOrder(sortOrder, projectionMap.keySet());
1281         }
1282 
1283         // Use the default sort order only if no sort order is specified.
1284         if (!TextUtils.isEmpty(sortOrder)) {
1285             orderBy = sortOrder;
1286         }
1287 
1288         // Get the database and run the query.
1289         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1290         Cursor c = queryBuilder.query(db, projection, params.getSelection(),
1291                 params.getSelectionArgs(), null, null, orderBy);
1292 
1293         // Tell the cursor what URI to watch, so it knows when its source data changes.
1294         c.setNotificationUri(getContext().getContentResolver(), uri);
1295         return c;
1296     }
1297 
1298     @Override
insert(Uri uri, ContentValues values)1299     public Uri insert(Uri uri, ContentValues values) {
1300         ensureInitialized();
1301         mTransientRowHelper.ensureOldTransientRowsDeleted();
1302         switch (sUriMatcher.match(uri)) {
1303             case MATCH_CHANNEL:
1304                 // Preview channels are not necessarily associated with TV input service.
1305                 // Therefore, we fill a fake ID to meet not null restriction for preview channels.
1306                 if (values.get(Channels.COLUMN_INPUT_ID) == null
1307                         && Channels.TYPE_PREVIEW.equals(values.get(Channels.COLUMN_TYPE))) {
1308                     values.put(Channels.COLUMN_INPUT_ID, EMPTY_STRING);
1309                 }
1310                 filterContentValues(values, sChannelProjectionMap);
1311                 return insertChannel(uri, values);
1312             case MATCH_PROGRAM:
1313                 filterContentValues(values, sProgramProjectionMap);
1314                 return insertProgram(uri, values);
1315             case MATCH_WATCHED_PROGRAM:
1316                 return insertWatchedProgram(uri, values);
1317             case MATCH_RECORDED_PROGRAM:
1318                 filterContentValues(values, sRecordedProgramProjectionMap);
1319                 return insertRecordedProgram(uri, values);
1320             case MATCH_PREVIEW_PROGRAM:
1321                 filterContentValues(values, sPreviewProgramProjectionMap);
1322                 return insertPreviewProgram(uri, values);
1323             case MATCH_WATCH_NEXT_PROGRAM:
1324                 filterContentValues(values, sWatchNextProgramProjectionMap);
1325                 return insertWatchNextProgram(uri, values);
1326             case MATCH_CHANNEL_ID:
1327             case MATCH_CHANNEL_ID_LOGO:
1328             case MATCH_PASSTHROUGH_ID:
1329             case MATCH_PROGRAM_ID:
1330             case MATCH_WATCHED_PROGRAM_ID:
1331             case MATCH_RECORDED_PROGRAM_ID:
1332             case MATCH_PREVIEW_PROGRAM_ID:
1333                 throw new UnsupportedOperationException("Cannot insert into that URI: " + uri);
1334             default:
1335                 throw new IllegalArgumentException("Unknown URI " + uri);
1336         }
1337     }
1338 
insertChannel(Uri uri, ContentValues values)1339     private Uri insertChannel(Uri uri, ContentValues values) {
1340         if (TextUtils.equals(values.getAsString(Channels.COLUMN_TYPE), Channels.TYPE_PREVIEW)) {
1341             blockIllegalAccessFromBlockedPackage();
1342         }
1343         // Mark the owner package of this channel.
1344         values.put(Channels.COLUMN_PACKAGE_NAME, getCallingPackage_());
1345         blockIllegalAccessToChannelsSystemColumns(values);
1346 
1347         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1348         long rowId = db.insert(CHANNELS_TABLE, null, values);
1349         if (rowId > 0) {
1350             Uri channelUri = TvContract.buildChannelUri(rowId);
1351             notifyChange(channelUri);
1352             return channelUri;
1353         }
1354 
1355         throw new SQLException("Failed to insert row into " + uri);
1356     }
1357 
insertProgram(Uri uri, ContentValues values)1358     private Uri insertProgram(Uri uri, ContentValues values) {
1359         if (!callerHasAccessAllEpgDataPermission() ||
1360                 !values.containsKey(Programs.COLUMN_PACKAGE_NAME)) {
1361             // Mark the owner package of this program. System app with a proper permission may
1362             // change the owner of the program.
1363             values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
1364         }
1365 
1366         checkAndConvertGenre(values);
1367         checkAndConvertDeprecatedColumns(values);
1368 
1369         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1370         long rowId = db.insert(PROGRAMS_TABLE, null, values);
1371         if (rowId > 0) {
1372             Uri programUri = TvContract.buildProgramUri(rowId);
1373             notifyChange(programUri);
1374             return programUri;
1375         }
1376 
1377         throw new SQLException("Failed to insert row into " + uri);
1378     }
1379 
insertWatchedProgram(Uri uri, ContentValues values)1380     private Uri insertWatchedProgram(Uri uri, ContentValues values) {
1381         if (DEBUG) {
1382             Log.d(TAG, "insertWatchedProgram(uri=" + uri + ", values={" + values + "})");
1383         }
1384         Long watchStartTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS);
1385         Long watchEndTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS);
1386         // The system sends only two kinds of watch events:
1387         // 1. The user tunes to a new channel. (COLUMN_WATCH_START_TIME_UTC_MILLIS)
1388         // 2. The user stops watching. (COLUMN_WATCH_END_TIME_UTC_MILLIS)
1389         if (watchStartTime != null && watchEndTime == null) {
1390             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1391             long rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values);
1392             if (rowId > 0) {
1393                 mLogHandler.removeMessages(WatchLogHandler.MSG_TRY_CONSOLIDATE_ALL);
1394                 mLogHandler.sendEmptyMessageDelayed(WatchLogHandler.MSG_TRY_CONSOLIDATE_ALL,
1395                         MAX_PROGRAM_DATA_DELAY_IN_MILLIS);
1396                 return TvContract.buildWatchedProgramUri(rowId);
1397             }
1398             Log.w(TAG, "Failed to insert row for " + values + ". Channel does not exist.");
1399             return null;
1400         } else if (watchStartTime == null && watchEndTime != null) {
1401             SomeArgs args = SomeArgs.obtain();
1402             args.arg1 = values.getAsString(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN);
1403             args.arg2 = watchEndTime;
1404             Message msg = mLogHandler.obtainMessage(WatchLogHandler.MSG_CONSOLIDATE, args);
1405             mLogHandler.sendMessageDelayed(msg, MAX_PROGRAM_DATA_DELAY_IN_MILLIS);
1406             return null;
1407         }
1408         // All the other cases are invalid.
1409         throw new IllegalArgumentException("Only one of COLUMN_WATCH_START_TIME_UTC_MILLIS and"
1410                 + " COLUMN_WATCH_END_TIME_UTC_MILLIS should be specified");
1411     }
1412 
insertRecordedProgram(Uri uri, ContentValues values)1413     private Uri insertRecordedProgram(Uri uri, ContentValues values) {
1414         // Mark the owner package of this program.
1415         values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
1416 
1417         checkAndConvertGenre(values);
1418 
1419         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1420         long rowId = db.insert(RECORDED_PROGRAMS_TABLE, null, values);
1421         if (rowId > 0) {
1422             Uri recordedProgramUri = TvContract.buildRecordedProgramUri(rowId);
1423             notifyChange(recordedProgramUri);
1424             return recordedProgramUri;
1425         }
1426 
1427         throw new SQLException("Failed to insert row into " + uri);
1428     }
1429 
insertPreviewProgram(Uri uri, ContentValues values)1430     private Uri insertPreviewProgram(Uri uri, ContentValues values) {
1431         if (!values.containsKey(PreviewPrograms.COLUMN_TYPE)) {
1432             throw new IllegalArgumentException("Missing the required column: " +
1433                     PreviewPrograms.COLUMN_TYPE);
1434         }
1435         blockIllegalAccessFromBlockedPackage();
1436         // Mark the owner package of this program.
1437         values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
1438         blockIllegalAccessToPreviewProgramsSystemColumns(values);
1439 
1440         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1441         long rowId = db.insert(PREVIEW_PROGRAMS_TABLE, null, values);
1442         if (rowId > 0) {
1443             Uri previewProgramUri = TvContract.buildPreviewProgramUri(rowId);
1444             notifyChange(previewProgramUri);
1445             return previewProgramUri;
1446         }
1447 
1448         throw new SQLException("Failed to insert row into " + uri);
1449     }
1450 
insertWatchNextProgram(Uri uri, ContentValues values)1451     private Uri insertWatchNextProgram(Uri uri, ContentValues values) {
1452         if (!values.containsKey(WatchNextPrograms.COLUMN_TYPE)) {
1453             throw new IllegalArgumentException("Missing the required column: " +
1454                     WatchNextPrograms.COLUMN_TYPE);
1455         }
1456         blockIllegalAccessFromBlockedPackage();
1457         if (!callerHasAccessAllEpgDataPermission() ||
1458                 !values.containsKey(Programs.COLUMN_PACKAGE_NAME)) {
1459             // Mark the owner package of this program. System app with a proper permission may
1460             // change the owner of the program.
1461             values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
1462         }
1463         blockIllegalAccessToPreviewProgramsSystemColumns(values);
1464 
1465         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1466         long rowId = db.insert(WATCH_NEXT_PROGRAMS_TABLE, null, values);
1467         if (rowId > 0) {
1468             Uri watchNextProgramUri = TvContract.buildWatchNextProgramUri(rowId);
1469             notifyChange(watchNextProgramUri);
1470             return watchNextProgramUri;
1471         }
1472 
1473         throw new SQLException("Failed to insert row into " + uri);
1474     }
1475 
1476     @Override
delete(Uri uri, String selection, String[] selectionArgs)1477     public int delete(Uri uri, String selection, String[] selectionArgs) {
1478         mTransientRowHelper.ensureOldTransientRowsDeleted();
1479         SqlParams params = createSqlParams(OP_DELETE, uri, selection, selectionArgs);
1480         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1481         int count;
1482         switch (sUriMatcher.match(uri)) {
1483             case MATCH_CHANNEL_ID_LOGO:
1484                 ContentValues values = new ContentValues();
1485                 values.putNull(CHANNELS_COLUMN_LOGO);
1486                 count = db.update(params.getTables(), values, params.getSelection(),
1487                         params.getSelectionArgs());
1488                 break;
1489             case MATCH_CHANNEL:
1490             case MATCH_PROGRAM:
1491             case MATCH_WATCHED_PROGRAM:
1492             case MATCH_RECORDED_PROGRAM:
1493             case MATCH_PREVIEW_PROGRAM:
1494             case MATCH_WATCH_NEXT_PROGRAM:
1495             case MATCH_CHANNEL_ID:
1496             case MATCH_PASSTHROUGH_ID:
1497             case MATCH_PROGRAM_ID:
1498             case MATCH_WATCHED_PROGRAM_ID:
1499             case MATCH_RECORDED_PROGRAM_ID:
1500             case MATCH_PREVIEW_PROGRAM_ID:
1501             case MATCH_WATCH_NEXT_PROGRAM_ID:
1502                 count = db.delete(params.getTables(), params.getSelection(),
1503                         params.getSelectionArgs());
1504                 break;
1505             default:
1506                 throw new IllegalArgumentException("Unknown URI " + uri);
1507         }
1508         if (count > 0) {
1509             notifyChange(uri);
1510         }
1511         return count;
1512     }
1513 
1514     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)1515     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
1516         mTransientRowHelper.ensureOldTransientRowsDeleted();
1517         SqlParams params = createSqlParams(OP_UPDATE, uri, selection, selectionArgs);
1518         blockIllegalAccessToIdAndPackageName(uri, values);
1519         boolean containImmutableColumn = false;
1520         if (params.getTables().equals(CHANNELS_TABLE)) {
1521             filterContentValues(values, sChannelProjectionMap);
1522             containImmutableColumn = disallowModifyChannelType(values, params);
1523             if (containImmutableColumn && sUriMatcher.match(uri) != MATCH_CHANNEL_ID) {
1524                 Log.i(TAG, "Updating failed. Attempt to change immutable column for channels.");
1525                 return 0;
1526             }
1527             blockIllegalAccessToChannelsSystemColumns(values);
1528         } else if (params.getTables().equals(PROGRAMS_TABLE)) {
1529             filterContentValues(values, sProgramProjectionMap);
1530             checkAndConvertGenre(values);
1531             checkAndConvertDeprecatedColumns(values);
1532         } else if (params.getTables().equals(RECORDED_PROGRAMS_TABLE)) {
1533             filterContentValues(values, sRecordedProgramProjectionMap);
1534             checkAndConvertGenre(values);
1535         } else if (params.getTables().equals(PREVIEW_PROGRAMS_TABLE)) {
1536             filterContentValues(values, sPreviewProgramProjectionMap);
1537             containImmutableColumn = disallowModifyChannelId(values, params);
1538             if (containImmutableColumn && PreviewPrograms.CONTENT_URI.equals(uri)) {
1539                 Log.i(TAG, "Updating failed. Attempt to change unmodifiable column for "
1540                         + "preview programs.");
1541                 return 0;
1542             }
1543             blockIllegalAccessToPreviewProgramsSystemColumns(values);
1544         } else if (params.getTables().equals(WATCH_NEXT_PROGRAMS_TABLE)) {
1545             filterContentValues(values, sWatchNextProgramProjectionMap);
1546             blockIllegalAccessToPreviewProgramsSystemColumns(values);
1547         }
1548         if (values.size() == 0) {
1549             // All values may be filtered out, no need to update
1550             return 0;
1551         }
1552         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1553         int count = db.update(params.getTables(), values, params.getSelection(),
1554                 params.getSelectionArgs());
1555         if (count > 0) {
1556             notifyChange(uri);
1557         } else if (containImmutableColumn) {
1558             Log.i(TAG, "Updating failed. The item may not exist or attempt to change "
1559                     + "immutable column.");
1560         }
1561         return count;
1562     }
1563 
ensureInitialized()1564     private synchronized void ensureInitialized() {
1565         if (!sInitialized) {
1566             // Database is not accessed before and the projection maps and the blocked package list
1567             // are not updated yet. Gets database here to make it initialized.
1568             mOpenHelper.getReadableDatabase();
1569         }
1570     }
1571 
initOnOpenIfNeeded(Context context, SQLiteDatabase db)1572     private static synchronized void initOnOpenIfNeeded(Context context, SQLiteDatabase db) {
1573         if (!sInitialized) {
1574             updateProjectionMap(db, CHANNELS_TABLE, sChannelProjectionMap);
1575             updateProjectionMap(db, PROGRAMS_TABLE, sProgramProjectionMap);
1576             updateProjectionMap(db, WATCHED_PROGRAMS_TABLE, sWatchedProgramProjectionMap);
1577             updateProjectionMap(db, RECORDED_PROGRAMS_TABLE, sRecordedProgramProjectionMap);
1578             updateProjectionMap(db, PREVIEW_PROGRAMS_TABLE, sPreviewProgramProjectionMap);
1579             updateProjectionMap(db, WATCH_NEXT_PROGRAMS_TABLE, sWatchNextProgramProjectionMap);
1580             sBlockedPackagesSharedPreference = PreferenceManager.getDefaultSharedPreferences(
1581                     context);
1582             sBlockedPackages = new ConcurrentHashMap<>();
1583             for (String packageName : sBlockedPackagesSharedPreference.getStringSet(
1584                     SHARED_PREF_BLOCKED_PACKAGES_KEY, new HashSet<>())) {
1585                 sBlockedPackages.put(packageName, true);
1586             }
1587             sInitialized = true;
1588         }
1589     }
1590 
updateProjectionMap(SQLiteDatabase db, String tableName, Map<String, String> projectionMap)1591     private static void updateProjectionMap(SQLiteDatabase db, String tableName,
1592             Map<String, String> projectionMap) {
1593         try (Cursor cursor = db.rawQuery("SELECT * FROM " + tableName + " LIMIT 0", null)) {
1594             for (String columnName : cursor.getColumnNames()) {
1595                 if (!projectionMap.containsKey(columnName)) {
1596                     projectionMap.put(columnName, tableName + '.' + columnName);
1597                 }
1598             }
1599         }
1600     }
1601 
createProjectionMapForQuery(String[] projection, Map<String, String> projectionMap)1602     private Map<String, String> createProjectionMapForQuery(String[] projection,
1603             Map<String, String> projectionMap) {
1604         if (projection == null) {
1605             return projectionMap;
1606         }
1607         Map<String, String> columnProjectionMap = new HashMap<>();
1608         for (String columnName : projection) {
1609             String value = projectionMap.get(columnName);
1610             if (value != null) {
1611                 columnProjectionMap.put(columnName, value);
1612             } else {
1613                 // Value NULL will be provided if the requested column does not exist in the
1614                 // database.
1615                 value = "NULL AS " + DatabaseUtils.sqlEscapeString(columnName);
1616                 columnProjectionMap.put(columnName, value);
1617 
1618                 if (needEventLog(columnName)) {
1619                     android.util.EventLog.writeEvent(0x534e4554, "135269669", -1, "");
1620                 }
1621             }
1622         }
1623         return columnProjectionMap;
1624     }
1625 
needEventLog(String columnName)1626     private boolean needEventLog(String columnName) {
1627         for (int i = 0; i < columnName.length(); i++) {
1628             char c = columnName.charAt(i);
1629             if (!Character.isLetterOrDigit(c) && c != '_') {
1630                 return true;
1631             }
1632         }
1633         return false;
1634     }
1635 
filterContentValues(ContentValues values, Map<String, String> projectionMap)1636     private void filterContentValues(ContentValues values, Map<String, String> projectionMap) {
1637         Iterator<String> iter = values.keySet().iterator();
1638         while (iter.hasNext()) {
1639             String columnName = iter.next();
1640             if (!projectionMap.containsKey(columnName)) {
1641                 iter.remove();
1642             }
1643         }
1644     }
1645 
createSqlParams(String operation, Uri uri, String selection, String[] selectionArgs)1646     private SqlParams createSqlParams(String operation, Uri uri, String selection,
1647             String[] selectionArgs) {
1648         int match = sUriMatcher.match(uri);
1649 
1650         SqliteTokenFinder.findTokens(selection, p -> {
1651             if (p.first == SqliteTokenFinder.TYPE_REGULAR
1652                     && TextUtils.equals(p.second.toUpperCase(Locale.US), "SELECT")) {
1653                 // only when a keyword is not in quotes or brackets
1654                 // see https://www.sqlite.org/lang_keywords.html
1655                 android.util.EventLog.writeEvent(0x534e4554, "135269669", -1, "");
1656                 throw new SecurityException(
1657                         "Subquery is not allowed in selection: " + selection);
1658             }
1659         });
1660 
1661         SqlParams params = new SqlParams(null, selection, selectionArgs);
1662 
1663         // Control access to EPG data (excluding watched programs) when the caller doesn't have all
1664         // access.
1665         String prefix = match == MATCH_CHANNEL ? CHANNELS_TABLE + "." : "";
1666         if (!callerHasAccessAllEpgDataPermission()
1667                 && match != MATCH_WATCHED_PROGRAM && match != MATCH_WATCHED_PROGRAM_ID) {
1668             if (!TextUtils.isEmpty(selection)) {
1669                 throw new SecurityException("Selection not allowed for " + uri);
1670             }
1671             // Limit the operation only to the data that the calling package owns except for when
1672             // the caller tries to read TV listings and has the appropriate permission.
1673             if (operation.equals(OP_QUERY) && callerHasReadTvListingsPermission()) {
1674                 params.setWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=? OR "
1675                         + Channels.COLUMN_SEARCHABLE + "=?", getCallingPackage_(), "1");
1676             } else {
1677                 params.setWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=?",
1678                         getCallingPackage_());
1679             }
1680         }
1681         String packageName = uri.getQueryParameter(TvContract.PARAM_PACKAGE);
1682         if (packageName != null) {
1683             params.appendWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=?", packageName);
1684         }
1685 
1686         switch (match) {
1687             case MATCH_CHANNEL:
1688                 String genre = uri.getQueryParameter(TvContract.PARAM_CANONICAL_GENRE);
1689                 if (genre == null) {
1690                     params.setTables(CHANNELS_TABLE);
1691                 } else {
1692                     if (!operation.equals(OP_QUERY)) {
1693                         throw new SecurityException(capitalize(operation)
1694                                 + " not allowed for " + uri);
1695                     }
1696                     if (!Genres.isCanonical(genre)) {
1697                         throw new IllegalArgumentException("Not a canonical genre : " + genre);
1698                     }
1699                     params.setTables(CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE);
1700                     String curTime = String.valueOf(System.currentTimeMillis());
1701                     params.appendWhere("LIKE(?, " + Programs.COLUMN_CANONICAL_GENRE + ") AND "
1702                             + Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
1703                             + Programs.COLUMN_END_TIME_UTC_MILLIS + ">=?",
1704                             "%" + genre + "%", curTime, curTime);
1705                 }
1706                 String inputId = uri.getQueryParameter(TvContract.PARAM_INPUT);
1707                 if (inputId != null) {
1708                     params.appendWhere(Channels.COLUMN_INPUT_ID + "=?", inputId);
1709                 }
1710                 boolean browsableOnly = uri.getBooleanQueryParameter(
1711                         TvContract.PARAM_BROWSABLE_ONLY, false);
1712                 if (browsableOnly) {
1713                     params.appendWhere(Channels.COLUMN_BROWSABLE + "=1");
1714                 }
1715                 String preview = uri.getQueryParameter(TvContract.PARAM_PREVIEW);
1716                 if (preview != null) {
1717                     String previewSelection = Channels.COLUMN_TYPE
1718                             + (preview.equals(String.valueOf(true)) ? "=?" : "!=?");
1719                     params.appendWhere(previewSelection, Channels.TYPE_PREVIEW);
1720                 }
1721                 break;
1722             case MATCH_CHANNEL_ID:
1723                 params.setTables(CHANNELS_TABLE);
1724                 params.appendWhere(Channels._ID + "=?", uri.getLastPathSegment());
1725                 break;
1726             case MATCH_PROGRAM:
1727                 params.setTables(PROGRAMS_TABLE);
1728                 String paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL);
1729                 if (paramChannelId != null) {
1730                     String channelId = String.valueOf(Long.parseLong(paramChannelId));
1731                     params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId);
1732                 }
1733                 String paramStartTime = uri.getQueryParameter(TvContract.PARAM_START_TIME);
1734                 String paramEndTime = uri.getQueryParameter(TvContract.PARAM_END_TIME);
1735                 if (paramStartTime != null && paramEndTime != null) {
1736                     String startTime = String.valueOf(Long.parseLong(paramStartTime));
1737                     String endTime = String.valueOf(Long.parseLong(paramEndTime));
1738                     params.appendWhere(Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
1739                             + Programs.COLUMN_END_TIME_UTC_MILLIS + ">=? AND ?<=?", endTime,
1740                             startTime, startTime, endTime);
1741                 }
1742                 break;
1743             case MATCH_PROGRAM_ID:
1744                 params.setTables(PROGRAMS_TABLE);
1745                 params.appendWhere(Programs._ID + "=?", uri.getLastPathSegment());
1746                 break;
1747             case MATCH_WATCHED_PROGRAM:
1748                 if (!callerHasAccessWatchedProgramsPermission()) {
1749                     throw new SecurityException("Access not allowed for " + uri);
1750                 }
1751                 params.setTables(WATCHED_PROGRAMS_TABLE);
1752                 params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1");
1753                 break;
1754             case MATCH_WATCHED_PROGRAM_ID:
1755                 if (!callerHasAccessWatchedProgramsPermission()) {
1756                     throw new SecurityException("Access not allowed for " + uri);
1757                 }
1758                 params.setTables(WATCHED_PROGRAMS_TABLE);
1759                 params.appendWhere(WatchedPrograms._ID + "=?", uri.getLastPathSegment());
1760                 params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1");
1761                 break;
1762             case MATCH_RECORDED_PROGRAM_ID:
1763                 params.appendWhere(RecordedPrograms._ID + "=?", uri.getLastPathSegment());
1764                 // fall-through
1765             case MATCH_RECORDED_PROGRAM:
1766                 params.setTables(RECORDED_PROGRAMS_TABLE);
1767                 paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL);
1768                 if (paramChannelId != null) {
1769                     String channelId = String.valueOf(Long.parseLong(paramChannelId));
1770                     params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId);
1771                 }
1772                 break;
1773             case MATCH_PREVIEW_PROGRAM_ID:
1774                 params.appendWhere(PreviewPrograms._ID + "=?", uri.getLastPathSegment());
1775                 // fall-through
1776             case MATCH_PREVIEW_PROGRAM:
1777                 params.setTables(PREVIEW_PROGRAMS_TABLE);
1778                 paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL);
1779                 if (paramChannelId != null) {
1780                     String channelId = String.valueOf(Long.parseLong(paramChannelId));
1781                     params.appendWhere(PreviewPrograms.COLUMN_CHANNEL_ID + "=?", channelId);
1782                 }
1783                 break;
1784             case MATCH_WATCH_NEXT_PROGRAM_ID:
1785                 params.appendWhere(WatchNextPrograms._ID + "=?", uri.getLastPathSegment());
1786                 // fall-through
1787             case MATCH_WATCH_NEXT_PROGRAM:
1788                 params.setTables(WATCH_NEXT_PROGRAMS_TABLE);
1789                 break;
1790             case MATCH_CHANNEL_ID_LOGO:
1791                 if (operation.equals(OP_DELETE)) {
1792                     params.setTables(CHANNELS_TABLE);
1793                     params.appendWhere(Channels._ID + "=?", uri.getPathSegments().get(1));
1794                     break;
1795                 }
1796                 // fall-through
1797             case MATCH_PASSTHROUGH_ID:
1798                 throw new UnsupportedOperationException(operation + " not permmitted on " + uri);
1799             default:
1800                 throw new IllegalArgumentException("Unknown URI " + uri);
1801         }
1802         return params;
1803     }
1804 
generateDefaultClause(String dataType, String defaultValue)1805     private static String generateDefaultClause(String dataType, String defaultValue)
1806             throws IllegalArgumentException {
1807         String defaultValueString = " DEFAULT ";
1808         switch (dataType.toLowerCase()) {
1809             case "integer":
1810                 return defaultValueString + Integer.parseInt(defaultValue);
1811             case "real":
1812                 return defaultValueString + Double.parseDouble(defaultValue);
1813             case "text":
1814             case "blob":
1815                 return defaultValueString + DatabaseUtils.sqlEscapeString(defaultValue);
1816             default:
1817                 throw new IllegalArgumentException("Illegal data type \"" + dataType
1818                         + "\" with default value: " + defaultValue);
1819         }
1820     }
1821 
capitalize(String str)1822     private static String capitalize(String str) {
1823         return Character.toUpperCase(str.charAt(0)) + str.substring(1);
1824     }
1825 
1826     @SuppressLint("DefaultLocale")
checkAndConvertGenre(ContentValues values)1827     private void checkAndConvertGenre(ContentValues values) {
1828         String canonicalGenres = values.getAsString(Programs.COLUMN_CANONICAL_GENRE);
1829 
1830         if (!TextUtils.isEmpty(canonicalGenres)) {
1831             // Check if the canonical genres are valid. If not, clear them.
1832             String[] genres = Genres.decode(canonicalGenres);
1833             for (String genre : genres) {
1834                 if (!Genres.isCanonical(genre)) {
1835                     values.putNull(Programs.COLUMN_CANONICAL_GENRE);
1836                     canonicalGenres = null;
1837                     break;
1838                 }
1839             }
1840         }
1841 
1842         if (TextUtils.isEmpty(canonicalGenres)) {
1843             // If the canonical genre is not set, try to map the broadcast genre to the canonical
1844             // genre.
1845             String broadcastGenres = values.getAsString(Programs.COLUMN_BROADCAST_GENRE);
1846             if (!TextUtils.isEmpty(broadcastGenres)) {
1847                 Set<String> genreSet = new HashSet<>();
1848                 String[] genres = Genres.decode(broadcastGenres);
1849                 for (String genre : genres) {
1850                     String canonicalGenre = sGenreMap.get(genre.toUpperCase());
1851                     if (Genres.isCanonical(canonicalGenre)) {
1852                         genreSet.add(canonicalGenre);
1853                     }
1854                 }
1855                 if (genreSet.size() > 0) {
1856                     values.put(Programs.COLUMN_CANONICAL_GENRE,
1857                             Genres.encode(genreSet.toArray(new String[genreSet.size()])));
1858                 }
1859             }
1860         }
1861     }
1862 
checkAndConvertDeprecatedColumns(ContentValues values)1863     private void checkAndConvertDeprecatedColumns(ContentValues values) {
1864         if (values.containsKey(Programs.COLUMN_SEASON_NUMBER)) {
1865             if (!values.containsKey(Programs.COLUMN_SEASON_DISPLAY_NUMBER)) {
1866                 values.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, values.getAsInteger(
1867                         Programs.COLUMN_SEASON_NUMBER));
1868             }
1869             values.remove(Programs.COLUMN_SEASON_NUMBER);
1870         }
1871         if (values.containsKey(Programs.COLUMN_EPISODE_NUMBER)) {
1872             if (!values.containsKey(Programs.COLUMN_EPISODE_DISPLAY_NUMBER)) {
1873                 values.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER, values.getAsInteger(
1874                         Programs.COLUMN_EPISODE_NUMBER));
1875             }
1876             values.remove(Programs.COLUMN_EPISODE_NUMBER);
1877         }
1878     }
1879 
1880     // We might have more than one thread trying to make its way through applyBatch() so the
1881     // notification coalescing needs to be thread-local to work correctly.
1882     private final ThreadLocal<Set<Uri>> mTLBatchNotifications = new ThreadLocal<>();
1883 
getBatchNotificationsSet()1884     private Set<Uri> getBatchNotificationsSet() {
1885         return mTLBatchNotifications.get();
1886     }
1887 
setBatchNotificationsSet(Set<Uri> batchNotifications)1888     private void setBatchNotificationsSet(Set<Uri> batchNotifications) {
1889         mTLBatchNotifications.set(batchNotifications);
1890     }
1891 
1892     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)1893     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
1894             throws OperationApplicationException {
1895         setBatchNotificationsSet(new HashSet<Uri>());
1896         Context context = getContext();
1897         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1898         db.beginTransaction();
1899         try {
1900             ContentProviderResult[] results = super.applyBatch(operations);
1901             db.setTransactionSuccessful();
1902             return results;
1903         } finally {
1904             db.endTransaction();
1905             final Set<Uri> notifications = getBatchNotificationsSet();
1906             setBatchNotificationsSet(null);
1907             for (final Uri uri : notifications) {
1908                 context.getContentResolver().notifyChange(uri, null);
1909             }
1910         }
1911     }
1912 
1913     @Override
bulkInsert(Uri uri, ContentValues[] values)1914     public int bulkInsert(Uri uri, ContentValues[] values) {
1915         setBatchNotificationsSet(new HashSet<Uri>());
1916         Context context = getContext();
1917         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1918         db.beginTransaction();
1919         try {
1920             int result = super.bulkInsert(uri, values);
1921             db.setTransactionSuccessful();
1922             return result;
1923         } finally {
1924             db.endTransaction();
1925             final Set<Uri> notifications = getBatchNotificationsSet();
1926             setBatchNotificationsSet(null);
1927             for (final Uri notificationUri : notifications) {
1928                 context.getContentResolver().notifyChange(notificationUri, null);
1929             }
1930         }
1931     }
1932 
notifyChange(Uri uri)1933     private void notifyChange(Uri uri) {
1934         final Set<Uri> batchNotifications = getBatchNotificationsSet();
1935         if (batchNotifications != null) {
1936             batchNotifications.add(uri);
1937         } else {
1938             getContext().getContentResolver().notifyChange(uri, null);
1939         }
1940     }
1941 
callerHasReadTvListingsPermission()1942     private boolean callerHasReadTvListingsPermission() {
1943         return getContext().checkCallingOrSelfPermission(PERMISSION_READ_TV_LISTINGS)
1944                 == PackageManager.PERMISSION_GRANTED;
1945     }
1946 
callerHasAccessAllEpgDataPermission()1947     private boolean callerHasAccessAllEpgDataPermission() {
1948         return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_ALL_EPG_DATA)
1949                 == PackageManager.PERMISSION_GRANTED;
1950     }
1951 
callerHasAccessWatchedProgramsPermission()1952     private boolean callerHasAccessWatchedProgramsPermission() {
1953         return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_WATCHED_PROGRAMS)
1954                 == PackageManager.PERMISSION_GRANTED;
1955     }
1956 
callerHasModifyParentalControlsPermission()1957     private boolean callerHasModifyParentalControlsPermission() {
1958         return getContext().checkCallingOrSelfPermission(
1959                 android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
1960                 == PackageManager.PERMISSION_GRANTED;
1961     }
1962 
blockIllegalAccessToIdAndPackageName(Uri uri, ContentValues values)1963     private void blockIllegalAccessToIdAndPackageName(Uri uri, ContentValues values) {
1964         if (values.containsKey(BaseColumns._ID)) {
1965             int match = sUriMatcher.match(uri);
1966             switch (match) {
1967                 case MATCH_CHANNEL_ID:
1968                 case MATCH_PROGRAM_ID:
1969                 case MATCH_PREVIEW_PROGRAM_ID:
1970                 case MATCH_RECORDED_PROGRAM_ID:
1971                 case MATCH_WATCH_NEXT_PROGRAM_ID:
1972                 case MATCH_WATCHED_PROGRAM_ID:
1973                     if (TextUtils.equals(values.getAsString(BaseColumns._ID),
1974                             uri.getLastPathSegment())) {
1975                         break;
1976                     }
1977                 default:
1978                     throw new IllegalArgumentException("Not allowed to change ID.");
1979             }
1980         }
1981         if (values.containsKey(BaseTvColumns.COLUMN_PACKAGE_NAME)
1982                 && !callerHasAccessAllEpgDataPermission() && !TextUtils.equals(values.getAsString(
1983                         BaseTvColumns.COLUMN_PACKAGE_NAME), getCallingPackage_())) {
1984             throw new SecurityException("Not allowed to change package name.");
1985         }
1986     }
1987 
blockIllegalAccessToChannelsSystemColumns(ContentValues values)1988     private void blockIllegalAccessToChannelsSystemColumns(ContentValues values) {
1989         if (values.containsKey(Channels.COLUMN_LOCKED)
1990                 && !callerHasModifyParentalControlsPermission()) {
1991             throw new SecurityException("Not allowed to access Channels.COLUMN_LOCKED");
1992         }
1993         Boolean hasAccessAllEpgDataPermission = null;
1994         if (values.containsKey(Channels.COLUMN_BROWSABLE)) {
1995             hasAccessAllEpgDataPermission = callerHasAccessAllEpgDataPermission();
1996             if (!hasAccessAllEpgDataPermission) {
1997                 throw new SecurityException("Not allowed to access Channels.COLUMN_BROWSABLE");
1998             }
1999         }
2000     }
2001 
blockIllegalAccessToPreviewProgramsSystemColumns(ContentValues values)2002     private void blockIllegalAccessToPreviewProgramsSystemColumns(ContentValues values) {
2003         if (values.containsKey(PreviewPrograms.COLUMN_BROWSABLE)
2004                 && !callerHasAccessAllEpgDataPermission()) {
2005             throw new SecurityException("Not allowed to access Programs.COLUMN_BROWSABLE");
2006         }
2007     }
2008 
blockIllegalAccessFromBlockedPackage()2009     private void blockIllegalAccessFromBlockedPackage() {
2010         String callingPackageName = getCallingPackage_();
2011         if (sBlockedPackages.containsKey(callingPackageName)) {
2012             throw new SecurityException(
2013                     "Not allowed to access " + TvContract.AUTHORITY + ", "
2014                     + callingPackageName + " is blocked");
2015         }
2016     }
2017 
disallowModifyChannelType(ContentValues values, SqlParams params)2018     private boolean disallowModifyChannelType(ContentValues values, SqlParams params) {
2019         if (values.containsKey(Channels.COLUMN_TYPE)) {
2020             params.appendWhere(Channels.COLUMN_TYPE + "=?",
2021                     values.getAsString(Channels.COLUMN_TYPE));
2022             return true;
2023         }
2024         return false;
2025     }
2026 
disallowModifyChannelId(ContentValues values, SqlParams params)2027     private boolean disallowModifyChannelId(ContentValues values, SqlParams params) {
2028         if (values.containsKey(PreviewPrograms.COLUMN_CHANNEL_ID)) {
2029             params.appendWhere(PreviewPrograms.COLUMN_CHANNEL_ID + "=?",
2030                     values.getAsString(PreviewPrograms.COLUMN_CHANNEL_ID));
2031             return true;
2032         }
2033         return false;
2034     }
2035 
2036     @Override
openFile(Uri uri, String mode)2037     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
2038         switch (sUriMatcher.match(uri)) {
2039             case MATCH_CHANNEL_ID_LOGO:
2040                 return openLogoFile(uri, mode);
2041             default:
2042                 throw new FileNotFoundException(uri.toString());
2043         }
2044     }
2045 
openLogoFile(Uri uri, String mode)2046     private ParcelFileDescriptor openLogoFile(Uri uri, String mode) throws FileNotFoundException {
2047         long channelId = Long.parseLong(uri.getPathSegments().get(1));
2048 
2049         SqlParams params = new SqlParams(CHANNELS_TABLE, Channels._ID + "=?",
2050                 String.valueOf(channelId));
2051         if (!callerHasAccessAllEpgDataPermission()) {
2052             if (callerHasReadTvListingsPermission()) {
2053                 params.appendWhere(Channels.COLUMN_PACKAGE_NAME + "=? OR "
2054                         + Channels.COLUMN_SEARCHABLE + "=?", getCallingPackage_(), "1");
2055             } else {
2056                 params.appendWhere(Channels.COLUMN_PACKAGE_NAME + "=?", getCallingPackage_());
2057             }
2058         }
2059 
2060         SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
2061         queryBuilder.setTables(params.getTables());
2062 
2063         // We don't write the database here.
2064         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2065         if (mode.equals("r")) {
2066             String sql = queryBuilder.buildQuery(new String[] { CHANNELS_COLUMN_LOGO },
2067                     params.getSelection(), null, null, null, null);
2068             ParcelFileDescriptor fd = DatabaseUtils.blobFileDescriptorForQuery(
2069                     db, sql, params.getSelectionArgs());
2070             if (fd == null) {
2071                 throw new FileNotFoundException(uri.toString());
2072             }
2073             return fd;
2074         } else {
2075             try (Cursor cursor = queryBuilder.query(db, new String[] { Channels._ID },
2076                     params.getSelection(), params.getSelectionArgs(), null, null, null)) {
2077                 if (cursor.getCount() < 1) {
2078                     // Fails early if corresponding channel does not exist.
2079                     // PipeMonitor may still fail to update DB later.
2080                     throw new FileNotFoundException(uri.toString());
2081                 }
2082             }
2083 
2084             try {
2085                 ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe();
2086                 PipeMonitor pipeMonitor = new PipeMonitor(pipeFds[0], channelId, params);
2087                 pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
2088                 return pipeFds[1];
2089             } catch (IOException ioe) {
2090                 FileNotFoundException fne = new FileNotFoundException(uri.toString());
2091                 fne.initCause(ioe);
2092                 throw fne;
2093             }
2094         }
2095     }
2096 
2097     /**
2098      * Validates the sort order based on the given field set.
2099      *
2100      * @throws IllegalArgumentException if there is any unknown field.
2101      */
2102     @SuppressLint("DefaultLocale")
validateSortOrder(String sortOrder, Set<String> possibleFields)2103     private static void validateSortOrder(String sortOrder, Set<String> possibleFields) {
2104         if (TextUtils.isEmpty(sortOrder) || possibleFields.isEmpty()) {
2105             return;
2106         }
2107         String[] orders = sortOrder.split(",");
2108         for (String order : orders) {
2109             String field = order.replaceAll("\\s+", " ").trim().toLowerCase().replace(" asc", "")
2110                     .replace(" desc", "");
2111             if (!possibleFields.contains(field)) {
2112                 throw new IllegalArgumentException("Illegal field in sort order " + order);
2113             }
2114         }
2115     }
2116 
2117     private class PipeMonitor extends AsyncTask<Void, Void, Void> {
2118         private final ParcelFileDescriptor mPfd;
2119         private final long mChannelId;
2120         private final SqlParams mParams;
2121 
PipeMonitor(ParcelFileDescriptor pfd, long channelId, SqlParams params)2122         private PipeMonitor(ParcelFileDescriptor pfd, long channelId, SqlParams params) {
2123             mPfd = pfd;
2124             mChannelId = channelId;
2125             mParams = params;
2126         }
2127 
2128         @Override
doInBackground(Void... params)2129         protected Void doInBackground(Void... params) {
2130             AutoCloseInputStream is = new AutoCloseInputStream(mPfd);
2131             ByteArrayOutputStream baos = null;
2132             int count = 0;
2133             try {
2134                 Bitmap bitmap = BitmapFactory.decodeStream(is);
2135                 if (bitmap == null) {
2136                     Log.e(TAG, "Failed to decode logo image for channel ID " + mChannelId);
2137                     return null;
2138                 }
2139 
2140                 float scaleFactor = Math.min(1f, ((float) MAX_LOGO_IMAGE_SIZE) /
2141                         Math.max(bitmap.getWidth(), bitmap.getHeight()));
2142                 if (scaleFactor < 1f) {
2143                     bitmap = Bitmap.createScaledBitmap(bitmap,
2144                             (int) (bitmap.getWidth() * scaleFactor),
2145                             (int) (bitmap.getHeight() * scaleFactor), false);
2146                 }
2147 
2148                 baos = new ByteArrayOutputStream();
2149                 bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
2150                 byte[] bytes = baos.toByteArray();
2151 
2152                 ContentValues values = new ContentValues();
2153                 values.put(CHANNELS_COLUMN_LOGO, bytes);
2154 
2155                 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
2156                 count = db.update(mParams.getTables(), values, mParams.getSelection(),
2157                         mParams.getSelectionArgs());
2158                 if (count > 0) {
2159                     Uri uri = TvContract.buildChannelLogoUri(mChannelId);
2160                     notifyChange(uri);
2161                 }
2162             } finally {
2163                 if (count == 0) {
2164                     try {
2165                         mPfd.closeWithError("Failed to write logo for channel ID " + mChannelId);
2166                     } catch (IOException ioe) {
2167                         Log.e(TAG, "Failed to close pipe", ioe);
2168                     }
2169                 }
2170                 IoUtils.closeQuietly(baos);
2171                 IoUtils.closeQuietly(is);
2172             }
2173             return null;
2174         }
2175     }
2176 
deleteUnconsolidatedWatchedProgramsRows()2177     private void deleteUnconsolidatedWatchedProgramsRows() {
2178         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
2179         db.delete(WATCHED_PROGRAMS_TABLE, WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0", null);
2180     }
2181 
2182     @SuppressLint("HandlerLeak")
2183     private final class WatchLogHandler extends Handler {
2184         private static final int MSG_CONSOLIDATE = 1;
2185         private static final int MSG_TRY_CONSOLIDATE_ALL = 2;
2186 
2187         @Override
handleMessage(Message msg)2188         public void handleMessage(Message msg) {
2189             switch (msg.what) {
2190                 case MSG_CONSOLIDATE: {
2191                     SomeArgs args = (SomeArgs) msg.obj;
2192                     String sessionToken = (String) args.arg1;
2193                     long watchEndTime = (long) args.arg2;
2194                     onConsolidate(sessionToken, watchEndTime);
2195                     args.recycle();
2196                     return;
2197                 }
2198                 case MSG_TRY_CONSOLIDATE_ALL: {
2199                     onTryConsolidateAll();
2200                     return;
2201                 }
2202                 default: {
2203                     Log.w(TAG, "Unhandled message code: " + msg.what);
2204                     return;
2205                 }
2206             }
2207         }
2208 
2209         // Consolidates all WatchedPrograms rows for a given session with watch end time information
2210         // of the most recent log entry. After this method is called, it is guaranteed that there
2211         // remain consolidated rows only for that session.
onConsolidate(String sessionToken, long watchEndTime)2212         private void onConsolidate(String sessionToken, long watchEndTime) {
2213             if (DEBUG) {
2214                 Log.d(TAG, "onConsolidate(sessionToken=" + sessionToken + ", watchEndTime="
2215                         + watchEndTime + ")");
2216             }
2217 
2218             SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
2219             queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
2220             SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2221 
2222             // Pick up the last row with the same session token.
2223             String[] projection = {
2224                     WatchedPrograms._ID,
2225                     WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
2226                     WatchedPrograms.COLUMN_CHANNEL_ID
2227             };
2228             String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=? AND "
2229                     + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + "=?";
2230             String[] selectionArgs = {
2231                     "0",
2232                     sessionToken
2233             };
2234             String sortOrder = WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC";
2235 
2236             int consolidatedRowCount = 0;
2237             try (Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null,
2238                     null, sortOrder)) {
2239                 long oldWatchStartTime = watchEndTime;
2240                 while (cursor != null && cursor.moveToNext()) {
2241                     long id = cursor.getLong(0);
2242                     long watchStartTime = cursor.getLong(1);
2243                     long channelId = cursor.getLong(2);
2244                     consolidatedRowCount += consolidateRow(id, watchStartTime, oldWatchStartTime,
2245                             channelId, false);
2246                     oldWatchStartTime = watchStartTime;
2247                 }
2248             }
2249             if (consolidatedRowCount > 0) {
2250                 deleteUnsearchable();
2251             }
2252         }
2253 
2254         // Tries to consolidate all WatchedPrograms rows regardless of the session. After this
2255         // method is called, it is guaranteed that we have at most one unconsolidated log entry per
2256         // session that represents the user's ongoing watch activity.
2257         // Also, this method automatically schedules the next consolidation if there still remains
2258         // an unconsolidated entry.
onTryConsolidateAll()2259         private void onTryConsolidateAll() {
2260             if (DEBUG) {
2261                 Log.d(TAG, "onTryConsolidateAll()");
2262             }
2263 
2264             SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
2265             queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
2266             SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2267 
2268             // Pick up all unconsolidated rows grouped by session. The most recent log entry goes on
2269             // top.
2270             String[] projection = {
2271                     WatchedPrograms._ID,
2272                     WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
2273                     WatchedPrograms.COLUMN_CHANNEL_ID,
2274                     WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN
2275             };
2276             String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0";
2277             String sortOrder = WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + " DESC,"
2278                     + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC";
2279 
2280             int consolidatedRowCount = 0;
2281             try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null,
2282                     sortOrder)) {
2283                 long oldWatchStartTime = 0;
2284                 String oldSessionToken = null;
2285                 while (cursor != null && cursor.moveToNext()) {
2286                     long id = cursor.getLong(0);
2287                     long watchStartTime = cursor.getLong(1);
2288                     long channelId = cursor.getLong(2);
2289                     String sessionToken = cursor.getString(3);
2290 
2291                     if (!sessionToken.equals(oldSessionToken)) {
2292                         // The most recent log entry for the current session, which may be still
2293                         // active. Just go through a dry run with the current time to see if this
2294                         // entry can be split into multiple rows.
2295                         consolidatedRowCount += consolidateRow(id, watchStartTime,
2296                                 System.currentTimeMillis(), channelId, true);
2297                         oldSessionToken = sessionToken;
2298                     } else {
2299                         // The later entries after the most recent one all fall into here. We now
2300                         // know that this watch activity ended exactly at the same time when the
2301                         // next activity started.
2302                         consolidatedRowCount += consolidateRow(id, watchStartTime,
2303                                 oldWatchStartTime, channelId, false);
2304                     }
2305                     oldWatchStartTime = watchStartTime;
2306                 }
2307             }
2308             if (consolidatedRowCount > 0) {
2309                 deleteUnsearchable();
2310             }
2311             scheduleConsolidationIfNeeded();
2312         }
2313 
2314         // Consolidates a WatchedPrograms row.
2315         // A row is 'consolidated' if and only if the following information is complete:
2316         // 1. WatchedPrograms.COLUMN_CHANNEL_ID
2317         // 2. WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS
2318         // 3. WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS
2319         // where COLUMN_WATCH_START_TIME_UTC_MILLIS <= COLUMN_WATCH_END_TIME_UTC_MILLIS.
2320         // This is the minimal but useful enough set of information to comprise the user's watch
2321         // history. (The program data are considered optional although we do try to fill them while
2322         // consolidating the row.) It is guaranteed that the target row is either consolidated or
2323         // deleted after this method is called.
2324         // Set {@code dryRun} to {@code true} if you think it's necessary to split the row without
2325         // consolidating the most recent row because the user stayed on the same channel for a very
2326         // long time.
2327         // This method returns the number of consolidated rows, which can be 0 or more.
consolidateRow( long id, long watchStartTime, long watchEndTime, long channelId, boolean dryRun)2328         private int consolidateRow(
2329                 long id, long watchStartTime, long watchEndTime, long channelId, boolean dryRun) {
2330             if (DEBUG) {
2331                 Log.d(TAG, "consolidateRow(id=" + id + ", watchStartTime=" + watchStartTime
2332                         + ", watchEndTime=" + watchEndTime + ", channelId=" + channelId
2333                         + ", dryRun=" + dryRun + ")");
2334             }
2335 
2336             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
2337 
2338             if (watchStartTime > watchEndTime) {
2339                 Log.e(TAG, "watchEndTime cannot be less than watchStartTime");
2340                 db.delete(WATCHED_PROGRAMS_TABLE, WatchedPrograms._ID + "=" + String.valueOf(id),
2341                         null);
2342                 return 0;
2343             }
2344 
2345             ContentValues values = getProgramValues(channelId, watchStartTime);
2346             Long endTime = values.getAsLong(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS);
2347             boolean needsToSplit = endTime != null && endTime < watchEndTime;
2348 
2349             values.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
2350                     String.valueOf(watchStartTime));
2351             if (!dryRun || needsToSplit) {
2352                 values.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
2353                         String.valueOf(needsToSplit ? endTime : watchEndTime));
2354                 values.put(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED, "1");
2355                 db.update(WATCHED_PROGRAMS_TABLE, values,
2356                         WatchedPrograms._ID + "=" + String.valueOf(id), null);
2357                 // Treat the watched program is inserted when WATCHED_PROGRAMS_COLUMN_CONSOLIDATED
2358                 // becomes 1.
2359                 notifyChange(TvContract.buildWatchedProgramUri(id));
2360             } else {
2361                 db.update(WATCHED_PROGRAMS_TABLE, values,
2362                         WatchedPrograms._ID + "=" + String.valueOf(id), null);
2363             }
2364             int count = dryRun ? 0 : 1;
2365             if (needsToSplit) {
2366                 // This means that the program ended before the user stops watching the current
2367                 // channel. In this case we duplicate the log entry as many as the number of
2368                 // programs watched on the same channel. Here the end time of the current program
2369                 // becomes the new watch start time of the next program.
2370                 long duplicatedId = duplicateRow(id);
2371                 if (duplicatedId > 0) {
2372                     count += consolidateRow(duplicatedId, endTime, watchEndTime, channelId, dryRun);
2373                 }
2374             }
2375             return count;
2376         }
2377 
2378         // Deletes the log entries from unsearchable channels. Note that only consolidated log
2379         // entries are safe to delete.
deleteUnsearchable()2380         private void deleteUnsearchable() {
2381             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
2382             String deleteWhere = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=1 AND "
2383                     + WatchedPrograms.COLUMN_CHANNEL_ID + " IN (SELECT " + Channels._ID
2384                     + " FROM " + CHANNELS_TABLE + " WHERE " + Channels.COLUMN_SEARCHABLE + "=0)";
2385             db.delete(WATCHED_PROGRAMS_TABLE, deleteWhere, null);
2386         }
2387 
scheduleConsolidationIfNeeded()2388         private void scheduleConsolidationIfNeeded() {
2389             if (DEBUG) {
2390                 Log.d(TAG, "scheduleConsolidationIfNeeded()");
2391             }
2392             SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
2393             queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
2394             SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2395 
2396             // Pick up all unconsolidated rows.
2397             String[] projection = {
2398                     WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
2399                     WatchedPrograms.COLUMN_CHANNEL_ID,
2400             };
2401             String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0";
2402 
2403             try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null,
2404                     null)) {
2405                 // Find the earliest time that any of the currently watching programs ends and
2406                 // schedule the next consolidation at that time.
2407                 long minEndTime = Long.MAX_VALUE;
2408                 while (cursor != null && cursor.moveToNext()) {
2409                     long watchStartTime = cursor.getLong(0);
2410                     long channelId = cursor.getLong(1);
2411                     ContentValues values = getProgramValues(channelId, watchStartTime);
2412                     Long endTime = values.getAsLong(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS);
2413 
2414                     if (endTime != null && endTime < minEndTime
2415                             && endTime > System.currentTimeMillis()) {
2416                         minEndTime = endTime;
2417                     }
2418                 }
2419                 if (minEndTime != Long.MAX_VALUE) {
2420                     sendEmptyMessageAtTime(MSG_TRY_CONSOLIDATE_ALL, minEndTime);
2421                     if (DEBUG) {
2422                         CharSequence minEndTimeStr = DateUtils.getRelativeTimeSpanString(
2423                                 minEndTime, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS);
2424                         Log.d(TAG, "onTryConsolidateAll() scheduled " + minEndTimeStr);
2425                     }
2426                 }
2427             }
2428         }
2429 
2430         // Returns non-null ContentValues of the program data that the user watched on the channel
2431         // {@code channelId} at the time {@code time}.
getProgramValues(long channelId, long time)2432         private ContentValues getProgramValues(long channelId, long time) {
2433             SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
2434             queryBuilder.setTables(PROGRAMS_TABLE);
2435             SQLiteDatabase db = mOpenHelper.getReadableDatabase();
2436 
2437             String[] projection = {
2438                     Programs.COLUMN_TITLE,
2439                     Programs.COLUMN_START_TIME_UTC_MILLIS,
2440                     Programs.COLUMN_END_TIME_UTC_MILLIS,
2441                     Programs.COLUMN_SHORT_DESCRIPTION
2442             };
2443             String selection = Programs.COLUMN_CHANNEL_ID + "=? AND "
2444                     + Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
2445                     + Programs.COLUMN_END_TIME_UTC_MILLIS + ">?";
2446             String[] selectionArgs = {
2447                     String.valueOf(channelId),
2448                     String.valueOf(time),
2449                     String.valueOf(time)
2450             };
2451             String sortOrder = Programs.COLUMN_START_TIME_UTC_MILLIS + " ASC";
2452 
2453             try (Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null,
2454                     null, sortOrder)) {
2455                 ContentValues values = new ContentValues();
2456                 if (cursor != null && cursor.moveToNext()) {
2457                     values.put(WatchedPrograms.COLUMN_TITLE, cursor.getString(0));
2458                     values.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, cursor.getLong(1));
2459                     values.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, cursor.getLong(2));
2460                     values.put(WatchedPrograms.COLUMN_DESCRIPTION, cursor.getString(3));
2461                 }
2462                 return values;
2463             }
2464         }
2465 
2466         // Duplicates the WatchedPrograms row with a given ID and returns the ID of the duplicated
2467         // row. Returns -1 if failed.
duplicateRow(long id)2468         private long duplicateRow(long id) {
2469             if (DEBUG) {
2470                 Log.d(TAG, "duplicateRow(" + id + ")");
2471             }
2472 
2473             SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
2474             queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
2475             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
2476 
2477             String[] projection = {
2478                     WatchedPrograms.COLUMN_PACKAGE_NAME,
2479                     WatchedPrograms.COLUMN_CHANNEL_ID,
2480                     WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN
2481             };
2482             String selection = WatchedPrograms._ID + "=" + String.valueOf(id);
2483 
2484             try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null,
2485                     null)) {
2486                 long rowId = -1;
2487                 if (cursor != null && cursor.moveToNext()) {
2488                     ContentValues values = new ContentValues();
2489                     values.put(WatchedPrograms.COLUMN_PACKAGE_NAME, cursor.getString(0));
2490                     values.put(WatchedPrograms.COLUMN_CHANNEL_ID, cursor.getLong(1));
2491                     values.put(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, cursor.getString(2));
2492                     rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values);
2493                 }
2494                 return rowId;
2495             }
2496         }
2497     }
2498 }
2499