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