TvProvider.java revision 711f02f31b2be633a19fc929761581116cb0c64b
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.content.ComponentName; 20import android.content.ContentProvider; 21import android.content.ContentProviderOperation; 22import android.content.ContentProviderResult; 23import android.content.ContentValues; 24import android.content.Context; 25import android.content.OperationApplicationException; 26import android.content.UriMatcher; 27import android.content.pm.PackageManager; 28import android.database.Cursor; 29import android.database.DatabaseUtils; 30import android.database.SQLException; 31import android.database.sqlite.SQLiteDatabase; 32import android.database.sqlite.SQLiteOpenHelper; 33import android.database.sqlite.SQLiteQueryBuilder; 34import android.graphics.Bitmap; 35import android.graphics.BitmapFactory; 36import android.media.tv.TvContract; 37import android.media.tv.TvContract.BaseTvColumns; 38import android.media.tv.TvContract.Channels; 39import android.media.tv.TvContract.Programs; 40import android.media.tv.TvContract.WatchedPrograms; 41import android.net.Uri; 42import android.os.AsyncTask; 43import android.os.ParcelFileDescriptor; 44import android.os.ParcelFileDescriptor.AutoCloseInputStream; 45import android.text.TextUtils; 46import android.util.Log; 47 48import com.google.android.collect.Sets; 49 50import libcore.io.IoUtils; 51 52import java.io.ByteArrayOutputStream; 53import java.io.File; 54import java.io.FileNotFoundException; 55import java.io.IOException; 56import java.util.ArrayList; 57import java.util.HashMap; 58import java.util.Set; 59 60/** 61 * TV content provider. The contract between this provider and applications is defined in 62 * {@link android.media.tv.TvContract}. 63 */ 64public class TvProvider extends ContentProvider { 65 // STOPSHIP: Turn debugging off. 66 private static final boolean DEBUG = true; 67 private static final String TAG = "TvProvider"; 68 69 private static final UriMatcher sUriMatcher; 70 private static final int MATCH_CHANNEL = 1; 71 private static final int MATCH_CHANNEL_ID = 2; 72 private static final int MATCH_CHANNEL_ID_LOGO = 3; 73 private static final int MATCH_CHANNEL_ID_PROGRAM = 4; 74 private static final int MATCH_INPUT_PACKAGE_SERVICE_CHANNEL = 5; 75 private static final int MATCH_PROGRAM = 6; 76 private static final int MATCH_PROGRAM_ID = 7; 77 private static final int MATCH_WATCHED_PROGRAM = 8; 78 private static final int MATCH_WATCHED_PROGRAM_ID = 9; 79 80 private static final String SELECTION_OVERLAPPED_PROGRAM = Programs.COLUMN_CHANNEL_ID 81 + "=? AND " + Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND " 82 + Programs.COLUMN_END_TIME_UTC_MILLIS + ">=?"; 83 84 private static final String SELECTION_CHANNEL_BY_INPUT = Channels.COLUMN_PACKAGE_NAME 85 + "=? AND " + Channels.COLUMN_SERVICE_NAME + "=?"; 86 87 private static final String CHANNELS_COLUMN_LOGO = "logo"; 88 private static final int MAX_LOGO_IMAGE_SIZE = 256; 89 90 // STOPSHIP: Put this into the contract class. 91 private static final String Programs_COLUMN_VIDEO_RESOLUTION = "video_resolution"; 92 93 private static HashMap<String, String> sChannelProjectionMap; 94 private static HashMap<String, String> sProgramProjectionMap; 95 private static HashMap<String, String> sWatchedProgramProjectionMap; 96 97 static { 98 sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 99 sUriMatcher.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL); 100 sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID); 101 sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#/logo", MATCH_CHANNEL_ID_LOGO); 102 sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#/program", MATCH_CHANNEL_ID_PROGRAM); 103 sUriMatcher.addURI(TvContract.AUTHORITY, "input/*/*/channel", 104 MATCH_INPUT_PACKAGE_SERVICE_CHANNEL); 105 sUriMatcher.addURI(TvContract.AUTHORITY, "program", MATCH_PROGRAM); 106 sUriMatcher.addURI(TvContract.AUTHORITY, "program/#", MATCH_PROGRAM_ID); 107 sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program", MATCH_WATCHED_PROGRAM); 108 sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID); 109 110 sChannelProjectionMap = new HashMap<String, String>(); 111 sChannelProjectionMap.put(Channels._ID, Channels._ID); 112 sChannelProjectionMap.put(Channels.COLUMN_PACKAGE_NAME, Channels.COLUMN_PACKAGE_NAME); 113 sChannelProjectionMap.put(Channels.COLUMN_SERVICE_NAME, Channels.COLUMN_SERVICE_NAME); 114 sChannelProjectionMap.put(Channels.COLUMN_TYPE, Channels.COLUMN_TYPE); 115 sChannelProjectionMap.put(Channels.COLUMN_TRANSPORT_STREAM_ID, 116 Channels.COLUMN_TRANSPORT_STREAM_ID); 117 sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NUMBER, Channels.COLUMN_DISPLAY_NUMBER); 118 sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NAME, Channels.COLUMN_DISPLAY_NAME); 119 sChannelProjectionMap.put(Channels.COLUMN_DESCRIPTION, Channels.COLUMN_DESCRIPTION); 120 sChannelProjectionMap.put(Channels.COLUMN_BROWSABLE, Channels.COLUMN_BROWSABLE); 121 sChannelProjectionMap.put(Channels.COLUMN_SEARCHABLE, Channels.COLUMN_SEARCHABLE); 122 sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA, 123 Channels.COLUMN_INTERNAL_PROVIDER_DATA); 124 sChannelProjectionMap.put(Channels.COLUMN_VERSION_NUMBER, Channels.COLUMN_VERSION_NUMBER); 125 126 sProgramProjectionMap = new HashMap<String, String>(); 127 sProgramProjectionMap.put(Programs._ID, Programs._ID); 128 sProgramProjectionMap.put(Programs.COLUMN_PACKAGE_NAME, Programs.COLUMN_PACKAGE_NAME); 129 sProgramProjectionMap.put(Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_CHANNEL_ID); 130 sProgramProjectionMap.put(Programs.COLUMN_TITLE, Programs.COLUMN_TITLE); 131 sProgramProjectionMap.put(Programs.COLUMN_START_TIME_UTC_MILLIS, 132 Programs.COLUMN_START_TIME_UTC_MILLIS); 133 sProgramProjectionMap.put(Programs.COLUMN_END_TIME_UTC_MILLIS, 134 Programs.COLUMN_END_TIME_UTC_MILLIS); 135 sProgramProjectionMap.put(Programs.COLUMN_SHORT_DESCRIPTION, 136 Programs.COLUMN_SHORT_DESCRIPTION); 137 sProgramProjectionMap.put(Programs.COLUMN_LONG_DESCRIPTION, 138 Programs.COLUMN_LONG_DESCRIPTION); 139 sProgramProjectionMap.put(Programs_COLUMN_VIDEO_RESOLUTION, 140 Programs_COLUMN_VIDEO_RESOLUTION); 141 sProgramProjectionMap.put(Programs.COLUMN_POSTER_ART_URI, 142 Programs.COLUMN_POSTER_ART_URI); 143 sProgramProjectionMap.put(Programs.COLUMN_THUMBNAIL_URI, 144 Programs.COLUMN_THUMBNAIL_URI); 145 sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_DATA, 146 Programs.COLUMN_INTERNAL_PROVIDER_DATA); 147 sProgramProjectionMap.put(Programs.COLUMN_VERSION_NUMBER, Programs.COLUMN_VERSION_NUMBER); 148 149 sWatchedProgramProjectionMap = new HashMap<String, String>(); 150 sWatchedProgramProjectionMap.put(WatchedPrograms._ID, WatchedPrograms._ID); 151 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, 152 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS); 153 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, 154 WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS); 155 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_CHANNEL_ID, 156 WatchedPrograms.COLUMN_CHANNEL_ID); 157 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_TITLE, 158 WatchedPrograms.COLUMN_TITLE); 159 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, 160 WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS); 161 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, 162 WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS); 163 sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_DESCRIPTION, 164 WatchedPrograms.COLUMN_DESCRIPTION); 165 } 166 167 private static final int DATABASE_VERSION = 7; 168 private static final String DATABASE_NAME = "tv.db"; 169 private static final String CHANNELS_TABLE = "channels"; 170 private static final String PROGRAMS_TABLE = "programs"; 171 private static final String WATCHED_PROGRAMS_TABLE = "watched_programs"; 172 private static final String DEFAULT_CHANNELS_SORT_ORDER = Channels.COLUMN_DISPLAY_NUMBER 173 + " ASC"; 174 private static final String DEFAULT_PROGRAMS_SORT_ORDER = Programs.COLUMN_START_TIME_UTC_MILLIS 175 + " ASC"; 176 private static final String DEFAULT_WATCHED_PROGRAMS_SORT_ORDER = 177 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC"; 178 179 private static final String PERMISSION_ALL_EPG_DATA = "android.permission.ALL_EPG_DATA"; 180 181 private static class DatabaseHelper extends SQLiteOpenHelper { 182 private Context mContext; 183 184 DatabaseHelper(Context context) { 185 super(context, DATABASE_NAME, null, DATABASE_VERSION); 186 mContext = context; 187 } 188 189 @Override 190 public void onConfigure(SQLiteDatabase db) { 191 db.setForeignKeyConstraintsEnabled(true); 192 } 193 194 @Override 195 public void onCreate(SQLiteDatabase db) { 196 if (DEBUG) { 197 Log.d(TAG, "Creating database"); 198 } 199 // Set up the database schema. 200 db.execSQL("CREATE TABLE " + CHANNELS_TABLE + " (" 201 + Channels._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 202 + Channels.COLUMN_PACKAGE_NAME + " TEXT NOT NULL," 203 + Channels.COLUMN_SERVICE_NAME + " TEXT NOT NULL," 204 + Channels.COLUMN_TYPE + " INTEGER NOT NULL DEFAULT 0," 205 + Channels.COLUMN_SERVICE_TYPE + " INTEGER NOT NULL DEFAULT 1," 206 + Channels.COLUMN_ORIGINAL_NETWORK_ID + " INTEGER," 207 + Channels.COLUMN_TRANSPORT_STREAM_ID + " INTEGER," 208 + Channels.COLUMN_SERVICE_ID + " INTEGER," 209 + Channels.COLUMN_DISPLAY_NUMBER + " TEXT," 210 + Channels.COLUMN_DISPLAY_NAME + " TEXT," 211 + Channels.COLUMN_DESCRIPTION + " TEXT," 212 + Channels.COLUMN_BROWSABLE + " INTEGER NOT NULL DEFAULT 1," 213 + Channels.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1," 214 + Channels.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB," 215 + CHANNELS_COLUMN_LOGO + " BLOB," 216 + Channels.COLUMN_VERSION_NUMBER + " INTEGER" 217 + ");"); 218 db.execSQL("CREATE TABLE " + PROGRAMS_TABLE + " (" 219 + Programs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 220 + Programs.COLUMN_PACKAGE_NAME + " TEXT NOT NULL," 221 + Programs.COLUMN_CHANNEL_ID + " INTEGER," 222 + Programs.COLUMN_TITLE + " TEXT," 223 + Programs.COLUMN_START_TIME_UTC_MILLIS + " INTEGER," 224 + Programs.COLUMN_END_TIME_UTC_MILLIS + " INTEGER," 225 + Programs.COLUMN_SHORT_DESCRIPTION + " TEXT," 226 + Programs.COLUMN_LONG_DESCRIPTION + " TEXT," 227 + Programs_COLUMN_VIDEO_RESOLUTION + " TEXT," 228 + Programs.COLUMN_POSTER_ART_URI + " TEXT," 229 + Programs.COLUMN_THUMBNAIL_URI + " TEXT," 230 + Programs.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB," 231 + Programs.COLUMN_VERSION_NUMBER + " INTEGER," 232 + "FOREIGN KEY(" + Programs.COLUMN_CHANNEL_ID + ") REFERENCES " 233 + CHANNELS_TABLE + "(" + Channels._ID + ")" 234 + " ON UPDATE CASCADE ON DELETE CASCADE" 235 + ");"); 236 db.execSQL("CREATE TABLE " + WATCHED_PROGRAMS_TABLE + " (" 237 + WatchedPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 238 + WatchedPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL," 239 + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " INTEGER," 240 + WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS + " INTEGER," 241 + WatchedPrograms.COLUMN_CHANNEL_ID + " INTEGER," 242 + WatchedPrograms.COLUMN_TITLE + " TEXT," 243 + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS + " INTEGER," 244 + WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS + " INTEGER," 245 + WatchedPrograms.COLUMN_DESCRIPTION + " TEXT," 246 + "FOREIGN KEY(" + WatchedPrograms.COLUMN_CHANNEL_ID + ") REFERENCES " 247 + CHANNELS_TABLE + "(" + Channels._ID + ")" 248 + " ON UPDATE CASCADE ON DELETE CASCADE" 249 + ");"); 250 } 251 252 @Override 253 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 254 if (DEBUG) { 255 Log.d(TAG, "Upgrading database from " + oldVersion + " to " + newVersion); 256 } 257 258 // Default upgrade case. 259 db.execSQL("DROP TABLE IF EXISTS " + WATCHED_PROGRAMS_TABLE); 260 db.execSQL("DROP TABLE IF EXISTS " + PROGRAMS_TABLE); 261 db.execSQL("DROP TABLE IF EXISTS " + CHANNELS_TABLE); 262 263 // Clear legacy logo directory 264 File logoPath = new File(mContext.getFilesDir(), "logo"); 265 if (logoPath.exists()) { 266 for (File file : logoPath.listFiles()) { 267 file.delete(); 268 } 269 logoPath.delete(); 270 } 271 272 onCreate(db); 273 } 274 } 275 276 private DatabaseHelper mOpenHelper; 277 278 @Override 279 public boolean onCreate() { 280 if (DEBUG) { 281 Log.d(TAG, "Creating TvProvider"); 282 } 283 mOpenHelper = new DatabaseHelper(getContext()); 284 return true; 285 } 286 287 @Override 288 public String getType(Uri uri) { 289 switch (sUriMatcher.match(uri)) { 290 case MATCH_CHANNEL: 291 return Channels.CONTENT_TYPE; 292 case MATCH_CHANNEL_ID: 293 return Channels.CONTENT_ITEM_TYPE; 294 case MATCH_CHANNEL_ID_LOGO: 295 return "image/png"; 296 case MATCH_CHANNEL_ID_PROGRAM: 297 return Programs.CONTENT_TYPE; 298 case MATCH_INPUT_PACKAGE_SERVICE_CHANNEL: 299 return Channels.CONTENT_TYPE; 300 case MATCH_PROGRAM: 301 return Programs.CONTENT_TYPE; 302 case MATCH_PROGRAM_ID: 303 return Programs.CONTENT_ITEM_TYPE; 304 case MATCH_WATCHED_PROGRAM: 305 return WatchedPrograms.CONTENT_TYPE; 306 case MATCH_WATCHED_PROGRAM_ID: 307 return WatchedPrograms.CONTENT_ITEM_TYPE; 308 default: 309 throw new IllegalArgumentException("Unknown URI " + uri); 310 } 311 } 312 313 @Override 314 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 315 String sortOrder) { 316 if (needsToLimitPackage(uri)) { 317 if (!TextUtils.isEmpty(selection)) { 318 throw new IllegalArgumentException("Selection not allowed for " + uri); 319 } 320 selection = BaseTvColumns.COLUMN_PACKAGE_NAME + "=?"; 321 selectionArgs = new String[] { getCallingPackage() }; 322 } 323 324 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 325 String orderBy; 326 327 switch (sUriMatcher.match(uri)) { 328 case MATCH_CHANNEL: 329 queryBuilder.setTables(CHANNELS_TABLE); 330 queryBuilder.setProjectionMap(sChannelProjectionMap); 331 orderBy = DEFAULT_CHANNELS_SORT_ORDER; 332 break; 333 case MATCH_CHANNEL_ID: 334 queryBuilder.setTables(CHANNELS_TABLE); 335 queryBuilder.setProjectionMap(sChannelProjectionMap); 336 selection = DatabaseUtils.concatenateWhere(selection, Channels._ID + "=?"); 337 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 338 uri.getLastPathSegment() 339 }); 340 orderBy = DEFAULT_CHANNELS_SORT_ORDER; 341 break; 342 case MATCH_CHANNEL_ID_PROGRAM: 343 queryBuilder.setTables(PROGRAMS_TABLE); 344 queryBuilder.setProjectionMap(sProgramProjectionMap); 345 selection = DatabaseUtils.concatenateWhere(selection, 346 Programs.COLUMN_CHANNEL_ID + "=?"); 347 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 348 TvContract.getChannelId(uri) 349 }); 350 String paramStartTime = uri.getQueryParameter(TvContract.PARAM_START_TIME); 351 String paramEndTime = uri.getQueryParameter(TvContract.PARAM_END_TIME); 352 if (paramStartTime != null && paramEndTime != null) { 353 String startTime = String.valueOf(Long.parseLong(paramStartTime)); 354 String endTime = String.valueOf(Long.parseLong(paramEndTime)); 355 selection = DatabaseUtils.concatenateWhere(selection, 356 SELECTION_OVERLAPPED_PROGRAM); 357 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 358 TvContract.getChannelId(uri), endTime, startTime 359 }); 360 } 361 orderBy = DEFAULT_PROGRAMS_SORT_ORDER; 362 break; 363 case MATCH_INPUT_PACKAGE_SERVICE_CHANNEL: 364 queryBuilder.setTables(CHANNELS_TABLE); 365 queryBuilder.setProjectionMap(sChannelProjectionMap); 366 boolean browsableOnly = uri.getBooleanQueryParameter( 367 TvContract.PARAM_BROWSABLE_ONLY, true); 368 selection = DatabaseUtils.concatenateWhere(selection, SELECTION_CHANNEL_BY_INPUT 369 + (browsableOnly ? " AND " + Channels.COLUMN_BROWSABLE + "=1" : "")); 370 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 371 TvContract.getPackageName(uri), TvContract.getServiceName(uri) 372 }); 373 orderBy = DEFAULT_CHANNELS_SORT_ORDER; 374 break; 375 case MATCH_PROGRAM: 376 queryBuilder.setTables(PROGRAMS_TABLE); 377 queryBuilder.setProjectionMap(sProgramProjectionMap); 378 orderBy = DEFAULT_PROGRAMS_SORT_ORDER; 379 break; 380 case MATCH_PROGRAM_ID: 381 queryBuilder.setTables(PROGRAMS_TABLE); 382 queryBuilder.setProjectionMap(sProgramProjectionMap); 383 selection = DatabaseUtils.concatenateWhere(selection, Programs._ID + "=?"); 384 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 385 uri.getLastPathSegment() 386 }); 387 orderBy = DEFAULT_PROGRAMS_SORT_ORDER; 388 break; 389 case MATCH_WATCHED_PROGRAM: 390 queryBuilder.setTables(WATCHED_PROGRAMS_TABLE); 391 queryBuilder.setProjectionMap(sWatchedProgramProjectionMap); 392 orderBy = DEFAULT_WATCHED_PROGRAMS_SORT_ORDER; 393 break; 394 case MATCH_WATCHED_PROGRAM_ID: 395 queryBuilder.setTables(WATCHED_PROGRAMS_TABLE); 396 queryBuilder.setProjectionMap(sWatchedProgramProjectionMap); 397 selection = DatabaseUtils.concatenateWhere(selection, WatchedPrograms._ID + "=?"); 398 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 399 uri.getLastPathSegment() 400 }); 401 orderBy = DEFAULT_WATCHED_PROGRAMS_SORT_ORDER; 402 break; 403 default: 404 throw new IllegalArgumentException("Unknown URI " + uri); 405 } 406 407 // Use the default sort order only if no sort order is specified. 408 if (!TextUtils.isEmpty(sortOrder)) { 409 orderBy = sortOrder; 410 } 411 412 // Get the database and run the query. 413 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 414 Cursor c = queryBuilder.query(db, projection, selection, selectionArgs, null, null, 415 orderBy); 416 417 // Tell the cursor what URI to watch, so it knows when its source data changes. 418 c.setNotificationUri(getContext().getContentResolver(), uri); 419 return c; 420 } 421 422 @Override 423 public Uri insert(Uri uri, ContentValues values) { 424 switch (sUriMatcher.match(uri)) { 425 case MATCH_CHANNEL: 426 case MATCH_CHANNEL_ID: 427 return insertChannel(uri, values); 428 case MATCH_PROGRAM: 429 case MATCH_PROGRAM_ID: 430 return insertProgram(uri, values); 431 case MATCH_WATCHED_PROGRAM: 432 case MATCH_WATCHED_PROGRAM_ID: 433 return insertWatchedProgram(uri, values); 434 default: 435 throw new IllegalArgumentException("Unknown URI " + uri); 436 } 437 } 438 439 private Uri insertChannel(Uri uri, ContentValues values) { 440 validateServiceName(values.getAsString(Channels.COLUMN_SERVICE_NAME)); 441 442 // Mark the owner package of this channel. 443 values.put(Channels.COLUMN_PACKAGE_NAME, getCallingPackage()); 444 445 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 446 long rowId = db.insert(CHANNELS_TABLE, null, values); 447 if (rowId > 0) { 448 Uri channelUri = TvContract.buildChannelUri(rowId); 449 notifyChange(channelUri); 450 return channelUri; 451 } 452 453 throw new SQLException("Failed to insert row into " + uri); 454 } 455 456 private Uri insertProgram(Uri uri, ContentValues values) { 457 // Mark the owner package of this program. 458 values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage()); 459 460 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 461 long rowId = db.insert(PROGRAMS_TABLE, null, values); 462 if (rowId > 0) { 463 Uri programUri = TvContract.buildProgramUri(rowId); 464 notifyChange(programUri); 465 return programUri; 466 } 467 468 throw new SQLException("Failed to insert row into " + uri); 469 } 470 471 private Uri insertWatchedProgram(Uri uri, ContentValues values) { 472 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 473 long rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values); 474 if (rowId > 0) { 475 Uri watchedProgramUri = TvContract.buildWatchedProgramUri(rowId); 476 notifyChange(watchedProgramUri); 477 return watchedProgramUri; 478 } 479 480 throw new SQLException("Failed to insert row into " + uri); 481 } 482 483 @Override 484 public int delete(Uri uri, String selection, String[] selectionArgs) { 485 if (needsToLimitPackage(uri)) { 486 if (!TextUtils.isEmpty(selection)) { 487 throw new IllegalArgumentException("Selection not allowed for " + uri); 488 } 489 selection = BaseTvColumns.COLUMN_PACKAGE_NAME + "=?"; 490 selectionArgs = new String[] { getCallingPackage() }; 491 } 492 493 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 494 int count = 0; 495 496 switch (sUriMatcher.match(uri)) { 497 case MATCH_CHANNEL: 498 count = db.delete(CHANNELS_TABLE, selection, selectionArgs); 499 break; 500 case MATCH_CHANNEL_ID: 501 selection = DatabaseUtils.concatenateWhere(selection, Channels._ID + "=?"); 502 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 503 uri.getLastPathSegment() 504 }); 505 count = db.delete(CHANNELS_TABLE, selection, selectionArgs); 506 break; 507 case MATCH_CHANNEL_ID_PROGRAM: 508 String paramStartTime = uri.getQueryParameter(TvContract.PARAM_START_TIME); 509 String paramEndTime = uri.getQueryParameter(TvContract.PARAM_END_TIME); 510 if (paramStartTime != null && paramEndTime != null) { 511 String startTime = String.valueOf(Long.parseLong(paramStartTime)); 512 String endTime = String.valueOf(Long.parseLong(paramEndTime)); 513 selection = DatabaseUtils.concatenateWhere(selection, 514 SELECTION_OVERLAPPED_PROGRAM); 515 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 516 TvContract.getChannelId(uri), endTime, startTime 517 }); 518 count = db.delete(PROGRAMS_TABLE, selection, selectionArgs); 519 if (count > 1) { 520 Log.e(TAG, "Deleted more than one current program"); 521 } 522 } else { 523 selection = DatabaseUtils.concatenateWhere(selection, Programs.COLUMN_CHANNEL_ID 524 + "=?"); 525 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 526 TvContract.getChannelId(uri) 527 }); 528 count = db.delete(PROGRAMS_TABLE, selection, selectionArgs); 529 } 530 break; 531 case MATCH_INPUT_PACKAGE_SERVICE_CHANNEL: 532 boolean browsableOnly = uri.getBooleanQueryParameter( 533 TvContract.PARAM_BROWSABLE_ONLY, true); 534 selection = DatabaseUtils.concatenateWhere(selection, SELECTION_CHANNEL_BY_INPUT 535 + (browsableOnly ? " AND " + Channels.COLUMN_BROWSABLE + "=1" : "")); 536 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 537 TvContract.getPackageName(uri), TvContract.getServiceName(uri) 538 }); 539 count = db.delete(CHANNELS_TABLE, selection, selectionArgs); 540 break; 541 case MATCH_PROGRAM: 542 count = db.delete(PROGRAMS_TABLE, selection, selectionArgs); 543 break; 544 case MATCH_PROGRAM_ID: 545 selection = DatabaseUtils.concatenateWhere(selection, Programs._ID + "=?"); 546 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 547 uri.getLastPathSegment() 548 }); 549 count = db.delete(PROGRAMS_TABLE, selection, selectionArgs); 550 break; 551 case MATCH_WATCHED_PROGRAM: 552 count = db.delete(WATCHED_PROGRAMS_TABLE, selection, selectionArgs); 553 break; 554 case MATCH_WATCHED_PROGRAM_ID: 555 selection = DatabaseUtils.concatenateWhere(selection, WatchedPrograms._ID + "=?"); 556 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 557 uri.getLastPathSegment() 558 }); 559 count = db.delete(WATCHED_PROGRAMS_TABLE, selection, selectionArgs); 560 break; 561 default: 562 throw new IllegalArgumentException("Unknown URI " + uri); 563 } 564 565 if (count > 0) { 566 notifyChange(uri); 567 } 568 return count; 569 } 570 571 @Override 572 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 573 if (needsToLimitPackage(uri)) { 574 if (!TextUtils.isEmpty(selection)) { 575 throw new IllegalArgumentException("Selection not allowed for " + uri); 576 } 577 selection = BaseTvColumns.COLUMN_PACKAGE_NAME + "=?"; 578 selectionArgs = new String[] { getCallingPackage() }; 579 } 580 581 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 582 int count = 0; 583 584 switch (sUriMatcher.match(uri)) { 585 case MATCH_CHANNEL: 586 count = db.update(CHANNELS_TABLE, values, selection, selectionArgs); 587 break; 588 case MATCH_CHANNEL_ID: 589 selection = DatabaseUtils.concatenateWhere(selection, Channels._ID + "=?"); 590 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 591 uri.getLastPathSegment() 592 }); 593 count = db.update(CHANNELS_TABLE, values, selection, selectionArgs); 594 break; 595 case MATCH_CHANNEL_ID_PROGRAM: 596 String paramStartTime = uri.getQueryParameter(TvContract.PARAM_START_TIME); 597 String paramEndTime = uri.getQueryParameter(TvContract.PARAM_END_TIME); 598 if (paramStartTime != null && paramEndTime != null) { 599 String startTime = String.valueOf(Long.parseLong(paramStartTime)); 600 String endTime = String.valueOf(Long.parseLong(paramEndTime)); 601 selection = DatabaseUtils.concatenateWhere(selection, 602 SELECTION_OVERLAPPED_PROGRAM); 603 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 604 TvContract.getChannelId(uri), endTime, startTime 605 }); 606 count = db.update(PROGRAMS_TABLE, values, selection, selectionArgs); 607 if (count > 1) { 608 Log.e(TAG, "Updated more than one current program"); 609 } 610 } else { 611 selection = DatabaseUtils.concatenateWhere(selection, Programs.COLUMN_CHANNEL_ID 612 + "=?"); 613 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 614 TvContract.getChannelId(uri) 615 }); 616 count = db.update(PROGRAMS_TABLE, values, selection, selectionArgs); 617 } 618 break; 619 case MATCH_INPUT_PACKAGE_SERVICE_CHANNEL: 620 boolean browsableOnly = uri.getBooleanQueryParameter( 621 TvContract.PARAM_BROWSABLE_ONLY, true); 622 selection = DatabaseUtils.concatenateWhere(selection, SELECTION_CHANNEL_BY_INPUT 623 + (browsableOnly ? " AND " + Channels.COLUMN_BROWSABLE + "=1" : "")); 624 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 625 TvContract.getPackageName(uri), TvContract.getServiceName(uri) 626 }); 627 count = db.update(CHANNELS_TABLE, values, selection, selectionArgs); 628 break; 629 case MATCH_PROGRAM: 630 count = db.update(PROGRAMS_TABLE, values, selection, selectionArgs); 631 break; 632 case MATCH_PROGRAM_ID: 633 selection = DatabaseUtils.concatenateWhere(selection, Programs._ID + "=?"); 634 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 635 uri.getLastPathSegment() 636 }); 637 count = db.update(PROGRAMS_TABLE, values, selection, selectionArgs); 638 break; 639 case MATCH_WATCHED_PROGRAM: 640 count = db.update(WATCHED_PROGRAMS_TABLE, values, selection, selectionArgs); 641 break; 642 case MATCH_WATCHED_PROGRAM_ID: 643 selection = DatabaseUtils.concatenateWhere(selection, WatchedPrograms._ID + "=?"); 644 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { 645 uri.getLastPathSegment() 646 }); 647 count = db.update(WATCHED_PROGRAMS_TABLE, values, selection, selectionArgs); 648 break; 649 default: 650 throw new IllegalArgumentException("Unknown URI " + uri); 651 } 652 653 if (count > 0) { 654 notifyChange(uri); 655 } 656 return count; 657 } 658 659 // We might have more than one thread trying to make its way through applyBatch() so the 660 // notification coalescing needs to be thread-local to work correctly. 661 private final ThreadLocal<Set<Uri>> mTLBatchNotifications = 662 new ThreadLocal<Set<Uri>>(); 663 664 private Set<Uri> getBatchNotificationsSet() { 665 return mTLBatchNotifications.get(); 666 } 667 668 private void setBatchNotificationsSet(Set<Uri> batchNotifications) { 669 mTLBatchNotifications.set(batchNotifications); 670 } 671 672 @Override 673 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 674 throws OperationApplicationException { 675 setBatchNotificationsSet(Sets.<Uri>newHashSet()); 676 Context context = getContext(); 677 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 678 db.beginTransaction(); 679 try { 680 ContentProviderResult[] results = super.applyBatch(operations); 681 db.setTransactionSuccessful(); 682 return results; 683 } finally { 684 db.endTransaction(); 685 final Set<Uri> notifications = getBatchNotificationsSet(); 686 setBatchNotificationsSet(null); 687 for (final Uri uri : notifications) { 688 context.getContentResolver().notifyChange(uri, null); 689 } 690 } 691 } 692 693 private void notifyChange(Uri uri) { 694 final Set<Uri> batchNotifications = getBatchNotificationsSet(); 695 if (batchNotifications != null) { 696 batchNotifications.add(uri); 697 } else { 698 getContext().getContentResolver().notifyChange(uri, null); 699 } 700 } 701 702 private boolean needsToLimitPackage(Uri uri) { 703 // If an application is trying to access channel or program data, we need to ensure that the 704 // access is limited to only those data entries that the application provided in the first 705 // place. The only exception is when the application has the full data access. Note that the 706 // user's watch log is treated separately with a special permission. 707 int match = sUriMatcher.match(uri); 708 return match != MATCH_WATCHED_PROGRAM && match != MATCH_WATCHED_PROGRAM_ID 709 && !callerHasFullEpgAccess(); 710 } 711 712 private boolean callerHasFullEpgAccess() { 713 return getContext().checkCallingPermission(PERMISSION_ALL_EPG_DATA) 714 == PackageManager.PERMISSION_GRANTED; 715 } 716 717 private void validateServiceName(String serviceName) { 718 String packageName = getCallingPackage(); 719 ComponentName componentName = new ComponentName(packageName, serviceName); 720 try { 721 getContext().getPackageManager().getServiceInfo(componentName, 0); 722 } catch (PackageManager.NameNotFoundException e) { 723 throw new IllegalArgumentException("Invalid service name: " + serviceName); 724 } 725 } 726 727 @Override 728 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 729 switch (sUriMatcher.match(uri)) { 730 case MATCH_CHANNEL_ID_LOGO: 731 return openLogoFile(uri, mode); 732 default: 733 throw new FileNotFoundException(uri.toString()); 734 } 735 } 736 737 private ParcelFileDescriptor openLogoFile(Uri uri, String mode) throws FileNotFoundException { 738 long channelId = Long.parseLong(uri.getPathSegments().get(1)); 739 740 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 741 queryBuilder.setTables(CHANNELS_TABLE); 742 743 String selection = Channels._ID + "=?"; 744 String[] selectionArgs = new String[] { String.valueOf(channelId) }; 745 if (!callerHasFullEpgAccess()) { 746 selection = DatabaseUtils.concatenateWhere( 747 selection, Channels.COLUMN_PACKAGE_NAME + "=?"); 748 selectionArgs = DatabaseUtils.appendSelectionArgs( 749 selectionArgs, new String[] { getCallingPackage() }); 750 } 751 752 // We don't write the database here. 753 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 754 if (mode.equals("r")) { 755 String sql = queryBuilder.buildQuery( 756 new String[] { CHANNELS_COLUMN_LOGO }, selection, null, null, null, null); 757 return DatabaseUtils.blobFileDescriptorForQuery(db, sql, selectionArgs); 758 } else { 759 Cursor cursor = queryBuilder.query( 760 db, new String[] { Channels._ID }, selection, selectionArgs, null, null, null); 761 try { 762 if (cursor.getCount() < 1) { 763 // Fails early if corresponding channel does not exist. 764 // PipeMonitor may still fail to update DB later. 765 throw new FileNotFoundException(uri.toString()); 766 } 767 } finally { 768 cursor.close(); 769 } 770 771 try { 772 ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe(); 773 PipeMonitor pipeMonitor = new PipeMonitor( 774 pipeFds[0], channelId, selection, selectionArgs); 775 pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 776 return pipeFds[1]; 777 } catch (IOException ioe) { 778 FileNotFoundException fne = new FileNotFoundException(uri.toString()); 779 fne.initCause(ioe); 780 throw fne; 781 } 782 } 783 } 784 785 private class PipeMonitor extends AsyncTask<Void, Void, Void> { 786 private final ParcelFileDescriptor mPfd; 787 private final long mChannelId; 788 private final String mSelection; 789 private final String[] mSelectionArgs; 790 791 private PipeMonitor(ParcelFileDescriptor pfd, long channelId, 792 String selection, String[] selectionArgs) { 793 mPfd = pfd; 794 mChannelId = channelId; 795 mSelection = selection; 796 mSelectionArgs = selectionArgs; 797 } 798 799 @Override 800 protected Void doInBackground(Void... params) { 801 AutoCloseInputStream is = new AutoCloseInputStream(mPfd); 802 ByteArrayOutputStream baos = null; 803 int count = 0; 804 try { 805 Bitmap bitmap = BitmapFactory.decodeStream(is); 806 if (bitmap == null) { 807 Log.e(TAG, "Failed to decode logo image for channel ID " + mChannelId); 808 return null; 809 } 810 811 float scaleFactor = Math.min(1f, ((float) MAX_LOGO_IMAGE_SIZE) / 812 Math.max(bitmap.getWidth(), bitmap.getHeight())); 813 if (scaleFactor < 1f) { 814 bitmap = Bitmap.createScaledBitmap(bitmap, 815 (int) (bitmap.getWidth() * scaleFactor), 816 (int) (bitmap.getHeight() * scaleFactor), false); 817 } 818 819 baos = new ByteArrayOutputStream(); 820 bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); 821 byte[] bytes = baos.toByteArray(); 822 823 ContentValues values = new ContentValues(); 824 values.put(CHANNELS_COLUMN_LOGO, bytes); 825 826 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 827 count = db.update(CHANNELS_TABLE, values, mSelection, mSelectionArgs); 828 if (count > 0) { 829 Uri uri = TvContract.buildChannelLogoUri(mChannelId); 830 notifyChange(uri); 831 } 832 } finally { 833 if (count == 0) { 834 try { 835 mPfd.closeWithError("Failed to write logo for channel ID " + mChannelId); 836 } catch (IOException ioe) { 837 Log.e(TAG, "Failed to close pipe", ioe); 838 } 839 } 840 IoUtils.closeQuietly(baos); 841 IoUtils.closeQuietly(is); 842 } 843 return null; 844 } 845 } 846} 847