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