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