1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.providers.tv;
18
19import android.annotation.SuppressLint;
20import android.app.AlarmManager;
21import android.app.PendingIntent;
22import android.content.ContentProvider;
23import android.content.ContentProviderOperation;
24import android.content.ContentProviderResult;
25import android.content.ContentValues;
26import android.content.Context;
27import android.content.Intent;
28import android.content.OperationApplicationException;
29import android.content.UriMatcher;
30import android.content.pm.PackageManager;
31import android.database.Cursor;
32import android.database.DatabaseUtils;
33import android.database.SQLException;
34import android.database.sqlite.SQLiteDatabase;
35import android.database.sqlite.SQLiteOpenHelper;
36import android.database.sqlite.SQLiteQueryBuilder;
37import android.graphics.Bitmap;
38import android.graphics.BitmapFactory;
39import android.media.tv.TvContract;
40import android.media.tv.TvContract.BaseTvColumns;
41import android.media.tv.TvContract.Channels;
42import android.media.tv.TvContract.Programs;
43import android.media.tv.TvContract.Programs.Genres;
44import android.media.tv.TvContract.RecordedPrograms;
45import android.media.tv.TvContract.WatchedPrograms;
46import android.net.Uri;
47import android.os.AsyncTask;
48import android.os.Handler;
49import android.os.Message;
50import android.os.ParcelFileDescriptor;
51import android.os.ParcelFileDescriptor.AutoCloseInputStream;
52import android.text.TextUtils;
53import android.text.format.DateUtils;
54import android.util.Log;
55
56import com.android.internal.annotations.VisibleForTesting;
57import com.android.internal.os.SomeArgs;
58import com.android.providers.tv.util.SqlParams;
59
60import libcore.io.IoUtils;
61
62import java.io.ByteArrayOutputStream;
63import java.io.FileNotFoundException;
64import java.io.IOException;
65import java.util.ArrayList;
66import java.util.HashMap;
67import java.util.HashSet;
68import java.util.Map;
69import java.util.Set;
70
71/**
72 * TV content provider. The contract between this provider and applications is defined in
73 * {@link android.media.tv.TvContract}.
74 */
75public class TvProvider extends ContentProvider {
76    private static final boolean DEBUG = false;
77    private static final String TAG = "TvProvider";
78
79    // Operation names for createSqlParams().
80    private static final String OP_QUERY = "query";
81    private static final String OP_UPDATE = "update";
82    private static final String OP_DELETE = "delete";
83
84    static final int DATABASE_VERSION = 31;
85    private static final String DATABASE_NAME = "tv.db";
86    private static final String CHANNELS_TABLE = "channels";
87    private static final String PROGRAMS_TABLE = "programs";
88    private static final String WATCHED_PROGRAMS_TABLE = "watched_programs";
89    private static final String RECORDED_PROGRAMS_TABLE = "recorded_programs";
90    private static final String DELETED_CHANNELS_TABLE = "deleted_channels";  // Deprecated
91    private static final String PROGRAMS_TABLE_PACKAGE_NAME_INDEX = "programs_package_name_index";
92    private static final String PROGRAMS_TABLE_CHANNEL_ID_INDEX = "programs_channel_id_index";
93    private static final String PROGRAMS_TABLE_START_TIME_INDEX = "programs_start_time_index";
94    private static final String PROGRAMS_TABLE_END_TIME_INDEX = "programs_end_time_index";
95    private static final String WATCHED_PROGRAMS_TABLE_CHANNEL_ID_INDEX =
96            "watched_programs_channel_id_index";
97    private static final String DEFAULT_PROGRAMS_SORT_ORDER = Programs.COLUMN_START_TIME_UTC_MILLIS
98            + " ASC";
99    private static final String DEFAULT_WATCHED_PROGRAMS_SORT_ORDER =
100            WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC";
101    private static final String CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE = CHANNELS_TABLE
102            + " INNER JOIN " + PROGRAMS_TABLE
103            + " ON (" + CHANNELS_TABLE + "." + Channels._ID + "="
104            + PROGRAMS_TABLE + "." + Programs.COLUMN_CHANNEL_ID + ")";
105
106    private static final UriMatcher sUriMatcher;
107    private static final int MATCH_CHANNEL = 1;
108    private static final int MATCH_CHANNEL_ID = 2;
109    private static final int MATCH_CHANNEL_ID_LOGO = 3;
110    private static final int MATCH_PASSTHROUGH_ID = 4;
111    private static final int MATCH_PROGRAM = 5;
112    private static final int MATCH_PROGRAM_ID = 6;
113    private static final int MATCH_WATCHED_PROGRAM = 7;
114    private static final int MATCH_WATCHED_PROGRAM_ID = 8;
115    private static final int MATCH_RECORDED_PROGRAM = 9;
116    private static final int MATCH_RECORDED_PROGRAM_ID = 10;
117
118    private static final String CHANNELS_COLUMN_LOGO = "logo";
119    private static final int MAX_LOGO_IMAGE_SIZE = 256;
120
121    // The internal column in the watched programs table to indicate whether the current log entry
122    // is consolidated or not. Unconsolidated entries may have columns with missing data.
123    private static final String WATCHED_PROGRAMS_COLUMN_CONSOLIDATED = "consolidated";
124
125    private static final long MAX_PROGRAM_DATA_DELAY_IN_MILLIS = 10 * 1000; // 10 seconds
126
127    private static final Map<String, String> sChannelProjectionMap;
128    private static final Map<String, String> sProgramProjectionMap;
129    private static final Map<String, String> sWatchedProgramProjectionMap;
130    private static final Map<String, String> sRecordedProgramProjectionMap;
131
132    static {
133        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
134        sUriMatcher.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL);
135        sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID);
136        sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#/logo", MATCH_CHANNEL_ID_LOGO);
137        sUriMatcher.addURI(TvContract.AUTHORITY, "passthrough/*", MATCH_PASSTHROUGH_ID);
138        sUriMatcher.addURI(TvContract.AUTHORITY, "program", MATCH_PROGRAM);
139        sUriMatcher.addURI(TvContract.AUTHORITY, "program/#", MATCH_PROGRAM_ID);
140        sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program", MATCH_WATCHED_PROGRAM);
141        sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID);
142        sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM);
143        sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID);
144
145        sChannelProjectionMap = new HashMap<>();
146        sChannelProjectionMap.put(Channels._ID, CHANNELS_TABLE + "." + Channels._ID);
147        sChannelProjectionMap.put(Channels.COLUMN_PACKAGE_NAME,
148                CHANNELS_TABLE + "." + Channels.COLUMN_PACKAGE_NAME);
149        sChannelProjectionMap.put(Channels.COLUMN_INPUT_ID,
150                CHANNELS_TABLE + "." + Channels.COLUMN_INPUT_ID);
151        sChannelProjectionMap.put(Channels.COLUMN_TYPE,
152                CHANNELS_TABLE + "." + Channels.COLUMN_TYPE);
153        sChannelProjectionMap.put(Channels.COLUMN_SERVICE_TYPE,
154                CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_TYPE);
155        sChannelProjectionMap.put(Channels.COLUMN_ORIGINAL_NETWORK_ID,
156                CHANNELS_TABLE + "." + Channels.COLUMN_ORIGINAL_NETWORK_ID);
157        sChannelProjectionMap.put(Channels.COLUMN_TRANSPORT_STREAM_ID,
158                CHANNELS_TABLE + "." + Channels.COLUMN_TRANSPORT_STREAM_ID);
159        sChannelProjectionMap.put(Channels.COLUMN_SERVICE_ID,
160                CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_ID);
161        sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NUMBER,
162                CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NUMBER);
163        sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NAME,
164                CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NAME);
165        sChannelProjectionMap.put(Channels.COLUMN_NETWORK_AFFILIATION,
166                CHANNELS_TABLE + "." + Channels.COLUMN_NETWORK_AFFILIATION);
167        sChannelProjectionMap.put(Channels.COLUMN_DESCRIPTION,
168                CHANNELS_TABLE + "." + Channels.COLUMN_DESCRIPTION);
169        sChannelProjectionMap.put(Channels.COLUMN_VIDEO_FORMAT,
170                CHANNELS_TABLE + "." + Channels.COLUMN_VIDEO_FORMAT);
171        sChannelProjectionMap.put(Channels.COLUMN_BROWSABLE,
172                CHANNELS_TABLE + "." + Channels.COLUMN_BROWSABLE);
173        sChannelProjectionMap.put(Channels.COLUMN_SEARCHABLE,
174                CHANNELS_TABLE + "." + Channels.COLUMN_SEARCHABLE);
175        sChannelProjectionMap.put(Channels.COLUMN_LOCKED,
176                CHANNELS_TABLE + "." + Channels.COLUMN_LOCKED);
177        sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_ICON_URI,
178                CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_ICON_URI);
179        sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_POSTER_ART_URI,
180                CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_POSTER_ART_URI);
181        sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_TEXT,
182                CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_TEXT);
183        sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_COLOR,
184                CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_COLOR);
185        sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_INTENT_URI,
186                CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_INTENT_URI);
187        sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA,
188                CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_DATA);
189        sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1,
190                CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1);
191        sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
192                CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2);
193        sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3,
194                CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3);
195        sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4,
196                CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4);
197        sChannelProjectionMap.put(Channels.COLUMN_VERSION_NUMBER,
198                CHANNELS_TABLE + "." + Channels.COLUMN_VERSION_NUMBER);
199
200        sProgramProjectionMap = new HashMap<>();
201        sProgramProjectionMap.put(Programs._ID, Programs._ID);
202        sProgramProjectionMap.put(Programs.COLUMN_PACKAGE_NAME, Programs.COLUMN_PACKAGE_NAME);
203        sProgramProjectionMap.put(Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_CHANNEL_ID);
204        sProgramProjectionMap.put(Programs.COLUMN_TITLE, Programs.COLUMN_TITLE);
205        // COLUMN_SEASON_NUMBER is deprecated. Return COLUMN_SEASON_DISPLAY_NUMBER instead.
206        sProgramProjectionMap.put(Programs.COLUMN_SEASON_NUMBER,
207                Programs.COLUMN_SEASON_DISPLAY_NUMBER + " AS " + Programs.COLUMN_SEASON_NUMBER);
208        sProgramProjectionMap.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER,
209                Programs.COLUMN_SEASON_DISPLAY_NUMBER);
210        sProgramProjectionMap.put(Programs.COLUMN_SEASON_TITLE, Programs.COLUMN_SEASON_TITLE);
211        // COLUMN_EPISODE_NUMBER is deprecated. Return COLUMN_EPISODE_DISPLAY_NUMBER instead.
212        sProgramProjectionMap.put(Programs.COLUMN_EPISODE_NUMBER,
213                Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " AS " + Programs.COLUMN_EPISODE_NUMBER);
214        sProgramProjectionMap.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
215                Programs.COLUMN_EPISODE_DISPLAY_NUMBER);
216        sProgramProjectionMap.put(Programs.COLUMN_EPISODE_TITLE, Programs.COLUMN_EPISODE_TITLE);
217        sProgramProjectionMap.put(Programs.COLUMN_START_TIME_UTC_MILLIS,
218                Programs.COLUMN_START_TIME_UTC_MILLIS);
219        sProgramProjectionMap.put(Programs.COLUMN_END_TIME_UTC_MILLIS,
220                Programs.COLUMN_END_TIME_UTC_MILLIS);
221        sProgramProjectionMap.put(Programs.COLUMN_BROADCAST_GENRE, Programs.COLUMN_BROADCAST_GENRE);
222        sProgramProjectionMap.put(Programs.COLUMN_CANONICAL_GENRE, Programs.COLUMN_CANONICAL_GENRE);
223        sProgramProjectionMap.put(Programs.COLUMN_SHORT_DESCRIPTION,
224                Programs.COLUMN_SHORT_DESCRIPTION);
225        sProgramProjectionMap.put(Programs.COLUMN_LONG_DESCRIPTION,
226                Programs.COLUMN_LONG_DESCRIPTION);
227        sProgramProjectionMap.put(Programs.COLUMN_VIDEO_WIDTH, Programs.COLUMN_VIDEO_WIDTH);
228        sProgramProjectionMap.put(Programs.COLUMN_VIDEO_HEIGHT, Programs.COLUMN_VIDEO_HEIGHT);
229        sProgramProjectionMap.put(Programs.COLUMN_AUDIO_LANGUAGE, Programs.COLUMN_AUDIO_LANGUAGE);
230        sProgramProjectionMap.put(Programs.COLUMN_CONTENT_RATING, Programs.COLUMN_CONTENT_RATING);
231        sProgramProjectionMap.put(Programs.COLUMN_POSTER_ART_URI, Programs.COLUMN_POSTER_ART_URI);
232        sProgramProjectionMap.put(Programs.COLUMN_THUMBNAIL_URI, Programs.COLUMN_THUMBNAIL_URI);
233        sProgramProjectionMap.put(Programs.COLUMN_SEARCHABLE, Programs.COLUMN_SEARCHABLE);
234        sProgramProjectionMap.put(Programs.COLUMN_RECORDING_PROHIBITED,
235                Programs.COLUMN_RECORDING_PROHIBITED);
236        sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_DATA,
237                Programs.COLUMN_INTERNAL_PROVIDER_DATA);
238        sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG1,
239                Programs.COLUMN_INTERNAL_PROVIDER_FLAG1);
240        sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG2,
241                Programs.COLUMN_INTERNAL_PROVIDER_FLAG2);
242        sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG3,
243                Programs.COLUMN_INTERNAL_PROVIDER_FLAG3);
244        sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG4,
245                Programs.COLUMN_INTERNAL_PROVIDER_FLAG4);
246        sProgramProjectionMap.put(Programs.COLUMN_VERSION_NUMBER, Programs.COLUMN_VERSION_NUMBER);
247
248        sWatchedProgramProjectionMap = new HashMap<>();
249        sWatchedProgramProjectionMap.put(WatchedPrograms._ID, WatchedPrograms._ID);
250        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
251                WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS);
252        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
253                WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS);
254        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_CHANNEL_ID,
255                WatchedPrograms.COLUMN_CHANNEL_ID);
256        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_TITLE,
257                WatchedPrograms.COLUMN_TITLE);
258        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS,
259                WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS);
260        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS,
261                WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS);
262        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_DESCRIPTION,
263                WatchedPrograms.COLUMN_DESCRIPTION);
264        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS,
265                WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS);
266        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN,
267                WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN);
268        sWatchedProgramProjectionMap.put(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED,
269                WATCHED_PROGRAMS_COLUMN_CONSOLIDATED);
270
271        sRecordedProgramProjectionMap = new HashMap<>();
272        sRecordedProgramProjectionMap.put(RecordedPrograms._ID, RecordedPrograms._ID);
273        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_PACKAGE_NAME,
274                RecordedPrograms.COLUMN_PACKAGE_NAME);
275        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INPUT_ID,
276                RecordedPrograms.COLUMN_INPUT_ID);
277        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CHANNEL_ID,
278                RecordedPrograms.COLUMN_CHANNEL_ID);
279        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_TITLE,
280                RecordedPrograms.COLUMN_TITLE);
281        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
282                RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER);
283        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_TITLE,
284                RecordedPrograms.COLUMN_SEASON_TITLE);
285        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
286                RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER);
287        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_TITLE,
288                RecordedPrograms.COLUMN_EPISODE_TITLE);
289        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
290                RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS);
291        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS,
292                RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS);
293        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_BROADCAST_GENRE,
294                RecordedPrograms.COLUMN_BROADCAST_GENRE);
295        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CANONICAL_GENRE,
296                RecordedPrograms.COLUMN_CANONICAL_GENRE);
297        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION,
298                RecordedPrograms.COLUMN_SHORT_DESCRIPTION);
299        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION,
300                RecordedPrograms.COLUMN_LONG_DESCRIPTION);
301        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_WIDTH,
302                RecordedPrograms.COLUMN_VIDEO_WIDTH);
303        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT,
304                RecordedPrograms.COLUMN_VIDEO_HEIGHT);
305        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE,
306                RecordedPrograms.COLUMN_AUDIO_LANGUAGE);
307        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CONTENT_RATING,
308                RecordedPrograms.COLUMN_CONTENT_RATING);
309        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_POSTER_ART_URI,
310                RecordedPrograms.COLUMN_POSTER_ART_URI);
311        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_THUMBNAIL_URI,
312                RecordedPrograms.COLUMN_THUMBNAIL_URI);
313        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEARCHABLE,
314                RecordedPrograms.COLUMN_SEARCHABLE);
315        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI,
316                RecordedPrograms.COLUMN_RECORDING_DATA_URI);
317        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES,
318                RecordedPrograms.COLUMN_RECORDING_DATA_BYTES);
319        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
320                RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS);
321        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
322                RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS);
323        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
324                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA);
325        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
326                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1);
327        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
328                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2);
329        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
330                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3);
331        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
332                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4);
333        sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VERSION_NUMBER,
334                RecordedPrograms.COLUMN_VERSION_NUMBER);
335    }
336
337    // Mapping from broadcast genre to canonical genre.
338    private static Map<String, String> sGenreMap;
339
340    private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
341
342    private static final String PERMISSION_ACCESS_ALL_EPG_DATA =
343            "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA";
344
345    private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS =
346            "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS";
347
348    private static final String CREATE_RECORDED_PROGRAMS_TABLE_SQL =
349            "CREATE TABLE " + RECORDED_PROGRAMS_TABLE + " ("
350            + RecordedPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
351            + RecordedPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
352            + RecordedPrograms.COLUMN_INPUT_ID + " TEXT NOT NULL,"
353            + RecordedPrograms.COLUMN_CHANNEL_ID + " INTEGER,"
354            + RecordedPrograms.COLUMN_TITLE + " TEXT,"
355            + RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER + " TEXT,"
356            + RecordedPrograms.COLUMN_SEASON_TITLE + " TEXT,"
357            + RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER + " TEXT,"
358            + RecordedPrograms.COLUMN_EPISODE_TITLE + " TEXT,"
359            + RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS + " INTEGER,"
360            + RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS + " INTEGER,"
361            + RecordedPrograms.COLUMN_BROADCAST_GENRE + " TEXT,"
362            + RecordedPrograms.COLUMN_CANONICAL_GENRE + " TEXT,"
363            + RecordedPrograms.COLUMN_SHORT_DESCRIPTION + " TEXT,"
364            + RecordedPrograms.COLUMN_LONG_DESCRIPTION + " TEXT,"
365            + RecordedPrograms.COLUMN_VIDEO_WIDTH + " INTEGER,"
366            + RecordedPrograms.COLUMN_VIDEO_HEIGHT + " INTEGER,"
367            + RecordedPrograms.COLUMN_AUDIO_LANGUAGE + " TEXT,"
368            + RecordedPrograms.COLUMN_CONTENT_RATING + " TEXT,"
369            + RecordedPrograms.COLUMN_POSTER_ART_URI + " TEXT,"
370            + RecordedPrograms.COLUMN_THUMBNAIL_URI + " TEXT,"
371            + RecordedPrograms.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
372            + RecordedPrograms.COLUMN_RECORDING_DATA_URI + " TEXT,"
373            + RecordedPrograms.COLUMN_RECORDING_DATA_BYTES + " INTEGER,"
374            + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS + " INTEGER,"
375            + RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS + " INTEGER,"
376            + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
377            + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
378            + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
379            + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
380            + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
381            + RecordedPrograms.COLUMN_VERSION_NUMBER + " INTEGER,"
382            + "FOREIGN KEY(" + RecordedPrograms.COLUMN_CHANNEL_ID + ") "
383                    + "REFERENCES " + CHANNELS_TABLE + "(" + Channels._ID + ") "
384                    + "ON UPDATE CASCADE ON DELETE SET NULL);";
385
386    static class DatabaseHelper extends SQLiteOpenHelper {
387        private static DatabaseHelper sSingleton = null;
388
389        public static synchronized DatabaseHelper getInstance(Context context) {
390            if (sSingleton == null) {
391                sSingleton = new DatabaseHelper(context);
392            }
393            return sSingleton;
394        }
395
396        private DatabaseHelper(Context context) {
397            super(context, DATABASE_NAME, null, DATABASE_VERSION);
398        }
399
400        @Override
401        public void onConfigure(SQLiteDatabase db) {
402            db.setForeignKeyConstraintsEnabled(true);
403        }
404
405        @Override
406        public void onCreate(SQLiteDatabase db) {
407            if (DEBUG) {
408                Log.d(TAG, "Creating database");
409            }
410            // Set up the database schema.
411            db.execSQL("CREATE TABLE " + CHANNELS_TABLE + " ("
412                    + Channels._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
413                    + Channels.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
414                    + Channels.COLUMN_INPUT_ID + " TEXT NOT NULL,"
415                    + Channels.COLUMN_TYPE + " TEXT NOT NULL DEFAULT '" + Channels.TYPE_OTHER + "',"
416                    + Channels.COLUMN_SERVICE_TYPE + " TEXT NOT NULL DEFAULT '"
417                    + Channels.SERVICE_TYPE_AUDIO_VIDEO + "',"
418                    + Channels.COLUMN_ORIGINAL_NETWORK_ID + " INTEGER NOT NULL DEFAULT 0,"
419                    + Channels.COLUMN_TRANSPORT_STREAM_ID + " INTEGER NOT NULL DEFAULT 0,"
420                    + Channels.COLUMN_SERVICE_ID + " INTEGER NOT NULL DEFAULT 0,"
421                    + Channels.COLUMN_DISPLAY_NUMBER + " TEXT,"
422                    + Channels.COLUMN_DISPLAY_NAME + " TEXT,"
423                    + Channels.COLUMN_NETWORK_AFFILIATION + " TEXT,"
424                    + Channels.COLUMN_DESCRIPTION + " TEXT,"
425                    + Channels.COLUMN_VIDEO_FORMAT + " TEXT,"
426                    + Channels.COLUMN_BROWSABLE + " INTEGER NOT NULL DEFAULT 0,"
427                    + Channels.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
428                    + Channels.COLUMN_LOCKED + " INTEGER NOT NULL DEFAULT 0,"
429                    + Channels.COLUMN_APP_LINK_ICON_URI + " TEXT,"
430                    + Channels.COLUMN_APP_LINK_POSTER_ART_URI + " TEXT,"
431                    + Channels.COLUMN_APP_LINK_TEXT + " TEXT,"
432                    + Channels.COLUMN_APP_LINK_COLOR + " INTEGER,"
433                    + Channels.COLUMN_APP_LINK_INTENT_URI + " TEXT,"
434                    + Channels.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
435                    + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
436                    + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
437                    + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
438                    + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
439                    + CHANNELS_COLUMN_LOGO + " BLOB,"
440                    + Channels.COLUMN_VERSION_NUMBER + " INTEGER,"
441                    // Needed for foreign keys in other tables.
442                    + "UNIQUE(" + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME + ")"
443                    + ");");
444            db.execSQL("CREATE TABLE " + PROGRAMS_TABLE + " ("
445                    + Programs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
446                    + Programs.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
447                    + Programs.COLUMN_CHANNEL_ID + " INTEGER,"
448                    + Programs.COLUMN_TITLE + " TEXT,"
449                    + Programs.COLUMN_SEASON_DISPLAY_NUMBER + " TEXT,"
450                    + Programs.COLUMN_SEASON_TITLE + " TEXT,"
451                    + Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " TEXT,"
452                    + Programs.COLUMN_EPISODE_TITLE + " TEXT,"
453                    + Programs.COLUMN_START_TIME_UTC_MILLIS + " INTEGER,"
454                    + Programs.COLUMN_END_TIME_UTC_MILLIS + " INTEGER,"
455                    + Programs.COLUMN_BROADCAST_GENRE + " TEXT,"
456                    + Programs.COLUMN_CANONICAL_GENRE + " TEXT,"
457                    + Programs.COLUMN_SHORT_DESCRIPTION + " TEXT,"
458                    + Programs.COLUMN_LONG_DESCRIPTION + " TEXT,"
459                    + Programs.COLUMN_VIDEO_WIDTH + " INTEGER,"
460                    + Programs.COLUMN_VIDEO_HEIGHT + " INTEGER,"
461                    + Programs.COLUMN_AUDIO_LANGUAGE + " TEXT,"
462                    + Programs.COLUMN_CONTENT_RATING + " TEXT,"
463                    + Programs.COLUMN_POSTER_ART_URI + " TEXT,"
464                    + Programs.COLUMN_THUMBNAIL_URI + " TEXT,"
465                    + Programs.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
466                    + Programs.COLUMN_RECORDING_PROHIBITED + " INTEGER NOT NULL DEFAULT 0,"
467                    + Programs.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
468                    + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
469                    + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
470                    + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
471                    + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
472                    + Programs.COLUMN_VERSION_NUMBER + " INTEGER,"
473                    + "FOREIGN KEY("
474                            + Programs.COLUMN_CHANNEL_ID + "," + Programs.COLUMN_PACKAGE_NAME
475                            + ") REFERENCES " + CHANNELS_TABLE + "("
476                            + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME
477                            + ") ON UPDATE CASCADE ON DELETE CASCADE"
478                    + ");");
479            db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_PACKAGE_NAME_INDEX + " ON " + PROGRAMS_TABLE
480                    + "(" + Programs.COLUMN_PACKAGE_NAME + ");");
481            db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_CHANNEL_ID_INDEX + " ON " + PROGRAMS_TABLE
482                    + "(" + Programs.COLUMN_CHANNEL_ID + ");");
483            db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_START_TIME_INDEX + " ON " + PROGRAMS_TABLE
484                    + "(" + Programs.COLUMN_START_TIME_UTC_MILLIS + ");");
485            db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_END_TIME_INDEX + " ON " + PROGRAMS_TABLE
486                    + "(" + Programs.COLUMN_END_TIME_UTC_MILLIS + ");");
487            db.execSQL("CREATE TABLE " + WATCHED_PROGRAMS_TABLE + " ("
488                    + WatchedPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
489                    + WatchedPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
490                    + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS
491                    + " INTEGER NOT NULL DEFAULT 0,"
492                    + WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS
493                    + " INTEGER NOT NULL DEFAULT 0,"
494                    + WatchedPrograms.COLUMN_CHANNEL_ID + " INTEGER,"
495                    + WatchedPrograms.COLUMN_TITLE + " TEXT,"
496                    + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS + " INTEGER,"
497                    + WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS + " INTEGER,"
498                    + WatchedPrograms.COLUMN_DESCRIPTION + " TEXT,"
499                    + WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS + " TEXT,"
500                    + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + " TEXT NOT NULL,"
501                    + WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + " INTEGER NOT NULL DEFAULT 0,"
502                    + "FOREIGN KEY("
503                            + WatchedPrograms.COLUMN_CHANNEL_ID + ","
504                            + WatchedPrograms.COLUMN_PACKAGE_NAME
505                            + ") REFERENCES " + CHANNELS_TABLE + "("
506                            + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME
507                            + ") ON UPDATE CASCADE ON DELETE CASCADE"
508                    + ");");
509            db.execSQL("CREATE INDEX " + WATCHED_PROGRAMS_TABLE_CHANNEL_ID_INDEX + " ON "
510                    + WATCHED_PROGRAMS_TABLE + "(" + WatchedPrograms.COLUMN_CHANNEL_ID + ");");
511            db.execSQL(CREATE_RECORDED_PROGRAMS_TABLE_SQL);
512        }
513
514        @Override
515        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
516            if (oldVersion < 23) {
517                Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion
518                        + ", data will be lost!");
519                db.execSQL("DROP TABLE IF EXISTS " + DELETED_CHANNELS_TABLE);
520                db.execSQL("DROP TABLE IF EXISTS " + WATCHED_PROGRAMS_TABLE);
521                db.execSQL("DROP TABLE IF EXISTS " + PROGRAMS_TABLE);
522                db.execSQL("DROP TABLE IF EXISTS " + CHANNELS_TABLE);
523
524                onCreate(db);
525                return;
526            }
527
528            Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion + ".");
529            if (oldVersion == 23) {
530                db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
531                        + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER;");
532                db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
533                        + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER;");
534                db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
535                        + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER;");
536                db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
537                        + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER;");
538                oldVersion++;
539            }
540            if (oldVersion == 24) {
541                db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
542                        + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER;");
543                db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
544                        + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER;");
545                db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
546                        + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER;");
547                db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
548                        + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER;");
549                oldVersion++;
550            }
551            if (oldVersion == 25) {
552                db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
553                        + Channels.COLUMN_APP_LINK_ICON_URI + " TEXT;");
554                db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
555                        + Channels.COLUMN_APP_LINK_POSTER_ART_URI + " TEXT;");
556                db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
557                        + Channels.COLUMN_APP_LINK_TEXT + " TEXT;");
558                db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
559                        + Channels.COLUMN_APP_LINK_COLOR + " INTEGER;");
560                db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
561                        + Channels.COLUMN_APP_LINK_INTENT_URI + " TEXT;");
562                db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
563                        + Programs.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1;");
564                oldVersion++;
565            }
566            if (oldVersion <= 28) {
567                db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
568                        + Programs.COLUMN_SEASON_TITLE + " TEXT;");
569                migrateIntegerColumnToTextColumn(db, PROGRAMS_TABLE, Programs.COLUMN_SEASON_NUMBER,
570                        Programs.COLUMN_SEASON_DISPLAY_NUMBER);
571                migrateIntegerColumnToTextColumn(db, PROGRAMS_TABLE, Programs.COLUMN_EPISODE_NUMBER,
572                        Programs.COLUMN_EPISODE_DISPLAY_NUMBER);
573                oldVersion = 29;
574            }
575            if (oldVersion == 29) {
576                db.execSQL("DROP TABLE IF EXISTS " + RECORDED_PROGRAMS_TABLE);
577                db.execSQL(CREATE_RECORDED_PROGRAMS_TABLE_SQL);
578                oldVersion = 30;
579            }
580            if (oldVersion == 30) {
581                db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
582                        + Programs.COLUMN_RECORDING_PROHIBITED + " INTEGER NOT NULL DEFAULT 0;");
583                oldVersion = 31;
584            }
585            Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion + " is done.");
586        }
587
588        private static void migrateIntegerColumnToTextColumn(SQLiteDatabase db, String table,
589                String integerColumn, String textColumn) {
590            db.execSQL("ALTER TABLE " + table + " ADD " + textColumn + " TEXT;");
591            db.execSQL("UPDATE " + table + " SET " + textColumn + " = CAST(" + integerColumn
592                    + " AS TEXT);");
593        }
594    }
595
596    private DatabaseHelper mOpenHelper;
597
598    private final Handler mLogHandler = new WatchLogHandler();
599
600    @Override
601    public boolean onCreate() {
602        if (DEBUG) {
603            Log.d(TAG, "Creating TvProvider");
604        }
605        mOpenHelper = DatabaseHelper.getInstance(getContext());
606        scheduleEpgDataCleanup();
607        buildGenreMap();
608
609        // DB operation, which may trigger upgrade, should not happen in onCreate.
610        new AsyncTask<Void, Void, Void>() {
611            @Override
612            protected Void doInBackground(Void... params) {
613                deleteUnconsolidatedWatchedProgramsRows();
614                return null;
615            }
616        }.execute();
617        return true;
618    }
619
620    @VisibleForTesting
621    void scheduleEpgDataCleanup() {
622        Intent intent = new Intent(EpgDataCleanupService.ACTION_CLEAN_UP_EPG_DATA);
623        intent.setClass(getContext(), EpgDataCleanupService.class);
624        PendingIntent pendingIntent = PendingIntent.getService(
625                getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
626        AlarmManager alarmManager =
627                (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
628        alarmManager.setInexactRepeating(AlarmManager.RTC, System.currentTimeMillis(),
629                AlarmManager.INTERVAL_HALF_DAY, pendingIntent);
630    }
631
632    private void buildGenreMap() {
633        if (sGenreMap != null) {
634            return;
635        }
636
637        sGenreMap = new HashMap<>();
638        buildGenreMap(R.array.genre_mapping_atsc);
639        buildGenreMap(R.array.genre_mapping_dvb);
640        buildGenreMap(R.array.genre_mapping_isdb);
641        buildGenreMap(R.array.genre_mapping_isdb_br);
642    }
643
644    @SuppressLint("DefaultLocale")
645    private void buildGenreMap(int id) {
646        String[] maps = getContext().getResources().getStringArray(id);
647        for (String map : maps) {
648            String[] arr = map.split("\\|");
649            if (arr.length != 2) {
650                throw new IllegalArgumentException("Invalid genre mapping : " + map);
651            }
652            sGenreMap.put(arr[0].toUpperCase(), arr[1]);
653        }
654    }
655
656    @VisibleForTesting
657    String getCallingPackage_() {
658        return getCallingPackage();
659    }
660
661    @Override
662    public String getType(Uri uri) {
663        switch (sUriMatcher.match(uri)) {
664            case MATCH_CHANNEL:
665                return Channels.CONTENT_TYPE;
666            case MATCH_CHANNEL_ID:
667                return Channels.CONTENT_ITEM_TYPE;
668            case MATCH_CHANNEL_ID_LOGO:
669                return "image/png";
670            case MATCH_PASSTHROUGH_ID:
671                return Channels.CONTENT_ITEM_TYPE;
672            case MATCH_PROGRAM:
673                return Programs.CONTENT_TYPE;
674            case MATCH_PROGRAM_ID:
675                return Programs.CONTENT_ITEM_TYPE;
676            case MATCH_WATCHED_PROGRAM:
677                return WatchedPrograms.CONTENT_TYPE;
678            case MATCH_WATCHED_PROGRAM_ID:
679                return WatchedPrograms.CONTENT_ITEM_TYPE;
680            case MATCH_RECORDED_PROGRAM:
681                return RecordedPrograms.CONTENT_TYPE;
682            case MATCH_RECORDED_PROGRAM_ID:
683                return RecordedPrograms.CONTENT_ITEM_TYPE;
684            default:
685                throw new IllegalArgumentException("Unknown URI " + uri);
686        }
687    }
688
689    @Override
690    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
691            String sortOrder) {
692        boolean needsToValidateSortOrder = !callerHasAccessAllEpgDataPermission();
693        SqlParams params = createSqlParams(OP_QUERY, uri, selection, selectionArgs);
694
695        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
696        queryBuilder.setStrict(needsToValidateSortOrder);
697        queryBuilder.setTables(params.getTables());
698        String orderBy = null;
699        Map<String, String> projectionMap;
700        switch (params.getTables()) {
701            case PROGRAMS_TABLE:
702                projectionMap = sProgramProjectionMap;
703                orderBy = DEFAULT_PROGRAMS_SORT_ORDER;
704                break;
705            case WATCHED_PROGRAMS_TABLE:
706                projectionMap = sWatchedProgramProjectionMap;
707                orderBy = DEFAULT_WATCHED_PROGRAMS_SORT_ORDER;
708                break;
709            case RECORDED_PROGRAMS_TABLE:
710                projectionMap = sRecordedProgramProjectionMap;
711                break;
712            default:
713                projectionMap = sChannelProjectionMap;
714                break;
715        }
716        queryBuilder.setProjectionMap(projectionMap);
717        if (needsToValidateSortOrder) {
718            validateSortOrder(sortOrder, projectionMap.keySet());
719        }
720
721        // Use the default sort order only if no sort order is specified.
722        if (!TextUtils.isEmpty(sortOrder)) {
723            orderBy = sortOrder;
724        }
725
726        // Get the database and run the query.
727        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
728        Cursor c = queryBuilder.query(db, projection, params.getSelection(),
729                params.getSelectionArgs(), null, null, orderBy);
730
731        // Tell the cursor what URI to watch, so it knows when its source data changes.
732        c.setNotificationUri(getContext().getContentResolver(), uri);
733        return c;
734    }
735
736    @Override
737    public Uri insert(Uri uri, ContentValues values) {
738        switch (sUriMatcher.match(uri)) {
739            case MATCH_CHANNEL:
740                return insertChannel(uri, values);
741            case MATCH_PROGRAM:
742                return insertProgram(uri, values);
743            case MATCH_WATCHED_PROGRAM:
744                return insertWatchedProgram(uri, values);
745            case MATCH_RECORDED_PROGRAM:
746                return insertRecordedProgram(uri, values);
747            case MATCH_CHANNEL_ID:
748            case MATCH_CHANNEL_ID_LOGO:
749            case MATCH_PASSTHROUGH_ID:
750            case MATCH_PROGRAM_ID:
751            case MATCH_WATCHED_PROGRAM_ID:
752            case MATCH_RECORDED_PROGRAM_ID:
753                throw new UnsupportedOperationException("Cannot insert into that URI: " + uri);
754            default:
755                throw new IllegalArgumentException("Unknown URI " + uri);
756        }
757    }
758
759    private Uri insertChannel(Uri uri, ContentValues values) {
760        // Mark the owner package of this channel.
761        values.put(Channels.COLUMN_PACKAGE_NAME, getCallingPackage_());
762
763        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
764        long rowId = db.insert(CHANNELS_TABLE, null, values);
765        if (rowId > 0) {
766            Uri channelUri = TvContract.buildChannelUri(rowId);
767            notifyChange(channelUri);
768            return channelUri;
769        }
770
771        throw new SQLException("Failed to insert row into " + uri);
772    }
773
774    private Uri insertProgram(Uri uri, ContentValues values) {
775        // Mark the owner package of this program.
776        values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
777
778        checkAndConvertGenre(values);
779        checkAndConvertDeprecatedColumns(values);
780
781        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
782        long rowId = db.insert(PROGRAMS_TABLE, null, values);
783        if (rowId > 0) {
784            Uri programUri = TvContract.buildProgramUri(rowId);
785            notifyChange(programUri);
786            return programUri;
787        }
788
789        throw new SQLException("Failed to insert row into " + uri);
790    }
791
792    private Uri insertWatchedProgram(Uri uri, ContentValues values) {
793        if (DEBUG) {
794            Log.d(TAG, "insertWatchedProgram(uri=" + uri + ", values={" + values + "})");
795        }
796        Long watchStartTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS);
797        Long watchEndTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS);
798        // The system sends only two kinds of watch events:
799        // 1. The user tunes to a new channel. (COLUMN_WATCH_START_TIME_UTC_MILLIS)
800        // 2. The user stops watching. (COLUMN_WATCH_END_TIME_UTC_MILLIS)
801        if (watchStartTime != null && watchEndTime == null) {
802            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
803            long rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values);
804            if (rowId > 0) {
805                mLogHandler.removeMessages(WatchLogHandler.MSG_TRY_CONSOLIDATE_ALL);
806                mLogHandler.sendEmptyMessageDelayed(WatchLogHandler.MSG_TRY_CONSOLIDATE_ALL,
807                        MAX_PROGRAM_DATA_DELAY_IN_MILLIS);
808                return TvContract.buildWatchedProgramUri(rowId);
809            }
810            Log.w(TAG, "Failed to insert row for " + values + ". Channel does not exist.");
811            return null;
812        } else if (watchStartTime == null && watchEndTime != null) {
813            SomeArgs args = SomeArgs.obtain();
814            args.arg1 = values.getAsString(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN);
815            args.arg2 = watchEndTime;
816            Message msg = mLogHandler.obtainMessage(WatchLogHandler.MSG_CONSOLIDATE, args);
817            mLogHandler.sendMessageDelayed(msg, MAX_PROGRAM_DATA_DELAY_IN_MILLIS);
818            return null;
819        }
820        // All the other cases are invalid.
821        throw new IllegalArgumentException("Only one of COLUMN_WATCH_START_TIME_UTC_MILLIS and"
822                + " COLUMN_WATCH_END_TIME_UTC_MILLIS should be specified");
823    }
824
825    private Uri insertRecordedProgram(Uri uri, ContentValues values) {
826        // Mark the owner package of this program.
827        values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
828
829        checkAndConvertGenre(values);
830
831        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
832        long rowId = db.insert(RECORDED_PROGRAMS_TABLE, null, values);
833        if (rowId > 0) {
834            Uri recordedProgramUri = TvContract.buildRecordedProgramUri(rowId);
835            notifyChange(recordedProgramUri);
836            return recordedProgramUri;
837        }
838
839        throw new SQLException("Failed to insert row into " + uri);
840    }
841
842    @Override
843    public int delete(Uri uri, String selection, String[] selectionArgs) {
844        SqlParams params = createSqlParams(OP_DELETE, uri, selection, selectionArgs);
845        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
846        int count;
847        switch (sUriMatcher.match(uri)) {
848            case MATCH_CHANNEL_ID_LOGO:
849                ContentValues values = new ContentValues();
850                values.putNull(CHANNELS_COLUMN_LOGO);
851                count = db.update(params.getTables(), values, params.getSelection(),
852                        params.getSelectionArgs());
853                break;
854            case MATCH_CHANNEL:
855            case MATCH_PROGRAM:
856            case MATCH_WATCHED_PROGRAM:
857            case MATCH_RECORDED_PROGRAM:
858            case MATCH_CHANNEL_ID:
859            case MATCH_PASSTHROUGH_ID:
860            case MATCH_PROGRAM_ID:
861            case MATCH_WATCHED_PROGRAM_ID:
862            case MATCH_RECORDED_PROGRAM_ID:
863                count = db.delete(params.getTables(), params.getSelection(),
864                        params.getSelectionArgs());
865                break;
866            default:
867                throw new IllegalArgumentException("Unknown URI " + uri);
868        }
869        if (count > 0) {
870            notifyChange(uri);
871        }
872        return count;
873    }
874
875    @Override
876    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
877        SqlParams params = createSqlParams(OP_UPDATE, uri, selection, selectionArgs);
878        if (params.getTables().equals(CHANNELS_TABLE)) {
879            if (values.containsKey(Channels.COLUMN_LOCKED)
880                    && !callerHasModifyParentalControlsPermission()) {
881                throw new SecurityException("Not allowed to modify Channels.COLUMN_LOCKED");
882            }
883        } else if (params.getTables().equals(PROGRAMS_TABLE)) {
884            checkAndConvertGenre(values);
885            checkAndConvertDeprecatedColumns(values);
886        } else if (params.getTables().equals(RECORDED_PROGRAMS_TABLE)) {
887            checkAndConvertGenre(values);
888        }
889        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
890        int count = db.update(params.getTables(), values, params.getSelection(),
891                params.getSelectionArgs());
892        if (count > 0) {
893            notifyChange(uri);
894        }
895        return count;
896    }
897
898    private SqlParams createSqlParams(String operation, Uri uri, String selection,
899            String[] selectionArgs) {
900        int match = sUriMatcher.match(uri);
901        SqlParams params = new SqlParams(null, selection, selectionArgs);
902
903        // Control access to EPG data (excluding watched programs) when the caller doesn't have all
904        // access.
905        if (!callerHasAccessAllEpgDataPermission()
906                && match != MATCH_WATCHED_PROGRAM && match != MATCH_WATCHED_PROGRAM_ID) {
907            if (!TextUtils.isEmpty(selection)) {
908                throw new SecurityException("Selection not allowed for " + uri);
909            }
910            // Limit the operation only to the data that the calling package owns except for when
911            // the caller tries to read TV listings and has the appropriate permission.
912            String prefix = match == MATCH_CHANNEL ? CHANNELS_TABLE + "." : "";
913            if (operation.equals(OP_QUERY) && callerHasReadTvListingsPermission()) {
914                params.setWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=? OR "
915                        + Channels.COLUMN_SEARCHABLE + "=?", getCallingPackage_(), "1");
916            } else {
917                params.setWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=?",
918                        getCallingPackage_());
919            }
920        }
921
922        switch (match) {
923            case MATCH_CHANNEL:
924                String genre = uri.getQueryParameter(TvContract.PARAM_CANONICAL_GENRE);
925                if (genre == null) {
926                    params.setTables(CHANNELS_TABLE);
927                } else {
928                    if (!operation.equals(OP_QUERY)) {
929                        throw new SecurityException(capitalize(operation)
930                                + " not allowed for " + uri);
931                    }
932                    if (!Genres.isCanonical(genre)) {
933                        throw new IllegalArgumentException("Not a canonical genre : " + genre);
934                    }
935                    params.setTables(CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE);
936                    String curTime = String.valueOf(System.currentTimeMillis());
937                    params.appendWhere("LIKE(?, " + Programs.COLUMN_CANONICAL_GENRE + ") AND "
938                            + Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
939                            + Programs.COLUMN_END_TIME_UTC_MILLIS + ">=?",
940                            "%" + genre + "%", curTime, curTime);
941                }
942                String inputId = uri.getQueryParameter(TvContract.PARAM_INPUT);
943                if (inputId != null) {
944                    params.appendWhere(Channels.COLUMN_INPUT_ID + "=?", inputId);
945                }
946                boolean browsableOnly = uri.getBooleanQueryParameter(
947                        TvContract.PARAM_BROWSABLE_ONLY, false);
948                if (browsableOnly) {
949                    params.appendWhere(Channels.COLUMN_BROWSABLE + "=1");
950                }
951                break;
952            case MATCH_CHANNEL_ID:
953                params.setTables(CHANNELS_TABLE);
954                params.appendWhere(Channels._ID + "=?", uri.getLastPathSegment());
955                break;
956            case MATCH_PROGRAM:
957                params.setTables(PROGRAMS_TABLE);
958                String paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL);
959                if (paramChannelId != null) {
960                    String channelId = String.valueOf(Long.parseLong(paramChannelId));
961                    params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId);
962                }
963                String paramStartTime = uri.getQueryParameter(TvContract.PARAM_START_TIME);
964                String paramEndTime = uri.getQueryParameter(TvContract.PARAM_END_TIME);
965                if (paramStartTime != null && paramEndTime != null) {
966                    String startTime = String.valueOf(Long.parseLong(paramStartTime));
967                    String endTime = String.valueOf(Long.parseLong(paramEndTime));
968                    params.appendWhere(Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
969                            + Programs.COLUMN_END_TIME_UTC_MILLIS + ">=?", endTime, startTime);
970                }
971                break;
972            case MATCH_PROGRAM_ID:
973                params.setTables(PROGRAMS_TABLE);
974                params.appendWhere(Programs._ID + "=?", uri.getLastPathSegment());
975                break;
976            case MATCH_WATCHED_PROGRAM:
977                if (!callerHasAccessWatchedProgramsPermission()) {
978                    throw new SecurityException("Access not allowed for " + uri);
979                }
980                params.setTables(WATCHED_PROGRAMS_TABLE);
981                params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1");
982                break;
983            case MATCH_WATCHED_PROGRAM_ID:
984                if (!callerHasAccessWatchedProgramsPermission()) {
985                    throw new SecurityException("Access not allowed for " + uri);
986                }
987                params.setTables(WATCHED_PROGRAMS_TABLE);
988                params.appendWhere(WatchedPrograms._ID + "=?", uri.getLastPathSegment());
989                params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1");
990                break;
991            case MATCH_RECORDED_PROGRAM_ID:
992                params.appendWhere(RecordedPrograms._ID + "=?", uri.getLastPathSegment());
993                // fall-through
994            case MATCH_RECORDED_PROGRAM:
995                params.setTables(RECORDED_PROGRAMS_TABLE);
996                paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL);
997                if (paramChannelId != null) {
998                    String channelId = String.valueOf(Long.parseLong(paramChannelId));
999                    params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId);
1000                }
1001                break;
1002            case MATCH_CHANNEL_ID_LOGO:
1003                if (operation.equals(OP_DELETE)) {
1004                    params.setTables(CHANNELS_TABLE);
1005                    params.appendWhere(Channels._ID + "=?", uri.getPathSegments().get(1));
1006                    break;
1007                }
1008                // fall-through
1009            case MATCH_PASSTHROUGH_ID:
1010                throw new UnsupportedOperationException(operation + " not permmitted on " + uri);
1011            default:
1012                throw new IllegalArgumentException("Unknown URI " + uri);
1013        }
1014        return params;
1015    }
1016
1017    private static String capitalize(String str) {
1018        return Character.toUpperCase(str.charAt(0)) + str.substring(1);
1019    }
1020
1021    @SuppressLint("DefaultLocale")
1022    private void checkAndConvertGenre(ContentValues values) {
1023        String canonicalGenres = values.getAsString(Programs.COLUMN_CANONICAL_GENRE);
1024
1025        if (!TextUtils.isEmpty(canonicalGenres)) {
1026            // Check if the canonical genres are valid. If not, clear them.
1027            String[] genres = Genres.decode(canonicalGenres);
1028            for (String genre : genres) {
1029                if (!Genres.isCanonical(genre)) {
1030                    values.putNull(Programs.COLUMN_CANONICAL_GENRE);
1031                    canonicalGenres = null;
1032                    break;
1033                }
1034            }
1035        }
1036
1037        if (TextUtils.isEmpty(canonicalGenres)) {
1038            // If the canonical genre is not set, try to map the broadcast genre to the canonical
1039            // genre.
1040            String broadcastGenres = values.getAsString(Programs.COLUMN_BROADCAST_GENRE);
1041            if (!TextUtils.isEmpty(broadcastGenres)) {
1042                Set<String> genreSet = new HashSet<>();
1043                String[] genres = Genres.decode(broadcastGenres);
1044                for (String genre : genres) {
1045                    String canonicalGenre = sGenreMap.get(genre.toUpperCase());
1046                    if (Genres.isCanonical(canonicalGenre)) {
1047                        genreSet.add(canonicalGenre);
1048                    }
1049                }
1050                if (genreSet.size() > 0) {
1051                    values.put(Programs.COLUMN_CANONICAL_GENRE,
1052                            Genres.encode(genreSet.toArray(new String[genreSet.size()])));
1053                }
1054            }
1055        }
1056    }
1057
1058    private void checkAndConvertDeprecatedColumns(ContentValues values) {
1059        if (values.containsKey(Programs.COLUMN_SEASON_NUMBER)) {
1060            if (!values.containsKey(Programs.COLUMN_SEASON_DISPLAY_NUMBER)) {
1061                values.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, values.getAsInteger(
1062                        Programs.COLUMN_SEASON_NUMBER));
1063            }
1064            values.remove(Programs.COLUMN_SEASON_NUMBER);
1065        }
1066        if (values.containsKey(Programs.COLUMN_EPISODE_NUMBER)) {
1067            if (!values.containsKey(Programs.COLUMN_EPISODE_DISPLAY_NUMBER)) {
1068                values.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER, values.getAsInteger(
1069                        Programs.COLUMN_EPISODE_NUMBER));
1070            }
1071            values.remove(Programs.COLUMN_EPISODE_NUMBER);
1072        }
1073    }
1074
1075    // We might have more than one thread trying to make its way through applyBatch() so the
1076    // notification coalescing needs to be thread-local to work correctly.
1077    private final ThreadLocal<Set<Uri>> mTLBatchNotifications = new ThreadLocal<>();
1078
1079    private Set<Uri> getBatchNotificationsSet() {
1080        return mTLBatchNotifications.get();
1081    }
1082
1083    private void setBatchNotificationsSet(Set<Uri> batchNotifications) {
1084        mTLBatchNotifications.set(batchNotifications);
1085    }
1086
1087    @Override
1088    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
1089            throws OperationApplicationException {
1090        setBatchNotificationsSet(new HashSet<Uri>());
1091        Context context = getContext();
1092        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1093        db.beginTransaction();
1094        try {
1095            ContentProviderResult[] results = super.applyBatch(operations);
1096            db.setTransactionSuccessful();
1097            return results;
1098        } finally {
1099            db.endTransaction();
1100            final Set<Uri> notifications = getBatchNotificationsSet();
1101            setBatchNotificationsSet(null);
1102            for (final Uri uri : notifications) {
1103                context.getContentResolver().notifyChange(uri, null);
1104            }
1105        }
1106    }
1107
1108    @Override
1109    public int bulkInsert(Uri uri, ContentValues[] values) {
1110        setBatchNotificationsSet(new HashSet<Uri>());
1111        Context context = getContext();
1112        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1113        db.beginTransaction();
1114        try {
1115            int result = super.bulkInsert(uri, values);
1116            db.setTransactionSuccessful();
1117            return result;
1118        } finally {
1119            db.endTransaction();
1120            final Set<Uri> notifications = getBatchNotificationsSet();
1121            setBatchNotificationsSet(null);
1122            for (final Uri notificationUri : notifications) {
1123                context.getContentResolver().notifyChange(notificationUri, null);
1124            }
1125        }
1126    }
1127
1128    private void notifyChange(Uri uri) {
1129        final Set<Uri> batchNotifications = getBatchNotificationsSet();
1130        if (batchNotifications != null) {
1131            batchNotifications.add(uri);
1132        } else {
1133            getContext().getContentResolver().notifyChange(uri, null);
1134        }
1135    }
1136
1137    private boolean callerHasReadTvListingsPermission() {
1138        return getContext().checkCallingOrSelfPermission(PERMISSION_READ_TV_LISTINGS)
1139                == PackageManager.PERMISSION_GRANTED;
1140    }
1141
1142    private boolean callerHasAccessAllEpgDataPermission() {
1143        return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_ALL_EPG_DATA)
1144                == PackageManager.PERMISSION_GRANTED;
1145    }
1146
1147    private boolean callerHasAccessWatchedProgramsPermission() {
1148        return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_WATCHED_PROGRAMS)
1149                == PackageManager.PERMISSION_GRANTED;
1150    }
1151
1152    private boolean callerHasModifyParentalControlsPermission() {
1153        return getContext().checkCallingOrSelfPermission(
1154                android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
1155                == PackageManager.PERMISSION_GRANTED;
1156    }
1157
1158    @Override
1159    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
1160        switch (sUriMatcher.match(uri)) {
1161            case MATCH_CHANNEL_ID_LOGO:
1162                return openLogoFile(uri, mode);
1163            default:
1164                throw new FileNotFoundException(uri.toString());
1165        }
1166    }
1167
1168    private ParcelFileDescriptor openLogoFile(Uri uri, String mode) throws FileNotFoundException {
1169        long channelId = Long.parseLong(uri.getPathSegments().get(1));
1170
1171        SqlParams params = new SqlParams(CHANNELS_TABLE, Channels._ID + "=?",
1172                String.valueOf(channelId));
1173        if (!callerHasAccessAllEpgDataPermission()) {
1174            if (callerHasReadTvListingsPermission()) {
1175                params.appendWhere(Channels.COLUMN_PACKAGE_NAME + "=? OR "
1176                        + Channels.COLUMN_SEARCHABLE + "=?", getCallingPackage_(), "1");
1177            } else {
1178                params.appendWhere(Channels.COLUMN_PACKAGE_NAME + "=?", getCallingPackage_());
1179            }
1180        }
1181
1182        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
1183        queryBuilder.setTables(params.getTables());
1184
1185        // We don't write the database here.
1186        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1187        if (mode.equals("r")) {
1188            String sql = queryBuilder.buildQuery(new String[] { CHANNELS_COLUMN_LOGO },
1189                    params.getSelection(), null, null, null, null);
1190            ParcelFileDescriptor fd = DatabaseUtils.blobFileDescriptorForQuery(
1191                    db, sql, params.getSelectionArgs());
1192            if (fd == null) {
1193                throw new FileNotFoundException(uri.toString());
1194            }
1195            return fd;
1196        } else {
1197            try (Cursor cursor = queryBuilder.query(db, new String[] { Channels._ID },
1198                    params.getSelection(), params.getSelectionArgs(), null, null, null)) {
1199                if (cursor.getCount() < 1) {
1200                    // Fails early if corresponding channel does not exist.
1201                    // PipeMonitor may still fail to update DB later.
1202                    throw new FileNotFoundException(uri.toString());
1203                }
1204            }
1205
1206            try {
1207                ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe();
1208                PipeMonitor pipeMonitor = new PipeMonitor(pipeFds[0], channelId, params);
1209                pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
1210                return pipeFds[1];
1211            } catch (IOException ioe) {
1212                FileNotFoundException fne = new FileNotFoundException(uri.toString());
1213                fne.initCause(ioe);
1214                throw fne;
1215            }
1216        }
1217    }
1218
1219    /**
1220     * Validates the sort order based on the given field set.
1221     *
1222     * @throws IllegalArgumentException if there is any unknown field.
1223     */
1224    @SuppressLint("DefaultLocale")
1225    private static void validateSortOrder(String sortOrder, Set<String> possibleFields) {
1226        if (TextUtils.isEmpty(sortOrder) || possibleFields.isEmpty()) {
1227            return;
1228        }
1229        String[] orders = sortOrder.split(",");
1230        for (String order : orders) {
1231            String field = order.replaceAll("\\s+", " ").trim().toLowerCase().replace(" asc", "")
1232                    .replace(" desc", "");
1233            if (!possibleFields.contains(field)) {
1234                throw new IllegalArgumentException("Illegal field in sort order " + order);
1235            }
1236        }
1237    }
1238
1239    private class PipeMonitor extends AsyncTask<Void, Void, Void> {
1240        private final ParcelFileDescriptor mPfd;
1241        private final long mChannelId;
1242        private final SqlParams mParams;
1243
1244        private PipeMonitor(ParcelFileDescriptor pfd, long channelId, SqlParams params) {
1245            mPfd = pfd;
1246            mChannelId = channelId;
1247            mParams = params;
1248        }
1249
1250        @Override
1251        protected Void doInBackground(Void... params) {
1252            AutoCloseInputStream is = new AutoCloseInputStream(mPfd);
1253            ByteArrayOutputStream baos = null;
1254            int count = 0;
1255            try {
1256                Bitmap bitmap = BitmapFactory.decodeStream(is);
1257                if (bitmap == null) {
1258                    Log.e(TAG, "Failed to decode logo image for channel ID " + mChannelId);
1259                    return null;
1260                }
1261
1262                float scaleFactor = Math.min(1f, ((float) MAX_LOGO_IMAGE_SIZE) /
1263                        Math.max(bitmap.getWidth(), bitmap.getHeight()));
1264                if (scaleFactor < 1f) {
1265                    bitmap = Bitmap.createScaledBitmap(bitmap,
1266                            (int) (bitmap.getWidth() * scaleFactor),
1267                            (int) (bitmap.getHeight() * scaleFactor), false);
1268                }
1269
1270                baos = new ByteArrayOutputStream();
1271                bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
1272                byte[] bytes = baos.toByteArray();
1273
1274                ContentValues values = new ContentValues();
1275                values.put(CHANNELS_COLUMN_LOGO, bytes);
1276
1277                SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1278                count = db.update(mParams.getTables(), values, mParams.getSelection(),
1279                        mParams.getSelectionArgs());
1280                if (count > 0) {
1281                    Uri uri = TvContract.buildChannelLogoUri(mChannelId);
1282                    notifyChange(uri);
1283                }
1284            } finally {
1285                if (count == 0) {
1286                    try {
1287                        mPfd.closeWithError("Failed to write logo for channel ID " + mChannelId);
1288                    } catch (IOException ioe) {
1289                        Log.e(TAG, "Failed to close pipe", ioe);
1290                    }
1291                }
1292                IoUtils.closeQuietly(baos);
1293                IoUtils.closeQuietly(is);
1294            }
1295            return null;
1296        }
1297    }
1298
1299    private void deleteUnconsolidatedWatchedProgramsRows() {
1300        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1301        db.delete(WATCHED_PROGRAMS_TABLE, WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0", null);
1302    }
1303
1304    @SuppressLint("HandlerLeak")
1305    private final class WatchLogHandler extends Handler {
1306        private static final int MSG_CONSOLIDATE = 1;
1307        private static final int MSG_TRY_CONSOLIDATE_ALL = 2;
1308
1309        @Override
1310        public void handleMessage(Message msg) {
1311            switch (msg.what) {
1312                case MSG_CONSOLIDATE: {
1313                    SomeArgs args = (SomeArgs) msg.obj;
1314                    String sessionToken = (String) args.arg1;
1315                    long watchEndTime = (long) args.arg2;
1316                    onConsolidate(sessionToken, watchEndTime);
1317                    args.recycle();
1318                    return;
1319                }
1320                case MSG_TRY_CONSOLIDATE_ALL: {
1321                    onTryConsolidateAll();
1322                    return;
1323                }
1324                default: {
1325                    Log.w(TAG, "Unhandled message code: " + msg.what);
1326                    return;
1327                }
1328            }
1329        }
1330
1331        // Consolidates all WatchedPrograms rows for a given session with watch end time information
1332        // of the most recent log entry. After this method is called, it is guaranteed that there
1333        // remain consolidated rows only for that session.
1334        private void onConsolidate(String sessionToken, long watchEndTime) {
1335            if (DEBUG) {
1336                Log.d(TAG, "onConsolidate(sessionToken=" + sessionToken + ", watchEndTime="
1337                        + watchEndTime + ")");
1338            }
1339
1340            SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
1341            queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
1342            SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1343
1344            // Pick up the last row with the same session token.
1345            String[] projection = {
1346                    WatchedPrograms._ID,
1347                    WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
1348                    WatchedPrograms.COLUMN_CHANNEL_ID
1349            };
1350            String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=? AND "
1351                    + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + "=?";
1352            String[] selectionArgs = {
1353                    "0",
1354                    sessionToken
1355            };
1356            String sortOrder = WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC";
1357
1358            int consolidatedRowCount = 0;
1359            try (Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null,
1360                    null, sortOrder)) {
1361                long oldWatchStartTime = watchEndTime;
1362                while (cursor != null && cursor.moveToNext()) {
1363                    long id = cursor.getLong(0);
1364                    long watchStartTime = cursor.getLong(1);
1365                    long channelId = cursor.getLong(2);
1366                    consolidatedRowCount += consolidateRow(id, watchStartTime, oldWatchStartTime,
1367                            channelId, false);
1368                    oldWatchStartTime = watchStartTime;
1369                }
1370            }
1371            if (consolidatedRowCount > 0) {
1372                deleteUnsearchable();
1373            }
1374        }
1375
1376        // Tries to consolidate all WatchedPrograms rows regardless of the session. After this
1377        // method is called, it is guaranteed that we have at most one unconsolidated log entry per
1378        // session that represents the user's ongoing watch activity.
1379        // Also, this method automatically schedules the next consolidation if there still remains
1380        // an unconsolidated entry.
1381        private void onTryConsolidateAll() {
1382            if (DEBUG) {
1383                Log.d(TAG, "onTryConsolidateAll()");
1384            }
1385
1386            SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
1387            queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
1388            SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1389
1390            // Pick up all unconsolidated rows grouped by session. The most recent log entry goes on
1391            // top.
1392            String[] projection = {
1393                    WatchedPrograms._ID,
1394                    WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
1395                    WatchedPrograms.COLUMN_CHANNEL_ID,
1396                    WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN
1397            };
1398            String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0";
1399            String sortOrder = WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + " DESC,"
1400                    + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC";
1401
1402            int consolidatedRowCount = 0;
1403            try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null,
1404                    sortOrder)) {
1405                long oldWatchStartTime = 0;
1406                String oldSessionToken = null;
1407                while (cursor != null && cursor.moveToNext()) {
1408                    long id = cursor.getLong(0);
1409                    long watchStartTime = cursor.getLong(1);
1410                    long channelId = cursor.getLong(2);
1411                    String sessionToken = cursor.getString(3);
1412
1413                    if (!sessionToken.equals(oldSessionToken)) {
1414                        // The most recent log entry for the current session, which may be still
1415                        // active. Just go through a dry run with the current time to see if this
1416                        // entry can be split into multiple rows.
1417                        consolidatedRowCount += consolidateRow(id, watchStartTime,
1418                                System.currentTimeMillis(), channelId, true);
1419                        oldSessionToken = sessionToken;
1420                    } else {
1421                        // The later entries after the most recent one all fall into here. We now
1422                        // know that this watch activity ended exactly at the same time when the
1423                        // next activity started.
1424                        consolidatedRowCount += consolidateRow(id, watchStartTime,
1425                                oldWatchStartTime, channelId, false);
1426                    }
1427                    oldWatchStartTime = watchStartTime;
1428                }
1429            }
1430            if (consolidatedRowCount > 0) {
1431                deleteUnsearchable();
1432            }
1433            scheduleConsolidationIfNeeded();
1434        }
1435
1436        // Consolidates a WatchedPrograms row.
1437        // A row is 'consolidated' if and only if the following information is complete:
1438        // 1. WatchedPrograms.COLUMN_CHANNEL_ID
1439        // 2. WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS
1440        // 3. WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS
1441        // where COLUMN_WATCH_START_TIME_UTC_MILLIS <= COLUMN_WATCH_END_TIME_UTC_MILLIS.
1442        // This is the minimal but useful enough set of information to comprise the user's watch
1443        // history. (The program data are considered optional although we do try to fill them while
1444        // consolidating the row.) It is guaranteed that the target row is either consolidated or
1445        // deleted after this method is called.
1446        // Set {@code dryRun} to {@code true} if you think it's necessary to split the row without
1447        // consolidating the most recent row because the user stayed on the same channel for a very
1448        // long time.
1449        // This method returns the number of consolidated rows, which can be 0 or more.
1450        private int consolidateRow(
1451                long id, long watchStartTime, long watchEndTime, long channelId, boolean dryRun) {
1452            if (DEBUG) {
1453                Log.d(TAG, "consolidateRow(id=" + id + ", watchStartTime=" + watchStartTime
1454                        + ", watchEndTime=" + watchEndTime + ", channelId=" + channelId
1455                        + ", dryRun=" + dryRun + ")");
1456            }
1457
1458            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1459
1460            if (watchStartTime > watchEndTime) {
1461                Log.e(TAG, "watchEndTime cannot be less than watchStartTime");
1462                db.delete(WATCHED_PROGRAMS_TABLE, WatchedPrograms._ID + "=" + String.valueOf(id),
1463                        null);
1464                return 0;
1465            }
1466
1467            ContentValues values = getProgramValues(channelId, watchStartTime);
1468            Long endTime = values.getAsLong(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS);
1469            boolean needsToSplit = endTime != null && endTime < watchEndTime;
1470
1471            values.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
1472                    String.valueOf(watchStartTime));
1473            if (!dryRun || needsToSplit) {
1474                values.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
1475                        String.valueOf(needsToSplit ? endTime : watchEndTime));
1476                values.put(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED, "1");
1477                db.update(WATCHED_PROGRAMS_TABLE, values,
1478                        WatchedPrograms._ID + "=" + String.valueOf(id), null);
1479                // Treat the watched program is inserted when WATCHED_PROGRAMS_COLUMN_CONSOLIDATED
1480                // becomes 1.
1481                notifyChange(TvContract.buildWatchedProgramUri(id));
1482            } else {
1483                db.update(WATCHED_PROGRAMS_TABLE, values,
1484                        WatchedPrograms._ID + "=" + String.valueOf(id), null);
1485            }
1486            int count = dryRun ? 0 : 1;
1487            if (needsToSplit) {
1488                // This means that the program ended before the user stops watching the current
1489                // channel. In this case we duplicate the log entry as many as the number of
1490                // programs watched on the same channel. Here the end time of the current program
1491                // becomes the new watch start time of the next program.
1492                long duplicatedId = duplicateRow(id);
1493                if (duplicatedId > 0) {
1494                    count += consolidateRow(duplicatedId, endTime, watchEndTime, channelId, dryRun);
1495                }
1496            }
1497            return count;
1498        }
1499
1500        // Deletes the log entries from unsearchable channels. Note that only consolidated log
1501        // entries are safe to delete.
1502        private void deleteUnsearchable() {
1503            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1504            String deleteWhere = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=1 AND "
1505                    + WatchedPrograms.COLUMN_CHANNEL_ID + " IN (SELECT " + Channels._ID
1506                    + " FROM " + CHANNELS_TABLE + " WHERE " + Channels.COLUMN_SEARCHABLE + "=0)";
1507            db.delete(WATCHED_PROGRAMS_TABLE, deleteWhere, null);
1508        }
1509
1510        private void scheduleConsolidationIfNeeded() {
1511            if (DEBUG) {
1512                Log.d(TAG, "scheduleConsolidationIfNeeded()");
1513            }
1514            SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
1515            queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
1516            SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1517
1518            // Pick up all unconsolidated rows.
1519            String[] projection = {
1520                    WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
1521                    WatchedPrograms.COLUMN_CHANNEL_ID,
1522            };
1523            String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0";
1524
1525            try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null,
1526                    null)) {
1527                // Find the earliest time that any of the currently watching programs ends and
1528                // schedule the next consolidation at that time.
1529                long minEndTime = Long.MAX_VALUE;
1530                while (cursor != null && cursor.moveToNext()) {
1531                    long watchStartTime = cursor.getLong(0);
1532                    long channelId = cursor.getLong(1);
1533                    ContentValues values = getProgramValues(channelId, watchStartTime);
1534                    Long endTime = values.getAsLong(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS);
1535
1536                    if (endTime != null && endTime < minEndTime
1537                            && endTime > System.currentTimeMillis()) {
1538                        minEndTime = endTime;
1539                    }
1540                }
1541                if (minEndTime != Long.MAX_VALUE) {
1542                    sendEmptyMessageAtTime(MSG_TRY_CONSOLIDATE_ALL, minEndTime);
1543                    if (DEBUG) {
1544                        CharSequence minEndTimeStr = DateUtils.getRelativeTimeSpanString(
1545                                minEndTime, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS);
1546                        Log.d(TAG, "onTryConsolidateAll() scheduled " + minEndTimeStr);
1547                    }
1548                }
1549            }
1550        }
1551
1552        // Returns non-null ContentValues of the program data that the user watched on the channel
1553        // {@code channelId} at the time {@code time}.
1554        private ContentValues getProgramValues(long channelId, long time) {
1555            SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
1556            queryBuilder.setTables(PROGRAMS_TABLE);
1557            SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1558
1559            String[] projection = {
1560                    Programs.COLUMN_TITLE,
1561                    Programs.COLUMN_START_TIME_UTC_MILLIS,
1562                    Programs.COLUMN_END_TIME_UTC_MILLIS,
1563                    Programs.COLUMN_SHORT_DESCRIPTION
1564            };
1565            String selection = Programs.COLUMN_CHANNEL_ID + "=? AND "
1566                    + Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
1567                    + Programs.COLUMN_END_TIME_UTC_MILLIS + ">?";
1568            String[] selectionArgs = {
1569                    String.valueOf(channelId),
1570                    String.valueOf(time),
1571                    String.valueOf(time)
1572            };
1573            String sortOrder = Programs.COLUMN_START_TIME_UTC_MILLIS + " ASC";
1574
1575            try (Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null,
1576                    null, sortOrder)) {
1577                ContentValues values = new ContentValues();
1578                if (cursor != null && cursor.moveToNext()) {
1579                    values.put(WatchedPrograms.COLUMN_TITLE, cursor.getString(0));
1580                    values.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, cursor.getLong(1));
1581                    values.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, cursor.getLong(2));
1582                    values.put(WatchedPrograms.COLUMN_DESCRIPTION, cursor.getString(3));
1583                }
1584                return values;
1585            }
1586        }
1587
1588        // Duplicates the WatchedPrograms row with a given ID and returns the ID of the duplicated
1589        // row. Returns -1 if failed.
1590        private long duplicateRow(long id) {
1591            if (DEBUG) {
1592                Log.d(TAG, "duplicateRow(" + id + ")");
1593            }
1594
1595            SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
1596            queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
1597            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1598
1599            String[] projection = {
1600                    WatchedPrograms.COLUMN_PACKAGE_NAME,
1601                    WatchedPrograms.COLUMN_CHANNEL_ID,
1602                    WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN
1603            };
1604            String selection = WatchedPrograms._ID + "=" + String.valueOf(id);
1605
1606            try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null,
1607                    null)) {
1608                long rowId = -1;
1609                if (cursor != null && cursor.moveToNext()) {
1610                    ContentValues values = new ContentValues();
1611                    values.put(WatchedPrograms.COLUMN_PACKAGE_NAME, cursor.getString(0));
1612                    values.put(WatchedPrograms.COLUMN_CHANNEL_ID, cursor.getLong(1));
1613                    values.put(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, cursor.getString(2));
1614                    rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values);
1615                }
1616                return rowId;
1617            }
1618        }
1619    }
1620}
1621