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