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