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