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