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