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