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