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