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