1 /* 2 * Copyright (C) 2016 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.tv.dvr.provider; 18 19 import android.annotation.SuppressLint; 20 import android.annotation.TargetApi; 21 import android.content.ContentUris; 22 import android.content.Context; 23 import android.database.ContentObserver; 24 import android.media.tv.TvContract.Programs; 25 import android.net.Uri; 26 import android.os.Build; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.support.annotation.MainThread; 30 import android.support.annotation.VisibleForTesting; 31 import android.util.Log; 32 33 import com.android.tv.common.flags.DvrFlags; 34 import com.android.tv.data.ChannelDataManager; 35 import com.android.tv.data.api.Program; 36 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; 37 import com.android.tv.dvr.DvrManager; 38 import com.android.tv.dvr.WritableDvrDataManager; 39 import com.android.tv.dvr.data.ScheduledRecording; 40 import com.android.tv.dvr.data.SeriesRecording; 41 import com.android.tv.dvr.recorder.SeriesRecordingScheduler; 42 import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask; 43 import com.android.tv.util.AsyncDbTask.DbExecutor; 44 import com.android.tv.util.TvUriMatcher; 45 import com.google.auto.factory.AutoFactory; 46 import com.google.auto.factory.Provided; 47 48 import java.util.ArrayList; 49 import java.util.Collections; 50 import java.util.HashSet; 51 import java.util.LinkedList; 52 import java.util.List; 53 import java.util.Objects; 54 import java.util.Queue; 55 import java.util.Set; 56 import java.util.concurrent.Executor; 57 import java.util.concurrent.TimeUnit; 58 59 /** 60 * A class to synchronizes DVR DB with TvProvider. 61 * 62 * <p>The current implementation of AsyncDbTask allows only one task to run at a time, and all the 63 * other tasks are blocked until the current one finishes. As this class performs the low priority 64 * jobs which take long time, it should not block others if possible. For this reason, only one 65 * program is queried at a time and others are queued and will be executed on the other 66 * AsyncDbTask's after the current one finishes to minimize the execution time of one AsyncDbTask. 67 */ 68 @MainThread 69 @TargetApi(Build.VERSION_CODES.N) 70 public class DvrDbSync { 71 private static final String TAG = "DvrDbSync"; 72 private static final boolean DEBUG = false; 73 74 private static final long RECORD_MARGIN_MS = TimeUnit.SECONDS.toMillis(10); 75 76 private final Context mContext; 77 private final DvrManager mDvrManager; 78 private final WritableDvrDataManager mDataManager; 79 private final ChannelDataManager mChannelDataManager; 80 private final Executor mDbExecutor; 81 private final Queue<Long> mProgramIdQueue = new LinkedList<>(); 82 private final DvrFlags mDvrFlags; 83 private QueryProgramTask mQueryProgramTask; 84 private final SeriesRecordingScheduler mSeriesRecordingScheduler; 85 private final ContentObserver mContentObserver = 86 new ContentObserver(new Handler(Looper.getMainLooper())) { 87 @SuppressLint("SwitchIntDef") 88 @Override 89 public void onChange(boolean selfChange, Uri uri) { 90 switch (TvUriMatcher.match(uri)) { 91 case TvUriMatcher.MATCH_PROGRAM: 92 if (DEBUG) Log.d(TAG, "onProgramsUpdated"); 93 onProgramsUpdated(); 94 break; 95 case TvUriMatcher.MATCH_PROGRAM_ID: 96 if (DEBUG) { 97 Log.d( 98 TAG, 99 "onProgramUpdated: programId=" + ContentUris.parseId(uri)); 100 } 101 onProgramUpdated(ContentUris.parseId(uri)); 102 break; 103 } 104 } 105 }; 106 107 private final ChannelDataManager.Listener mChannelDataManagerListener = 108 new ChannelDataManager.Listener() { 109 @Override 110 public void onLoadFinished() { 111 start(); 112 } 113 114 @Override 115 public void onChannelListUpdated() { 116 onChannelsUpdated(); 117 } 118 119 @Override 120 public void onChannelBrowsableChanged() {} 121 }; 122 123 private final ScheduledRecordingListener mScheduleListener = 124 new ScheduledRecordingListener() { 125 @Override 126 public void onScheduledRecordingAdded(ScheduledRecording... schedules) { 127 for (ScheduledRecording schedule : schedules) { 128 addProgramIdToCheckIfNeeded(schedule); 129 } 130 startNextUpdateIfNeeded(); 131 } 132 133 @Override 134 public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { 135 for (ScheduledRecording schedule : schedules) { 136 mProgramIdQueue.remove(schedule.getProgramId()); 137 } 138 } 139 140 @Override 141 public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { 142 for (ScheduledRecording schedule : schedules) { 143 mProgramIdQueue.remove(schedule.getProgramId()); 144 addProgramIdToCheckIfNeeded(schedule); 145 } 146 startNextUpdateIfNeeded(); 147 } 148 }; 149 150 /** 151 * Factory for {@link DvrDbSync}. 152 * 153 * <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory} 154 * generated class. 155 */ 156 public interface Factory { create(Context context, WritableDvrDataManager dataManager)157 public DvrDbSync create(Context context, WritableDvrDataManager dataManager); 158 } 159 160 @AutoFactory(implementing = Factory.class) DvrDbSync( Context context, WritableDvrDataManager dataManager, @Provided DvrFlags dvrFlags, @Provided ChannelDataManager channelDataManager, @Provided DvrManager dvrManager, @Provided @DbExecutor Executor dbExecutor)161 public DvrDbSync( 162 Context context, 163 WritableDvrDataManager dataManager, 164 @Provided DvrFlags dvrFlags, 165 @Provided ChannelDataManager channelDataManager, 166 @Provided DvrManager dvrManager, 167 @Provided @DbExecutor Executor dbExecutor) { 168 this( 169 context, 170 dataManager, 171 dvrFlags, 172 channelDataManager, 173 dvrManager, 174 SeriesRecordingScheduler.getInstance(context), 175 dbExecutor); 176 } 177 178 @VisibleForTesting DvrDbSync( Context context, WritableDvrDataManager dataManager, DvrFlags dvrFlags, ChannelDataManager channelDataManager, DvrManager dvrManager, SeriesRecordingScheduler seriesRecordingScheduler, Executor dbExecutor)179 DvrDbSync( 180 Context context, 181 WritableDvrDataManager dataManager, 182 DvrFlags dvrFlags, 183 ChannelDataManager channelDataManager, 184 DvrManager dvrManager, 185 SeriesRecordingScheduler seriesRecordingScheduler, 186 Executor dbExecutor) { 187 mContext = context; 188 mDvrManager = dvrManager; 189 mDvrFlags = dvrFlags; 190 mDataManager = dataManager; 191 mChannelDataManager = channelDataManager; 192 mSeriesRecordingScheduler = seriesRecordingScheduler; 193 mDbExecutor = dbExecutor; 194 } 195 196 /** Starts the DB sync. */ start()197 public void start() { 198 if (!mChannelDataManager.isDbLoadFinished()) { 199 mChannelDataManager.addListener(mChannelDataManagerListener); 200 return; 201 } 202 mContext.getContentResolver() 203 .registerContentObserver(Programs.CONTENT_URI, true, mContentObserver); 204 mDataManager.addScheduledRecordingListener(mScheduleListener); 205 onChannelsUpdated(); 206 onProgramsUpdated(); 207 } 208 209 /** Stops the DB sync. */ stop()210 public void stop() { 211 mProgramIdQueue.clear(); 212 if (mQueryProgramTask != null) { 213 mQueryProgramTask.cancel(true); 214 } 215 mChannelDataManager.removeListener(mChannelDataManagerListener); 216 mDataManager.removeScheduledRecordingListener(mScheduleListener); 217 mContext.getContentResolver().unregisterContentObserver(mContentObserver); 218 } 219 onChannelsUpdated()220 private void onChannelsUpdated() { 221 List<SeriesRecording> seriesRecordingsToUpdate = new ArrayList<>(); 222 for (SeriesRecording r : mDataManager.getSeriesRecordings()) { 223 if (r.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE 224 && !mChannelDataManager.doesChannelExistInDb(r.getChannelId())) { 225 seriesRecordingsToUpdate.add( 226 SeriesRecording.buildFrom(r) 227 .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL) 228 .setState(SeriesRecording.STATE_SERIES_STOPPED) 229 .build()); 230 } 231 } 232 if (!seriesRecordingsToUpdate.isEmpty()) { 233 mDataManager.updateSeriesRecording(SeriesRecording.toArray(seriesRecordingsToUpdate)); 234 } 235 List<ScheduledRecording> schedulesToRemove = new ArrayList<>(); 236 for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) { 237 if (!mChannelDataManager.doesChannelExistInDb(r.getChannelId())) { 238 schedulesToRemove.add(r); 239 mProgramIdQueue.remove(r.getProgramId()); 240 } 241 } 242 if (!schedulesToRemove.isEmpty()) { 243 mDataManager.removeScheduledRecording(ScheduledRecording.toArray(schedulesToRemove)); 244 } 245 } 246 onProgramsUpdated()247 private void onProgramsUpdated() { 248 for (ScheduledRecording schedule : mDataManager.getAvailableScheduledRecordings()) { 249 addProgramIdToCheckIfNeeded(schedule); 250 } 251 startNextUpdateIfNeeded(); 252 } 253 onProgramUpdated(long programId)254 private void onProgramUpdated(long programId) { 255 addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId(programId)); 256 startNextUpdateIfNeeded(); 257 } 258 addProgramIdToCheckIfNeeded(ScheduledRecording schedule)259 private void addProgramIdToCheckIfNeeded(ScheduledRecording schedule) { 260 if (schedule == null) { 261 return; 262 } 263 long programId = schedule.getProgramId(); 264 if (programId != ScheduledRecording.ID_NOT_SET 265 && !mProgramIdQueue.contains(programId) 266 && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED 267 || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { 268 if (DEBUG) Log.d(TAG, "Program ID enqueued: " + programId); 269 mProgramIdQueue.offer(programId); 270 // There are schedules to be updated. Pause the SeriesRecordingScheduler until all the 271 // schedule updates finish. 272 // Note that the SeriesRecordingScheduler should be paused even though the program to 273 // check is not episodic because it can be changed to the episodic program after the 274 // update, which affect the SeriesRecordingScheduler. 275 mSeriesRecordingScheduler.pauseUpdate(); 276 } 277 } 278 startNextUpdateIfNeeded()279 private void startNextUpdateIfNeeded() { 280 if (mQueryProgramTask != null && !mQueryProgramTask.isCancelled()) { 281 return; 282 } 283 if (!mProgramIdQueue.isEmpty()) { 284 if (DEBUG) Log.d(TAG, "Program ID dequeued: " + mProgramIdQueue.peek()); 285 mQueryProgramTask = new QueryProgramTask(mProgramIdQueue.poll()); 286 mQueryProgramTask.executeOnDbThread(); 287 } else { 288 mSeriesRecordingScheduler.resumeUpdate(); 289 } 290 } 291 292 @VisibleForTesting handleUpdateProgram(Program program, long programId)293 void handleUpdateProgram(Program program, long programId) { 294 Set<SeriesRecording> seriesRecordingsToUpdate = new HashSet<>(); 295 ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(programId); 296 if (schedule != null 297 && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED 298 || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { 299 if (program == null) { 300 mDataManager.removeScheduledRecording(schedule); 301 if (schedule.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { 302 SeriesRecording seriesRecording = 303 mDataManager.getSeriesRecording(schedule.getSeriesRecordingId()); 304 if (seriesRecording != null) { 305 seriesRecordingsToUpdate.add(seriesRecording); 306 } 307 } 308 } else { 309 ScheduledRecording.Builder builder = 310 ScheduledRecording.buildFrom(schedule) 311 .setSeasonNumber(program.getSeasonNumber()) 312 .setEpisodeNumber(program.getEpisodeNumber()) 313 .setEpisodeTitle(program.getEpisodeTitle()) 314 .setProgramDescription(program.getDescription()) 315 .setProgramLongDescription(program.getLongDescription()) 316 .setProgramPosterArtUri(program.getPosterArtUri()) 317 .setProgramThumbnailUri(program.getThumbnailUri()); 318 boolean needUpdate = false; 319 // Check the series recording. 320 SeriesRecording seriesRecordingForOldSchedule = 321 mDataManager.getSeriesRecording(schedule.getSeriesRecordingId()); 322 if (program.isEpisodic()) { 323 // New program belongs to a series. 324 SeriesRecording seriesRecording = 325 mDataManager.getSeriesRecording(program.getSeriesId()); 326 if (seriesRecording == null) { 327 // The new program is episodic while the previous one isn't. 328 SeriesRecording newSeriesRecording = 329 mDvrManager.addSeriesRecording( 330 program, 331 Collections.singletonList(program), 332 SeriesRecording.STATE_SERIES_STOPPED); 333 builder.setSeriesRecordingId(newSeriesRecording.getId()); 334 needUpdate = true; 335 } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) { 336 // The new program belongs to the other series. 337 builder.setSeriesRecordingId(seriesRecording.getId()); 338 needUpdate = true; 339 seriesRecordingsToUpdate.add(seriesRecording); 340 if (seriesRecordingForOldSchedule != null) { 341 seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); 342 } 343 } else if (!Objects.equals( 344 schedule.getSeasonNumber(), program.getSeasonNumber()) 345 || !Objects.equals( 346 schedule.getEpisodeNumber(), program.getEpisodeNumber())) { 347 // The episode number has been changed. 348 if (seriesRecordingForOldSchedule != null) { 349 seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); 350 } 351 } 352 } else if (seriesRecordingForOldSchedule != null) { 353 // Old program belongs to a series but the new one doesn't. 354 seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); 355 } 356 // Change start time only when the recording is not started yet and if it is not 357 // within marginal time of current time. Marginal check is needed to prevent the 358 // update of start time if recording is just triggered or about to get triggered. 359 if (mDvrFlags.startEarlyEndLateEnabled()) { 360 ScheduledRecording.Builder builderUpdatedTime = 361 handleUpdateProgramTime(program, schedule, builder); 362 if (builderUpdatedTime != null) { 363 builder = builderUpdatedTime; 364 needUpdate = true; 365 } 366 } else { 367 boolean marginalToCurrentTime = RECORD_MARGIN_MS > 368 Math.abs(System.currentTimeMillis() - schedule.getStartTimeMs()); 369 boolean needToChangeStartTime = 370 schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS 371 && program.getStartTimeUtcMillis() != schedule.getStartTimeMs() 372 && !marginalToCurrentTime; 373 if (needToChangeStartTime) { 374 builder.setStartTimeMs(program.getStartTimeUtcMillis()); 375 needUpdate = true; 376 } 377 if (schedule.getEndTimeMs() != program.getEndTimeUtcMillis()) { 378 builder.setEndTimeMs(program.getEndTimeUtcMillis()); 379 needUpdate = true; 380 } 381 } 382 if (needUpdate 383 || !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber()) 384 || !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber()) 385 || !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle()) 386 || !Objects.equals( 387 schedule.getProgramDescription(), program.getDescription()) 388 || !Objects.equals( 389 schedule.getProgramLongDescription(), program.getLongDescription()) 390 || !Objects.equals( 391 schedule.getProgramPosterArtUri(), program.getPosterArtUri()) 392 || !Objects.equals( 393 schedule.getProgramThumbnailUri(), program.getThumbnailUri())) { 394 mDataManager.updateScheduledRecording(builder.build()); 395 } 396 if (!seriesRecordingsToUpdate.isEmpty()) { 397 // The series recordings will be updated after it's resumed. 398 mSeriesRecordingScheduler.updateSchedules(seriesRecordingsToUpdate); 399 } 400 } 401 } 402 } 403 handleUpdateProgramTime( Program program, ScheduledRecording schedule, ScheduledRecording.Builder builder)404 private static ScheduledRecording.Builder handleUpdateProgramTime( 405 Program program, ScheduledRecording schedule, ScheduledRecording.Builder builder) { 406 boolean needUpdate = false; 407 long currentTime = System.currentTimeMillis(); 408 boolean marginalToCurrentTime = RECORD_MARGIN_MS > 409 Math.abs(currentTime - schedule.getStartTimeMs()); 410 long updatedStartTime = program.getStartTimeUtcMillis() - schedule.getStartOffsetMs(); 411 boolean needToChangeStartTime = 412 schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS 413 && schedule.getStartTimeMs() != updatedStartTime 414 && !marginalToCurrentTime; 415 if (needToChangeStartTime) { 416 // Check if updated program time has already passed. 417 if (updatedStartTime < currentTime) { 418 updatedStartTime = currentTime + RECORD_MARGIN_MS; 419 long updatedStartOffset = program.getStartTimeUtcMillis() - updatedStartTime; 420 builder.setStartOffsetMs(updatedStartOffset > 0 ? updatedStartOffset : 0); 421 } 422 builder.setStartTimeMs(updatedStartTime); 423 needUpdate = true; 424 } 425 long updatedEndTime = program.getEndTimeUtcMillis() + schedule.getEndOffsetMs(); 426 if (schedule.getEndTimeMs() != updatedEndTime) { 427 builder.setEndTimeMs(updatedEndTime); 428 needUpdate = true; 429 } 430 return (needUpdate ? builder : null); 431 } 432 433 private class QueryProgramTask extends AsyncQueryProgramTask { 434 private final long mProgramId; 435 QueryProgramTask(long programId)436 QueryProgramTask(long programId) { 437 super(mDbExecutor, mContext, programId); 438 mProgramId = programId; 439 } 440 441 @Override onCancelled(Program program)442 protected void onCancelled(Program program) { 443 if (mQueryProgramTask == this) { 444 mQueryProgramTask = null; 445 } 446 startNextUpdateIfNeeded(); 447 } 448 449 @Override onPostExecute(Program program)450 protected void onPostExecute(Program program) { 451 if (mQueryProgramTask == this) { 452 mQueryProgramTask = null; 453 } 454 handleUpdateProgram(program, mProgramId); 455 startNextUpdateIfNeeded(); 456 } 457 } 458 } 459