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.recorder; 18 19 import android.annotation.SuppressLint; 20 import android.annotation.TargetApi; 21 import android.content.Context; 22 import android.content.SharedPreferences; 23 import android.os.AsyncTask; 24 import android.os.Build; 25 import android.support.annotation.MainThread; 26 import android.text.TextUtils; 27 import android.util.ArraySet; 28 import android.util.Log; 29 import android.util.LongSparseArray; 30 31 import com.android.tv.TvSingletons; 32 import com.android.tv.common.SoftPreconditions; 33 import com.android.tv.common.util.CollectionUtils; 34 import com.android.tv.common.util.SharedPreferencesUtils; 35 import com.android.tv.data.api.Program; 36 import com.android.tv.data.epg.EpgReader; 37 import com.android.tv.dvr.DvrDataManager; 38 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; 39 import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; 40 import com.android.tv.dvr.DvrManager; 41 import com.android.tv.dvr.WritableDvrDataManager; 42 import com.android.tv.dvr.data.ScheduledRecording; 43 import com.android.tv.dvr.data.SeasonEpisodeNumber; 44 import com.android.tv.dvr.data.SeriesInfo; 45 import com.android.tv.dvr.data.SeriesRecording; 46 import com.android.tv.dvr.provider.EpisodicProgramLoadTask; 47 48 import dagger.Lazy; 49 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.Collection; 53 import java.util.Collections; 54 import java.util.HashMap; 55 import java.util.HashSet; 56 import java.util.Iterator; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.Map.Entry; 60 import java.util.Set; 61 62 /** 63 * Creates the {@link com.android.tv.dvr.data.ScheduledRecording}s for the {@link 64 * com.android.tv.dvr.data.SeriesRecording}. 65 * 66 * <p>The current implementation assumes that the series recordings are scheduled only for one 67 * channel. 68 */ 69 @TargetApi(Build.VERSION_CODES.N) 70 public class SeriesRecordingScheduler { 71 private static final String TAG = "SeriesRecordingSchd"; 72 private static final boolean DEBUG = false; 73 74 private static final String KEY_FETCHED_SERIES_IDS = 75 "SeriesRecordingScheduler.fetched_series_ids"; 76 77 @SuppressLint("StaticFieldLeak") 78 private static SeriesRecordingScheduler sInstance; 79 80 /** Creates and returns the {@link SeriesRecordingScheduler}. */ getInstance(Context context)81 public static synchronized SeriesRecordingScheduler getInstance(Context context) { 82 if (sInstance == null) { 83 sInstance = new SeriesRecordingScheduler(context); 84 } 85 return sInstance; 86 } 87 88 private final Context mContext; 89 private final DvrManager mDvrManager; 90 private final WritableDvrDataManager mDataManager; 91 private final List<SeriesRecordingUpdateTask> mScheduleTasks = new ArrayList<>(); 92 private final LongSparseArray<FetchSeriesInfoTask> mFetchSeriesInfoTasks = 93 new LongSparseArray<>(); 94 private final Set<String> mFetchedSeriesIds = new ArraySet<>(); 95 private final SharedPreferences mSharedPreferences; 96 private boolean mStarted; 97 private boolean mPaused; 98 private final Set<Long> mPendingSeriesRecordings = new ArraySet<>(); 99 100 private final SeriesRecordingListener mSeriesRecordingListener = 101 new SeriesRecordingListener() { 102 @Override 103 public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { 104 for (SeriesRecording seriesRecording : seriesRecordings) { 105 executeFetchSeriesInfoTask(seriesRecording); 106 } 107 } 108 109 @Override 110 public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { 111 // Cancel the update. 112 for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator(); 113 iter.hasNext(); ) { 114 SeriesRecordingUpdateTask task = iter.next(); 115 if (CollectionUtils.subtract( 116 task.getSeriesRecordings(), 117 seriesRecordings, 118 SeriesRecording.ID_COMPARATOR) 119 .isEmpty()) { 120 task.cancel(true); 121 iter.remove(); 122 } 123 } 124 for (SeriesRecording seriesRecording : seriesRecordings) { 125 FetchSeriesInfoTask task = 126 mFetchSeriesInfoTasks.get(seriesRecording.getId()); 127 if (task != null) { 128 task.cancel(true); 129 mFetchSeriesInfoTasks.remove(seriesRecording.getId()); 130 } 131 } 132 } 133 134 @Override 135 public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { 136 List<SeriesRecording> stopped = new ArrayList<>(); 137 List<SeriesRecording> normal = new ArrayList<>(); 138 for (SeriesRecording r : seriesRecordings) { 139 if (r.isStopped()) { 140 stopped.add(r); 141 } else { 142 normal.add(r); 143 } 144 } 145 if (!stopped.isEmpty()) { 146 onSeriesRecordingRemoved(SeriesRecording.toArray(stopped)); 147 } 148 if (!normal.isEmpty()) { 149 updateSchedules(normal); 150 } 151 } 152 }; 153 154 private final ScheduledRecordingListener mScheduledRecordingListener = 155 new ScheduledRecordingListener() { 156 @Override 157 public void onScheduledRecordingAdded(ScheduledRecording... schedules) { 158 // No need to update series recordings when the new schedule is added. 159 } 160 161 @Override 162 public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { 163 handleScheduledRecordingChange(Arrays.asList(schedules)); 164 } 165 166 @Override 167 public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { 168 List<ScheduledRecording> schedulesForUpdate = new ArrayList<>(); 169 for (ScheduledRecording r : schedules) { 170 if ((r.getState() == ScheduledRecording.STATE_RECORDING_FAILED 171 || r.getState() 172 == ScheduledRecording.STATE_RECORDING_CLIPPED) 173 && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET 174 && !TextUtils.isEmpty(r.getSeasonNumber()) 175 && !TextUtils.isEmpty(r.getEpisodeNumber())) { 176 schedulesForUpdate.add(r); 177 } 178 } 179 if (!schedulesForUpdate.isEmpty()) { 180 handleScheduledRecordingChange(schedulesForUpdate); 181 } 182 } 183 184 private void handleScheduledRecordingChange(List<ScheduledRecording> schedules) { 185 if (schedules.isEmpty()) { 186 return; 187 } 188 Set<Long> seriesRecordingIds = new HashSet<>(); 189 for (ScheduledRecording r : schedules) { 190 if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { 191 seriesRecordingIds.add(r.getSeriesRecordingId()); 192 } 193 } 194 if (!seriesRecordingIds.isEmpty()) { 195 List<SeriesRecording> seriesRecordings = new ArrayList<>(); 196 for (Long id : seriesRecordingIds) { 197 SeriesRecording seriesRecording = mDataManager.getSeriesRecording(id); 198 if (seriesRecording != null) { 199 seriesRecordings.add(seriesRecording); 200 } 201 } 202 if (!seriesRecordings.isEmpty()) { 203 updateSchedules(seriesRecordings); 204 } 205 } 206 } 207 }; 208 SeriesRecordingScheduler(Context context)209 private SeriesRecordingScheduler(Context context) { 210 mContext = context.getApplicationContext(); 211 TvSingletons tvSingletons = TvSingletons.getSingletons(context); 212 mDvrManager = tvSingletons.getDvrManager(); 213 mDataManager = (WritableDvrDataManager) tvSingletons.getDvrDataManager(); 214 mSharedPreferences = 215 context.getSharedPreferences( 216 SharedPreferencesUtils.SHARED_PREF_SERIES_RECORDINGS, Context.MODE_PRIVATE); 217 mFetchedSeriesIds.addAll( 218 mSharedPreferences.getStringSet(KEY_FETCHED_SERIES_IDS, Collections.emptySet())); 219 } 220 221 /** Starts the scheduler. */ 222 @MainThread start()223 public void start() { 224 SoftPreconditions.checkState(mDataManager.isInitialized()); 225 if (mStarted) { 226 return; 227 } 228 if (DEBUG) Log.d(TAG, "start"); 229 mStarted = true; 230 mDataManager.addSeriesRecordingListener(mSeriesRecordingListener); 231 mDataManager.addScheduledRecordingListener(mScheduledRecordingListener); 232 startFetchingSeriesInfo(); 233 updateSchedules(mDataManager.getSeriesRecordings()); 234 } 235 236 @MainThread stop()237 public void stop() { 238 if (!mStarted) { 239 return; 240 } 241 if (DEBUG) Log.d(TAG, "stop"); 242 mStarted = false; 243 for (int i = 0; i < mFetchSeriesInfoTasks.size(); i++) { 244 FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(mFetchSeriesInfoTasks.keyAt(i)); 245 task.cancel(true); 246 } 247 mFetchSeriesInfoTasks.clear(); 248 for (SeriesRecordingUpdateTask task : mScheduleTasks) { 249 task.cancel(true); 250 } 251 mScheduleTasks.clear(); 252 mDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); 253 mDataManager.removeSeriesRecordingListener(mSeriesRecordingListener); 254 } 255 startFetchingSeriesInfo()256 private void startFetchingSeriesInfo() { 257 for (SeriesRecording seriesRecording : mDataManager.getSeriesRecordings()) { 258 if (!mFetchedSeriesIds.contains(seriesRecording.getSeriesId())) { 259 executeFetchSeriesInfoTask(seriesRecording); 260 } 261 } 262 } 263 executeFetchSeriesInfoTask(SeriesRecording seriesRecording)264 private void executeFetchSeriesInfoTask(SeriesRecording seriesRecording) { 265 FetchSeriesInfoTask task = 266 new FetchSeriesInfoTask( 267 seriesRecording, TvSingletons.getSingletons(mContext).providesEpgReader()); 268 task.execute(); 269 mFetchSeriesInfoTasks.put(seriesRecording.getId(), task); 270 } 271 272 /** Pauses the updates of the series recordings. */ pauseUpdate()273 public void pauseUpdate() { 274 if (DEBUG) Log.d(TAG, "Schedule paused"); 275 if (mPaused) { 276 return; 277 } 278 mPaused = true; 279 if (!mStarted) { 280 return; 281 } 282 for (SeriesRecordingUpdateTask task : mScheduleTasks) { 283 for (SeriesRecording r : task.getSeriesRecordings()) { 284 mPendingSeriesRecordings.add(r.getId()); 285 } 286 task.cancel(true); 287 } 288 } 289 290 /** Resumes the updates of the series recordings. */ resumeUpdate()291 public void resumeUpdate() { 292 if (DEBUG) Log.d(TAG, "Schedule resumed"); 293 if (!mPaused) { 294 return; 295 } 296 mPaused = false; 297 if (!mStarted) { 298 return; 299 } 300 if (!mPendingSeriesRecordings.isEmpty()) { 301 List<SeriesRecording> seriesRecordings = new ArrayList<>(); 302 for (long seriesRecordingId : mPendingSeriesRecordings) { 303 SeriesRecording seriesRecording = 304 mDataManager.getSeriesRecording(seriesRecordingId); 305 if (seriesRecording != null) { 306 seriesRecordings.add(seriesRecording); 307 } 308 } 309 if (!seriesRecordings.isEmpty()) { 310 updateSchedules(seriesRecordings); 311 } 312 } 313 } 314 315 /** 316 * Update schedules for the given series recordings. If it's paused, the update will be done 317 * after it's resumed. 318 */ updateSchedules(Collection<SeriesRecording> seriesRecordings)319 public void updateSchedules(Collection<SeriesRecording> seriesRecordings) { 320 if (DEBUG) Log.d(TAG, "updateSchedules:" + seriesRecordings); 321 if (!mStarted) { 322 if (DEBUG) Log.d(TAG, "Not started yet."); 323 return; 324 } 325 if (mPaused) { 326 for (SeriesRecording r : seriesRecordings) { 327 mPendingSeriesRecordings.add(r.getId()); 328 } 329 if (DEBUG) { 330 Log.d( 331 TAG, 332 "The scheduler has been paused. Adding to the pending list. size=" 333 + mPendingSeriesRecordings.size()); 334 } 335 return; 336 } 337 Set<SeriesRecording> previousSeriesRecordings = new HashSet<>(); 338 for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator(); 339 iter.hasNext(); ) { 340 SeriesRecordingUpdateTask task = iter.next(); 341 if (CollectionUtils.containsAny( 342 task.getSeriesRecordings(), seriesRecordings, SeriesRecording.ID_COMPARATOR)) { 343 // The task is affected by the seriesRecordings 344 task.cancel(true); 345 previousSeriesRecordings.addAll(task.getSeriesRecordings()); 346 iter.remove(); 347 } 348 } 349 List<SeriesRecording> seriesRecordingsToUpdate = 350 CollectionUtils.union( 351 seriesRecordings, previousSeriesRecordings, SeriesRecording.ID_COMPARATOR); 352 for (Iterator<SeriesRecording> iter = seriesRecordingsToUpdate.iterator(); 353 iter.hasNext(); ) { 354 SeriesRecording seriesRecording = mDataManager.getSeriesRecording(iter.next().getId()); 355 if (seriesRecording == null || seriesRecording.isStopped()) { 356 // Series recording has been removed or stopped. 357 iter.remove(); 358 } 359 } 360 if (seriesRecordingsToUpdate.isEmpty()) { 361 return; 362 } 363 if (needToReadAllChannels(seriesRecordingsToUpdate)) { 364 SeriesRecordingUpdateTask task = 365 new SeriesRecordingUpdateTask(seriesRecordingsToUpdate); 366 mScheduleTasks.add(task); 367 if (DEBUG) Log.d(TAG, "Added schedule task: " + task); 368 task.execute(); 369 } else { 370 for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) { 371 SeriesRecordingUpdateTask task = 372 new SeriesRecordingUpdateTask(Collections.singletonList(seriesRecording)); 373 mScheduleTasks.add(task); 374 if (DEBUG) Log.d(TAG, "Added schedule task: " + task); 375 task.execute(); 376 } 377 } 378 } 379 needToReadAllChannels(List<SeriesRecording> seriesRecordingsToUpdate)380 private boolean needToReadAllChannels(List<SeriesRecording> seriesRecordingsToUpdate) { 381 for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) { 382 if (seriesRecording.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ALL) { 383 return true; 384 } 385 } 386 return false; 387 } 388 389 /** 390 * Pick one program per an episode. 391 * 392 * <p>Note that the programs which has been already scheduled have the highest priority, and all 393 * of them are added even though they are the same episodes. That's because the schedules should 394 * be added to the series recording. 395 * 396 * <p>If there are no existing schedules for an episode, one program which starts earlier is 397 * picked. 398 */ pickOneProgramPerEpisode( List<SeriesRecording> seriesRecordings, List<Program> programs)399 private LongSparseArray<List<Program>> pickOneProgramPerEpisode( 400 List<SeriesRecording> seriesRecordings, List<Program> programs) { 401 return pickOneProgramPerEpisode(mDataManager, seriesRecordings, programs); 402 } 403 404 /** @see #pickOneProgramPerEpisode(List, List) */ pickOneProgramPerEpisode( DvrDataManager dataManager, List<SeriesRecording> seriesRecordings, List<Program> programs)405 public static LongSparseArray<List<Program>> pickOneProgramPerEpisode( 406 DvrDataManager dataManager, 407 List<SeriesRecording> seriesRecordings, 408 List<Program> programs) { 409 // Initialize. 410 LongSparseArray<List<Program>> result = new LongSparseArray<>(); 411 Map<String, Long> seriesRecordingIds = new HashMap<>(); 412 for (SeriesRecording seriesRecording : seriesRecordings) { 413 result.put(seriesRecording.getId(), new ArrayList<>()); 414 seriesRecordingIds.put(seriesRecording.getSeriesId(), seriesRecording.getId()); 415 } 416 // Group programs by the episode. 417 Map<SeasonEpisodeNumber, List<Program>> programsForEpisodeMap = new HashMap<>(); 418 for (Program program : programs) { 419 long seriesRecordingId = seriesRecordingIds.get(program.getSeriesId()); 420 if (TextUtils.isEmpty(program.getSeasonNumber()) 421 || TextUtils.isEmpty(program.getEpisodeNumber())) { 422 // Add all the programs if it doesn't have season number or episode number. 423 result.get(seriesRecordingId).add(program); 424 continue; 425 } 426 SeasonEpisodeNumber seasonEpisodeNumber = 427 new SeasonEpisodeNumber( 428 seriesRecordingId, 429 program.getSeasonNumber(), 430 program.getEpisodeNumber()); 431 List<Program> programsForEpisode = programsForEpisodeMap.get(seasonEpisodeNumber); 432 if (programsForEpisode == null) { 433 programsForEpisode = new ArrayList<>(); 434 programsForEpisodeMap.put(seasonEpisodeNumber, programsForEpisode); 435 } 436 programsForEpisode.add(program); 437 } 438 // Pick one program. 439 for (Entry<SeasonEpisodeNumber, List<Program>> entry : programsForEpisodeMap.entrySet()) { 440 List<Program> programsForEpisode = entry.getValue(); 441 Collections.sort( 442 programsForEpisode, 443 (Program lhs, Program rhs) -> { 444 // Place the existing schedule first. 445 boolean lhsScheduled = isProgramScheduled(dataManager, lhs); 446 boolean rhsScheduled = isProgramScheduled(dataManager, rhs); 447 if (lhsScheduled && !rhsScheduled) { 448 return -1; 449 } 450 if (!lhsScheduled && rhsScheduled) { 451 return 1; 452 } 453 // Sort by the start time in ascending order. 454 return lhs.compareTo(rhs); 455 }); 456 boolean added = false; 457 // Add all the scheduled programs 458 List<Program> programsForSeries = result.get(entry.getKey().seriesRecordingId); 459 for (Program program : programsForEpisode) { 460 if (isProgramScheduled(dataManager, program)) { 461 programsForSeries.add(program); 462 added = true; 463 } else if (!added) { 464 programsForSeries.add(program); 465 break; 466 } 467 } 468 } 469 return result; 470 } 471 isProgramScheduled(DvrDataManager dataManager, Program program)472 private static boolean isProgramScheduled(DvrDataManager dataManager, Program program) { 473 ScheduledRecording schedule = 474 dataManager.getScheduledRecordingForProgramId(program.getId()); 475 return schedule != null 476 && schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED; 477 } 478 updateFetchedSeries()479 private void updateFetchedSeries() { 480 mSharedPreferences.edit().putStringSet(KEY_FETCHED_SERIES_IDS, mFetchedSeriesIds).apply(); 481 } 482 483 /** 484 * This works only for the existing series recordings. Do not use this task for the "adding 485 * series recording" UI. 486 */ 487 private class SeriesRecordingUpdateTask extends EpisodicProgramLoadTask { SeriesRecordingUpdateTask(List<SeriesRecording> seriesRecordings)488 SeriesRecordingUpdateTask(List<SeriesRecording> seriesRecordings) { 489 super(mContext, seriesRecordings); 490 } 491 492 @Override onPostExecute(List<Program> programs)493 protected void onPostExecute(List<Program> programs) { 494 if (DEBUG) Log.d(TAG, "onPostExecute: updating schedules with programs:" + programs); 495 mScheduleTasks.remove(this); 496 if (programs == null) { 497 Log.e( 498 TAG, 499 "Creating schedules for series recording failed: " + getSeriesRecordings()); 500 return; 501 } 502 LongSparseArray<List<Program>> seriesProgramMap = 503 pickOneProgramPerEpisode(getSeriesRecordings(), programs); 504 for (SeriesRecording seriesRecording : getSeriesRecordings()) { 505 // Check the series recording is still valid. 506 SeriesRecording actualSeriesRecording = 507 mDataManager.getSeriesRecording(seriesRecording.getId()); 508 if (actualSeriesRecording == null || actualSeriesRecording.isStopped()) { 509 continue; 510 } 511 List<Program> programsToSchedule = seriesProgramMap.get(seriesRecording.getId()); 512 if (mDataManager.getSeriesRecording(seriesRecording.getId()) != null 513 && !programsToSchedule.isEmpty()) { 514 mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule); 515 } 516 } 517 } 518 519 @Override onCancelled(List<Program> programs)520 protected void onCancelled(List<Program> programs) { 521 mScheduleTasks.remove(this); 522 } 523 524 @Override toString()525 public String toString() { 526 return "SeriesRecordingUpdateTask:{" 527 + "series_recordings=" 528 + getSeriesRecordings() 529 + "}"; 530 } 531 } 532 533 private class FetchSeriesInfoTask extends AsyncTask<Void, Void, SeriesInfo> { 534 private final SeriesRecording mSeriesRecording; 535 private final Lazy<EpgReader> mEpgReaderProvider; 536 FetchSeriesInfoTask(SeriesRecording seriesRecording, Lazy<EpgReader> epgReaderProvider)537 FetchSeriesInfoTask(SeriesRecording seriesRecording, Lazy<EpgReader> epgReaderProvider) { 538 mSeriesRecording = seriesRecording; 539 mEpgReaderProvider = epgReaderProvider; 540 } 541 542 @Override doInBackground(Void... voids)543 protected SeriesInfo doInBackground(Void... voids) { 544 return mEpgReaderProvider.get().getSeriesInfo(mSeriesRecording.getSeriesId()); 545 } 546 547 @Override onPostExecute(SeriesInfo seriesInfo)548 protected void onPostExecute(SeriesInfo seriesInfo) { 549 if (seriesInfo != null) { 550 mDataManager.updateSeriesRecording( 551 SeriesRecording.buildFrom(mSeriesRecording) 552 .setTitle(seriesInfo.getTitle()) 553 .setDescription(seriesInfo.getDescription()) 554 .setLongDescription(seriesInfo.getLongDescription()) 555 .setCanonicalGenreIds(seriesInfo.getCanonicalGenreIds()) 556 .setPosterUri(seriesInfo.getPosterUri()) 557 .setPhotoUri(seriesInfo.getPhotoUri()) 558 .build()); 559 mFetchedSeriesIds.add(seriesInfo.getId()); 560 updateFetchedSeries(); 561 } 562 mFetchSeriesInfoTasks.remove(mSeriesRecording.getId()); 563 } 564 565 @Override onCancelled(SeriesInfo seriesInfo)566 protected void onCancelled(SeriesInfo seriesInfo) { 567 mFetchSeriesInfoTasks.remove(mSeriesRecording.getId()); 568 } 569 } 570 } 571