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 long currentTimeMs = System.currentTimeMillis(); 281 ScheduledRecording.Builder builder = 282 ScheduledRecording.buildFrom(schedule) 283 .setEndTimeMs(program.getEndTimeUtcMillis()) 284 .setSeasonNumber(program.getSeasonNumber()) 285 .setEpisodeNumber(program.getEpisodeNumber()) 286 .setEpisodeTitle(program.getEpisodeTitle()) 287 .setProgramDescription(program.getDescription()) 288 .setProgramLongDescription(program.getLongDescription()) 289 .setProgramPosterArtUri(program.getPosterArtUri()) 290 .setProgramThumbnailUri(program.getThumbnailUri()); 291 boolean needUpdate = false; 292 // Check the series recording. 293 SeriesRecording seriesRecordingForOldSchedule = 294 mDataManager.getSeriesRecording(schedule.getSeriesRecordingId()); 295 if (program.isEpisodic()) { 296 // New program belongs to a series. 297 SeriesRecording seriesRecording = 298 mDataManager.getSeriesRecording(program.getSeriesId()); 299 if (seriesRecording == null) { 300 // The new program is episodic while the previous one isn't. 301 SeriesRecording newSeriesRecording = 302 mDvrManager.addSeriesRecording( 303 program, 304 Collections.singletonList(program), 305 SeriesRecording.STATE_SERIES_STOPPED); 306 builder.setSeriesRecordingId(newSeriesRecording.getId()); 307 needUpdate = true; 308 } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) { 309 // The new program belongs to the other series. 310 builder.setSeriesRecordingId(seriesRecording.getId()); 311 needUpdate = true; 312 seriesRecordingsToUpdate.add(seriesRecording); 313 if (seriesRecordingForOldSchedule != null) { 314 seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); 315 } 316 } else if (!Objects.equals( 317 schedule.getSeasonNumber(), program.getSeasonNumber()) 318 || !Objects.equals( 319 schedule.getEpisodeNumber(), program.getEpisodeNumber())) { 320 // The episode number has been changed. 321 if (seriesRecordingForOldSchedule != null) { 322 seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); 323 } 324 } 325 } else if (seriesRecordingForOldSchedule != null) { 326 // Old program belongs to a series but the new one doesn't. 327 seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); 328 } 329 // Change start time only when the recording is not started yet. 330 boolean needToChangeStartTime = 331 schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS 332 && program.getStartTimeUtcMillis() != schedule.getStartTimeMs(); 333 if (needToChangeStartTime) { 334 builder.setStartTimeMs(program.getStartTimeUtcMillis()); 335 needUpdate = true; 336 } 337 if (needUpdate 338 || schedule.getEndTimeMs() != program.getEndTimeUtcMillis() 339 || !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber()) 340 || !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber()) 341 || !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle()) 342 || !Objects.equals( 343 schedule.getProgramDescription(), program.getDescription()) 344 || !Objects.equals( 345 schedule.getProgramLongDescription(), program.getLongDescription()) 346 || !Objects.equals( 347 schedule.getProgramPosterArtUri(), program.getPosterArtUri()) 348 || !Objects.equals( 349 schedule.getProgramThumbnailUri(), program.getThumbnailUri())) { 350 mDataManager.updateScheduledRecording(builder.build()); 351 } 352 if (!seriesRecordingsToUpdate.isEmpty()) { 353 // The series recordings will be updated after it's resumed. 354 mSeriesRecordingScheduler.updateSchedules(seriesRecordingsToUpdate); 355 } 356 } 357 } 358 } 359 360 private class QueryProgramTask extends AsyncQueryProgramTask { 361 private final long mProgramId; 362 QueryProgramTask(long programId)363 QueryProgramTask(long programId) { 364 super(mDbExecutor, mContext.getContentResolver(), programId); 365 mProgramId = programId; 366 } 367 368 @Override onCancelled(Program program)369 protected void onCancelled(Program program) { 370 if (mQueryProgramTask == this) { 371 mQueryProgramTask = null; 372 } 373 startNextUpdateIfNeeded(); 374 } 375 376 @Override onPostExecute(Program program)377 protected void onPostExecute(Program program) { 378 if (mQueryProgramTask == this) { 379 mQueryProgramTask = null; 380 } 381 handleUpdateProgram(program, mProgramId); 382 startNextUpdateIfNeeded(); 383 } 384 } 385 } 386