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