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