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