1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.tv; 18 19 import android.annotation.SuppressLint; 20 import android.app.AlarmManager; 21 import android.app.PendingIntent; 22 import android.content.ContentProvider; 23 import android.content.ContentProviderOperation; 24 import android.content.ContentProviderResult; 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.OperationApplicationException; 29 import android.content.SharedPreferences; 30 import android.content.UriMatcher; 31 import android.content.pm.PackageManager; 32 import android.database.Cursor; 33 import android.database.DatabaseUtils; 34 import android.database.SQLException; 35 import android.database.sqlite.SQLiteDatabase; 36 import android.database.sqlite.SQLiteOpenHelper; 37 import android.database.sqlite.SQLiteQueryBuilder; 38 import android.graphics.Bitmap; 39 import android.graphics.BitmapFactory; 40 import android.media.tv.TvContract; 41 import android.media.tv.TvContract.BaseTvColumns; 42 import android.media.tv.TvContract.Channels; 43 import android.media.tv.TvContract.PreviewPrograms; 44 import android.media.tv.TvContract.Programs; 45 import android.media.tv.TvContract.Programs.Genres; 46 import android.media.tv.TvContract.RecordedPrograms; 47 import android.media.tv.TvContract.WatchedPrograms; 48 import android.media.tv.TvContract.WatchNextPrograms; 49 import android.net.Uri; 50 import android.os.AsyncTask; 51 import android.os.Bundle; 52 import android.os.Handler; 53 import android.os.Message; 54 import android.os.ParcelFileDescriptor; 55 import android.os.ParcelFileDescriptor.AutoCloseInputStream; 56 import android.preference.PreferenceManager; 57 import android.provider.BaseColumns; 58 import android.text.TextUtils; 59 import android.text.format.DateUtils; 60 import android.util.Log; 61 62 import com.android.internal.annotations.VisibleForTesting; 63 import com.android.internal.os.SomeArgs; 64 import com.android.providers.tv.util.SqlParams; 65 66 import libcore.io.IoUtils; 67 68 import java.io.ByteArrayOutputStream; 69 import java.io.FileNotFoundException; 70 import java.io.IOException; 71 import java.util.ArrayList; 72 import java.util.HashMap; 73 import java.util.HashSet; 74 import java.util.Iterator; 75 import java.util.Map; 76 import java.util.Set; 77 import 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 */ 83 public 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 // This method is thread-safe. It's guaranteed by the implementation of SQLiteOpenHelper 974 if (!sInitialized) { 975 buildProjectionMap(db); 976 sBlockedPackagesSharedPreference = PreferenceManager.getDefaultSharedPreferences( 977 mContext); 978 sBlockedPackages = new ConcurrentHashMap<>(); 979 for (String packageName : sBlockedPackagesSharedPreference.getStringSet( 980 SHARED_PREF_BLOCKED_PACKAGES_KEY, new HashSet<>())) { 981 sBlockedPackages.put(packageName, true); 982 } 983 sInitialized = true; 984 } 985 } 986 987 private void buildProjectionMap(SQLiteDatabase db) { 988 updateProjectionMap(db, CHANNELS_TABLE, sChannelProjectionMap); 989 updateProjectionMap(db, PROGRAMS_TABLE, sProgramProjectionMap); 990 updateProjectionMap(db, WATCHED_PROGRAMS_TABLE, sWatchedProgramProjectionMap); 991 updateProjectionMap(db, RECORDED_PROGRAMS_TABLE, sRecordedProgramProjectionMap); 992 updateProjectionMap(db, PREVIEW_PROGRAMS_TABLE, sPreviewProgramProjectionMap); 993 updateProjectionMap(db, WATCH_NEXT_PROGRAMS_TABLE, sWatchNextProgramProjectionMap); 994 } 995 996 private void updateProjectionMap(SQLiteDatabase db, String tableName, 997 Map<String, String> projectionMap) { 998 try(Cursor cursor = db.rawQuery("SELECT * FROM " + tableName + " LIMIT 0", null)) { 999 for (String columnName : cursor.getColumnNames()) { 1000 if (!projectionMap.containsKey(columnName)) { 1001 projectionMap.put(columnName, tableName + '.' + columnName); 1002 } 1003 } 1004 } 1005 } 1006 1007 private static void migrateIntegerColumnToTextColumn(SQLiteDatabase db, String table, 1008 String integerColumn, String textColumn) { 1009 db.execSQL("ALTER TABLE " + table + " ADD " + textColumn + " TEXT;"); 1010 db.execSQL("UPDATE " + table + " SET " + textColumn + " = CAST(" + integerColumn 1011 + " AS TEXT);"); 1012 } 1013 } 1014 1015 private DatabaseHelper mOpenHelper; 1016 private static SharedPreferences sBlockedPackagesSharedPreference; 1017 private static Map<String, Boolean> sBlockedPackages; 1018 @VisibleForTesting 1019 protected TransientRowHelper mTransientRowHelper; 1020 1021 private final Handler mLogHandler = new WatchLogHandler(); 1022 1023 @Override 1024 public boolean onCreate() { 1025 if (DEBUG) { 1026 Log.d(TAG, "Creating TvProvider"); 1027 } 1028 if (mOpenHelper == null) { 1029 mOpenHelper = DatabaseHelper.getInstance(getContext()); 1030 } 1031 mTransientRowHelper = TransientRowHelper.getInstance(getContext()); 1032 scheduleEpgDataCleanup(); 1033 buildGenreMap(); 1034 1035 // DB operation, which may trigger upgrade, should not happen in onCreate. 1036 new AsyncTask<Void, Void, Void>() { 1037 @Override 1038 protected Void doInBackground(Void... params) { 1039 deleteUnconsolidatedWatchedProgramsRows(); 1040 return null; 1041 } 1042 }.execute(); 1043 return true; 1044 } 1045 1046 @VisibleForTesting 1047 void scheduleEpgDataCleanup() { 1048 Intent intent = new Intent(EpgDataCleanupService.ACTION_CLEAN_UP_EPG_DATA); 1049 intent.setClass(getContext(), EpgDataCleanupService.class); 1050 PendingIntent pendingIntent = PendingIntent.getService( 1051 getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 1052 AlarmManager alarmManager = 1053 (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); 1054 alarmManager.setInexactRepeating(AlarmManager.RTC, System.currentTimeMillis(), 1055 AlarmManager.INTERVAL_HALF_DAY, pendingIntent); 1056 } 1057 1058 private void buildGenreMap() { 1059 if (sGenreMap != null) { 1060 return; 1061 } 1062 1063 sGenreMap = new HashMap<>(); 1064 buildGenreMap(R.array.genre_mapping_atsc); 1065 buildGenreMap(R.array.genre_mapping_dvb); 1066 buildGenreMap(R.array.genre_mapping_isdb); 1067 buildGenreMap(R.array.genre_mapping_isdb_br); 1068 } 1069 1070 @SuppressLint("DefaultLocale") 1071 private void buildGenreMap(int id) { 1072 String[] maps = getContext().getResources().getStringArray(id); 1073 for (String map : maps) { 1074 String[] arr = map.split("\\|"); 1075 if (arr.length != 2) { 1076 throw new IllegalArgumentException("Invalid genre mapping : " + map); 1077 } 1078 sGenreMap.put(arr[0].toUpperCase(), arr[1]); 1079 } 1080 } 1081 1082 @VisibleForTesting 1083 String getCallingPackage_() { 1084 return getCallingPackage(); 1085 } 1086 1087 @VisibleForTesting 1088 void setOpenHelper(DatabaseHelper helper) { 1089 mOpenHelper = helper; 1090 } 1091 1092 @Override 1093 public String getType(Uri uri) { 1094 switch (sUriMatcher.match(uri)) { 1095 case MATCH_CHANNEL: 1096 return Channels.CONTENT_TYPE; 1097 case MATCH_CHANNEL_ID: 1098 return Channels.CONTENT_ITEM_TYPE; 1099 case MATCH_CHANNEL_ID_LOGO: 1100 return "image/png"; 1101 case MATCH_PASSTHROUGH_ID: 1102 return Channels.CONTENT_ITEM_TYPE; 1103 case MATCH_PROGRAM: 1104 return Programs.CONTENT_TYPE; 1105 case MATCH_PROGRAM_ID: 1106 return Programs.CONTENT_ITEM_TYPE; 1107 case MATCH_WATCHED_PROGRAM: 1108 return WatchedPrograms.CONTENT_TYPE; 1109 case MATCH_WATCHED_PROGRAM_ID: 1110 return WatchedPrograms.CONTENT_ITEM_TYPE; 1111 case MATCH_RECORDED_PROGRAM: 1112 return RecordedPrograms.CONTENT_TYPE; 1113 case MATCH_RECORDED_PROGRAM_ID: 1114 return RecordedPrograms.CONTENT_ITEM_TYPE; 1115 case MATCH_PREVIEW_PROGRAM: 1116 return PreviewPrograms.CONTENT_TYPE; 1117 case MATCH_PREVIEW_PROGRAM_ID: 1118 return PreviewPrograms.CONTENT_ITEM_TYPE; 1119 case MATCH_WATCH_NEXT_PROGRAM: 1120 return WatchNextPrograms.CONTENT_TYPE; 1121 case MATCH_WATCH_NEXT_PROGRAM_ID: 1122 return WatchNextPrograms.CONTENT_ITEM_TYPE; 1123 default: 1124 throw new IllegalArgumentException("Unknown URI " + uri); 1125 } 1126 } 1127 1128 @Override 1129 public Bundle call(String method, String arg, Bundle extras) { 1130 if (!callerHasAccessAllEpgDataPermission()) { 1131 return null; 1132 } 1133 ensureInitialized(); 1134 Map<String, String> projectionMap; 1135 switch (method) { 1136 case TvContract.METHOD_GET_COLUMNS: 1137 switch (sUriMatcher.match(Uri.parse(arg))) { 1138 case MATCH_CHANNEL: 1139 projectionMap = sChannelProjectionMap; 1140 break; 1141 case MATCH_PROGRAM: 1142 projectionMap = sProgramProjectionMap; 1143 break; 1144 case MATCH_PREVIEW_PROGRAM: 1145 projectionMap = sPreviewProgramProjectionMap; 1146 break; 1147 case MATCH_WATCH_NEXT_PROGRAM: 1148 projectionMap = sWatchNextProgramProjectionMap; 1149 break; 1150 case MATCH_RECORDED_PROGRAM: 1151 projectionMap = sRecordedProgramProjectionMap; 1152 break; 1153 default: 1154 return null; 1155 } 1156 Bundle result = new Bundle(); 1157 result.putStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES, 1158 projectionMap.keySet().toArray(new String[projectionMap.size()])); 1159 return result; 1160 case TvContract.METHOD_ADD_COLUMN: 1161 CharSequence columnName = extras.getCharSequence(TvContract.EXTRA_COLUMN_NAME); 1162 CharSequence dataType = extras.getCharSequence(TvContract.EXTRA_DATA_TYPE); 1163 if (TextUtils.isEmpty(columnName) || TextUtils.isEmpty(dataType)) { 1164 return null; 1165 } 1166 CharSequence defaultValue = extras.getCharSequence(TvContract.EXTRA_DEFAULT_VALUE); 1167 try { 1168 defaultValue = TextUtils.isEmpty(defaultValue) ? "" : generateDefaultClause( 1169 dataType.toString(), defaultValue.toString()); 1170 } catch (IllegalArgumentException e) { 1171 return null; 1172 } 1173 String tableName; 1174 switch (sUriMatcher.match(Uri.parse(arg))) { 1175 case MATCH_CHANNEL: 1176 tableName = CHANNELS_TABLE; 1177 projectionMap = sChannelProjectionMap; 1178 break; 1179 case MATCH_PROGRAM: 1180 tableName = PROGRAMS_TABLE; 1181 projectionMap = sProgramProjectionMap; 1182 break; 1183 case MATCH_PREVIEW_PROGRAM: 1184 tableName = PREVIEW_PROGRAMS_TABLE; 1185 projectionMap = sPreviewProgramProjectionMap; 1186 break; 1187 case MATCH_WATCH_NEXT_PROGRAM: 1188 tableName = WATCH_NEXT_PROGRAMS_TABLE; 1189 projectionMap = sWatchNextProgramProjectionMap; 1190 break; 1191 case MATCH_RECORDED_PROGRAM: 1192 tableName = RECORDED_PROGRAMS_TABLE; 1193 projectionMap = sRecordedProgramProjectionMap; 1194 break; 1195 default: 1196 return null; 1197 } 1198 try (SQLiteDatabase db = mOpenHelper.getWritableDatabase()) { 1199 db.execSQL("ALTER TABLE " + tableName + " ADD " 1200 + columnName + " " + dataType + defaultValue + ";"); 1201 projectionMap.put(columnName.toString(), tableName + '.' + columnName); 1202 Bundle returnValue = new Bundle(); 1203 returnValue.putStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES, 1204 projectionMap.keySet().toArray(new String[projectionMap.size()])); 1205 return returnValue; 1206 } catch (SQLException e) { 1207 return null; 1208 } 1209 case TvContract.METHOD_GET_BLOCKED_PACKAGES: 1210 Bundle allBlockedPackages = new Bundle(); 1211 allBlockedPackages.putStringArray(TvContract.EXTRA_BLOCKED_PACKAGES, 1212 sBlockedPackages.keySet().toArray(new String[sBlockedPackages.size()])); 1213 return allBlockedPackages; 1214 case TvContract.METHOD_BLOCK_PACKAGE: 1215 String packageNameToBlock = arg; 1216 Bundle blockPackageResult = new Bundle(); 1217 if (!TextUtils.isEmpty(packageNameToBlock)) { 1218 sBlockedPackages.put(packageNameToBlock, true); 1219 if (sBlockedPackagesSharedPreference.edit().putStringSet( 1220 SHARED_PREF_BLOCKED_PACKAGES_KEY, sBlockedPackages.keySet()).commit()) { 1221 String[] channelSelectionArgs = new String[] { 1222 packageNameToBlock, Channels.TYPE_PREVIEW }; 1223 delete(TvContract.Channels.CONTENT_URI, 1224 Channels.COLUMN_PACKAGE_NAME + "=? AND " 1225 + Channels.COLUMN_TYPE + "=?", 1226 channelSelectionArgs); 1227 String[] programsSelectionArgs = new String[] { 1228 packageNameToBlock }; 1229 delete(TvContract.PreviewPrograms.CONTENT_URI, 1230 PreviewPrograms.COLUMN_PACKAGE_NAME + "=?", programsSelectionArgs); 1231 delete(TvContract.WatchNextPrograms.CONTENT_URI, 1232 WatchNextPrograms.COLUMN_PACKAGE_NAME + "=?", 1233 programsSelectionArgs); 1234 blockPackageResult.putInt( 1235 TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_OK); 1236 } else { 1237 Log.e(TAG, "Blocking package " + packageNameToBlock + " failed"); 1238 sBlockedPackages.remove(packageNameToBlock); 1239 blockPackageResult.putInt(TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_ERROR_IO); 1240 } 1241 } else { 1242 blockPackageResult.putInt( 1243 TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_ERROR_INVALID_ARGUMENT); 1244 } 1245 return blockPackageResult; 1246 case TvContract.METHOD_UNBLOCK_PACKAGE: 1247 String packageNameToUnblock = arg; 1248 Bundle unblockPackageResult = new Bundle(); 1249 if (!TextUtils.isEmpty(packageNameToUnblock)) { 1250 sBlockedPackages.remove(packageNameToUnblock); 1251 if (sBlockedPackagesSharedPreference.edit().putStringSet( 1252 SHARED_PREF_BLOCKED_PACKAGES_KEY, sBlockedPackages.keySet()).commit()) { 1253 unblockPackageResult.putInt( 1254 TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_OK); 1255 } else { 1256 Log.e(TAG, "Unblocking package " + packageNameToUnblock + " failed"); 1257 sBlockedPackages.put(packageNameToUnblock, true); 1258 unblockPackageResult.putInt( 1259 TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_ERROR_IO); 1260 } 1261 } else { 1262 unblockPackageResult.putInt( 1263 TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_ERROR_INVALID_ARGUMENT); 1264 } 1265 return unblockPackageResult; 1266 } 1267 return null; 1268 } 1269 1270 @Override 1271 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1272 String sortOrder) { 1273 ensureInitialized(); 1274 mTransientRowHelper.ensureOldTransientRowsDeleted(); 1275 boolean needsToValidateSortOrder = !callerHasAccessAllEpgDataPermission(); 1276 SqlParams params = createSqlParams(OP_QUERY, uri, selection, selectionArgs); 1277 1278 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 1279 queryBuilder.setStrict(needsToValidateSortOrder); 1280 queryBuilder.setTables(params.getTables()); 1281 String orderBy = null; 1282 Map<String, String> projectionMap; 1283 switch (params.getTables()) { 1284 case PROGRAMS_TABLE: 1285 projectionMap = sProgramProjectionMap; 1286 orderBy = DEFAULT_PROGRAMS_SORT_ORDER; 1287 break; 1288 case WATCHED_PROGRAMS_TABLE: 1289 projectionMap = sWatchedProgramProjectionMap; 1290 orderBy = DEFAULT_WATCHED_PROGRAMS_SORT_ORDER; 1291 break; 1292 case RECORDED_PROGRAMS_TABLE: 1293 projectionMap = sRecordedProgramProjectionMap; 1294 break; 1295 case PREVIEW_PROGRAMS_TABLE: 1296 projectionMap = sPreviewProgramProjectionMap; 1297 break; 1298 case WATCH_NEXT_PROGRAMS_TABLE: 1299 projectionMap = sWatchNextProgramProjectionMap; 1300 break; 1301 default: 1302 projectionMap = sChannelProjectionMap; 1303 break; 1304 } 1305 queryBuilder.setProjectionMap(createProjectionMapForQuery(projection, projectionMap)); 1306 if (needsToValidateSortOrder) { 1307 validateSortOrder(sortOrder, projectionMap.keySet()); 1308 } 1309 1310 // Use the default sort order only if no sort order is specified. 1311 if (!TextUtils.isEmpty(sortOrder)) { 1312 orderBy = sortOrder; 1313 } 1314 1315 // Get the database and run the query. 1316 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1317 Cursor c = queryBuilder.query(db, projection, params.getSelection(), 1318 params.getSelectionArgs(), null, null, orderBy); 1319 1320 // Tell the cursor what URI to watch, so it knows when its source data changes. 1321 c.setNotificationUri(getContext().getContentResolver(), uri); 1322 return c; 1323 } 1324 1325 @Override 1326 public Uri insert(Uri uri, ContentValues values) { 1327 ensureInitialized(); 1328 mTransientRowHelper.ensureOldTransientRowsDeleted(); 1329 switch (sUriMatcher.match(uri)) { 1330 case MATCH_CHANNEL: 1331 // Preview channels are not necessarily associated with TV input service. 1332 // Therefore, we fill a fake ID to meet not null restriction for preview channels. 1333 if (values.get(Channels.COLUMN_INPUT_ID) == null 1334 && Channels.TYPE_PREVIEW.equals(values.get(Channels.COLUMN_TYPE))) { 1335 values.put(Channels.COLUMN_INPUT_ID, EMPTY_STRING); 1336 } 1337 filterContentValues(values, sChannelProjectionMap); 1338 return insertChannel(uri, values); 1339 case MATCH_PROGRAM: 1340 filterContentValues(values, sProgramProjectionMap); 1341 return insertProgram(uri, values); 1342 case MATCH_WATCHED_PROGRAM: 1343 return insertWatchedProgram(uri, values); 1344 case MATCH_RECORDED_PROGRAM: 1345 filterContentValues(values, sRecordedProgramProjectionMap); 1346 return insertRecordedProgram(uri, values); 1347 case MATCH_PREVIEW_PROGRAM: 1348 filterContentValues(values, sPreviewProgramProjectionMap); 1349 return insertPreviewProgram(uri, values); 1350 case MATCH_WATCH_NEXT_PROGRAM: 1351 filterContentValues(values, sWatchNextProgramProjectionMap); 1352 return insertWatchNextProgram(uri, values); 1353 case MATCH_CHANNEL_ID: 1354 case MATCH_CHANNEL_ID_LOGO: 1355 case MATCH_PASSTHROUGH_ID: 1356 case MATCH_PROGRAM_ID: 1357 case MATCH_WATCHED_PROGRAM_ID: 1358 case MATCH_RECORDED_PROGRAM_ID: 1359 case MATCH_PREVIEW_PROGRAM_ID: 1360 throw new UnsupportedOperationException("Cannot insert into that URI: " + uri); 1361 default: 1362 throw new IllegalArgumentException("Unknown URI " + uri); 1363 } 1364 } 1365 1366 private Uri insertChannel(Uri uri, ContentValues values) { 1367 if (TextUtils.equals(values.getAsString(Channels.COLUMN_TYPE), Channels.TYPE_PREVIEW)) { 1368 blockIllegalAccessFromBlockedPackage(); 1369 } 1370 // Mark the owner package of this channel. 1371 values.put(Channels.COLUMN_PACKAGE_NAME, getCallingPackage_()); 1372 blockIllegalAccessToChannelsSystemColumns(values); 1373 1374 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1375 long rowId = db.insert(CHANNELS_TABLE, null, values); 1376 if (rowId > 0) { 1377 Uri channelUri = TvContract.buildChannelUri(rowId); 1378 notifyChange(channelUri); 1379 return channelUri; 1380 } 1381 1382 throw new SQLException("Failed to insert row into " + uri); 1383 } 1384 1385 private Uri insertProgram(Uri uri, ContentValues values) { 1386 // Mark the owner package of this program. 1387 values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_()); 1388 1389 checkAndConvertGenre(values); 1390 checkAndConvertDeprecatedColumns(values); 1391 1392 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1393 long rowId = db.insert(PROGRAMS_TABLE, null, values); 1394 if (rowId > 0) { 1395 Uri programUri = TvContract.buildProgramUri(rowId); 1396 notifyChange(programUri); 1397 return programUri; 1398 } 1399 1400 throw new SQLException("Failed to insert row into " + uri); 1401 } 1402 1403 private Uri insertWatchedProgram(Uri uri, ContentValues values) { 1404 if (DEBUG) { 1405 Log.d(TAG, "insertWatchedProgram(uri=" + uri + ", values={" + values + "})"); 1406 } 1407 Long watchStartTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS); 1408 Long watchEndTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS); 1409 // The system sends only two kinds of watch events: 1410 // 1. The user tunes to a new channel. (COLUMN_WATCH_START_TIME_UTC_MILLIS) 1411 // 2. The user stops watching. (COLUMN_WATCH_END_TIME_UTC_MILLIS) 1412 if (watchStartTime != null && watchEndTime == null) { 1413 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1414 long rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values); 1415 if (rowId > 0) { 1416 mLogHandler.removeMessages(WatchLogHandler.MSG_TRY_CONSOLIDATE_ALL); 1417 mLogHandler.sendEmptyMessageDelayed(WatchLogHandler.MSG_TRY_CONSOLIDATE_ALL, 1418 MAX_PROGRAM_DATA_DELAY_IN_MILLIS); 1419 return TvContract.buildWatchedProgramUri(rowId); 1420 } 1421 Log.w(TAG, "Failed to insert row for " + values + ". Channel does not exist."); 1422 return null; 1423 } else if (watchStartTime == null && watchEndTime != null) { 1424 SomeArgs args = SomeArgs.obtain(); 1425 args.arg1 = values.getAsString(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN); 1426 args.arg2 = watchEndTime; 1427 Message msg = mLogHandler.obtainMessage(WatchLogHandler.MSG_CONSOLIDATE, args); 1428 mLogHandler.sendMessageDelayed(msg, MAX_PROGRAM_DATA_DELAY_IN_MILLIS); 1429 return null; 1430 } 1431 // All the other cases are invalid. 1432 throw new IllegalArgumentException("Only one of COLUMN_WATCH_START_TIME_UTC_MILLIS and" 1433 + " COLUMN_WATCH_END_TIME_UTC_MILLIS should be specified"); 1434 } 1435 1436 private Uri insertRecordedProgram(Uri uri, ContentValues values) { 1437 // Mark the owner package of this program. 1438 values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_()); 1439 1440 checkAndConvertGenre(values); 1441 1442 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1443 long rowId = db.insert(RECORDED_PROGRAMS_TABLE, null, values); 1444 if (rowId > 0) { 1445 Uri recordedProgramUri = TvContract.buildRecordedProgramUri(rowId); 1446 notifyChange(recordedProgramUri); 1447 return recordedProgramUri; 1448 } 1449 1450 throw new SQLException("Failed to insert row into " + uri); 1451 } 1452 1453 private Uri insertPreviewProgram(Uri uri, ContentValues values) { 1454 if (!values.containsKey(PreviewPrograms.COLUMN_TYPE)) { 1455 throw new IllegalArgumentException("Missing the required column: " + 1456 PreviewPrograms.COLUMN_TYPE); 1457 } 1458 blockIllegalAccessFromBlockedPackage(); 1459 // Mark the owner package of this program. 1460 values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_()); 1461 blockIllegalAccessToPreviewProgramsSystemColumns(values); 1462 1463 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1464 long rowId = db.insert(PREVIEW_PROGRAMS_TABLE, null, values); 1465 if (rowId > 0) { 1466 Uri previewProgramUri = TvContract.buildPreviewProgramUri(rowId); 1467 notifyChange(previewProgramUri); 1468 return previewProgramUri; 1469 } 1470 1471 throw new SQLException("Failed to insert row into " + uri); 1472 } 1473 1474 private Uri insertWatchNextProgram(Uri uri, ContentValues values) { 1475 if (!values.containsKey(WatchNextPrograms.COLUMN_TYPE)) { 1476 throw new IllegalArgumentException("Missing the required column: " + 1477 WatchNextPrograms.COLUMN_TYPE); 1478 } 1479 blockIllegalAccessFromBlockedPackage(); 1480 if (!callerHasAccessAllEpgDataPermission() || 1481 !values.containsKey(Programs.COLUMN_PACKAGE_NAME)) { 1482 // Mark the owner package of this program. System app with a proper permission may 1483 // change the owner of the program. 1484 values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_()); 1485 } 1486 blockIllegalAccessToPreviewProgramsSystemColumns(values); 1487 1488 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1489 long rowId = db.insert(WATCH_NEXT_PROGRAMS_TABLE, null, values); 1490 if (rowId > 0) { 1491 Uri watchNextProgramUri = TvContract.buildWatchNextProgramUri(rowId); 1492 notifyChange(watchNextProgramUri); 1493 return watchNextProgramUri; 1494 } 1495 1496 throw new SQLException("Failed to insert row into " + uri); 1497 } 1498 1499 @Override 1500 public int delete(Uri uri, String selection, String[] selectionArgs) { 1501 mTransientRowHelper.ensureOldTransientRowsDeleted(); 1502 SqlParams params = createSqlParams(OP_DELETE, uri, selection, selectionArgs); 1503 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1504 int count; 1505 switch (sUriMatcher.match(uri)) { 1506 case MATCH_CHANNEL_ID_LOGO: 1507 ContentValues values = new ContentValues(); 1508 values.putNull(CHANNELS_COLUMN_LOGO); 1509 count = db.update(params.getTables(), values, params.getSelection(), 1510 params.getSelectionArgs()); 1511 break; 1512 case MATCH_CHANNEL: 1513 case MATCH_PROGRAM: 1514 case MATCH_WATCHED_PROGRAM: 1515 case MATCH_RECORDED_PROGRAM: 1516 case MATCH_PREVIEW_PROGRAM: 1517 case MATCH_WATCH_NEXT_PROGRAM: 1518 case MATCH_CHANNEL_ID: 1519 case MATCH_PASSTHROUGH_ID: 1520 case MATCH_PROGRAM_ID: 1521 case MATCH_WATCHED_PROGRAM_ID: 1522 case MATCH_RECORDED_PROGRAM_ID: 1523 case MATCH_PREVIEW_PROGRAM_ID: 1524 case MATCH_WATCH_NEXT_PROGRAM_ID: 1525 count = db.delete(params.getTables(), params.getSelection(), 1526 params.getSelectionArgs()); 1527 break; 1528 default: 1529 throw new IllegalArgumentException("Unknown URI " + uri); 1530 } 1531 if (count > 0) { 1532 notifyChange(uri); 1533 } 1534 return count; 1535 } 1536 1537 @Override 1538 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1539 mTransientRowHelper.ensureOldTransientRowsDeleted(); 1540 SqlParams params = createSqlParams(OP_UPDATE, uri, selection, selectionArgs); 1541 blockIllegalAccessToIdAndPackageName(uri, values); 1542 boolean containImmutableColumn = false; 1543 if (params.getTables().equals(CHANNELS_TABLE)) { 1544 filterContentValues(values, sChannelProjectionMap); 1545 containImmutableColumn = disallowModifyChannelType(values, params); 1546 if (containImmutableColumn && sUriMatcher.match(uri) != MATCH_CHANNEL_ID) { 1547 Log.i(TAG, "Updating failed. Attempt to change immutable column for channels."); 1548 return 0; 1549 } 1550 blockIllegalAccessToChannelsSystemColumns(values); 1551 } else if (params.getTables().equals(PROGRAMS_TABLE)) { 1552 filterContentValues(values, sProgramProjectionMap); 1553 checkAndConvertGenre(values); 1554 checkAndConvertDeprecatedColumns(values); 1555 } else if (params.getTables().equals(RECORDED_PROGRAMS_TABLE)) { 1556 filterContentValues(values, sRecordedProgramProjectionMap); 1557 checkAndConvertGenre(values); 1558 } else if (params.getTables().equals(PREVIEW_PROGRAMS_TABLE)) { 1559 filterContentValues(values, sPreviewProgramProjectionMap); 1560 containImmutableColumn = disallowModifyChannelId(values, params); 1561 if (containImmutableColumn && PreviewPrograms.CONTENT_URI.equals(uri)) { 1562 Log.i(TAG, "Updating failed. Attempt to change unmodifiable column for " 1563 + "preview programs."); 1564 return 0; 1565 } 1566 blockIllegalAccessToPreviewProgramsSystemColumns(values); 1567 } else if (params.getTables().equals(WATCH_NEXT_PROGRAMS_TABLE)) { 1568 filterContentValues(values, sWatchNextProgramProjectionMap); 1569 blockIllegalAccessToPreviewProgramsSystemColumns(values); 1570 } 1571 if (values.size() == 0) { 1572 // All values may be filtered out, no need to update 1573 return 0; 1574 } 1575 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1576 int count = db.update(params.getTables(), values, params.getSelection(), 1577 params.getSelectionArgs()); 1578 if (count > 0) { 1579 notifyChange(uri); 1580 } else if (containImmutableColumn) { 1581 Log.i(TAG, "Updating failed. The item may not exist or attempt to change " 1582 + "immutable column."); 1583 } 1584 return count; 1585 } 1586 1587 private synchronized void ensureInitialized() { 1588 if (!sInitialized) { 1589 // Database is not accessed before and the projection maps and the blocked package list 1590 // are not updated yet. Gets database here to make it initialized. 1591 mOpenHelper.getReadableDatabase(); 1592 } 1593 } 1594 1595 private Map<String, String> createProjectionMapForQuery(String[] projection, 1596 Map<String, String> projectionMap) { 1597 if (projection == null) { 1598 return projectionMap; 1599 } 1600 Map<String, String> columnProjectionMap = new HashMap<>(); 1601 for (String columnName : projection) { 1602 // Value NULL will be provided if the requested column does not exist in the database. 1603 columnProjectionMap.put(columnName, 1604 projectionMap.getOrDefault(columnName, "NULL as " + columnName)); 1605 } 1606 return columnProjectionMap; 1607 } 1608 1609 private void filterContentValues(ContentValues values, Map<String, String> projectionMap) { 1610 Iterator<String> iter = values.keySet().iterator(); 1611 while (iter.hasNext()) { 1612 String columnName = iter.next(); 1613 if (!projectionMap.containsKey(columnName)) { 1614 iter.remove(); 1615 } 1616 } 1617 } 1618 1619 private SqlParams createSqlParams(String operation, Uri uri, String selection, 1620 String[] selectionArgs) { 1621 int match = sUriMatcher.match(uri); 1622 SqlParams params = new SqlParams(null, selection, selectionArgs); 1623 1624 // Control access to EPG data (excluding watched programs) when the caller doesn't have all 1625 // access. 1626 String prefix = match == MATCH_CHANNEL ? CHANNELS_TABLE + "." : ""; 1627 if (!callerHasAccessAllEpgDataPermission() 1628 && match != MATCH_WATCHED_PROGRAM && match != MATCH_WATCHED_PROGRAM_ID) { 1629 if (!TextUtils.isEmpty(selection)) { 1630 throw new SecurityException("Selection not allowed for " + uri); 1631 } 1632 // Limit the operation only to the data that the calling package owns except for when 1633 // the caller tries to read TV listings and has the appropriate permission. 1634 if (operation.equals(OP_QUERY) && callerHasReadTvListingsPermission()) { 1635 params.setWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=? OR " 1636 + Channels.COLUMN_SEARCHABLE + "=?", getCallingPackage_(), "1"); 1637 } else { 1638 params.setWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=?", 1639 getCallingPackage_()); 1640 } 1641 } 1642 String packageName = uri.getQueryParameter(TvContract.PARAM_PACKAGE); 1643 if (packageName != null) { 1644 params.appendWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=?", packageName); 1645 } 1646 1647 switch (match) { 1648 case MATCH_CHANNEL: 1649 String genre = uri.getQueryParameter(TvContract.PARAM_CANONICAL_GENRE); 1650 if (genre == null) { 1651 params.setTables(CHANNELS_TABLE); 1652 } else { 1653 if (!operation.equals(OP_QUERY)) { 1654 throw new SecurityException(capitalize(operation) 1655 + " not allowed for " + uri); 1656 } 1657 if (!Genres.isCanonical(genre)) { 1658 throw new IllegalArgumentException("Not a canonical genre : " + genre); 1659 } 1660 params.setTables(CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE); 1661 String curTime = String.valueOf(System.currentTimeMillis()); 1662 params.appendWhere("LIKE(?, " + Programs.COLUMN_CANONICAL_GENRE + ") AND " 1663 + Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND " 1664 + Programs.COLUMN_END_TIME_UTC_MILLIS + ">=?", 1665 "%" + genre + "%", curTime, curTime); 1666 } 1667 String inputId = uri.getQueryParameter(TvContract.PARAM_INPUT); 1668 if (inputId != null) { 1669 params.appendWhere(Channels.COLUMN_INPUT_ID + "=?", inputId); 1670 } 1671 boolean browsableOnly = uri.getBooleanQueryParameter( 1672 TvContract.PARAM_BROWSABLE_ONLY, false); 1673 if (browsableOnly) { 1674 params.appendWhere(Channels.COLUMN_BROWSABLE + "=1"); 1675 } 1676 String preview = uri.getQueryParameter(TvContract.PARAM_PREVIEW); 1677 if (preview != null) { 1678 String previewSelection = Channels.COLUMN_TYPE 1679 + (preview.equals(String.valueOf(true)) ? "=?" : "!=?"); 1680 params.appendWhere(previewSelection, Channels.TYPE_PREVIEW); 1681 } 1682 break; 1683 case MATCH_CHANNEL_ID: 1684 params.setTables(CHANNELS_TABLE); 1685 params.appendWhere(Channels._ID + "=?", uri.getLastPathSegment()); 1686 break; 1687 case MATCH_PROGRAM: 1688 params.setTables(PROGRAMS_TABLE); 1689 String paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL); 1690 if (paramChannelId != null) { 1691 String channelId = String.valueOf(Long.parseLong(paramChannelId)); 1692 params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId); 1693 } 1694 String paramStartTime = uri.getQueryParameter(TvContract.PARAM_START_TIME); 1695 String paramEndTime = uri.getQueryParameter(TvContract.PARAM_END_TIME); 1696 if (paramStartTime != null && paramEndTime != null) { 1697 String startTime = String.valueOf(Long.parseLong(paramStartTime)); 1698 String endTime = String.valueOf(Long.parseLong(paramEndTime)); 1699 params.appendWhere(Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND " 1700 + Programs.COLUMN_END_TIME_UTC_MILLIS + ">=?", endTime, startTime); 1701 } 1702 break; 1703 case MATCH_PROGRAM_ID: 1704 params.setTables(PROGRAMS_TABLE); 1705 params.appendWhere(Programs._ID + "=?", uri.getLastPathSegment()); 1706 break; 1707 case MATCH_WATCHED_PROGRAM: 1708 if (!callerHasAccessWatchedProgramsPermission()) { 1709 throw new SecurityException("Access not allowed for " + uri); 1710 } 1711 params.setTables(WATCHED_PROGRAMS_TABLE); 1712 params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1"); 1713 break; 1714 case MATCH_WATCHED_PROGRAM_ID: 1715 if (!callerHasAccessWatchedProgramsPermission()) { 1716 throw new SecurityException("Access not allowed for " + uri); 1717 } 1718 params.setTables(WATCHED_PROGRAMS_TABLE); 1719 params.appendWhere(WatchedPrograms._ID + "=?", uri.getLastPathSegment()); 1720 params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1"); 1721 break; 1722 case MATCH_RECORDED_PROGRAM_ID: 1723 params.appendWhere(RecordedPrograms._ID + "=?", uri.getLastPathSegment()); 1724 // fall-through 1725 case MATCH_RECORDED_PROGRAM: 1726 params.setTables(RECORDED_PROGRAMS_TABLE); 1727 paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL); 1728 if (paramChannelId != null) { 1729 String channelId = String.valueOf(Long.parseLong(paramChannelId)); 1730 params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId); 1731 } 1732 break; 1733 case MATCH_PREVIEW_PROGRAM_ID: 1734 params.appendWhere(PreviewPrograms._ID + "=?", uri.getLastPathSegment()); 1735 // fall-through 1736 case MATCH_PREVIEW_PROGRAM: 1737 params.setTables(PREVIEW_PROGRAMS_TABLE); 1738 paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL); 1739 if (paramChannelId != null) { 1740 String channelId = String.valueOf(Long.parseLong(paramChannelId)); 1741 params.appendWhere(PreviewPrograms.COLUMN_CHANNEL_ID + "=?", channelId); 1742 } 1743 break; 1744 case MATCH_WATCH_NEXT_PROGRAM_ID: 1745 params.appendWhere(WatchNextPrograms._ID + "=?", uri.getLastPathSegment()); 1746 // fall-through 1747 case MATCH_WATCH_NEXT_PROGRAM: 1748 params.setTables(WATCH_NEXT_PROGRAMS_TABLE); 1749 break; 1750 case MATCH_CHANNEL_ID_LOGO: 1751 if (operation.equals(OP_DELETE)) { 1752 params.setTables(CHANNELS_TABLE); 1753 params.appendWhere(Channels._ID + "=?", uri.getPathSegments().get(1)); 1754 break; 1755 } 1756 // fall-through 1757 case MATCH_PASSTHROUGH_ID: 1758 throw new UnsupportedOperationException(operation + " not permmitted on " + uri); 1759 default: 1760 throw new IllegalArgumentException("Unknown URI " + uri); 1761 } 1762 return params; 1763 } 1764 1765 private static String generateDefaultClause(String dataType, String defaultValue) 1766 throws IllegalArgumentException { 1767 String defaultValueString = " DEFAULT "; 1768 switch (dataType.toLowerCase()) { 1769 case "integer": 1770 return defaultValueString + Integer.parseInt(defaultValue); 1771 case "real": 1772 return defaultValueString + Double.parseDouble(defaultValue); 1773 case "text": 1774 case "blob": 1775 return defaultValueString + DatabaseUtils.sqlEscapeString(defaultValue); 1776 default: 1777 throw new IllegalArgumentException("Illegal data type \"" + dataType 1778 + "\" with default value: " + defaultValue); 1779 } 1780 } 1781 1782 private static String capitalize(String str) { 1783 return Character.toUpperCase(str.charAt(0)) + str.substring(1); 1784 } 1785 1786 @SuppressLint("DefaultLocale") 1787 private void checkAndConvertGenre(ContentValues values) { 1788 String canonicalGenres = values.getAsString(Programs.COLUMN_CANONICAL_GENRE); 1789 1790 if (!TextUtils.isEmpty(canonicalGenres)) { 1791 // Check if the canonical genres are valid. If not, clear them. 1792 String[] genres = Genres.decode(canonicalGenres); 1793 for (String genre : genres) { 1794 if (!Genres.isCanonical(genre)) { 1795 values.putNull(Programs.COLUMN_CANONICAL_GENRE); 1796 canonicalGenres = null; 1797 break; 1798 } 1799 } 1800 } 1801 1802 if (TextUtils.isEmpty(canonicalGenres)) { 1803 // If the canonical genre is not set, try to map the broadcast genre to the canonical 1804 // genre. 1805 String broadcastGenres = values.getAsString(Programs.COLUMN_BROADCAST_GENRE); 1806 if (!TextUtils.isEmpty(broadcastGenres)) { 1807 Set<String> genreSet = new HashSet<>(); 1808 String[] genres = Genres.decode(broadcastGenres); 1809 for (String genre : genres) { 1810 String canonicalGenre = sGenreMap.get(genre.toUpperCase()); 1811 if (Genres.isCanonical(canonicalGenre)) { 1812 genreSet.add(canonicalGenre); 1813 } 1814 } 1815 if (genreSet.size() > 0) { 1816 values.put(Programs.COLUMN_CANONICAL_GENRE, 1817 Genres.encode(genreSet.toArray(new String[genreSet.size()]))); 1818 } 1819 } 1820 } 1821 } 1822 1823 private void checkAndConvertDeprecatedColumns(ContentValues values) { 1824 if (values.containsKey(Programs.COLUMN_SEASON_NUMBER)) { 1825 if (!values.containsKey(Programs.COLUMN_SEASON_DISPLAY_NUMBER)) { 1826 values.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, values.getAsInteger( 1827 Programs.COLUMN_SEASON_NUMBER)); 1828 } 1829 values.remove(Programs.COLUMN_SEASON_NUMBER); 1830 } 1831 if (values.containsKey(Programs.COLUMN_EPISODE_NUMBER)) { 1832 if (!values.containsKey(Programs.COLUMN_EPISODE_DISPLAY_NUMBER)) { 1833 values.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER, values.getAsInteger( 1834 Programs.COLUMN_EPISODE_NUMBER)); 1835 } 1836 values.remove(Programs.COLUMN_EPISODE_NUMBER); 1837 } 1838 } 1839 1840 // We might have more than one thread trying to make its way through applyBatch() so the 1841 // notification coalescing needs to be thread-local to work correctly. 1842 private final ThreadLocal<Set<Uri>> mTLBatchNotifications = new ThreadLocal<>(); 1843 1844 private Set<Uri> getBatchNotificationsSet() { 1845 return mTLBatchNotifications.get(); 1846 } 1847 1848 private void setBatchNotificationsSet(Set<Uri> batchNotifications) { 1849 mTLBatchNotifications.set(batchNotifications); 1850 } 1851 1852 @Override 1853 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1854 throws OperationApplicationException { 1855 setBatchNotificationsSet(new HashSet<Uri>()); 1856 Context context = getContext(); 1857 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1858 db.beginTransaction(); 1859 try { 1860 ContentProviderResult[] results = super.applyBatch(operations); 1861 db.setTransactionSuccessful(); 1862 return results; 1863 } finally { 1864 db.endTransaction(); 1865 final Set<Uri> notifications = getBatchNotificationsSet(); 1866 setBatchNotificationsSet(null); 1867 for (final Uri uri : notifications) { 1868 context.getContentResolver().notifyChange(uri, null); 1869 } 1870 } 1871 } 1872 1873 @Override 1874 public int bulkInsert(Uri uri, ContentValues[] values) { 1875 setBatchNotificationsSet(new HashSet<Uri>()); 1876 Context context = getContext(); 1877 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1878 db.beginTransaction(); 1879 try { 1880 int result = super.bulkInsert(uri, values); 1881 db.setTransactionSuccessful(); 1882 return result; 1883 } finally { 1884 db.endTransaction(); 1885 final Set<Uri> notifications = getBatchNotificationsSet(); 1886 setBatchNotificationsSet(null); 1887 for (final Uri notificationUri : notifications) { 1888 context.getContentResolver().notifyChange(notificationUri, null); 1889 } 1890 } 1891 } 1892 1893 private void notifyChange(Uri uri) { 1894 final Set<Uri> batchNotifications = getBatchNotificationsSet(); 1895 if (batchNotifications != null) { 1896 batchNotifications.add(uri); 1897 } else { 1898 getContext().getContentResolver().notifyChange(uri, null); 1899 } 1900 } 1901 1902 private boolean callerHasReadTvListingsPermission() { 1903 return getContext().checkCallingOrSelfPermission(PERMISSION_READ_TV_LISTINGS) 1904 == PackageManager.PERMISSION_GRANTED; 1905 } 1906 1907 private boolean callerHasAccessAllEpgDataPermission() { 1908 return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_ALL_EPG_DATA) 1909 == PackageManager.PERMISSION_GRANTED; 1910 } 1911 1912 private boolean callerHasAccessWatchedProgramsPermission() { 1913 return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_WATCHED_PROGRAMS) 1914 == PackageManager.PERMISSION_GRANTED; 1915 } 1916 1917 private boolean callerHasModifyParentalControlsPermission() { 1918 return getContext().checkCallingOrSelfPermission( 1919 android.Manifest.permission.MODIFY_PARENTAL_CONTROLS) 1920 == PackageManager.PERMISSION_GRANTED; 1921 } 1922 1923 private void blockIllegalAccessToIdAndPackageName(Uri uri, ContentValues values) { 1924 if (values.containsKey(BaseColumns._ID)) { 1925 int match = sUriMatcher.match(uri); 1926 switch (match) { 1927 case MATCH_CHANNEL_ID: 1928 case MATCH_PROGRAM_ID: 1929 case MATCH_PREVIEW_PROGRAM_ID: 1930 case MATCH_RECORDED_PROGRAM_ID: 1931 case MATCH_WATCH_NEXT_PROGRAM_ID: 1932 case MATCH_WATCHED_PROGRAM_ID: 1933 if (TextUtils.equals(values.getAsString(BaseColumns._ID), 1934 uri.getLastPathSegment())) { 1935 break; 1936 } 1937 default: 1938 throw new IllegalArgumentException("Not allowed to change ID."); 1939 } 1940 } 1941 if (values.containsKey(BaseTvColumns.COLUMN_PACKAGE_NAME) 1942 && !callerHasAccessAllEpgDataPermission() && !TextUtils.equals(values.getAsString( 1943 BaseTvColumns.COLUMN_PACKAGE_NAME), getCallingPackage_())) { 1944 throw new SecurityException("Not allowed to change package name."); 1945 } 1946 } 1947 1948 private void blockIllegalAccessToChannelsSystemColumns(ContentValues values) { 1949 if (values.containsKey(Channels.COLUMN_LOCKED) 1950 && !callerHasModifyParentalControlsPermission()) { 1951 throw new SecurityException("Not allowed to access Channels.COLUMN_LOCKED"); 1952 } 1953 Boolean hasAccessAllEpgDataPermission = null; 1954 if (values.containsKey(Channels.COLUMN_BROWSABLE)) { 1955 hasAccessAllEpgDataPermission = callerHasAccessAllEpgDataPermission(); 1956 if (!hasAccessAllEpgDataPermission) { 1957 throw new SecurityException("Not allowed to access Channels.COLUMN_BROWSABLE"); 1958 } 1959 } 1960 } 1961 1962 private void blockIllegalAccessToPreviewProgramsSystemColumns(ContentValues values) { 1963 if (values.containsKey(PreviewPrograms.COLUMN_BROWSABLE) 1964 && !callerHasAccessAllEpgDataPermission()) { 1965 throw new SecurityException("Not allowed to access Programs.COLUMN_BROWSABLE"); 1966 } 1967 } 1968 1969 private void blockIllegalAccessFromBlockedPackage() { 1970 String callingPackageName = getCallingPackage_(); 1971 if (sBlockedPackages.containsKey(callingPackageName)) { 1972 throw new SecurityException( 1973 "Not allowed to access " + TvContract.AUTHORITY + ", " 1974 + callingPackageName + " is blocked"); 1975 } 1976 } 1977 1978 private boolean disallowModifyChannelType(ContentValues values, SqlParams params) { 1979 if (values.containsKey(Channels.COLUMN_TYPE)) { 1980 params.appendWhere(Channels.COLUMN_TYPE + "=?", 1981 values.getAsString(Channels.COLUMN_TYPE)); 1982 return true; 1983 } 1984 return false; 1985 } 1986 1987 private boolean disallowModifyChannelId(ContentValues values, SqlParams params) { 1988 if (values.containsKey(PreviewPrograms.COLUMN_CHANNEL_ID)) { 1989 params.appendWhere(PreviewPrograms.COLUMN_CHANNEL_ID + "=?", 1990 values.getAsString(PreviewPrograms.COLUMN_CHANNEL_ID)); 1991 return true; 1992 } 1993 return false; 1994 } 1995 1996 @Override 1997 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 1998 switch (sUriMatcher.match(uri)) { 1999 case MATCH_CHANNEL_ID_LOGO: 2000 return openLogoFile(uri, mode); 2001 default: 2002 throw new FileNotFoundException(uri.toString()); 2003 } 2004 } 2005 2006 private ParcelFileDescriptor openLogoFile(Uri uri, String mode) throws FileNotFoundException { 2007 long channelId = Long.parseLong(uri.getPathSegments().get(1)); 2008 2009 SqlParams params = new SqlParams(CHANNELS_TABLE, Channels._ID + "=?", 2010 String.valueOf(channelId)); 2011 if (!callerHasAccessAllEpgDataPermission()) { 2012 if (callerHasReadTvListingsPermission()) { 2013 params.appendWhere(Channels.COLUMN_PACKAGE_NAME + "=? OR " 2014 + Channels.COLUMN_SEARCHABLE + "=?", getCallingPackage_(), "1"); 2015 } else { 2016 params.appendWhere(Channels.COLUMN_PACKAGE_NAME + "=?", getCallingPackage_()); 2017 } 2018 } 2019 2020 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 2021 queryBuilder.setTables(params.getTables()); 2022 2023 // We don't write the database here. 2024 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 2025 if (mode.equals("r")) { 2026 String sql = queryBuilder.buildQuery(new String[] { CHANNELS_COLUMN_LOGO }, 2027 params.getSelection(), null, null, null, null); 2028 ParcelFileDescriptor fd = DatabaseUtils.blobFileDescriptorForQuery( 2029 db, sql, params.getSelectionArgs()); 2030 if (fd == null) { 2031 throw new FileNotFoundException(uri.toString()); 2032 } 2033 return fd; 2034 } else { 2035 try (Cursor cursor = queryBuilder.query(db, new String[] { Channels._ID }, 2036 params.getSelection(), params.getSelectionArgs(), null, null, null)) { 2037 if (cursor.getCount() < 1) { 2038 // Fails early if corresponding channel does not exist. 2039 // PipeMonitor may still fail to update DB later. 2040 throw new FileNotFoundException(uri.toString()); 2041 } 2042 } 2043 2044 try { 2045 ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe(); 2046 PipeMonitor pipeMonitor = new PipeMonitor(pipeFds[0], channelId, params); 2047 pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 2048 return pipeFds[1]; 2049 } catch (IOException ioe) { 2050 FileNotFoundException fne = new FileNotFoundException(uri.toString()); 2051 fne.initCause(ioe); 2052 throw fne; 2053 } 2054 } 2055 } 2056 2057 /** 2058 * Validates the sort order based on the given field set. 2059 * 2060 * @throws IllegalArgumentException if there is any unknown field. 2061 */ 2062 @SuppressLint("DefaultLocale") 2063 private static void validateSortOrder(String sortOrder, Set<String> possibleFields) { 2064 if (TextUtils.isEmpty(sortOrder) || possibleFields.isEmpty()) { 2065 return; 2066 } 2067 String[] orders = sortOrder.split(","); 2068 for (String order : orders) { 2069 String field = order.replaceAll("\\s+", " ").trim().toLowerCase().replace(" asc", "") 2070 .replace(" desc", ""); 2071 if (!possibleFields.contains(field)) { 2072 throw new IllegalArgumentException("Illegal field in sort order " + order); 2073 } 2074 } 2075 } 2076 2077 private class PipeMonitor extends AsyncTask<Void, Void, Void> { 2078 private final ParcelFileDescriptor mPfd; 2079 private final long mChannelId; 2080 private final SqlParams mParams; 2081 2082 private PipeMonitor(ParcelFileDescriptor pfd, long channelId, SqlParams params) { 2083 mPfd = pfd; 2084 mChannelId = channelId; 2085 mParams = params; 2086 } 2087 2088 @Override 2089 protected Void doInBackground(Void... params) { 2090 AutoCloseInputStream is = new AutoCloseInputStream(mPfd); 2091 ByteArrayOutputStream baos = null; 2092 int count = 0; 2093 try { 2094 Bitmap bitmap = BitmapFactory.decodeStream(is); 2095 if (bitmap == null) { 2096 Log.e(TAG, "Failed to decode logo image for channel ID " + mChannelId); 2097 return null; 2098 } 2099 2100 float scaleFactor = Math.min(1f, ((float) MAX_LOGO_IMAGE_SIZE) / 2101 Math.max(bitmap.getWidth(), bitmap.getHeight())); 2102 if (scaleFactor < 1f) { 2103 bitmap = Bitmap.createScaledBitmap(bitmap, 2104 (int) (bitmap.getWidth() * scaleFactor), 2105 (int) (bitmap.getHeight() * scaleFactor), false); 2106 } 2107 2108 baos = new ByteArrayOutputStream(); 2109 bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); 2110 byte[] bytes = baos.toByteArray(); 2111 2112 ContentValues values = new ContentValues(); 2113 values.put(CHANNELS_COLUMN_LOGO, bytes); 2114 2115 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 2116 count = db.update(mParams.getTables(), values, mParams.getSelection(), 2117 mParams.getSelectionArgs()); 2118 if (count > 0) { 2119 Uri uri = TvContract.buildChannelLogoUri(mChannelId); 2120 notifyChange(uri); 2121 } 2122 } finally { 2123 if (count == 0) { 2124 try { 2125 mPfd.closeWithError("Failed to write logo for channel ID " + mChannelId); 2126 } catch (IOException ioe) { 2127 Log.e(TAG, "Failed to close pipe", ioe); 2128 } 2129 } 2130 IoUtils.closeQuietly(baos); 2131 IoUtils.closeQuietly(is); 2132 } 2133 return null; 2134 } 2135 } 2136 2137 private void deleteUnconsolidatedWatchedProgramsRows() { 2138 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 2139 db.delete(WATCHED_PROGRAMS_TABLE, WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0", null); 2140 } 2141 2142 @SuppressLint("HandlerLeak") 2143 private final class WatchLogHandler extends Handler { 2144 private static final int MSG_CONSOLIDATE = 1; 2145 private static final int MSG_TRY_CONSOLIDATE_ALL = 2; 2146 2147 @Override 2148 public void handleMessage(Message msg) { 2149 switch (msg.what) { 2150 case MSG_CONSOLIDATE: { 2151 SomeArgs args = (SomeArgs) msg.obj; 2152 String sessionToken = (String) args.arg1; 2153 long watchEndTime = (long) args.arg2; 2154 onConsolidate(sessionToken, watchEndTime); 2155 args.recycle(); 2156 return; 2157 } 2158 case MSG_TRY_CONSOLIDATE_ALL: { 2159 onTryConsolidateAll(); 2160 return; 2161 } 2162 default: { 2163 Log.w(TAG, "Unhandled message code: " + msg.what); 2164 return; 2165 } 2166 } 2167 } 2168 2169 // Consolidates all WatchedPrograms rows for a given session with watch end time information 2170 // of the most recent log entry. After this method is called, it is guaranteed that there 2171 // remain consolidated rows only for that session. 2172 private void onConsolidate(String sessionToken, long watchEndTime) { 2173 if (DEBUG) { 2174 Log.d(TAG, "onConsolidate(sessionToken=" + sessionToken + ", watchEndTime=" 2175 + watchEndTime + ")"); 2176 } 2177 2178 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 2179 queryBuilder.setTables(WATCHED_PROGRAMS_TABLE); 2180 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 2181 2182 // Pick up the last row with the same session token. 2183 String[] projection = { 2184 WatchedPrograms._ID, 2185 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, 2186 WatchedPrograms.COLUMN_CHANNEL_ID 2187 }; 2188 String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=? AND " 2189 + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + "=?"; 2190 String[] selectionArgs = { 2191 "0", 2192 sessionToken 2193 }; 2194 String sortOrder = WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC"; 2195 2196 int consolidatedRowCount = 0; 2197 try (Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, 2198 null, sortOrder)) { 2199 long oldWatchStartTime = watchEndTime; 2200 while (cursor != null && cursor.moveToNext()) { 2201 long id = cursor.getLong(0); 2202 long watchStartTime = cursor.getLong(1); 2203 long channelId = cursor.getLong(2); 2204 consolidatedRowCount += consolidateRow(id, watchStartTime, oldWatchStartTime, 2205 channelId, false); 2206 oldWatchStartTime = watchStartTime; 2207 } 2208 } 2209 if (consolidatedRowCount > 0) { 2210 deleteUnsearchable(); 2211 } 2212 } 2213 2214 // Tries to consolidate all WatchedPrograms rows regardless of the session. After this 2215 // method is called, it is guaranteed that we have at most one unconsolidated log entry per 2216 // session that represents the user's ongoing watch activity. 2217 // Also, this method automatically schedules the next consolidation if there still remains 2218 // an unconsolidated entry. 2219 private void onTryConsolidateAll() { 2220 if (DEBUG) { 2221 Log.d(TAG, "onTryConsolidateAll()"); 2222 } 2223 2224 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 2225 queryBuilder.setTables(WATCHED_PROGRAMS_TABLE); 2226 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 2227 2228 // Pick up all unconsolidated rows grouped by session. The most recent log entry goes on 2229 // top. 2230 String[] projection = { 2231 WatchedPrograms._ID, 2232 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, 2233 WatchedPrograms.COLUMN_CHANNEL_ID, 2234 WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN 2235 }; 2236 String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0"; 2237 String sortOrder = WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + " DESC," 2238 + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC"; 2239 2240 int consolidatedRowCount = 0; 2241 try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null, 2242 sortOrder)) { 2243 long oldWatchStartTime = 0; 2244 String oldSessionToken = null; 2245 while (cursor != null && cursor.moveToNext()) { 2246 long id = cursor.getLong(0); 2247 long watchStartTime = cursor.getLong(1); 2248 long channelId = cursor.getLong(2); 2249 String sessionToken = cursor.getString(3); 2250 2251 if (!sessionToken.equals(oldSessionToken)) { 2252 // The most recent log entry for the current session, which may be still 2253 // active. Just go through a dry run with the current time to see if this 2254 // entry can be split into multiple rows. 2255 consolidatedRowCount += consolidateRow(id, watchStartTime, 2256 System.currentTimeMillis(), channelId, true); 2257 oldSessionToken = sessionToken; 2258 } else { 2259 // The later entries after the most recent one all fall into here. We now 2260 // know that this watch activity ended exactly at the same time when the 2261 // next activity started. 2262 consolidatedRowCount += consolidateRow(id, watchStartTime, 2263 oldWatchStartTime, channelId, false); 2264 } 2265 oldWatchStartTime = watchStartTime; 2266 } 2267 } 2268 if (consolidatedRowCount > 0) { 2269 deleteUnsearchable(); 2270 } 2271 scheduleConsolidationIfNeeded(); 2272 } 2273 2274 // Consolidates a WatchedPrograms row. 2275 // A row is 'consolidated' if and only if the following information is complete: 2276 // 1. WatchedPrograms.COLUMN_CHANNEL_ID 2277 // 2. WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS 2278 // 3. WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS 2279 // where COLUMN_WATCH_START_TIME_UTC_MILLIS <= COLUMN_WATCH_END_TIME_UTC_MILLIS. 2280 // This is the minimal but useful enough set of information to comprise the user's watch 2281 // history. (The program data are considered optional although we do try to fill them while 2282 // consolidating the row.) It is guaranteed that the target row is either consolidated or 2283 // deleted after this method is called. 2284 // Set {@code dryRun} to {@code true} if you think it's necessary to split the row without 2285 // consolidating the most recent row because the user stayed on the same channel for a very 2286 // long time. 2287 // This method returns the number of consolidated rows, which can be 0 or more. 2288 private int consolidateRow( 2289 long id, long watchStartTime, long watchEndTime, long channelId, boolean dryRun) { 2290 if (DEBUG) { 2291 Log.d(TAG, "consolidateRow(id=" + id + ", watchStartTime=" + watchStartTime 2292 + ", watchEndTime=" + watchEndTime + ", channelId=" + channelId 2293 + ", dryRun=" + dryRun + ")"); 2294 } 2295 2296 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 2297 2298 if (watchStartTime > watchEndTime) { 2299 Log.e(TAG, "watchEndTime cannot be less than watchStartTime"); 2300 db.delete(WATCHED_PROGRAMS_TABLE, WatchedPrograms._ID + "=" + String.valueOf(id), 2301 null); 2302 return 0; 2303 } 2304 2305 ContentValues values = getProgramValues(channelId, watchStartTime); 2306 Long endTime = values.getAsLong(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS); 2307 boolean needsToSplit = endTime != null && endTime < watchEndTime; 2308 2309 values.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, 2310 String.valueOf(watchStartTime)); 2311 if (!dryRun || needsToSplit) { 2312 values.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, 2313 String.valueOf(needsToSplit ? endTime : watchEndTime)); 2314 values.put(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED, "1"); 2315 db.update(WATCHED_PROGRAMS_TABLE, values, 2316 WatchedPrograms._ID + "=" + String.valueOf(id), null); 2317 // Treat the watched program is inserted when WATCHED_PROGRAMS_COLUMN_CONSOLIDATED 2318 // becomes 1. 2319 notifyChange(TvContract.buildWatchedProgramUri(id)); 2320 } else { 2321 db.update(WATCHED_PROGRAMS_TABLE, values, 2322 WatchedPrograms._ID + "=" + String.valueOf(id), null); 2323 } 2324 int count = dryRun ? 0 : 1; 2325 if (needsToSplit) { 2326 // This means that the program ended before the user stops watching the current 2327 // channel. In this case we duplicate the log entry as many as the number of 2328 // programs watched on the same channel. Here the end time of the current program 2329 // becomes the new watch start time of the next program. 2330 long duplicatedId = duplicateRow(id); 2331 if (duplicatedId > 0) { 2332 count += consolidateRow(duplicatedId, endTime, watchEndTime, channelId, dryRun); 2333 } 2334 } 2335 return count; 2336 } 2337 2338 // Deletes the log entries from unsearchable channels. Note that only consolidated log 2339 // entries are safe to delete. 2340 private void deleteUnsearchable() { 2341 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 2342 String deleteWhere = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=1 AND " 2343 + WatchedPrograms.COLUMN_CHANNEL_ID + " IN (SELECT " + Channels._ID 2344 + " FROM " + CHANNELS_TABLE + " WHERE " + Channels.COLUMN_SEARCHABLE + "=0)"; 2345 db.delete(WATCHED_PROGRAMS_TABLE, deleteWhere, null); 2346 } 2347 2348 private void scheduleConsolidationIfNeeded() { 2349 if (DEBUG) { 2350 Log.d(TAG, "scheduleConsolidationIfNeeded()"); 2351 } 2352 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 2353 queryBuilder.setTables(WATCHED_PROGRAMS_TABLE); 2354 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 2355 2356 // Pick up all unconsolidated rows. 2357 String[] projection = { 2358 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, 2359 WatchedPrograms.COLUMN_CHANNEL_ID, 2360 }; 2361 String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0"; 2362 2363 try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null, 2364 null)) { 2365 // Find the earliest time that any of the currently watching programs ends and 2366 // schedule the next consolidation at that time. 2367 long minEndTime = Long.MAX_VALUE; 2368 while (cursor != null && cursor.moveToNext()) { 2369 long watchStartTime = cursor.getLong(0); 2370 long channelId = cursor.getLong(1); 2371 ContentValues values = getProgramValues(channelId, watchStartTime); 2372 Long endTime = values.getAsLong(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS); 2373 2374 if (endTime != null && endTime < minEndTime 2375 && endTime > System.currentTimeMillis()) { 2376 minEndTime = endTime; 2377 } 2378 } 2379 if (minEndTime != Long.MAX_VALUE) { 2380 sendEmptyMessageAtTime(MSG_TRY_CONSOLIDATE_ALL, minEndTime); 2381 if (DEBUG) { 2382 CharSequence minEndTimeStr = DateUtils.getRelativeTimeSpanString( 2383 minEndTime, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS); 2384 Log.d(TAG, "onTryConsolidateAll() scheduled " + minEndTimeStr); 2385 } 2386 } 2387 } 2388 } 2389 2390 // Returns non-null ContentValues of the program data that the user watched on the channel 2391 // {@code channelId} at the time {@code time}. 2392 private ContentValues getProgramValues(long channelId, long time) { 2393 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 2394 queryBuilder.setTables(PROGRAMS_TABLE); 2395 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 2396 2397 String[] projection = { 2398 Programs.COLUMN_TITLE, 2399 Programs.COLUMN_START_TIME_UTC_MILLIS, 2400 Programs.COLUMN_END_TIME_UTC_MILLIS, 2401 Programs.COLUMN_SHORT_DESCRIPTION 2402 }; 2403 String selection = Programs.COLUMN_CHANNEL_ID + "=? AND " 2404 + Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND " 2405 + Programs.COLUMN_END_TIME_UTC_MILLIS + ">?"; 2406 String[] selectionArgs = { 2407 String.valueOf(channelId), 2408 String.valueOf(time), 2409 String.valueOf(time) 2410 }; 2411 String sortOrder = Programs.COLUMN_START_TIME_UTC_MILLIS + " ASC"; 2412 2413 try (Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, 2414 null, sortOrder)) { 2415 ContentValues values = new ContentValues(); 2416 if (cursor != null && cursor.moveToNext()) { 2417 values.put(WatchedPrograms.COLUMN_TITLE, cursor.getString(0)); 2418 values.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, cursor.getLong(1)); 2419 values.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, cursor.getLong(2)); 2420 values.put(WatchedPrograms.COLUMN_DESCRIPTION, cursor.getString(3)); 2421 } 2422 return values; 2423 } 2424 } 2425 2426 // Duplicates the WatchedPrograms row with a given ID and returns the ID of the duplicated 2427 // row. Returns -1 if failed. 2428 private long duplicateRow(long id) { 2429 if (DEBUG) { 2430 Log.d(TAG, "duplicateRow(" + id + ")"); 2431 } 2432 2433 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 2434 queryBuilder.setTables(WATCHED_PROGRAMS_TABLE); 2435 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 2436 2437 String[] projection = { 2438 WatchedPrograms.COLUMN_PACKAGE_NAME, 2439 WatchedPrograms.COLUMN_CHANNEL_ID, 2440 WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN 2441 }; 2442 String selection = WatchedPrograms._ID + "=" + String.valueOf(id); 2443 2444 try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null, 2445 null)) { 2446 long rowId = -1; 2447 if (cursor != null && cursor.moveToNext()) { 2448 ContentValues values = new ContentValues(); 2449 values.put(WatchedPrograms.COLUMN_PACKAGE_NAME, cursor.getString(0)); 2450 values.put(WatchedPrograms.COLUMN_CHANNEL_ID, cursor.getLong(1)); 2451 values.put(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, cursor.getString(2)); 2452 rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values); 2453 } 2454 return rowId; 2455 } 2456 } 2457 } 2458 } 2459