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