1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.tv;
18 
19 import android.annotation.SuppressLint;
20 import android.app.AlarmManager;
21 import android.app.PendingIntent;
22 import android.content.ContentProvider;
23 import android.content.ContentProviderOperation;
24 import android.content.ContentProviderResult;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.OperationApplicationException;
29 import android.content.UriMatcher;
30 import android.content.pm.PackageManager;
31 import android.database.Cursor;
32 import android.database.DatabaseUtils;
33 import android.database.SQLException;
34 import android.database.sqlite.SQLiteDatabase;
35 import android.database.sqlite.SQLiteOpenHelper;
36 import android.database.sqlite.SQLiteQueryBuilder;
37 import android.graphics.Bitmap;
38 import android.graphics.BitmapFactory;
39 import android.media.tv.TvContract;
40 import android.media.tv.TvContract.BaseTvColumns;
41 import android.media.tv.TvContract.Channels;
42 import android.media.tv.TvContract.Programs;
43 import android.media.tv.TvContract.Programs.Genres;
44 import android.media.tv.TvContract.RecordedPrograms;
45 import android.media.tv.TvContract.WatchedPrograms;
46 import android.net.Uri;
47 import android.os.AsyncTask;
48 import android.os.Handler;
49 import android.os.Message;
50 import android.os.ParcelFileDescriptor;
51 import android.os.ParcelFileDescriptor.AutoCloseInputStream;
52 import android.text.TextUtils;
53 import android.text.format.DateUtils;
54 import android.util.Log;
55 
56 import com.android.internal.annotations.VisibleForTesting;
57 import com.android.internal.os.SomeArgs;
58 import com.android.providers.tv.util.SqlParams;
59 
60 import libcore.io.IoUtils;
61 
62 import java.io.ByteArrayOutputStream;
63 import java.io.FileNotFoundException;
64 import java.io.IOException;
65 import java.util.ArrayList;
66 import java.util.HashMap;
67 import java.util.HashSet;
68 import java.util.Map;
69 import 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  */
75 public 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);
sUriMatcher.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL)134         sUriMatcher.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL);
sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID)135         sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID);
sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#/logo", MATCH_CHANNEL_ID_LOGO)136         sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#/logo", MATCH_CHANNEL_ID_LOGO);
sUriMatcher.addURI(TvContract.AUTHORITY, "passthrough/*", MATCH_PASSTHROUGH_ID)137         sUriMatcher.addURI(TvContract.AUTHORITY, "passthrough/*", MATCH_PASSTHROUGH_ID);
sUriMatcher.addURI(TvContract.AUTHORITY, "program", MATCH_PROGRAM)138         sUriMatcher.addURI(TvContract.AUTHORITY, "program", MATCH_PROGRAM);
sUriMatcher.addURI(TvContract.AUTHORITY, "program/#", MATCH_PROGRAM_ID)139         sUriMatcher.addURI(TvContract.AUTHORITY, "program/#", MATCH_PROGRAM_ID);
sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program", MATCH_WATCHED_PROGRAM)140         sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program", MATCH_WATCHED_PROGRAM);
sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID)141         sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID);
sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM)142         sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM);
sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID)143         sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID);
144 
145         sChannelProjectionMap = new HashMap<>();
sChannelProjectionMap.put(Channels._ID, CHANNELS_TABLE + "." + Channels._ID)146         sChannelProjectionMap.put(Channels._ID, CHANNELS_TABLE + "." + Channels._ID);
sChannelProjectionMap.put(Channels.COLUMN_PACKAGE_NAME, CHANNELS_TABLE + "." + Channels.COLUMN_PACKAGE_NAME)147         sChannelProjectionMap.put(Channels.COLUMN_PACKAGE_NAME,
148                 CHANNELS_TABLE + "." + Channels.COLUMN_PACKAGE_NAME);
sChannelProjectionMap.put(Channels.COLUMN_INPUT_ID, CHANNELS_TABLE + "." + Channels.COLUMN_INPUT_ID)149         sChannelProjectionMap.put(Channels.COLUMN_INPUT_ID,
150                 CHANNELS_TABLE + "." + Channels.COLUMN_INPUT_ID);
sChannelProjectionMap.put(Channels.COLUMN_TYPE, CHANNELS_TABLE + "." + Channels.COLUMN_TYPE)151         sChannelProjectionMap.put(Channels.COLUMN_TYPE,
152                 CHANNELS_TABLE + "." + Channels.COLUMN_TYPE);
sChannelProjectionMap.put(Channels.COLUMN_SERVICE_TYPE, CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_TYPE)153         sChannelProjectionMap.put(Channels.COLUMN_SERVICE_TYPE,
154                 CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_TYPE);
sChannelProjectionMap.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, CHANNELS_TABLE + "." + Channels.COLUMN_ORIGINAL_NETWORK_ID)155         sChannelProjectionMap.put(Channels.COLUMN_ORIGINAL_NETWORK_ID,
156                 CHANNELS_TABLE + "." + Channels.COLUMN_ORIGINAL_NETWORK_ID);
sChannelProjectionMap.put(Channels.COLUMN_TRANSPORT_STREAM_ID, CHANNELS_TABLE + "." + Channels.COLUMN_TRANSPORT_STREAM_ID)157         sChannelProjectionMap.put(Channels.COLUMN_TRANSPORT_STREAM_ID,
158                 CHANNELS_TABLE + "." + Channels.COLUMN_TRANSPORT_STREAM_ID);
sChannelProjectionMap.put(Channels.COLUMN_SERVICE_ID, CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_ID)159         sChannelProjectionMap.put(Channels.COLUMN_SERVICE_ID,
160                 CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_ID);
sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NUMBER, CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NUMBER)161         sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NUMBER,
162                 CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NUMBER);
sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NAME, CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NAME)163         sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NAME,
164                 CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NAME);
sChannelProjectionMap.put(Channels.COLUMN_NETWORK_AFFILIATION, CHANNELS_TABLE + "." + Channels.COLUMN_NETWORK_AFFILIATION)165         sChannelProjectionMap.put(Channels.COLUMN_NETWORK_AFFILIATION,
166                 CHANNELS_TABLE + "." + Channels.COLUMN_NETWORK_AFFILIATION);
sChannelProjectionMap.put(Channels.COLUMN_DESCRIPTION, CHANNELS_TABLE + "." + Channels.COLUMN_DESCRIPTION)167         sChannelProjectionMap.put(Channels.COLUMN_DESCRIPTION,
168                 CHANNELS_TABLE + "." + Channels.COLUMN_DESCRIPTION);
sChannelProjectionMap.put(Channels.COLUMN_VIDEO_FORMAT, CHANNELS_TABLE + "." + Channels.COLUMN_VIDEO_FORMAT)169         sChannelProjectionMap.put(Channels.COLUMN_VIDEO_FORMAT,
170                 CHANNELS_TABLE + "." + Channels.COLUMN_VIDEO_FORMAT);
sChannelProjectionMap.put(Channels.COLUMN_BROWSABLE, CHANNELS_TABLE + "." + Channels.COLUMN_BROWSABLE)171         sChannelProjectionMap.put(Channels.COLUMN_BROWSABLE,
172                 CHANNELS_TABLE + "." + Channels.COLUMN_BROWSABLE);
sChannelProjectionMap.put(Channels.COLUMN_SEARCHABLE, CHANNELS_TABLE + "." + Channels.COLUMN_SEARCHABLE)173         sChannelProjectionMap.put(Channels.COLUMN_SEARCHABLE,
174                 CHANNELS_TABLE + "." + Channels.COLUMN_SEARCHABLE);
sChannelProjectionMap.put(Channels.COLUMN_LOCKED, CHANNELS_TABLE + "." + Channels.COLUMN_LOCKED)175         sChannelProjectionMap.put(Channels.COLUMN_LOCKED,
176                 CHANNELS_TABLE + "." + Channels.COLUMN_LOCKED);
sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_ICON_URI, CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_ICON_URI)177         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_ICON_URI,
178                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_ICON_URI);
sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_POSTER_ART_URI, CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_POSTER_ART_URI)179         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_POSTER_ART_URI,
180                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_POSTER_ART_URI);
sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_TEXT, CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_TEXT)181         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_TEXT,
182                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_TEXT);
sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_COLOR, CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_COLOR)183         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_COLOR,
184                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_COLOR);
sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_INTENT_URI, CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_INTENT_URI)185         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_INTENT_URI,
186                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_INTENT_URI);
sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA, CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_DATA)187         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA,
188                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_DATA);
sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1)189         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1,
190                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1);
sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2)191         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
192                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2);
sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3, CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3)193         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3,
194                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3);
sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4, CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4)195         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4,
196                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4);
sChannelProjectionMap.put(Channels.COLUMN_VERSION_NUMBER, CHANNELS_TABLE + "." + Channels.COLUMN_VERSION_NUMBER)197         sChannelProjectionMap.put(Channels.COLUMN_VERSION_NUMBER,
198                 CHANNELS_TABLE + "." + Channels.COLUMN_VERSION_NUMBER);
199 
200         sProgramProjectionMap = new HashMap<>();
sProgramProjectionMap.put(Programs._ID, Programs._ID)201         sProgramProjectionMap.put(Programs._ID, Programs._ID);
sProgramProjectionMap.put(Programs.COLUMN_PACKAGE_NAME, Programs.COLUMN_PACKAGE_NAME)202         sProgramProjectionMap.put(Programs.COLUMN_PACKAGE_NAME, Programs.COLUMN_PACKAGE_NAME);
sProgramProjectionMap.put(Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_CHANNEL_ID)203         sProgramProjectionMap.put(Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_CHANNEL_ID);
sProgramProjectionMap.put(Programs.COLUMN_TITLE, Programs.COLUMN_TITLE)204         sProgramProjectionMap.put(Programs.COLUMN_TITLE, Programs.COLUMN_TITLE);
205         // COLUMN_SEASON_NUMBER is deprecated. Return COLUMN_SEASON_DISPLAY_NUMBER instead.
sProgramProjectionMap.put(Programs.COLUMN_SEASON_NUMBER, Programs.COLUMN_SEASON_DISPLAY_NUMBER + " AS " + Programs.COLUMN_SEASON_NUMBER)206         sProgramProjectionMap.put(Programs.COLUMN_SEASON_NUMBER,
207                 Programs.COLUMN_SEASON_DISPLAY_NUMBER + " AS " + Programs.COLUMN_SEASON_NUMBER);
sProgramProjectionMap.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, Programs.COLUMN_SEASON_DISPLAY_NUMBER)208         sProgramProjectionMap.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER,
209                 Programs.COLUMN_SEASON_DISPLAY_NUMBER);
sProgramProjectionMap.put(Programs.COLUMN_SEASON_TITLE, Programs.COLUMN_SEASON_TITLE)210         sProgramProjectionMap.put(Programs.COLUMN_SEASON_TITLE, Programs.COLUMN_SEASON_TITLE);
211         // COLUMN_EPISODE_NUMBER is deprecated. Return COLUMN_EPISODE_DISPLAY_NUMBER instead.
sProgramProjectionMap.put(Programs.COLUMN_EPISODE_NUMBER, Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " AS " + Programs.COLUMN_EPISODE_NUMBER)212         sProgramProjectionMap.put(Programs.COLUMN_EPISODE_NUMBER,
213                 Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " AS " + Programs.COLUMN_EPISODE_NUMBER);
sProgramProjectionMap.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER, Programs.COLUMN_EPISODE_DISPLAY_NUMBER)214         sProgramProjectionMap.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
215                 Programs.COLUMN_EPISODE_DISPLAY_NUMBER);
sProgramProjectionMap.put(Programs.COLUMN_EPISODE_TITLE, Programs.COLUMN_EPISODE_TITLE)216         sProgramProjectionMap.put(Programs.COLUMN_EPISODE_TITLE, Programs.COLUMN_EPISODE_TITLE);
sProgramProjectionMap.put(Programs.COLUMN_START_TIME_UTC_MILLIS, Programs.COLUMN_START_TIME_UTC_MILLIS)217         sProgramProjectionMap.put(Programs.COLUMN_START_TIME_UTC_MILLIS,
218                 Programs.COLUMN_START_TIME_UTC_MILLIS);
sProgramProjectionMap.put(Programs.COLUMN_END_TIME_UTC_MILLIS, Programs.COLUMN_END_TIME_UTC_MILLIS)219         sProgramProjectionMap.put(Programs.COLUMN_END_TIME_UTC_MILLIS,
220                 Programs.COLUMN_END_TIME_UTC_MILLIS);
sProgramProjectionMap.put(Programs.COLUMN_BROADCAST_GENRE, Programs.COLUMN_BROADCAST_GENRE)221         sProgramProjectionMap.put(Programs.COLUMN_BROADCAST_GENRE, Programs.COLUMN_BROADCAST_GENRE);
sProgramProjectionMap.put(Programs.COLUMN_CANONICAL_GENRE, Programs.COLUMN_CANONICAL_GENRE)222         sProgramProjectionMap.put(Programs.COLUMN_CANONICAL_GENRE, Programs.COLUMN_CANONICAL_GENRE);
sProgramProjectionMap.put(Programs.COLUMN_SHORT_DESCRIPTION, Programs.COLUMN_SHORT_DESCRIPTION)223         sProgramProjectionMap.put(Programs.COLUMN_SHORT_DESCRIPTION,
224                 Programs.COLUMN_SHORT_DESCRIPTION);
sProgramProjectionMap.put(Programs.COLUMN_LONG_DESCRIPTION, Programs.COLUMN_LONG_DESCRIPTION)225         sProgramProjectionMap.put(Programs.COLUMN_LONG_DESCRIPTION,
226                 Programs.COLUMN_LONG_DESCRIPTION);
sProgramProjectionMap.put(Programs.COLUMN_VIDEO_WIDTH, Programs.COLUMN_VIDEO_WIDTH)227         sProgramProjectionMap.put(Programs.COLUMN_VIDEO_WIDTH, Programs.COLUMN_VIDEO_WIDTH);
sProgramProjectionMap.put(Programs.COLUMN_VIDEO_HEIGHT, Programs.COLUMN_VIDEO_HEIGHT)228         sProgramProjectionMap.put(Programs.COLUMN_VIDEO_HEIGHT, Programs.COLUMN_VIDEO_HEIGHT);
sProgramProjectionMap.put(Programs.COLUMN_AUDIO_LANGUAGE, Programs.COLUMN_AUDIO_LANGUAGE)229         sProgramProjectionMap.put(Programs.COLUMN_AUDIO_LANGUAGE, Programs.COLUMN_AUDIO_LANGUAGE);
sProgramProjectionMap.put(Programs.COLUMN_CONTENT_RATING, Programs.COLUMN_CONTENT_RATING)230         sProgramProjectionMap.put(Programs.COLUMN_CONTENT_RATING, Programs.COLUMN_CONTENT_RATING);
sProgramProjectionMap.put(Programs.COLUMN_POSTER_ART_URI, Programs.COLUMN_POSTER_ART_URI)231         sProgramProjectionMap.put(Programs.COLUMN_POSTER_ART_URI, Programs.COLUMN_POSTER_ART_URI);
sProgramProjectionMap.put(Programs.COLUMN_THUMBNAIL_URI, Programs.COLUMN_THUMBNAIL_URI)232         sProgramProjectionMap.put(Programs.COLUMN_THUMBNAIL_URI, Programs.COLUMN_THUMBNAIL_URI);
sProgramProjectionMap.put(Programs.COLUMN_SEARCHABLE, Programs.COLUMN_SEARCHABLE)233         sProgramProjectionMap.put(Programs.COLUMN_SEARCHABLE, Programs.COLUMN_SEARCHABLE);
sProgramProjectionMap.put(Programs.COLUMN_RECORDING_PROHIBITED, Programs.COLUMN_RECORDING_PROHIBITED)234         sProgramProjectionMap.put(Programs.COLUMN_RECORDING_PROHIBITED,
235                 Programs.COLUMN_RECORDING_PROHIBITED);
sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_DATA, Programs.COLUMN_INTERNAL_PROVIDER_DATA)236         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_DATA,
237                 Programs.COLUMN_INTERNAL_PROVIDER_DATA);
sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG1, Programs.COLUMN_INTERNAL_PROVIDER_FLAG1)238         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG1,
239                 Programs.COLUMN_INTERNAL_PROVIDER_FLAG1);
sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG2, Programs.COLUMN_INTERNAL_PROVIDER_FLAG2)240         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG2,
241                 Programs.COLUMN_INTERNAL_PROVIDER_FLAG2);
sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG3, Programs.COLUMN_INTERNAL_PROVIDER_FLAG3)242         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG3,
243                 Programs.COLUMN_INTERNAL_PROVIDER_FLAG3);
sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG4, Programs.COLUMN_INTERNAL_PROVIDER_FLAG4)244         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG4,
245                 Programs.COLUMN_INTERNAL_PROVIDER_FLAG4);
sProgramProjectionMap.put(Programs.COLUMN_VERSION_NUMBER, Programs.COLUMN_VERSION_NUMBER)246         sProgramProjectionMap.put(Programs.COLUMN_VERSION_NUMBER, Programs.COLUMN_VERSION_NUMBER);
247 
248         sWatchedProgramProjectionMap = new HashMap<>();
sWatchedProgramProjectionMap.put(WatchedPrograms._ID, WatchedPrograms._ID)249         sWatchedProgramProjectionMap.put(WatchedPrograms._ID, WatchedPrograms._ID);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS)250         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
251                 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS)252         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
253                 WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_CHANNEL_ID, WatchedPrograms.COLUMN_CHANNEL_ID)254         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_CHANNEL_ID,
255                 WatchedPrograms.COLUMN_CHANNEL_ID);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_TITLE, WatchedPrograms.COLUMN_TITLE)256         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_TITLE,
257                 WatchedPrograms.COLUMN_TITLE);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS)258         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS,
259                 WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS)260         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS,
261                 WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_DESCRIPTION, WatchedPrograms.COLUMN_DESCRIPTION)262         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_DESCRIPTION,
263                 WatchedPrograms.COLUMN_DESCRIPTION);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS, WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS)264         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS,
265                 WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS);
sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN)266         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN,
267                 WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN);
sWatchedProgramProjectionMap.put(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED, WATCHED_PROGRAMS_COLUMN_CONSOLIDATED)268         sWatchedProgramProjectionMap.put(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED,
269                 WATCHED_PROGRAMS_COLUMN_CONSOLIDATED);
270 
271         sRecordedProgramProjectionMap = new HashMap<>();
sRecordedProgramProjectionMap.put(RecordedPrograms._ID, RecordedPrograms._ID)272         sRecordedProgramProjectionMap.put(RecordedPrograms._ID, RecordedPrograms._ID);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_PACKAGE_NAME, RecordedPrograms.COLUMN_PACKAGE_NAME)273         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_PACKAGE_NAME,
274                 RecordedPrograms.COLUMN_PACKAGE_NAME);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INPUT_ID, RecordedPrograms.COLUMN_INPUT_ID)275         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INPUT_ID,
276                 RecordedPrograms.COLUMN_INPUT_ID);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CHANNEL_ID, RecordedPrograms.COLUMN_CHANNEL_ID)277         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CHANNEL_ID,
278                 RecordedPrograms.COLUMN_CHANNEL_ID);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_TITLE, RecordedPrograms.COLUMN_TITLE)279         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_TITLE,
280                 RecordedPrograms.COLUMN_TITLE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER)281         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
282                 RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_TITLE, RecordedPrograms.COLUMN_SEASON_TITLE)283         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_TITLE,
284                 RecordedPrograms.COLUMN_SEASON_TITLE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER)285         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
286                 RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_TITLE, RecordedPrograms.COLUMN_EPISODE_TITLE)287         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_TITLE,
288                 RecordedPrograms.COLUMN_EPISODE_TITLE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS)289         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
290                 RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS)291         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS,
292                 RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_BROADCAST_GENRE, RecordedPrograms.COLUMN_BROADCAST_GENRE)293         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_BROADCAST_GENRE,
294                 RecordedPrograms.COLUMN_BROADCAST_GENRE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CANONICAL_GENRE, RecordedPrograms.COLUMN_CANONICAL_GENRE)295         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CANONICAL_GENRE,
296                 RecordedPrograms.COLUMN_CANONICAL_GENRE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, RecordedPrograms.COLUMN_SHORT_DESCRIPTION)297         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION,
298                 RecordedPrograms.COLUMN_SHORT_DESCRIPTION);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, RecordedPrograms.COLUMN_LONG_DESCRIPTION)299         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION,
300                 RecordedPrograms.COLUMN_LONG_DESCRIPTION);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, RecordedPrograms.COLUMN_VIDEO_WIDTH)301         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_WIDTH,
302                 RecordedPrograms.COLUMN_VIDEO_WIDTH);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, RecordedPrograms.COLUMN_VIDEO_HEIGHT)303         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT,
304                 RecordedPrograms.COLUMN_VIDEO_HEIGHT);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, RecordedPrograms.COLUMN_AUDIO_LANGUAGE)305         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE,
306                 RecordedPrograms.COLUMN_AUDIO_LANGUAGE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CONTENT_RATING, RecordedPrograms.COLUMN_CONTENT_RATING)307         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CONTENT_RATING,
308                 RecordedPrograms.COLUMN_CONTENT_RATING);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_POSTER_ART_URI, RecordedPrograms.COLUMN_POSTER_ART_URI)309         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_POSTER_ART_URI,
310                 RecordedPrograms.COLUMN_POSTER_ART_URI);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, RecordedPrograms.COLUMN_THUMBNAIL_URI)311         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_THUMBNAIL_URI,
312                 RecordedPrograms.COLUMN_THUMBNAIL_URI);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEARCHABLE, RecordedPrograms.COLUMN_SEARCHABLE)313         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEARCHABLE,
314                 RecordedPrograms.COLUMN_SEARCHABLE);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI, RecordedPrograms.COLUMN_RECORDING_DATA_URI)315         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI,
316                 RecordedPrograms.COLUMN_RECORDING_DATA_URI);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, RecordedPrograms.COLUMN_RECORDING_DATA_BYTES)317         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES,
318                 RecordedPrograms.COLUMN_RECORDING_DATA_BYTES);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS)319         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
320                 RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS)321         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
322                 RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA)323         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
324                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1)325         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
326                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2)327         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
328                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3)329         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
330                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4)331         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
332                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4);
sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VERSION_NUMBER, RecordedPrograms.COLUMN_VERSION_NUMBER)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 
getInstance(Context context)389         public static synchronized DatabaseHelper getInstance(Context context) {
390             if (sSingleton == null) {
391                 sSingleton = new DatabaseHelper(context);
392             }
393             return sSingleton;
394         }
395 
DatabaseHelper(Context context)396         private DatabaseHelper(Context context) {
397             super(context, DATABASE_NAME, null, DATABASE_VERSION);
398         }
399 
400         @Override
onConfigure(SQLiteDatabase db)401         public void onConfigure(SQLiteDatabase db) {
402             db.setForeignKeyConstraintsEnabled(true);
403         }
404 
405         @Override
onCreate(SQLiteDatabase db)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
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)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 
migrateIntegerColumnToTextColumn(SQLiteDatabase db, String table, String integerColumn, String textColumn)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
onCreate()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
scheduleEpgDataCleanup()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 
buildGenreMap()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")
buildGenreMap(int id)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
getCallingPackage_()657     String getCallingPackage_() {
658         return getCallingPackage();
659     }
660 
661     @Override
getType(Uri uri)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
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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
insert(Uri uri, ContentValues values)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 
insertChannel(Uri uri, ContentValues values)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 
insertProgram(Uri uri, ContentValues values)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 
insertWatchedProgram(Uri uri, ContentValues values)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 
insertRecordedProgram(Uri uri, ContentValues values)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
delete(Uri uri, String selection, String[] selectionArgs)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
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)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 
createSqlParams(String operation, Uri uri, String selection, String[] selectionArgs)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 
capitalize(String str)1017     private static String capitalize(String str) {
1018         return Character.toUpperCase(str.charAt(0)) + str.substring(1);
1019     }
1020 
1021     @SuppressLint("DefaultLocale")
checkAndConvertGenre(ContentValues values)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 
checkAndConvertDeprecatedColumns(ContentValues values)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 
getBatchNotificationsSet()1079     private Set<Uri> getBatchNotificationsSet() {
1080         return mTLBatchNotifications.get();
1081     }
1082 
setBatchNotificationsSet(Set<Uri> batchNotifications)1083     private void setBatchNotificationsSet(Set<Uri> batchNotifications) {
1084         mTLBatchNotifications.set(batchNotifications);
1085     }
1086 
1087     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)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
bulkInsert(Uri uri, ContentValues[] values)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 
notifyChange(Uri uri)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 
callerHasReadTvListingsPermission()1137     private boolean callerHasReadTvListingsPermission() {
1138         return getContext().checkCallingOrSelfPermission(PERMISSION_READ_TV_LISTINGS)
1139                 == PackageManager.PERMISSION_GRANTED;
1140     }
1141 
callerHasAccessAllEpgDataPermission()1142     private boolean callerHasAccessAllEpgDataPermission() {
1143         return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_ALL_EPG_DATA)
1144                 == PackageManager.PERMISSION_GRANTED;
1145     }
1146 
callerHasAccessWatchedProgramsPermission()1147     private boolean callerHasAccessWatchedProgramsPermission() {
1148         return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_WATCHED_PROGRAMS)
1149                 == PackageManager.PERMISSION_GRANTED;
1150     }
1151 
callerHasModifyParentalControlsPermission()1152     private boolean callerHasModifyParentalControlsPermission() {
1153         return getContext().checkCallingOrSelfPermission(
1154                 android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
1155                 == PackageManager.PERMISSION_GRANTED;
1156     }
1157 
1158     @Override
openFile(Uri uri, String mode)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 
openLogoFile(Uri uri, String mode)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")
validateSortOrder(String sortOrder, Set<String> possibleFields)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 
PipeMonitor(ParcelFileDescriptor pfd, long channelId, SqlParams params)1244         private PipeMonitor(ParcelFileDescriptor pfd, long channelId, SqlParams params) {
1245             mPfd = pfd;
1246             mChannelId = channelId;
1247             mParams = params;
1248         }
1249 
1250         @Override
doInBackground(Void... params)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 
deleteUnconsolidatedWatchedProgramsRows()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
handleMessage(Message msg)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.
onConsolidate(String sessionToken, long watchEndTime)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.
onTryConsolidateAll()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.
consolidateRow( long id, long watchStartTime, long watchEndTime, long channelId, boolean dryRun)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.
deleteUnsearchable()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 
scheduleConsolidationIfNeeded()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}.
getProgramValues(long channelId, long 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.
duplicateRow(long id)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