TvProvider.java revision 6dc803a0142267d45899faaf637c6a2b184e9b83
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.providers.tv; 18 19import android.annotation.SuppressLint; 20import android.app.AlarmManager; 21import android.app.PendingIntent; 22import android.content.ContentProvider; 23import android.content.ContentProviderOperation; 24import android.content.ContentProviderResult; 25import android.content.ContentValues; 26import android.content.Context; 27import android.content.Intent; 28import android.content.OperationApplicationException; 29import android.content.UriMatcher; 30import android.content.pm.PackageManager; 31import android.database.Cursor; 32import android.database.DatabaseUtils; 33import android.database.SQLException; 34import android.database.sqlite.SQLiteDatabase; 35import android.database.sqlite.SQLiteOpenHelper; 36import android.database.sqlite.SQLiteQueryBuilder; 37import android.graphics.Bitmap; 38import android.graphics.BitmapFactory; 39import android.media.tv.TvContract; 40import android.media.tv.TvContract.BaseTvColumns; 41import android.media.tv.TvContract.Channels; 42import android.media.tv.TvContract.Programs; 43import android.media.tv.TvContract.Programs.Genres; 44import android.media.tv.TvContract.RecordedPrograms; 45import android.media.tv.TvContract.WatchedPrograms; 46import android.net.Uri; 47import android.os.AsyncTask; 48import android.os.Handler; 49import android.os.Message; 50import android.os.ParcelFileDescriptor; 51import android.os.ParcelFileDescriptor.AutoCloseInputStream; 52import android.text.TextUtils; 53import android.text.format.DateUtils; 54import android.util.Log; 55 56import com.android.internal.annotations.VisibleForTesting; 57import com.android.internal.os.SomeArgs; 58import com.android.providers.tv.util.SqlParams; 59 60import libcore.io.IoUtils; 61 62import java.io.ByteArrayOutputStream; 63import java.io.FileNotFoundException; 64import java.io.IOException; 65import java.util.ArrayList; 66import java.util.HashMap; 67import java.util.HashSet; 68import java.util.Map; 69import java.util.Set; 70 71/** 72 * TV content provider. The contract between this provider and applications is defined in 73 * {@link android.media.tv.TvContract}. 74 */ 75public class TvProvider extends ContentProvider { 76 private static final boolean DEBUG = false; 77 private static final String TAG = "TvProvider"; 78 79 // Operation names for createSqlParams(). 80 private static final String OP_QUERY = "query"; 81 private static final String OP_UPDATE = "update"; 82 private static final String OP_DELETE = "delete"; 83 84 private static final int DATABASE_VERSION = 28; 85 private static final String DATABASE_NAME = "tv.db"; 86 private static final String CHANNELS_TABLE = "channels"; 87 private static final String PROGRAMS_TABLE = "programs"; 88 private static final String WATCHED_PROGRAMS_TABLE = "watched_programs"; 89 private static final String RECORDED_PROGRAMS_TABLE = "recorded_programs"; 90 private static final String DELETED_CHANNELS_TABLE = "deleted_channels"; // Deprecated 91 private static final String PROGRAMS_TABLE_PACKAGE_NAME_INDEX = "programs_package_name_index"; 92 private static final String PROGRAMS_TABLE_CHANNEL_ID_INDEX = "programs_channel_id_index"; 93 private static final String PROGRAMS_TABLE_START_TIME_INDEX = "programs_start_time_index"; 94 private static final String PROGRAMS_TABLE_END_TIME_INDEX = "programs_end_time_index"; 95 private static final String WATCHED_PROGRAMS_TABLE_CHANNEL_ID_INDEX = 96 "watched_programs_channel_id_index"; 97 private static final String DEFAULT_PROGRAMS_SORT_ORDER = Programs.COLUMN_START_TIME_UTC_MILLIS 98 + " ASC"; 99 private static final String DEFAULT_WATCHED_PROGRAMS_SORT_ORDER = 100 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC"; 101 private static final String CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE = CHANNELS_TABLE 102 + " INNER JOIN " + PROGRAMS_TABLE 103 + " ON (" + CHANNELS_TABLE + "." + Channels._ID + "=" 104 + PROGRAMS_TABLE + "." + Programs.COLUMN_CHANNEL_ID + ")"; 105 106 private static final UriMatcher sUriMatcher; 107 private static final int MATCH_CHANNEL = 1; 108 private static final int MATCH_CHANNEL_ID = 2; 109 private static final int MATCH_CHANNEL_ID_LOGO = 3; 110 private static final int MATCH_PASSTHROUGH_ID = 4; 111 private static final int MATCH_PROGRAM = 5; 112 private static final int MATCH_PROGRAM_ID = 6; 113 private static final int MATCH_WATCHED_PROGRAM = 7; 114 private static final int MATCH_WATCHED_PROGRAM_ID = 8; 115 private static final int MATCH_RECORDED_PROGRAM = 9; 116 private static final int MATCH_RECORDED_PROGRAM_ID = 10; 117 118 private static final String CHANNELS_COLUMN_LOGO = "logo"; 119 private static final int MAX_LOGO_IMAGE_SIZE = 256; 120 121 // The internal column in the watched programs table to indicate whether the current log entry 122 // is consolidated or not. Unconsolidated entries may have columns with missing data. 123 private static final String WATCHED_PROGRAMS_COLUMN_CONSOLIDATED = "consolidated"; 124 125 private static final long MAX_PROGRAM_DATA_DELAY_IN_MILLIS = 10 * 1000; // 10 seconds 126 127 private static final Map<String, String> sChannelProjectionMap; 128 private static final Map<String, String> sProgramProjectionMap; 129 private static final Map<String, String> sWatchedProgramProjectionMap; 130 private static final Map<String, String> sRecordedProgramProjectionMap; 131 132 static { 133 sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 134 sUriMatcher.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL); 135 sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID); 136 sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#/logo", MATCH_CHANNEL_ID_LOGO); 137 sUriMatcher.addURI(TvContract.AUTHORITY, "passthrough/*", MATCH_PASSTHROUGH_ID); 138 sUriMatcher.addURI(TvContract.AUTHORITY, "program", MATCH_PROGRAM); 139 sUriMatcher.addURI(TvContract.AUTHORITY, "program/#", MATCH_PROGRAM_ID); 140 sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program", MATCH_WATCHED_PROGRAM); 141 sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID); 142 sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM); 143 sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID); 144 145 sChannelProjectionMap = new HashMap<>(); 146 sChannelProjectionMap.put(Channels._ID, CHANNELS_TABLE + "." + Channels._ID); 147 sChannelProjectionMap.put(Channels.COLUMN_PACKAGE_NAME, 148 CHANNELS_TABLE + "." + Channels.COLUMN_PACKAGE_NAME); 149 sChannelProjectionMap.put(Channels.COLUMN_INPUT_ID, 150 CHANNELS_TABLE + "." + Channels.COLUMN_INPUT_ID); 151 sChannelProjectionMap.put(Channels.COLUMN_TYPE, 152 CHANNELS_TABLE + "." + Channels.COLUMN_TYPE); 153 sChannelProjectionMap.put(Channels.COLUMN_SERVICE_TYPE, 154 CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_TYPE); 155 sChannelProjectionMap.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, 156 CHANNELS_TABLE + "." + Channels.COLUMN_ORIGINAL_NETWORK_ID); 157 sChannelProjectionMap.put(Channels.COLUMN_TRANSPORT_STREAM_ID, 158 CHANNELS_TABLE + "." + Channels.COLUMN_TRANSPORT_STREAM_ID); 159 sChannelProjectionMap.put(Channels.COLUMN_SERVICE_ID, 160 CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_ID); 161 sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NUMBER, 162 CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NUMBER); 163 sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NAME, 164 CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NAME); 165 sChannelProjectionMap.put(Channels.COLUMN_NETWORK_AFFILIATION, 166 CHANNELS_TABLE + "." + Channels.COLUMN_NETWORK_AFFILIATION); 167 sChannelProjectionMap.put(Channels.COLUMN_DESCRIPTION, 168 CHANNELS_TABLE + "." + Channels.COLUMN_DESCRIPTION); 169 sChannelProjectionMap.put(Channels.COLUMN_VIDEO_FORMAT, 170 CHANNELS_TABLE + "." + Channels.COLUMN_VIDEO_FORMAT); 171 sChannelProjectionMap.put(Channels.COLUMN_BROWSABLE, 172 CHANNELS_TABLE + "." + Channels.COLUMN_BROWSABLE); 173 sChannelProjectionMap.put(Channels.COLUMN_SEARCHABLE, 174 CHANNELS_TABLE + "." + Channels.COLUMN_SEARCHABLE); 175 sChannelProjectionMap.put(Channels.COLUMN_LOCKED, 176 CHANNELS_TABLE + "." + Channels.COLUMN_LOCKED); 177 sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_ICON_URI, 178 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_ICON_URI); 179 sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_POSTER_ART_URI, 180 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_POSTER_ART_URI); 181 sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_TEXT, 182 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_TEXT); 183 sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_COLOR, 184 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_COLOR); 185 sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_INTENT_URI, 186 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_INTENT_URI); 187 sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA, 188 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_DATA); 189 sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, 190 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1); 191 sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, 192 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2); 193 sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3, 194 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3); 195 sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4, 196 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4); 197 sChannelProjectionMap.put(Channels.COLUMN_VERSION_NUMBER, 198 CHANNELS_TABLE + "." + Channels.COLUMN_VERSION_NUMBER); 199 200 sProgramProjectionMap = new HashMap<>(); 201 sProgramProjectionMap.put(Programs._ID, Programs._ID); 202 sProgramProjectionMap.put(Programs.COLUMN_PACKAGE_NAME, Programs.COLUMN_PACKAGE_NAME); 203 sProgramProjectionMap.put(Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_CHANNEL_ID); 204 sProgramProjectionMap.put(Programs.COLUMN_TITLE, Programs.COLUMN_TITLE); 205 sProgramProjectionMap.put(Programs.COLUMN_SEASON_NUMBER, Programs.COLUMN_SEASON_NUMBER); 206 sProgramProjectionMap.put(Programs.COLUMN_EPISODE_NUMBER, Programs.COLUMN_EPISODE_NUMBER); 207 sProgramProjectionMap.put(Programs.COLUMN_EPISODE_TITLE, Programs.COLUMN_EPISODE_TITLE); 208 sProgramProjectionMap.put(Programs.COLUMN_START_TIME_UTC_MILLIS, 209 Programs.COLUMN_START_TIME_UTC_MILLIS); 210 sProgramProjectionMap.put(Programs.COLUMN_END_TIME_UTC_MILLIS, 211 Programs.COLUMN_END_TIME_UTC_MILLIS); 212 sProgramProjectionMap.put(Programs.COLUMN_BROADCAST_GENRE, Programs.COLUMN_BROADCAST_GENRE); 213 sProgramProjectionMap.put(Programs.COLUMN_CANONICAL_GENRE, Programs.COLUMN_CANONICAL_GENRE); 214 sProgramProjectionMap.put(Programs.COLUMN_SHORT_DESCRIPTION, 215 Programs.COLUMN_SHORT_DESCRIPTION); 216 sProgramProjectionMap.put(Programs.COLUMN_LONG_DESCRIPTION, 217 Programs.COLUMN_LONG_DESCRIPTION); 218 sProgramProjectionMap.put(Programs.COLUMN_VIDEO_WIDTH, Programs.COLUMN_VIDEO_WIDTH); 219 sProgramProjectionMap.put(Programs.COLUMN_VIDEO_HEIGHT, Programs.COLUMN_VIDEO_HEIGHT); 220 sProgramProjectionMap.put(Programs.COLUMN_AUDIO_LANGUAGE, Programs.COLUMN_AUDIO_LANGUAGE); 221 sProgramProjectionMap.put(Programs.COLUMN_CONTENT_RATING, Programs.COLUMN_CONTENT_RATING); 222 sProgramProjectionMap.put(Programs.COLUMN_POSTER_ART_URI, Programs.COLUMN_POSTER_ART_URI); 223 sProgramProjectionMap.put(Programs.COLUMN_THUMBNAIL_URI, Programs.COLUMN_THUMBNAIL_URI); 224 sProgramProjectionMap.put(Programs.COLUMN_SEARCHABLE, Programs.COLUMN_SEARCHABLE); 225 sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_DATA, 226 Programs.COLUMN_INTERNAL_PROVIDER_DATA); 227 sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG1, 228 Programs.COLUMN_INTERNAL_PROVIDER_FLAG1); 229 sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG2, 230 Programs.COLUMN_INTERNAL_PROVIDER_FLAG2); 231 sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG3, 232 Programs.COLUMN_INTERNAL_PROVIDER_FLAG3); 233 sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG4, 234 Programs.COLUMN_INTERNAL_PROVIDER_FLAG4); 235 sProgramProjectionMap.put(Programs.COLUMN_VERSION_NUMBER, Programs.COLUMN_VERSION_NUMBER); 236 237 sWatchedProgramProjectionMap = new HashMap<>(); 238 sWatchedProgramProjectionMap.put(WatchedPrograms._ID, WatchedPrograms._ID); 239 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, 240 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS); 241 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, 242 WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS); 243 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_CHANNEL_ID, 244 WatchedPrograms.COLUMN_CHANNEL_ID); 245 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_TITLE, 246 WatchedPrograms.COLUMN_TITLE); 247 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, 248 WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS); 249 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, 250 WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS); 251 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_DESCRIPTION, 252 WatchedPrograms.COLUMN_DESCRIPTION); 253 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS, 254 WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS); 255 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, 256 WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN); 257 sWatchedProgramProjectionMap.put(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED, 258 WATCHED_PROGRAMS_COLUMN_CONSOLIDATED); 259 260 sRecordedProgramProjectionMap = new HashMap<>(); 261 sRecordedProgramProjectionMap.put(RecordedPrograms._ID, RecordedPrograms._ID); 262 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_PACKAGE_NAME, 263 RecordedPrograms.COLUMN_PACKAGE_NAME); 264 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INPUT_ID, 265 RecordedPrograms.COLUMN_INPUT_ID); 266 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CHANNEL_ID, 267 RecordedPrograms.COLUMN_CHANNEL_ID); 268 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_TITLE, 269 RecordedPrograms.COLUMN_TITLE); 270 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_NUMBER, 271 RecordedPrograms.COLUMN_SEASON_NUMBER); 272 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_NUMBER, 273 RecordedPrograms.COLUMN_EPISODE_NUMBER); 274 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_TITLE, 275 RecordedPrograms.COLUMN_EPISODE_TITLE); 276 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, 277 RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS); 278 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, 279 RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS); 280 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_BROADCAST_GENRE, 281 RecordedPrograms.COLUMN_BROADCAST_GENRE); 282 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CANONICAL_GENRE, 283 RecordedPrograms.COLUMN_CANONICAL_GENRE); 284 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, 285 RecordedPrograms.COLUMN_SHORT_DESCRIPTION); 286 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, 287 RecordedPrograms.COLUMN_LONG_DESCRIPTION); 288 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, 289 RecordedPrograms.COLUMN_VIDEO_WIDTH); 290 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, 291 RecordedPrograms.COLUMN_VIDEO_HEIGHT); 292 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, 293 RecordedPrograms.COLUMN_AUDIO_LANGUAGE); 294 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CONTENT_RATING, 295 RecordedPrograms.COLUMN_CONTENT_RATING); 296 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_POSTER_ART_URI, 297 RecordedPrograms.COLUMN_POSTER_ART_URI); 298 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, 299 RecordedPrograms.COLUMN_THUMBNAIL_URI); 300 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEARCHABLE, 301 RecordedPrograms.COLUMN_SEARCHABLE); 302 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI, 303 RecordedPrograms.COLUMN_RECORDING_DATA_URI); 304 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, 305 RecordedPrograms.COLUMN_RECORDING_DATA_BYTES); 306 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, 307 RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS); 308 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, 309 RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS); 310 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, 311 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA); 312 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, 313 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1); 314 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, 315 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2); 316 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, 317 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3); 318 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, 319 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4); 320 sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VERSION_NUMBER, 321 RecordedPrograms.COLUMN_VERSION_NUMBER); 322 } 323 324 // Mapping from broadcast genre to canonical genre. 325 private static Map<String, String> sGenreMap; 326 327 private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS"; 328 329 private static final String PERMISSION_ACCESS_ALL_EPG_DATA = 330 "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA"; 331 332 private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS = 333 "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS"; 334 335 private static final String CREATE_RECORDED_PROGRAMS_TABLE_SQL = 336 "CREATE TABLE " + RECORDED_PROGRAMS_TABLE + " (" 337 + RecordedPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 338 + RecordedPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL," 339 + RecordedPrograms.COLUMN_INPUT_ID + " TEXT NOT NULL," 340 + RecordedPrograms.COLUMN_CHANNEL_ID + " INTEGER," 341 + RecordedPrograms.COLUMN_TITLE + " TEXT," 342 + RecordedPrograms.COLUMN_SEASON_NUMBER + " INTEGER," 343 + RecordedPrograms.COLUMN_EPISODE_NUMBER + " INTEGER," 344 + RecordedPrograms.COLUMN_EPISODE_TITLE + " TEXT," 345 + RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS + " INTEGER," 346 + RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS + " INTEGER," 347 + RecordedPrograms.COLUMN_BROADCAST_GENRE + " TEXT," 348 + RecordedPrograms.COLUMN_CANONICAL_GENRE + " TEXT," 349 + RecordedPrograms.COLUMN_SHORT_DESCRIPTION + " TEXT," 350 + RecordedPrograms.COLUMN_LONG_DESCRIPTION + " TEXT," 351 + RecordedPrograms.COLUMN_VIDEO_WIDTH + " INTEGER," 352 + RecordedPrograms.COLUMN_VIDEO_HEIGHT + " INTEGER," 353 + RecordedPrograms.COLUMN_AUDIO_LANGUAGE + " TEXT," 354 + RecordedPrograms.COLUMN_CONTENT_RATING + " TEXT," 355 + RecordedPrograms.COLUMN_POSTER_ART_URI + " TEXT," 356 + RecordedPrograms.COLUMN_THUMBNAIL_URI + " TEXT," 357 + RecordedPrograms.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1," 358 + RecordedPrograms.COLUMN_RECORDING_DATA_URI + " TEXT," 359 + RecordedPrograms.COLUMN_RECORDING_DATA_BYTES + " INTEGER," 360 + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS + " INTEGER," 361 + RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS + " INTEGER," 362 + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB," 363 + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER," 364 + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER," 365 + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER," 366 + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER," 367 + RecordedPrograms.COLUMN_VERSION_NUMBER + " INTEGER);"; 368 369 private static class DatabaseHelper extends SQLiteOpenHelper { 370 DatabaseHelper(Context context) { 371 super(context, DATABASE_NAME, null, DATABASE_VERSION); 372 } 373 374 @Override 375 public void onConfigure(SQLiteDatabase db) { 376 db.setForeignKeyConstraintsEnabled(true); 377 } 378 379 @Override 380 public void onCreate(SQLiteDatabase db) { 381 if (DEBUG) { 382 Log.d(TAG, "Creating database"); 383 } 384 // Set up the database schema. 385 db.execSQL("CREATE TABLE " + CHANNELS_TABLE + " (" 386 + Channels._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 387 + Channels.COLUMN_PACKAGE_NAME + " TEXT NOT NULL," 388 + Channels.COLUMN_INPUT_ID + " TEXT NOT NULL," 389 + Channels.COLUMN_TYPE + " TEXT NOT NULL DEFAULT '" + Channels.TYPE_OTHER + "'," 390 + Channels.COLUMN_SERVICE_TYPE + " TEXT NOT NULL DEFAULT '" 391 + Channels.SERVICE_TYPE_AUDIO_VIDEO + "'," 392 + Channels.COLUMN_ORIGINAL_NETWORK_ID + " INTEGER NOT NULL DEFAULT 0," 393 + Channels.COLUMN_TRANSPORT_STREAM_ID + " INTEGER NOT NULL DEFAULT 0," 394 + Channels.COLUMN_SERVICE_ID + " INTEGER NOT NULL DEFAULT 0," 395 + Channels.COLUMN_DISPLAY_NUMBER + " TEXT," 396 + Channels.COLUMN_DISPLAY_NAME + " TEXT," 397 + Channels.COLUMN_NETWORK_AFFILIATION + " TEXT," 398 + Channels.COLUMN_DESCRIPTION + " TEXT," 399 + Channels.COLUMN_VIDEO_FORMAT + " TEXT," 400 + Channels.COLUMN_BROWSABLE + " INTEGER NOT NULL DEFAULT 0," 401 + Channels.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1," 402 + Channels.COLUMN_LOCKED + " INTEGER NOT NULL DEFAULT 0," 403 + Channels.COLUMN_APP_LINK_ICON_URI + " TEXT," 404 + Channels.COLUMN_APP_LINK_POSTER_ART_URI + " TEXT," 405 + Channels.COLUMN_APP_LINK_TEXT + " TEXT," 406 + Channels.COLUMN_APP_LINK_COLOR + " INTEGER," 407 + Channels.COLUMN_APP_LINK_INTENT_URI + " TEXT," 408 + Channels.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB," 409 + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER," 410 + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER," 411 + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER," 412 + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER," 413 + CHANNELS_COLUMN_LOGO + " BLOB," 414 + Channels.COLUMN_VERSION_NUMBER + " INTEGER," 415 // Needed for foreign keys in other tables. 416 + "UNIQUE(" + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME + ")" 417 + ");"); 418 db.execSQL("CREATE TABLE " + PROGRAMS_TABLE + " (" 419 + Programs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 420 + Programs.COLUMN_PACKAGE_NAME + " TEXT NOT NULL," 421 + Programs.COLUMN_CHANNEL_ID + " INTEGER," 422 + Programs.COLUMN_TITLE + " TEXT," 423 + Programs.COLUMN_SEASON_NUMBER + " INTEGER," 424 + Programs.COLUMN_EPISODE_NUMBER + " INTEGER," 425 + Programs.COLUMN_EPISODE_TITLE + " TEXT," 426 + Programs.COLUMN_START_TIME_UTC_MILLIS + " INTEGER," 427 + Programs.COLUMN_END_TIME_UTC_MILLIS + " INTEGER," 428 + Programs.COLUMN_BROADCAST_GENRE + " TEXT," 429 + Programs.COLUMN_CANONICAL_GENRE + " TEXT," 430 + Programs.COLUMN_SHORT_DESCRIPTION + " TEXT," 431 + Programs.COLUMN_LONG_DESCRIPTION + " TEXT," 432 + Programs.COLUMN_VIDEO_WIDTH + " INTEGER," 433 + Programs.COLUMN_VIDEO_HEIGHT + " INTEGER," 434 + Programs.COLUMN_AUDIO_LANGUAGE + " TEXT," 435 + Programs.COLUMN_CONTENT_RATING + " TEXT," 436 + Programs.COLUMN_POSTER_ART_URI + " TEXT," 437 + Programs.COLUMN_THUMBNAIL_URI + " TEXT," 438 + Programs.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1," 439 + Programs.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB," 440 + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER," 441 + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER," 442 + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER," 443 + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER," 444 + Programs.COLUMN_VERSION_NUMBER + " INTEGER," 445 + "FOREIGN KEY(" 446 + Programs.COLUMN_CHANNEL_ID + "," + Programs.COLUMN_PACKAGE_NAME 447 + ") REFERENCES " + CHANNELS_TABLE + "(" 448 + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME 449 + ") ON UPDATE CASCADE ON DELETE CASCADE" 450 + ");"); 451 db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_PACKAGE_NAME_INDEX + " ON " + PROGRAMS_TABLE 452 + "(" + Programs.COLUMN_PACKAGE_NAME + ");"); 453 db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_CHANNEL_ID_INDEX + " ON " + PROGRAMS_TABLE 454 + "(" + Programs.COLUMN_CHANNEL_ID + ");"); 455 db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_START_TIME_INDEX + " ON " + PROGRAMS_TABLE 456 + "(" + Programs.COLUMN_START_TIME_UTC_MILLIS + ");"); 457 db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_END_TIME_INDEX + " ON " + PROGRAMS_TABLE 458 + "(" + Programs.COLUMN_END_TIME_UTC_MILLIS + ");"); 459 db.execSQL("CREATE TABLE " + WATCHED_PROGRAMS_TABLE + " (" 460 + WatchedPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 461 + WatchedPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL," 462 + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS 463 + " INTEGER NOT NULL DEFAULT 0," 464 + WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS 465 + " INTEGER NOT NULL DEFAULT 0," 466 + WatchedPrograms.COLUMN_CHANNEL_ID + " INTEGER," 467 + WatchedPrograms.COLUMN_TITLE + " TEXT," 468 + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS + " INTEGER," 469 + WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS + " INTEGER," 470 + WatchedPrograms.COLUMN_DESCRIPTION + " TEXT," 471 + WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS + " TEXT," 472 + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + " TEXT NOT NULL," 473 + WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + " INTEGER NOT NULL DEFAULT 0," 474 + "FOREIGN KEY(" 475 + WatchedPrograms.COLUMN_CHANNEL_ID + "," 476 + WatchedPrograms.COLUMN_PACKAGE_NAME 477 + ") REFERENCES " + CHANNELS_TABLE + "(" 478 + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME 479 + ") ON UPDATE CASCADE ON DELETE CASCADE" 480 + ");"); 481 db.execSQL("CREATE INDEX " + WATCHED_PROGRAMS_TABLE_CHANNEL_ID_INDEX + " ON " 482 + WATCHED_PROGRAMS_TABLE + "(" + WatchedPrograms.COLUMN_CHANNEL_ID + ");"); 483 db.execSQL(CREATE_RECORDED_PROGRAMS_TABLE_SQL); 484 } 485 486 @Override 487 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 488 if (oldVersion < 23) { 489 Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion 490 + ", data will be lost!"); 491 db.execSQL("DROP TABLE IF EXISTS " + DELETED_CHANNELS_TABLE); 492 db.execSQL("DROP TABLE IF EXISTS " + WATCHED_PROGRAMS_TABLE); 493 db.execSQL("DROP TABLE IF EXISTS " + PROGRAMS_TABLE); 494 db.execSQL("DROP TABLE IF EXISTS " + CHANNELS_TABLE); 495 496 onCreate(db); 497 return; 498 } 499 500 Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion + "."); 501 if (oldVersion == 23) { 502 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD " 503 + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER;"); 504 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD " 505 + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER;"); 506 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD " 507 + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER;"); 508 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD " 509 + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER;"); 510 oldVersion++; 511 } 512 if (oldVersion == 24) { 513 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD " 514 + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER;"); 515 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD " 516 + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER;"); 517 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD " 518 + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER;"); 519 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD " 520 + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER;"); 521 oldVersion++; 522 } 523 if (oldVersion == 25) { 524 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD " 525 + Channels.COLUMN_APP_LINK_ICON_URI + " TEXT;"); 526 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD " 527 + Channels.COLUMN_APP_LINK_POSTER_ART_URI + " TEXT;"); 528 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD " 529 + Channels.COLUMN_APP_LINK_TEXT + " TEXT;"); 530 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD " 531 + Channels.COLUMN_APP_LINK_COLOR + " INTEGER;"); 532 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD " 533 + Channels.COLUMN_APP_LINK_INTENT_URI + " TEXT;"); 534 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD " 535 + Programs.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1;"); 536 oldVersion++; 537 } 538 if (oldVersion >= 26) { 539 db.execSQL("DROP TABLE IF EXISTS " + RECORDED_PROGRAMS_TABLE); 540 db.execSQL(CREATE_RECORDED_PROGRAMS_TABLE_SQL); 541 } 542 } 543 } 544 545 private DatabaseHelper mOpenHelper; 546 547 private final Handler mLogHandler = new WatchLogHandler(); 548 549 @Override 550 public boolean onCreate() { 551 if (DEBUG) { 552 Log.d(TAG, "Creating TvProvider"); 553 } 554 mOpenHelper = new DatabaseHelper(getContext()); 555 deleteUnconsolidatedWatchedProgramsRows(); 556 scheduleEpgDataCleanup(); 557 buildGenreMap(); 558 return true; 559 } 560 561 @VisibleForTesting 562 void scheduleEpgDataCleanup() { 563 Intent intent = new Intent(EpgDataCleanupService.ACTION_CLEAN_UP_EPG_DATA); 564 intent.setClass(getContext(), EpgDataCleanupService.class); 565 PendingIntent pendingIntent = PendingIntent.getService( 566 getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 567 AlarmManager alarmManager = 568 (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); 569 alarmManager.setInexactRepeating(AlarmManager.RTC, System.currentTimeMillis(), 570 AlarmManager.INTERVAL_HALF_DAY, pendingIntent); 571 } 572 573 private void buildGenreMap() { 574 if (sGenreMap != null) { 575 return; 576 } 577 578 sGenreMap = new HashMap<>(); 579 buildGenreMap(R.array.genre_mapping_atsc); 580 buildGenreMap(R.array.genre_mapping_dvb); 581 buildGenreMap(R.array.genre_mapping_isdb); 582 buildGenreMap(R.array.genre_mapping_isdb_br); 583 } 584 585 @SuppressLint("DefaultLocale") 586 private void buildGenreMap(int id) { 587 String[] maps = getContext().getResources().getStringArray(id); 588 for (String map : maps) { 589 String[] arr = map.split("\\|"); 590 if (arr.length != 2) { 591 throw new IllegalArgumentException("Invalid genre mapping : " + map); 592 } 593 sGenreMap.put(arr[0].toUpperCase(), arr[1]); 594 } 595 } 596 597 @VisibleForTesting 598 String getCallingPackage_() { 599 return getCallingPackage(); 600 } 601 602 @Override 603 public String getType(Uri uri) { 604 switch (sUriMatcher.match(uri)) { 605 case MATCH_CHANNEL: 606 return Channels.CONTENT_TYPE; 607 case MATCH_CHANNEL_ID: 608 return Channels.CONTENT_ITEM_TYPE; 609 case MATCH_CHANNEL_ID_LOGO: 610 return "image/png"; 611 case MATCH_PASSTHROUGH_ID: 612 return Channels.CONTENT_ITEM_TYPE; 613 case MATCH_PROGRAM: 614 return Programs.CONTENT_TYPE; 615 case MATCH_PROGRAM_ID: 616 return Programs.CONTENT_ITEM_TYPE; 617 case MATCH_WATCHED_PROGRAM: 618 return WatchedPrograms.CONTENT_TYPE; 619 case MATCH_WATCHED_PROGRAM_ID: 620 return WatchedPrograms.CONTENT_ITEM_TYPE; 621 case MATCH_RECORDED_PROGRAM: 622 return RecordedPrograms.CONTENT_TYPE; 623 case MATCH_RECORDED_PROGRAM_ID: 624 return RecordedPrograms.CONTENT_ITEM_TYPE; 625 default: 626 throw new IllegalArgumentException("Unknown URI " + uri); 627 } 628 } 629 630 @Override 631 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 632 String sortOrder) { 633 boolean needsToValidateSortOrder = !callerHasAccessAllEpgDataPermission(); 634 SqlParams params = createSqlParams(OP_QUERY, uri, selection, selectionArgs); 635 636 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 637 queryBuilder.setStrict(needsToValidateSortOrder); 638 queryBuilder.setTables(params.getTables()); 639 String orderBy = null; 640 Map<String, String> projectionMap; 641 switch (params.getTables()) { 642 case PROGRAMS_TABLE: 643 projectionMap = sProgramProjectionMap; 644 orderBy = DEFAULT_PROGRAMS_SORT_ORDER; 645 break; 646 case WATCHED_PROGRAMS_TABLE: 647 projectionMap = sWatchedProgramProjectionMap; 648 orderBy = DEFAULT_WATCHED_PROGRAMS_SORT_ORDER; 649 break; 650 case RECORDED_PROGRAMS_TABLE: 651 projectionMap = sRecordedProgramProjectionMap; 652 break; 653 default: 654 projectionMap = sChannelProjectionMap; 655 break; 656 } 657 queryBuilder.setProjectionMap(projectionMap); 658 if (needsToValidateSortOrder) { 659 validateSortOrder(sortOrder, projectionMap.keySet()); 660 } 661 662 // Use the default sort order only if no sort order is specified. 663 if (!TextUtils.isEmpty(sortOrder)) { 664 orderBy = sortOrder; 665 } 666 667 // Get the database and run the query. 668 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 669 Cursor c = queryBuilder.query(db, projection, params.getSelection(), 670 params.getSelectionArgs(), null, null, orderBy); 671 672 // Tell the cursor what URI to watch, so it knows when its source data changes. 673 c.setNotificationUri(getContext().getContentResolver(), uri); 674 return c; 675 } 676 677 @Override 678 public Uri insert(Uri uri, ContentValues values) { 679 switch (sUriMatcher.match(uri)) { 680 case MATCH_CHANNEL: 681 return insertChannel(uri, values); 682 case MATCH_PROGRAM: 683 return insertProgram(uri, values); 684 case MATCH_WATCHED_PROGRAM: 685 return insertWatchedProgram(uri, values); 686 case MATCH_RECORDED_PROGRAM: 687 return insertRecordedProgram(uri, values); 688 case MATCH_CHANNEL_ID: 689 case MATCH_CHANNEL_ID_LOGO: 690 case MATCH_PASSTHROUGH_ID: 691 case MATCH_PROGRAM_ID: 692 case MATCH_WATCHED_PROGRAM_ID: 693 case MATCH_RECORDED_PROGRAM_ID: 694 throw new UnsupportedOperationException("Cannot insert into that URI: " + uri); 695 default: 696 throw new IllegalArgumentException("Unknown URI " + uri); 697 } 698 } 699 700 private Uri insertChannel(Uri uri, ContentValues values) { 701 // Mark the owner package of this channel. 702 values.put(Channels.COLUMN_PACKAGE_NAME, getCallingPackage_()); 703 704 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 705 long rowId = db.insert(CHANNELS_TABLE, null, values); 706 if (rowId > 0) { 707 Uri channelUri = TvContract.buildChannelUri(rowId); 708 notifyChange(channelUri); 709 return channelUri; 710 } 711 712 throw new SQLException("Failed to insert row into " + uri); 713 } 714 715 private Uri insertProgram(Uri uri, ContentValues values) { 716 // Mark the owner package of this program. 717 values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_()); 718 719 checkAndConvertGenre(values); 720 721 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 722 long rowId = db.insert(PROGRAMS_TABLE, null, values); 723 if (rowId > 0) { 724 Uri programUri = TvContract.buildProgramUri(rowId); 725 notifyChange(programUri); 726 return programUri; 727 } 728 729 throw new SQLException("Failed to insert row into " + uri); 730 } 731 732 private Uri insertWatchedProgram(Uri uri, ContentValues values) { 733 if (DEBUG) { 734 Log.d(TAG, "insertWatchedProgram(uri=" + uri + ", values={" + values + "})"); 735 } 736 Long watchStartTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS); 737 Long watchEndTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS); 738 // The system sends only two kinds of watch events: 739 // 1. The user tunes to a new channel. (COLUMN_WATCH_START_TIME_UTC_MILLIS) 740 // 2. The user stops watching. (COLUMN_WATCH_END_TIME_UTC_MILLIS) 741 if (watchStartTime != null && watchEndTime == null) { 742 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 743 long rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values); 744 if (rowId > 0) { 745 mLogHandler.removeMessages(WatchLogHandler.MSG_TRY_CONSOLIDATE_ALL); 746 mLogHandler.sendEmptyMessageDelayed(WatchLogHandler.MSG_TRY_CONSOLIDATE_ALL, 747 MAX_PROGRAM_DATA_DELAY_IN_MILLIS); 748 return TvContract.buildWatchedProgramUri(rowId); 749 } 750 Log.w(TAG, "Failed to insert row for " + values + ". Channel does not exist."); 751 return null; 752 } else if (watchStartTime == null && watchEndTime != null) { 753 SomeArgs args = SomeArgs.obtain(); 754 args.arg1 = values.getAsString(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN); 755 args.arg2 = watchEndTime; 756 Message msg = mLogHandler.obtainMessage(WatchLogHandler.MSG_CONSOLIDATE, args); 757 mLogHandler.sendMessageDelayed(msg, MAX_PROGRAM_DATA_DELAY_IN_MILLIS); 758 return null; 759 } 760 // All the other cases are invalid. 761 throw new IllegalArgumentException("Only one of COLUMN_WATCH_START_TIME_UTC_MILLIS and" 762 + " COLUMN_WATCH_END_TIME_UTC_MILLIS should be specified"); 763 } 764 765 private Uri insertRecordedProgram(Uri uri, ContentValues values) { 766 // Mark the owner package of this program. 767 values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_()); 768 769 checkAndConvertGenre(values); 770 771 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 772 long rowId = db.insert(RECORDED_PROGRAMS_TABLE, null, values); 773 if (rowId > 0) { 774 Uri recordedProgramUri = TvContract.buildRecordedProgramUri(rowId); 775 notifyChange(recordedProgramUri); 776 return recordedProgramUri; 777 } 778 779 throw new SQLException("Failed to insert row into " + uri); 780 } 781 782 @Override 783 public int delete(Uri uri, String selection, String[] selectionArgs) { 784 SqlParams params = createSqlParams(OP_DELETE, uri, selection, selectionArgs); 785 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 786 int count; 787 switch (sUriMatcher.match(uri)) { 788 case MATCH_CHANNEL_ID_LOGO: 789 ContentValues values = new ContentValues(); 790 values.putNull(CHANNELS_COLUMN_LOGO); 791 count = db.update(params.getTables(), values, params.getSelection(), 792 params.getSelectionArgs()); 793 break; 794 case MATCH_CHANNEL: 795 case MATCH_PROGRAM: 796 case MATCH_WATCHED_PROGRAM: 797 case MATCH_RECORDED_PROGRAM: 798 case MATCH_CHANNEL_ID: 799 case MATCH_PASSTHROUGH_ID: 800 case MATCH_PROGRAM_ID: 801 case MATCH_WATCHED_PROGRAM_ID: 802 case MATCH_RECORDED_PROGRAM_ID: 803 count = db.delete(params.getTables(), params.getSelection(), 804 params.getSelectionArgs()); 805 break; 806 default: 807 throw new IllegalArgumentException("Unknown URI " + uri); 808 } 809 if (count > 0) { 810 notifyChange(uri); 811 } 812 return count; 813 } 814 815 @Override 816 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 817 SqlParams params = createSqlParams(OP_UPDATE, uri, selection, selectionArgs); 818 if (params.getTables().equals(CHANNELS_TABLE)) { 819 if (values.containsKey(Channels.COLUMN_LOCKED) 820 && !callerHasModifyParentalControlsPermission()) { 821 throw new SecurityException("Not allowed to modify Channels.COLUMN_LOCKED"); 822 } 823 } else if (params.getTables().equals(PROGRAMS_TABLE) 824 || params.getTables().equals(RECORDED_PROGRAMS_TABLE)) { 825 checkAndConvertGenre(values); 826 } 827 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 828 int count = db.update(params.getTables(), values, params.getSelection(), 829 params.getSelectionArgs()); 830 if (count > 0) { 831 notifyChange(uri); 832 } 833 return count; 834 } 835 836 private SqlParams createSqlParams(String operation, Uri uri, String selection, 837 String[] selectionArgs) { 838 int match = sUriMatcher.match(uri); 839 SqlParams params = new SqlParams(null, selection, selectionArgs); 840 841 // Control access to EPG data (excluding watched programs) when the caller doesn't have all 842 // access. 843 if (!callerHasAccessAllEpgDataPermission() 844 && match != MATCH_WATCHED_PROGRAM && match != MATCH_WATCHED_PROGRAM_ID) { 845 if (!TextUtils.isEmpty(selection)) { 846 throw new SecurityException("Selection not allowed for " + uri); 847 } 848 // Limit the operation only to the data that the calling package owns except for when 849 // the caller tries to read TV listings and has the appropriate permission. 850 String prefix = match == MATCH_CHANNEL ? CHANNELS_TABLE + "." : ""; 851 if (operation.equals(OP_QUERY) && callerHasReadTvListingsPermission()) { 852 params.setWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=? OR " 853 + Channels.COLUMN_SEARCHABLE + "=?", getCallingPackage_(), "1"); 854 } else { 855 params.setWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=?", 856 getCallingPackage_()); 857 } 858 } 859 860 switch (match) { 861 case MATCH_CHANNEL: 862 String genre = uri.getQueryParameter(TvContract.PARAM_CANONICAL_GENRE); 863 if (genre == null) { 864 params.setTables(CHANNELS_TABLE); 865 } else { 866 if (!operation.equals(OP_QUERY)) { 867 throw new SecurityException(capitalize(operation) 868 + " not allowed for " + uri); 869 } 870 if (!Genres.isCanonical(genre)) { 871 throw new IllegalArgumentException("Not a canonical genre : " + genre); 872 } 873 params.setTables(CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE); 874 String curTime = String.valueOf(System.currentTimeMillis()); 875 params.appendWhere("LIKE(?, " + Programs.COLUMN_CANONICAL_GENRE + ") AND " 876 + Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND " 877 + Programs.COLUMN_END_TIME_UTC_MILLIS + ">=?", 878 "%" + genre + "%", curTime, curTime); 879 } 880 String inputId = uri.getQueryParameter(TvContract.PARAM_INPUT); 881 if (inputId != null) { 882 params.appendWhere(Channels.COLUMN_INPUT_ID + "=?", inputId); 883 } 884 boolean browsableOnly = uri.getBooleanQueryParameter( 885 TvContract.PARAM_BROWSABLE_ONLY, false); 886 if (browsableOnly) { 887 params.appendWhere(Channels.COLUMN_BROWSABLE + "=1"); 888 } 889 break; 890 case MATCH_CHANNEL_ID: 891 params.setTables(CHANNELS_TABLE); 892 params.appendWhere(Channels._ID + "=?", uri.getLastPathSegment()); 893 break; 894 case MATCH_PROGRAM: 895 params.setTables(PROGRAMS_TABLE); 896 String paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL); 897 if (paramChannelId != null) { 898 String channelId = String.valueOf(Long.parseLong(paramChannelId)); 899 params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId); 900 } 901 String paramStartTime = uri.getQueryParameter(TvContract.PARAM_START_TIME); 902 String paramEndTime = uri.getQueryParameter(TvContract.PARAM_END_TIME); 903 if (paramStartTime != null && paramEndTime != null) { 904 String startTime = String.valueOf(Long.parseLong(paramStartTime)); 905 String endTime = String.valueOf(Long.parseLong(paramEndTime)); 906 params.appendWhere(Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND " 907 + Programs.COLUMN_END_TIME_UTC_MILLIS + ">=?", endTime, startTime); 908 } 909 break; 910 case MATCH_PROGRAM_ID: 911 params.setTables(PROGRAMS_TABLE); 912 params.appendWhere(Programs._ID + "=?", uri.getLastPathSegment()); 913 break; 914 case MATCH_WATCHED_PROGRAM: 915 if (!callerHasAccessWatchedProgramsPermission()) { 916 throw new SecurityException("Access not allowed for " + uri); 917 } 918 params.setTables(WATCHED_PROGRAMS_TABLE); 919 params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1"); 920 break; 921 case MATCH_WATCHED_PROGRAM_ID: 922 if (!callerHasAccessWatchedProgramsPermission()) { 923 throw new SecurityException("Access not allowed for " + uri); 924 } 925 params.setTables(WATCHED_PROGRAMS_TABLE); 926 params.appendWhere(WatchedPrograms._ID + "=?", uri.getLastPathSegment()); 927 params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1"); 928 break; 929 case MATCH_RECORDED_PROGRAM_ID: 930 params.appendWhere(RecordedPrograms._ID + "=?", uri.getLastPathSegment()); 931 // fall-through 932 case MATCH_RECORDED_PROGRAM: 933 params.setTables(RECORDED_PROGRAMS_TABLE); 934 paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL); 935 if (paramChannelId != null) { 936 String channelId = String.valueOf(Long.parseLong(paramChannelId)); 937 params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId); 938 } 939 break; 940 case MATCH_CHANNEL_ID_LOGO: 941 if (operation.equals(OP_DELETE)) { 942 params.setTables(CHANNELS_TABLE); 943 params.appendWhere(Channels._ID + "=?", uri.getPathSegments().get(1)); 944 break; 945 } 946 // fall-through 947 case MATCH_PASSTHROUGH_ID: 948 throw new UnsupportedOperationException(operation + " not permmitted on " + uri); 949 default: 950 throw new IllegalArgumentException("Unknown URI " + uri); 951 } 952 return params; 953 } 954 955 private static String capitalize(String str) { 956 return Character.toUpperCase(str.charAt(0)) + str.substring(1); 957 } 958 959 @SuppressLint("DefaultLocale") 960 private void checkAndConvertGenre(ContentValues values) { 961 String canonicalGenres = values.getAsString(Programs.COLUMN_CANONICAL_GENRE); 962 963 if (!TextUtils.isEmpty(canonicalGenres)) { 964 // Check if the canonical genres are valid. If not, clear them. 965 String[] genres = Genres.decode(canonicalGenres); 966 for (String genre : genres) { 967 if (!Genres.isCanonical(genre)) { 968 values.putNull(Programs.COLUMN_CANONICAL_GENRE); 969 canonicalGenres = null; 970 break; 971 } 972 } 973 } 974 975 if (TextUtils.isEmpty(canonicalGenres)) { 976 // If the canonical genre is not set, try to map the broadcast genre to the canonical 977 // genre. 978 String broadcastGenres = values.getAsString(Programs.COLUMN_BROADCAST_GENRE); 979 if (!TextUtils.isEmpty(broadcastGenres)) { 980 Set<String> genreSet = new HashSet<>(); 981 String[] genres = Genres.decode(broadcastGenres); 982 for (String genre : genres) { 983 String canonicalGenre = sGenreMap.get(genre.toUpperCase()); 984 if (Genres.isCanonical(canonicalGenre)) { 985 genreSet.add(canonicalGenre); 986 } 987 } 988 if (genreSet.size() > 0) { 989 values.put(Programs.COLUMN_CANONICAL_GENRE, 990 Genres.encode(genreSet.toArray(new String[genreSet.size()]))); 991 } 992 } 993 } 994 } 995 996 // We might have more than one thread trying to make its way through applyBatch() so the 997 // notification coalescing needs to be thread-local to work correctly. 998 private final ThreadLocal<Set<Uri>> mTLBatchNotifications = new ThreadLocal<>(); 999 1000 private Set<Uri> getBatchNotificationsSet() { 1001 return mTLBatchNotifications.get(); 1002 } 1003 1004 private void setBatchNotificationsSet(Set<Uri> batchNotifications) { 1005 mTLBatchNotifications.set(batchNotifications); 1006 } 1007 1008 @Override 1009 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1010 throws OperationApplicationException { 1011 setBatchNotificationsSet(new HashSet<Uri>()); 1012 Context context = getContext(); 1013 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1014 db.beginTransaction(); 1015 try { 1016 ContentProviderResult[] results = super.applyBatch(operations); 1017 db.setTransactionSuccessful(); 1018 return results; 1019 } finally { 1020 db.endTransaction(); 1021 final Set<Uri> notifications = getBatchNotificationsSet(); 1022 setBatchNotificationsSet(null); 1023 for (final Uri uri : notifications) { 1024 context.getContentResolver().notifyChange(uri, null); 1025 } 1026 } 1027 } 1028 1029 @Override 1030 public int bulkInsert(Uri uri, ContentValues[] values) { 1031 setBatchNotificationsSet(new HashSet<Uri>()); 1032 Context context = getContext(); 1033 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1034 db.beginTransaction(); 1035 try { 1036 int result = super.bulkInsert(uri, values); 1037 db.setTransactionSuccessful(); 1038 return result; 1039 } finally { 1040 db.endTransaction(); 1041 final Set<Uri> notifications = getBatchNotificationsSet(); 1042 setBatchNotificationsSet(null); 1043 for (final Uri notificationUri : notifications) { 1044 context.getContentResolver().notifyChange(notificationUri, null); 1045 } 1046 } 1047 } 1048 1049 private void notifyChange(Uri uri) { 1050 final Set<Uri> batchNotifications = getBatchNotificationsSet(); 1051 if (batchNotifications != null) { 1052 batchNotifications.add(uri); 1053 } else { 1054 getContext().getContentResolver().notifyChange(uri, null); 1055 } 1056 } 1057 1058 private boolean callerHasReadTvListingsPermission() { 1059 return getContext().checkCallingOrSelfPermission(PERMISSION_READ_TV_LISTINGS) 1060 == PackageManager.PERMISSION_GRANTED; 1061 } 1062 1063 private boolean callerHasAccessAllEpgDataPermission() { 1064 return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_ALL_EPG_DATA) 1065 == PackageManager.PERMISSION_GRANTED; 1066 } 1067 1068 private boolean callerHasAccessWatchedProgramsPermission() { 1069 return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_WATCHED_PROGRAMS) 1070 == PackageManager.PERMISSION_GRANTED; 1071 } 1072 1073 private boolean callerHasModifyParentalControlsPermission() { 1074 return getContext().checkCallingOrSelfPermission( 1075 android.Manifest.permission.MODIFY_PARENTAL_CONTROLS) 1076 == PackageManager.PERMISSION_GRANTED; 1077 } 1078 1079 @Override 1080 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 1081 switch (sUriMatcher.match(uri)) { 1082 case MATCH_CHANNEL_ID_LOGO: 1083 return openLogoFile(uri, mode); 1084 default: 1085 throw new FileNotFoundException(uri.toString()); 1086 } 1087 } 1088 1089 private ParcelFileDescriptor openLogoFile(Uri uri, String mode) throws FileNotFoundException { 1090 long channelId = Long.parseLong(uri.getPathSegments().get(1)); 1091 1092 SqlParams params = new SqlParams(CHANNELS_TABLE, Channels._ID + "=?", 1093 String.valueOf(channelId)); 1094 if (!callerHasAccessAllEpgDataPermission()) { 1095 if (callerHasReadTvListingsPermission()) { 1096 params.appendWhere(Channels.COLUMN_PACKAGE_NAME + "=? OR " 1097 + Channels.COLUMN_SEARCHABLE + "=?", getCallingPackage_(), "1"); 1098 } else { 1099 params.appendWhere(Channels.COLUMN_PACKAGE_NAME + "=?", getCallingPackage_()); 1100 } 1101 } 1102 1103 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 1104 queryBuilder.setTables(params.getTables()); 1105 1106 // We don't write the database here. 1107 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1108 if (mode.equals("r")) { 1109 String sql = queryBuilder.buildQuery(new String[] { CHANNELS_COLUMN_LOGO }, 1110 params.getSelection(), null, null, null, null); 1111 ParcelFileDescriptor fd = DatabaseUtils.blobFileDescriptorForQuery( 1112 db, sql, params.getSelectionArgs()); 1113 if (fd == null) { 1114 throw new FileNotFoundException(uri.toString()); 1115 } 1116 return fd; 1117 } else { 1118 try (Cursor cursor = queryBuilder.query(db, new String[] { Channels._ID }, 1119 params.getSelection(), params.getSelectionArgs(), null, null, null)) { 1120 if (cursor.getCount() < 1) { 1121 // Fails early if corresponding channel does not exist. 1122 // PipeMonitor may still fail to update DB later. 1123 throw new FileNotFoundException(uri.toString()); 1124 } 1125 } 1126 1127 try { 1128 ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe(); 1129 PipeMonitor pipeMonitor = new PipeMonitor(pipeFds[0], channelId, params); 1130 pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 1131 return pipeFds[1]; 1132 } catch (IOException ioe) { 1133 FileNotFoundException fne = new FileNotFoundException(uri.toString()); 1134 fne.initCause(ioe); 1135 throw fne; 1136 } 1137 } 1138 } 1139 1140 /** 1141 * Validates the sort order based on the given field set. 1142 * 1143 * @throws IllegalArgumentException if there is any unknown field. 1144 */ 1145 @SuppressLint("DefaultLocale") 1146 private static void validateSortOrder(String sortOrder, Set<String> possibleFields) { 1147 if (TextUtils.isEmpty(sortOrder) || possibleFields.isEmpty()) { 1148 return; 1149 } 1150 String[] orders = sortOrder.split(","); 1151 for (String order : orders) { 1152 String field = order.replaceAll("\\s+", " ").trim().toLowerCase().replace(" asc", "") 1153 .replace(" desc", ""); 1154 if (!possibleFields.contains(field)) { 1155 throw new IllegalArgumentException("Illegal field in sort order " + order); 1156 } 1157 } 1158 } 1159 1160 private class PipeMonitor extends AsyncTask<Void, Void, Void> { 1161 private final ParcelFileDescriptor mPfd; 1162 private final long mChannelId; 1163 private final SqlParams mParams; 1164 1165 private PipeMonitor(ParcelFileDescriptor pfd, long channelId, SqlParams params) { 1166 mPfd = pfd; 1167 mChannelId = channelId; 1168 mParams = params; 1169 } 1170 1171 @Override 1172 protected Void doInBackground(Void... params) { 1173 AutoCloseInputStream is = new AutoCloseInputStream(mPfd); 1174 ByteArrayOutputStream baos = null; 1175 int count = 0; 1176 try { 1177 Bitmap bitmap = BitmapFactory.decodeStream(is); 1178 if (bitmap == null) { 1179 Log.e(TAG, "Failed to decode logo image for channel ID " + mChannelId); 1180 return null; 1181 } 1182 1183 float scaleFactor = Math.min(1f, ((float) MAX_LOGO_IMAGE_SIZE) / 1184 Math.max(bitmap.getWidth(), bitmap.getHeight())); 1185 if (scaleFactor < 1f) { 1186 bitmap = Bitmap.createScaledBitmap(bitmap, 1187 (int) (bitmap.getWidth() * scaleFactor), 1188 (int) (bitmap.getHeight() * scaleFactor), false); 1189 } 1190 1191 baos = new ByteArrayOutputStream(); 1192 bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); 1193 byte[] bytes = baos.toByteArray(); 1194 1195 ContentValues values = new ContentValues(); 1196 values.put(CHANNELS_COLUMN_LOGO, bytes); 1197 1198 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1199 count = db.update(mParams.getTables(), values, mParams.getSelection(), 1200 mParams.getSelectionArgs()); 1201 if (count > 0) { 1202 Uri uri = TvContract.buildChannelLogoUri(mChannelId); 1203 notifyChange(uri); 1204 } 1205 } finally { 1206 if (count == 0) { 1207 try { 1208 mPfd.closeWithError("Failed to write logo for channel ID " + mChannelId); 1209 } catch (IOException ioe) { 1210 Log.e(TAG, "Failed to close pipe", ioe); 1211 } 1212 } 1213 IoUtils.closeQuietly(baos); 1214 IoUtils.closeQuietly(is); 1215 } 1216 return null; 1217 } 1218 } 1219 1220 private void deleteUnconsolidatedWatchedProgramsRows() { 1221 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1222 db.delete(WATCHED_PROGRAMS_TABLE, WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0", null); 1223 } 1224 1225 @SuppressLint("HandlerLeak") 1226 private final class WatchLogHandler extends Handler { 1227 private static final int MSG_CONSOLIDATE = 1; 1228 private static final int MSG_TRY_CONSOLIDATE_ALL = 2; 1229 1230 @Override 1231 public void handleMessage(Message msg) { 1232 switch (msg.what) { 1233 case MSG_CONSOLIDATE: { 1234 SomeArgs args = (SomeArgs) msg.obj; 1235 String sessionToken = (String) args.arg1; 1236 long watchEndTime = (long) args.arg2; 1237 onConsolidate(sessionToken, watchEndTime); 1238 args.recycle(); 1239 return; 1240 } 1241 case MSG_TRY_CONSOLIDATE_ALL: { 1242 onTryConsolidateAll(); 1243 return; 1244 } 1245 default: { 1246 Log.w(TAG, "Unhandled message code: " + msg.what); 1247 return; 1248 } 1249 } 1250 } 1251 1252 // Consolidates all WatchedPrograms rows for a given session with watch end time information 1253 // of the most recent log entry. After this method is called, it is guaranteed that there 1254 // remain consolidated rows only for that session. 1255 private void onConsolidate(String sessionToken, long watchEndTime) { 1256 if (DEBUG) { 1257 Log.d(TAG, "onConsolidate(sessionToken=" + sessionToken + ", watchEndTime=" 1258 + watchEndTime + ")"); 1259 } 1260 1261 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 1262 queryBuilder.setTables(WATCHED_PROGRAMS_TABLE); 1263 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1264 1265 // Pick up the last row with the same session token. 1266 String[] projection = { 1267 WatchedPrograms._ID, 1268 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, 1269 WatchedPrograms.COLUMN_CHANNEL_ID 1270 }; 1271 String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=? AND " 1272 + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + "=?"; 1273 String[] selectionArgs = { 1274 "0", 1275 sessionToken 1276 }; 1277 String sortOrder = WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC"; 1278 1279 int consolidatedRowCount = 0; 1280 try (Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, 1281 null, sortOrder)) { 1282 long oldWatchStartTime = watchEndTime; 1283 while (cursor != null && cursor.moveToNext()) { 1284 long id = cursor.getLong(0); 1285 long watchStartTime = cursor.getLong(1); 1286 long channelId = cursor.getLong(2); 1287 consolidatedRowCount += consolidateRow(id, watchStartTime, oldWatchStartTime, 1288 channelId, false); 1289 oldWatchStartTime = watchStartTime; 1290 } 1291 } 1292 if (consolidatedRowCount > 0) { 1293 deleteUnsearchable(); 1294 } 1295 } 1296 1297 // Tries to consolidate all WatchedPrograms rows regardless of the session. After this 1298 // method is called, it is guaranteed that we have at most one unconsolidated log entry per 1299 // session that represents the user's ongoing watch activity. 1300 // Also, this method automatically schedules the next consolidation if there still remains 1301 // an unconsolidated entry. 1302 private void onTryConsolidateAll() { 1303 if (DEBUG) { 1304 Log.d(TAG, "onTryConsolidateAll()"); 1305 } 1306 1307 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 1308 queryBuilder.setTables(WATCHED_PROGRAMS_TABLE); 1309 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1310 1311 // Pick up all unconsolidated rows grouped by session. The most recent log entry goes on 1312 // top. 1313 String[] projection = { 1314 WatchedPrograms._ID, 1315 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, 1316 WatchedPrograms.COLUMN_CHANNEL_ID, 1317 WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN 1318 }; 1319 String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0"; 1320 String sortOrder = WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + " DESC," 1321 + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC"; 1322 1323 int consolidatedRowCount = 0; 1324 try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null, 1325 sortOrder)) { 1326 long oldWatchStartTime = 0; 1327 String oldSessionToken = null; 1328 while (cursor != null && cursor.moveToNext()) { 1329 long id = cursor.getLong(0); 1330 long watchStartTime = cursor.getLong(1); 1331 long channelId = cursor.getLong(2); 1332 String sessionToken = cursor.getString(3); 1333 1334 if (!sessionToken.equals(oldSessionToken)) { 1335 // The most recent log entry for the current session, which may be still 1336 // active. Just go through a dry run with the current time to see if this 1337 // entry can be split into multiple rows. 1338 consolidatedRowCount += consolidateRow(id, watchStartTime, 1339 System.currentTimeMillis(), channelId, true); 1340 oldSessionToken = sessionToken; 1341 } else { 1342 // The later entries after the most recent one all fall into here. We now 1343 // know that this watch activity ended exactly at the same time when the 1344 // next activity started. 1345 consolidatedRowCount += consolidateRow(id, watchStartTime, 1346 oldWatchStartTime, channelId, false); 1347 } 1348 oldWatchStartTime = watchStartTime; 1349 } 1350 } 1351 if (consolidatedRowCount > 0) { 1352 deleteUnsearchable(); 1353 } 1354 scheduleConsolidationIfNeeded(); 1355 } 1356 1357 // Consolidates a WatchedPrograms row. 1358 // A row is 'consolidated' if and only if the following information is complete: 1359 // 1. WatchedPrograms.COLUMN_CHANNEL_ID 1360 // 2. WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS 1361 // 3. WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS 1362 // where COLUMN_WATCH_START_TIME_UTC_MILLIS <= COLUMN_WATCH_END_TIME_UTC_MILLIS. 1363 // This is the minimal but useful enough set of information to comprise the user's watch 1364 // history. (The program data are considered optional although we do try to fill them while 1365 // consolidating the row.) It is guaranteed that the target row is either consolidated or 1366 // deleted after this method is called. 1367 // Set {@code dryRun} to {@code true} if you think it's necessary to split the row without 1368 // consolidating the most recent row because the user stayed on the same channel for a very 1369 // long time. 1370 // This method returns the number of consolidated rows, which can be 0 or more. 1371 private int consolidateRow( 1372 long id, long watchStartTime, long watchEndTime, long channelId, boolean dryRun) { 1373 if (DEBUG) { 1374 Log.d(TAG, "consolidateRow(id=" + id + ", watchStartTime=" + watchStartTime 1375 + ", watchEndTime=" + watchEndTime + ", channelId=" + channelId 1376 + ", dryRun=" + dryRun + ")"); 1377 } 1378 1379 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1380 1381 if (watchStartTime > watchEndTime) { 1382 Log.e(TAG, "watchEndTime cannot be less than watchStartTime"); 1383 db.delete(WATCHED_PROGRAMS_TABLE, WatchedPrograms._ID + "=" + String.valueOf(id), 1384 null); 1385 return 0; 1386 } 1387 1388 ContentValues values = getProgramValues(channelId, watchStartTime); 1389 Long endTime = values.getAsLong(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS); 1390 boolean needsToSplit = endTime != null && endTime < watchEndTime; 1391 1392 values.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, 1393 String.valueOf(watchStartTime)); 1394 if (!dryRun || needsToSplit) { 1395 values.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, 1396 String.valueOf(needsToSplit ? endTime : watchEndTime)); 1397 values.put(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED, "1"); 1398 db.update(WATCHED_PROGRAMS_TABLE, values, 1399 WatchedPrograms._ID + "=" + String.valueOf(id), null); 1400 // Treat the watched program is inserted when WATCHED_PROGRAMS_COLUMN_CONSOLIDATED 1401 // becomes 1. 1402 notifyChange(TvContract.buildWatchedProgramUri(id)); 1403 } else { 1404 db.update(WATCHED_PROGRAMS_TABLE, values, 1405 WatchedPrograms._ID + "=" + String.valueOf(id), null); 1406 } 1407 int count = dryRun ? 0 : 1; 1408 if (needsToSplit) { 1409 // This means that the program ended before the user stops watching the current 1410 // channel. In this case we duplicate the log entry as many as the number of 1411 // programs watched on the same channel. Here the end time of the current program 1412 // becomes the new watch start time of the next program. 1413 long duplicatedId = duplicateRow(id); 1414 if (duplicatedId > 0) { 1415 count += consolidateRow(duplicatedId, endTime, watchEndTime, channelId, dryRun); 1416 } 1417 } 1418 return count; 1419 } 1420 1421 // Deletes the log entries from unsearchable channels. Note that only consolidated log 1422 // entries are safe to delete. 1423 private void deleteUnsearchable() { 1424 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1425 String deleteWhere = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=1 AND " 1426 + WatchedPrograms.COLUMN_CHANNEL_ID + " IN (SELECT " + Channels._ID 1427 + " FROM " + CHANNELS_TABLE + " WHERE " + Channels.COLUMN_SEARCHABLE + "=0)"; 1428 db.delete(WATCHED_PROGRAMS_TABLE, deleteWhere, null); 1429 } 1430 1431 private void scheduleConsolidationIfNeeded() { 1432 if (DEBUG) { 1433 Log.d(TAG, "scheduleConsolidationIfNeeded()"); 1434 } 1435 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 1436 queryBuilder.setTables(WATCHED_PROGRAMS_TABLE); 1437 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1438 1439 // Pick up all unconsolidated rows. 1440 String[] projection = { 1441 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, 1442 WatchedPrograms.COLUMN_CHANNEL_ID, 1443 }; 1444 String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0"; 1445 1446 try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null, 1447 null)) { 1448 // Find the earliest time that any of the currently watching programs ends and 1449 // schedule the next consolidation at that time. 1450 long minEndTime = Long.MAX_VALUE; 1451 while (cursor != null && cursor.moveToNext()) { 1452 long watchStartTime = cursor.getLong(0); 1453 long channelId = cursor.getLong(1); 1454 ContentValues values = getProgramValues(channelId, watchStartTime); 1455 Long endTime = values.getAsLong(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS); 1456 1457 if (endTime != null && endTime < minEndTime 1458 && endTime > System.currentTimeMillis()) { 1459 minEndTime = endTime; 1460 } 1461 } 1462 if (minEndTime != Long.MAX_VALUE) { 1463 sendEmptyMessageAtTime(MSG_TRY_CONSOLIDATE_ALL, minEndTime); 1464 if (DEBUG) { 1465 CharSequence minEndTimeStr = DateUtils.getRelativeTimeSpanString( 1466 minEndTime, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS); 1467 Log.d(TAG, "onTryConsolidateAll() scheduled " + minEndTimeStr); 1468 } 1469 } 1470 } 1471 } 1472 1473 // Returns non-null ContentValues of the program data that the user watched on the channel 1474 // {@code channelId} at the time {@code time}. 1475 private ContentValues getProgramValues(long channelId, long time) { 1476 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 1477 queryBuilder.setTables(PROGRAMS_TABLE); 1478 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1479 1480 String[] projection = { 1481 Programs.COLUMN_TITLE, 1482 Programs.COLUMN_START_TIME_UTC_MILLIS, 1483 Programs.COLUMN_END_TIME_UTC_MILLIS, 1484 Programs.COLUMN_SHORT_DESCRIPTION 1485 }; 1486 String selection = Programs.COLUMN_CHANNEL_ID + "=? AND " 1487 + Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND " 1488 + Programs.COLUMN_END_TIME_UTC_MILLIS + ">?"; 1489 String[] selectionArgs = { 1490 String.valueOf(channelId), 1491 String.valueOf(time), 1492 String.valueOf(time) 1493 }; 1494 String sortOrder = Programs.COLUMN_START_TIME_UTC_MILLIS + " ASC"; 1495 1496 try (Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, 1497 null, sortOrder)) { 1498 ContentValues values = new ContentValues(); 1499 if (cursor != null && cursor.moveToNext()) { 1500 values.put(WatchedPrograms.COLUMN_TITLE, cursor.getString(0)); 1501 values.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, cursor.getLong(1)); 1502 values.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, cursor.getLong(2)); 1503 values.put(WatchedPrograms.COLUMN_DESCRIPTION, cursor.getString(3)); 1504 } 1505 return values; 1506 } 1507 } 1508 1509 // Duplicates the WatchedPrograms row with a given ID and returns the ID of the duplicated 1510 // row. Returns -1 if failed. 1511 private long duplicateRow(long id) { 1512 if (DEBUG) { 1513 Log.d(TAG, "duplicateRow(" + id + ")"); 1514 } 1515 1516 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 1517 queryBuilder.setTables(WATCHED_PROGRAMS_TABLE); 1518 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1519 1520 String[] projection = { 1521 WatchedPrograms.COLUMN_PACKAGE_NAME, 1522 WatchedPrograms.COLUMN_CHANNEL_ID, 1523 WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN 1524 }; 1525 String selection = WatchedPrograms._ID + "=" + String.valueOf(id); 1526 1527 try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null, 1528 null)) { 1529 long rowId = -1; 1530 if (cursor != null && cursor.moveToNext()) { 1531 ContentValues values = new ContentValues(); 1532 values.put(WatchedPrograms.COLUMN_PACKAGE_NAME, cursor.getString(0)); 1533 values.put(WatchedPrograms.COLUMN_CHANNEL_ID, cursor.getLong(1)); 1534 values.put(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, cursor.getString(2)); 1535 rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values); 1536 } 1537 return rowId; 1538 } 1539 } 1540 } 1541} 1542