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