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