1 /* 2 * Copyright (C) 2015 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.TargetApi; 20 import android.content.ContentProviderOperation; 21 import android.content.ContentResolver; 22 import android.content.ContentUris; 23 import android.content.Context; 24 import android.content.OperationApplicationException; 25 import android.media.tv.TvContract; 26 import android.media.tv.TvInputInfo; 27 import android.net.Uri; 28 import android.os.AsyncTask; 29 import android.os.Build; 30 import android.os.Handler; 31 import android.os.RemoteException; 32 import android.support.annotation.AnyThread; 33 import android.support.annotation.MainThread; 34 import android.support.annotation.NonNull; 35 import android.support.annotation.Nullable; 36 import android.support.annotation.VisibleForTesting; 37 import android.support.annotation.WorkerThread; 38 import android.util.Log; 39 import android.util.Range; 40 import com.android.tv.TvSingletons; 41 import com.android.tv.common.SoftPreconditions; 42 import com.android.tv.common.feature.CommonFeatures; 43 import com.android.tv.common.util.CommonUtils; 44 import com.android.tv.data.Program; 45 import com.android.tv.data.api.Channel; 46 import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; 47 import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; 48 import com.android.tv.dvr.DvrScheduleManager.OnInitializeListener; 49 import com.android.tv.dvr.data.RecordedProgram; 50 import com.android.tv.dvr.data.ScheduledRecording; 51 import com.android.tv.dvr.data.SeriesRecording; 52 import com.android.tv.util.AsyncDbTask; 53 import com.android.tv.util.Utils; 54 import java.io.File; 55 import java.util.ArrayList; 56 import java.util.Arrays; 57 import java.util.Collections; 58 import java.util.HashMap; 59 import java.util.List; 60 import java.util.Map; 61 import java.util.Map.Entry; 62 import java.util.concurrent.Executor; 63 64 /** 65 * DVR manager class to add and remove recordings. UI can modify recording list through this class, 66 * instead of modifying them directly through {@link DvrDataManager}. 67 */ 68 @MainThread 69 @TargetApi(Build.VERSION_CODES.N) 70 public class DvrManager { 71 private static final String TAG = "DvrManager"; 72 private static final boolean DEBUG = false; 73 74 private final WritableDvrDataManager mDataManager; 75 private final DvrScheduleManager mScheduleManager; 76 // @GuardedBy("mListener") 77 private final Map<Listener, Handler> mListener = new HashMap<>(); 78 private final Context mAppContext; 79 private final Executor mDbExecutor; 80 DvrManager(Context context)81 public DvrManager(Context context) { 82 SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG); 83 mAppContext = context.getApplicationContext(); 84 TvSingletons tvSingletons = TvSingletons.getSingletons(context); 85 mDbExecutor = tvSingletons.getDbExecutor(); 86 mDataManager = (WritableDvrDataManager) tvSingletons.getDvrDataManager(); 87 mScheduleManager = tvSingletons.getDvrScheduleManager(); 88 if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) { 89 createSeriesRecordingsForRecordedProgramsIfNeeded(mDataManager.getRecordedPrograms()); 90 } else { 91 // No need to handle DVR schedule load finished because schedule manager is initialized 92 // after the all the schedules are loaded. 93 if (!mDataManager.isRecordedProgramLoadFinished()) { 94 mDataManager.addRecordedProgramLoadFinishedListener( 95 new OnRecordedProgramLoadFinishedListener() { 96 @Override 97 public void onRecordedProgramLoadFinished() { 98 mDataManager.removeRecordedProgramLoadFinishedListener(this); 99 if (mDataManager.isInitialized() 100 && mScheduleManager.isInitialized()) { 101 createSeriesRecordingsForRecordedProgramsIfNeeded( 102 mDataManager.getRecordedPrograms()); 103 } 104 } 105 }); 106 } 107 if (!mScheduleManager.isInitialized()) { 108 mScheduleManager.addOnInitializeListener( 109 new OnInitializeListener() { 110 @Override 111 public void onInitialize() { 112 mScheduleManager.removeOnInitializeListener(this); 113 if (mDataManager.isInitialized() 114 && mScheduleManager.isInitialized()) { 115 createSeriesRecordingsForRecordedProgramsIfNeeded( 116 mDataManager.getRecordedPrograms()); 117 } 118 } 119 }); 120 } 121 } 122 mDataManager.addRecordedProgramListener( 123 new RecordedProgramListener() { 124 @Override 125 public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { 126 if (!mDataManager.isInitialized() || !mScheduleManager.isInitialized()) { 127 return; 128 } 129 for (RecordedProgram recordedProgram : recordedPrograms) { 130 createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram); 131 } 132 } 133 134 @Override 135 public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {} 136 137 @Override 138 public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { 139 // Removing series recording is handled in the 140 // SeriesRecordingDetailsFragment. 141 } 142 }); 143 } 144 createSeriesRecordingsForRecordedProgramsIfNeeded( List<RecordedProgram> recordedPrograms)145 private void createSeriesRecordingsForRecordedProgramsIfNeeded( 146 List<RecordedProgram> recordedPrograms) { 147 for (RecordedProgram recordedProgram : recordedPrograms) { 148 createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram); 149 } 150 } 151 createSeriesRecordingForRecordedProgramIfNeeded(RecordedProgram recordedProgram)152 private void createSeriesRecordingForRecordedProgramIfNeeded(RecordedProgram recordedProgram) { 153 if (recordedProgram.isEpisodic()) { 154 SeriesRecording seriesRecording = 155 mDataManager.getSeriesRecording(recordedProgram.getSeriesId()); 156 if (seriesRecording == null) { 157 addSeriesRecording(recordedProgram); 158 } 159 } 160 } 161 162 /** Schedules a recording for {@code program}. */ addSchedule(Program program)163 public ScheduledRecording addSchedule(Program program) { 164 if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 165 return null; 166 } 167 SeriesRecording seriesRecording = getSeriesRecording(program); 168 return addSchedule( 169 program, 170 seriesRecording == null 171 ? mScheduleManager.suggestNewPriority() 172 : seriesRecording.getPriority()); 173 } 174 175 /** 176 * Schedules a recording for {@code program} with the highest priority so that the schedule can 177 * be recorded. 178 */ addScheduleWithHighestPriority(Program program)179 public ScheduledRecording addScheduleWithHighestPriority(Program program) { 180 if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 181 return null; 182 } 183 SeriesRecording seriesRecording = getSeriesRecording(program); 184 return addSchedule( 185 program, 186 seriesRecording == null 187 ? mScheduleManager.suggestNewPriority() 188 : mScheduleManager.suggestHighestPriority( 189 seriesRecording.getInputId(), 190 new Range( 191 program.getStartTimeUtcMillis(), 192 program.getEndTimeUtcMillis()), 193 seriesRecording.getPriority())); 194 } 195 addSchedule(Program program, long priority)196 private ScheduledRecording addSchedule(Program program, long priority) { 197 TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, program); 198 if (input == null) { 199 Log.e(TAG, "Can't find input for program: " + program); 200 return null; 201 } 202 ScheduledRecording schedule; 203 SeriesRecording seriesRecording = getSeriesRecording(program); 204 schedule = 205 createScheduledRecordingBuilder(input.getId(), program) 206 .setPriority(priority) 207 .setSeriesRecordingId( 208 seriesRecording == null 209 ? SeriesRecording.ID_NOT_SET 210 : seriesRecording.getId()) 211 .build(); 212 mDataManager.addScheduledRecording(schedule); 213 return schedule; 214 } 215 216 /** Adds a recording schedule with a time range. */ addSchedule(Channel channel, long startTime, long endTime)217 public void addSchedule(Channel channel, long startTime, long endTime) { 218 Log.i( 219 TAG, 220 "Adding scheduled recording of channel " 221 + channel 222 + " starting at " 223 + Utils.toTimeString(startTime) 224 + " and ending at " 225 + Utils.toTimeString(endTime)); 226 if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 227 return; 228 } 229 TvInputInfo input = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId()); 230 if (input == null) { 231 Log.e(TAG, "Can't find input for channel: " + channel); 232 return; 233 } 234 addScheduleInternal(input.getId(), channel.getId(), startTime, endTime); 235 } 236 237 /** Adds the schedule. */ addSchedule(ScheduledRecording schedule)238 public void addSchedule(ScheduledRecording schedule) { 239 if (mDataManager.isDvrScheduleLoadFinished()) { 240 mDataManager.addScheduledRecording(schedule); 241 } 242 } 243 addScheduleInternal(String inputId, long channelId, long startTime, long endTime)244 private void addScheduleInternal(String inputId, long channelId, long startTime, long endTime) { 245 mDataManager.addScheduledRecording( 246 ScheduledRecording.builder(inputId, channelId, startTime, endTime) 247 .setPriority(mScheduleManager.suggestNewPriority()) 248 .build()); 249 } 250 251 /** Adds a new series recording and schedules for the programs with the initial state. */ addSeriesRecording( Program selectedProgram, List<Program> programsToSchedule, @SeriesRecording.SeriesState int initialState)252 public SeriesRecording addSeriesRecording( 253 Program selectedProgram, 254 List<Program> programsToSchedule, 255 @SeriesRecording.SeriesState int initialState) { 256 Log.i( 257 TAG, 258 "Adding series recording for program " 259 + selectedProgram 260 + ", and schedules: " 261 + programsToSchedule); 262 if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { 263 return null; 264 } 265 TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, selectedProgram); 266 if (input == null) { 267 Log.e(TAG, "Can't find input for program: " + selectedProgram); 268 return null; 269 } 270 SeriesRecording seriesRecording = 271 SeriesRecording.builder(input.getId(), selectedProgram) 272 .setPriority(mScheduleManager.suggestNewSeriesPriority()) 273 .setState(initialState) 274 .build(); 275 mDataManager.addSeriesRecording(seriesRecording); 276 // The schedules for the recorded programs should be added not to create the schedule the 277 // duplicate episodes. 278 addRecordedProgramToSeriesRecording(seriesRecording); 279 addScheduleToSeriesRecording(seriesRecording, programsToSchedule); 280 return seriesRecording; 281 } 282 addSeriesRecording(RecordedProgram recordedProgram)283 private void addSeriesRecording(RecordedProgram recordedProgram) { 284 SeriesRecording seriesRecording = 285 SeriesRecording.builder(recordedProgram.getInputId(), recordedProgram) 286 .setPriority(mScheduleManager.suggestNewSeriesPriority()) 287 .setState(SeriesRecording.STATE_SERIES_STOPPED) 288 .build(); 289 mDataManager.addSeriesRecording(seriesRecording); 290 // The schedules for the recorded programs should be added not to create the schedule the 291 // duplicate episodes. 292 addRecordedProgramToSeriesRecording(seriesRecording); 293 } 294 addRecordedProgramToSeriesRecording(SeriesRecording series)295 private void addRecordedProgramToSeriesRecording(SeriesRecording series) { 296 List<ScheduledRecording> toAdd = new ArrayList<>(); 297 for (RecordedProgram recordedProgram : mDataManager.getRecordedPrograms()) { 298 if (series.getSeriesId().equals(recordedProgram.getSeriesId()) 299 && !recordedProgram.isClipped()) { 300 // Duplicate schedules can exist, but they will be deleted in a few days. And it's 301 // also guaranteed that the schedules don't belong to any series recordings because 302 // there are no more than one series recordings which have the same program title. 303 toAdd.add( 304 ScheduledRecording.builder(recordedProgram) 305 .setPriority(series.getPriority()) 306 .setSeriesRecordingId(series.getId()) 307 .build()); 308 } 309 } 310 if (!toAdd.isEmpty()) { 311 mDataManager.addScheduledRecording(ScheduledRecording.toArray(toAdd)); 312 } 313 } 314 315 /** 316 * Adds {@link ScheduledRecording}s for the series recording. 317 * 318 * <p>This method doesn't add the series recording. 319 */ addScheduleToSeriesRecording( SeriesRecording series, List<Program> programsToSchedule)320 public void addScheduleToSeriesRecording( 321 SeriesRecording series, List<Program> programsToSchedule) { 322 if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 323 return; 324 } 325 TvInputInfo input = Utils.getTvInputInfoForInputId(mAppContext, series.getInputId()); 326 if (input == null) { 327 Log.e(TAG, "Can't find input with ID: " + series.getInputId()); 328 return; 329 } 330 List<ScheduledRecording> toAdd = new ArrayList<>(); 331 List<ScheduledRecording> toUpdate = new ArrayList<>(); 332 for (Program program : programsToSchedule) { 333 ScheduledRecording scheduleWithSameProgram = 334 mDataManager.getScheduledRecordingForProgramId(program.getId()); 335 if (scheduleWithSameProgram != null) { 336 if (scheduleWithSameProgram.isNotStarted()) { 337 ScheduledRecording r = 338 ScheduledRecording.buildFrom(scheduleWithSameProgram) 339 .setSeriesRecordingId(series.getId()) 340 .build(); 341 if (!r.equals(scheduleWithSameProgram)) { 342 toUpdate.add(r); 343 } 344 } 345 } else { 346 toAdd.add( 347 createScheduledRecordingBuilder(input.getId(), program) 348 .setPriority(series.getPriority()) 349 .setSeriesRecordingId(series.getId()) 350 .build()); 351 } 352 } 353 if (!toAdd.isEmpty()) { 354 mDataManager.addScheduledRecording(ScheduledRecording.toArray(toAdd)); 355 } 356 if (!toUpdate.isEmpty()) { 357 mDataManager.updateScheduledRecording(ScheduledRecording.toArray(toUpdate)); 358 } 359 } 360 361 /** Updates the series recording. */ updateSeriesRecording(SeriesRecording series)362 public void updateSeriesRecording(SeriesRecording series) { 363 if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 364 SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId()); 365 if (previousSeries != null) { 366 // If the channel option of series changed, remove the existing schedules. The new 367 // schedules will be added by SeriesRecordingScheduler or by SeriesSettingsFragment. 368 if (previousSeries.getChannelOption() != series.getChannelOption() 369 || (previousSeries.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE 370 && previousSeries.getChannelId() != series.getChannelId())) { 371 List<ScheduledRecording> schedules = 372 mDataManager.getScheduledRecordings(series.getId()); 373 List<ScheduledRecording> schedulesToRemove = new ArrayList<>(); 374 for (ScheduledRecording schedule : schedules) { 375 if (schedule.isNotStarted()) { 376 schedulesToRemove.add(schedule); 377 } else if (schedule.isInProgress() 378 && series.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE 379 && schedule.getChannelId() != series.getChannelId()) { 380 stopRecording(schedule); 381 } 382 } 383 List<ScheduledRecording> deletedSchedules = 384 new ArrayList<>(mDataManager.getDeletedSchedules()); 385 for (ScheduledRecording deletedSchedule : deletedSchedules) { 386 if (deletedSchedule.getSeriesRecordingId() == series.getId() 387 && deletedSchedule.getEndTimeMs() > System.currentTimeMillis()) { 388 schedulesToRemove.add(deletedSchedule); 389 } 390 } 391 mDataManager.removeScheduledRecording( 392 true, ScheduledRecording.toArray(schedulesToRemove)); 393 } 394 } 395 mDataManager.updateSeriesRecording(series); 396 if (previousSeries == null || previousSeries.getPriority() != series.getPriority()) { 397 long priority = series.getPriority(); 398 List<ScheduledRecording> schedulesToUpdate = new ArrayList<>(); 399 for (ScheduledRecording schedule : 400 mDataManager.getScheduledRecordings(series.getId())) { 401 if (schedule.isNotStarted() || schedule.isInProgress()) { 402 schedulesToUpdate.add( 403 ScheduledRecording.buildFrom(schedule) 404 .setPriority(priority) 405 .build()); 406 } 407 } 408 if (!schedulesToUpdate.isEmpty()) { 409 mDataManager.updateScheduledRecording( 410 ScheduledRecording.toArray(schedulesToUpdate)); 411 } 412 } 413 } 414 } 415 416 /** 417 * Removes the series recording and all the corresponding schedules which are not started yet. 418 */ removeSeriesRecording(long seriesRecordingId)419 public void removeSeriesRecording(long seriesRecordingId) { 420 if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 421 return; 422 } 423 SeriesRecording series = mDataManager.getSeriesRecording(seriesRecordingId); 424 if (series == null) { 425 return; 426 } 427 for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) { 428 if (schedule.getSeriesRecordingId() == seriesRecordingId) { 429 if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { 430 stopRecording(schedule); 431 break; 432 } 433 } 434 } 435 mDataManager.removeSeriesRecording(series); 436 } 437 438 /** Stops the currently recorded program */ stopRecording(final ScheduledRecording recording)439 public void stopRecording(final ScheduledRecording recording) { 440 if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 441 return; 442 } 443 synchronized (mListener) { 444 for (final Entry<Listener, Handler> entry : mListener.entrySet()) { 445 entry.getValue().post(() -> entry.getKey().onStopRecordingRequested(recording)); 446 } 447 } 448 } 449 450 /** Removes scheduled recordings or an existing recordings. */ removeScheduledRecording(ScheduledRecording... schedules)451 public void removeScheduledRecording(ScheduledRecording... schedules) { 452 Log.i(TAG, "Removing " + Arrays.asList(schedules)); 453 if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 454 return; 455 } 456 for (ScheduledRecording r : schedules) { 457 if (r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { 458 stopRecording(r); 459 } else { 460 mDataManager.removeScheduledRecording(r); 461 } 462 } 463 } 464 465 /** Removes scheduled recordings without changing to the DELETED state. */ forceRemoveScheduledRecording(ScheduledRecording... schedules)466 public void forceRemoveScheduledRecording(ScheduledRecording... schedules) { 467 Log.i(TAG, "Force removing " + Arrays.asList(schedules)); 468 if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 469 return; 470 } 471 for (ScheduledRecording r : schedules) { 472 if (r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { 473 stopRecording(r); 474 } else { 475 mDataManager.removeScheduledRecording(true, r); 476 } 477 } 478 } 479 480 /** Removes the recorded program. It deletes the file if possible. */ removeRecordedProgram(Uri recordedProgramUri, boolean deleteFile)481 public void removeRecordedProgram(Uri recordedProgramUri, boolean deleteFile) { 482 if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { 483 return; 484 } 485 removeRecordedProgram(ContentUris.parseId(recordedProgramUri), deleteFile); 486 } 487 488 /** Removes the recorded program. It deletes the file if possible. */ removeRecordedProgram(long recordedProgramId, boolean deleteFile)489 public void removeRecordedProgram(long recordedProgramId, boolean deleteFile) { 490 if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { 491 return; 492 } 493 RecordedProgram recordedProgram = mDataManager.getRecordedProgram(recordedProgramId); 494 if (recordedProgram != null) { 495 removeRecordedProgram(recordedProgram, deleteFile); 496 } 497 } 498 499 /** Removes the recorded program. It deletes the file if possible. */ removeRecordedProgram(final RecordedProgram recordedProgram, boolean deleteFile)500 public void removeRecordedProgram(final RecordedProgram recordedProgram, boolean deleteFile) { 501 if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { 502 return; 503 } 504 new AsyncDbTask<Void, Void, Integer>(mDbExecutor) { 505 @Override 506 protected Integer doInBackground(Void... params) { 507 ContentResolver resolver = mAppContext.getContentResolver(); 508 return resolver.delete(recordedProgram.getUri(), null, null); 509 } 510 511 @Override 512 protected void onPostExecute(Integer deletedCounts) { 513 if (deletedCounts > 0 && deleteFile) { 514 new AsyncTask<Void, Void, Void>() { 515 @Override 516 protected Void doInBackground(Void... params) { 517 removeRecordedData(recordedProgram.getDataUri()); 518 return null; 519 } 520 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 521 } 522 } 523 }.executeOnDbThread(); 524 } 525 removeRecordedPrograms(List<Long> recordedProgramIds, boolean deleteFiles)526 public void removeRecordedPrograms(List<Long> recordedProgramIds, boolean deleteFiles) { 527 final ArrayList<ContentProviderOperation> dbOperations = new ArrayList<>(); 528 final List<Uri> dataUris = new ArrayList<>(); 529 for (Long rId : recordedProgramIds) { 530 RecordedProgram r = mDataManager.getRecordedProgram(rId); 531 if (r != null) { 532 dataUris.add(r.getDataUri()); 533 dbOperations.add(ContentProviderOperation.newDelete(r.getUri()).build()); 534 } 535 } 536 new AsyncDbTask<Void, Void, Boolean>(mDbExecutor) { 537 @Override 538 protected Boolean doInBackground(Void... params) { 539 ContentResolver resolver = mAppContext.getContentResolver(); 540 try { 541 resolver.applyBatch(TvContract.AUTHORITY, dbOperations); 542 } catch (RemoteException | OperationApplicationException e) { 543 Log.w(TAG, "Remove recorded programs from DB failed.", e); 544 return false; 545 } 546 return true; 547 } 548 549 @Override 550 protected void onPostExecute(Boolean success) { 551 if (success && deleteFiles) { 552 new AsyncTask<Void, Void, Void>() { 553 @Override 554 protected Void doInBackground(Void... params) { 555 for (Uri dataUri : dataUris) { 556 removeRecordedData(dataUri); 557 } 558 return null; 559 } 560 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 561 } 562 } 563 }.executeOnDbThread(); 564 } 565 566 /** Updates the scheduled recording. */ updateScheduledRecording(ScheduledRecording recording)567 public void updateScheduledRecording(ScheduledRecording recording) { 568 if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 569 mDataManager.updateScheduledRecording(recording); 570 } 571 } 572 573 /** 574 * Returns priority ordered list of all scheduled recordings that will not be recorded if this 575 * program is. 576 * 577 * @see DvrScheduleManager#getConflictingSchedules(Program) 578 */ getConflictingSchedules(Program program)579 public List<ScheduledRecording> getConflictingSchedules(Program program) { 580 if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 581 return Collections.emptyList(); 582 } 583 return mScheduleManager.getConflictingSchedules(program); 584 } 585 586 /** 587 * Returns priority ordered list of all scheduled recordings that will not be recorded if this 588 * channel is. 589 * 590 * @see DvrScheduleManager#getConflictingSchedules(long, long, long) 591 */ getConflictingSchedules( long channelId, long startTimeMs, long endTimeMs)592 public List<ScheduledRecording> getConflictingSchedules( 593 long channelId, long startTimeMs, long endTimeMs) { 594 if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 595 return Collections.emptyList(); 596 } 597 return mScheduleManager.getConflictingSchedules(channelId, startTimeMs, endTimeMs); 598 } 599 600 /** 601 * Checks if the schedule is conflicting. 602 * 603 * <p>Note that the {@code schedule} should be the existing one. If not, this returns {@code 604 * false}. 605 */ isConflicting(ScheduledRecording schedule)606 public boolean isConflicting(ScheduledRecording schedule) { 607 return schedule != null 608 && SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished()) 609 && mScheduleManager.isConflicting(schedule); 610 } 611 612 /** 613 * Returns priority ordered list of all scheduled recording that will not be recorded if this 614 * channel is tuned to. 615 * 616 * @see DvrScheduleManager#getConflictingSchedulesForTune 617 */ getConflictingSchedulesForTune(long channelId)618 public List<ScheduledRecording> getConflictingSchedulesForTune(long channelId) { 619 if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 620 return Collections.emptyList(); 621 } 622 return mScheduleManager.getConflictingSchedulesForTune(channelId); 623 } 624 625 /** Sets the highest priority to the schedule. */ setHighestPriority(ScheduledRecording schedule)626 public void setHighestPriority(ScheduledRecording schedule) { 627 if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 628 long newPriority = mScheduleManager.suggestHighestPriority(schedule); 629 if (newPriority != schedule.getPriority()) { 630 mDataManager.updateScheduledRecording( 631 ScheduledRecording.buildFrom(schedule).setPriority(newPriority).build()); 632 } 633 } 634 } 635 636 /** Suggests the higher priority than the schedules which overlap with {@code schedule}. */ suggestHighestPriority(ScheduledRecording schedule)637 public long suggestHighestPriority(ScheduledRecording schedule) { 638 if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 639 return mScheduleManager.suggestHighestPriority(schedule); 640 } 641 return DvrScheduleManager.DEFAULT_PRIORITY; 642 } 643 644 /** 645 * Returns {@code true} if the channel can be recorded. 646 * 647 * <p>Note that this method doesn't check the conflict of the schedule or available tuners. This 648 * can be called from the UI before the schedules are loaded. 649 */ isChannelRecordable(Channel channel)650 public boolean isChannelRecordable(Channel channel) { 651 if (!mDataManager.isDvrScheduleLoadFinished() || channel == null) { 652 return false; 653 } 654 if (channel.isRecordingProhibited()) { 655 return false; 656 } 657 TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId()); 658 if (info == null) { 659 Log.w(TAG, "Could not find TvInputInfo for " + channel); 660 return false; 661 } 662 if (!info.canRecord()) { 663 return false; 664 } 665 Program program = 666 TvSingletons.getSingletons(mAppContext) 667 .getProgramDataManager() 668 .getCurrentProgram(channel.getId()); 669 return program == null || !program.isRecordingProhibited(); 670 } 671 672 /** 673 * Returns {@code true} if the program can be recorded. 674 * 675 * <p>Note that this method doesn't check the conflict of the schedule or available tuners. This 676 * can be called from the UI before the schedules are loaded. 677 */ isProgramRecordable(Program program)678 public boolean isProgramRecordable(Program program) { 679 if (!mDataManager.isInitialized()) { 680 return false; 681 } 682 Channel channel = 683 TvSingletons.getSingletons(mAppContext) 684 .getChannelDataManager() 685 .getChannel(program.getChannelId()); 686 if (channel == null || channel.isRecordingProhibited()) { 687 return false; 688 } 689 TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId()); 690 if (info == null) { 691 Log.w(TAG, "Could not find TvInputInfo for " + program); 692 return false; 693 } 694 return info.canRecord() && !program.isRecordingProhibited(); 695 } 696 697 /** 698 * Returns the current recording for the channel. 699 * 700 * <p>This can be called from the UI before the schedules are loaded. 701 */ getCurrentRecording(long channelId)702 public ScheduledRecording getCurrentRecording(long channelId) { 703 if (!mDataManager.isDvrScheduleLoadFinished()) { 704 return null; 705 } 706 for (ScheduledRecording recording : mDataManager.getStartedRecordings()) { 707 if (recording.getChannelId() == channelId) { 708 return recording; 709 } 710 } 711 return null; 712 } 713 714 /** 715 * Returns schedules which is available (i.e., isNotStarted or isInProgress) and belongs to the 716 * series recording {@code seriesRecordingId}. 717 */ getAvailableScheduledRecording(long seriesRecordingId)718 public List<ScheduledRecording> getAvailableScheduledRecording(long seriesRecordingId) { 719 if (!mDataManager.isDvrScheduleLoadFinished()) { 720 return Collections.emptyList(); 721 } 722 List<ScheduledRecording> schedules = new ArrayList<>(); 723 for (ScheduledRecording schedule : mDataManager.getScheduledRecordings(seriesRecordingId)) { 724 if (schedule.isInProgress() || schedule.isNotStarted()) { 725 schedules.add(schedule); 726 } 727 } 728 return schedules; 729 } 730 731 /** Returns the series recording related to the program. */ 732 @Nullable getSeriesRecording(Program program)733 public SeriesRecording getSeriesRecording(Program program) { 734 if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { 735 return null; 736 } 737 return mDataManager.getSeriesRecording(program.getSeriesId()); 738 } 739 740 /** 741 * Returns if there are valid items. Valid item contains {@link RecordedProgram}, available 742 * {@link ScheduledRecording} and {@link SeriesRecording}. 743 */ hasValidItems()744 public boolean hasValidItems() { 745 return !(mDataManager.getRecordedPrograms().isEmpty() 746 && mDataManager.getStartedRecordings().isEmpty() 747 && mDataManager.getNonStartedScheduledRecordings().isEmpty() 748 && mDataManager.getSeriesRecordings().isEmpty()); 749 } 750 751 @WorkerThread 752 @VisibleForTesting 753 // Should be public to use mock DvrManager object. addListener(Listener listener, @NonNull Handler handler)754 public void addListener(Listener listener, @NonNull Handler handler) { 755 SoftPreconditions.checkNotNull(handler); 756 synchronized (mListener) { 757 mListener.put(listener, handler); 758 } 759 } 760 761 @WorkerThread 762 @VisibleForTesting 763 // Should be public to use mock DvrManager object. removeListener(Listener listener)764 public void removeListener(Listener listener) { 765 synchronized (mListener) { 766 mListener.remove(listener); 767 } 768 } 769 770 /** 771 * Returns ScheduledRecording.builder based on {@code program}. If program is already started, 772 * recording started time is clipped to the current time. 773 */ createScheduledRecordingBuilder( String inputId, Program program)774 private ScheduledRecording.Builder createScheduledRecordingBuilder( 775 String inputId, Program program) { 776 ScheduledRecording.Builder builder = ScheduledRecording.builder(inputId, program); 777 long time = System.currentTimeMillis(); 778 if (program.getStartTimeUtcMillis() < time && time < program.getEndTimeUtcMillis()) { 779 builder.setStartTimeMs(time); 780 } 781 return builder; 782 } 783 784 /** Returns a schedule which matches to the given episode. */ getScheduledRecording( String title, String seasonNumber, String episodeNumber)785 public ScheduledRecording getScheduledRecording( 786 String title, String seasonNumber, String episodeNumber) { 787 if (!SoftPreconditions.checkState(mDataManager.isInitialized()) 788 || title == null 789 || seasonNumber == null 790 || episodeNumber == null) { 791 return null; 792 } 793 for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) { 794 if (title.equals(r.getProgramTitle()) 795 && seasonNumber.equals(r.getSeasonNumber()) 796 && episodeNumber.equals(r.getEpisodeNumber())) { 797 return r; 798 } 799 } 800 return null; 801 } 802 803 /** Returns a recorded program which is the same episode as the given {@code program}. */ getRecordedProgram( String title, String seasonNumber, String episodeNumber)804 public RecordedProgram getRecordedProgram( 805 String title, String seasonNumber, String episodeNumber) { 806 if (!SoftPreconditions.checkState(mDataManager.isInitialized()) 807 || title == null 808 || seasonNumber == null 809 || episodeNumber == null) { 810 return null; 811 } 812 for (RecordedProgram r : mDataManager.getRecordedPrograms()) { 813 if (title.equals(r.getTitle()) 814 && seasonNumber.equals(r.getSeasonNumber()) 815 && episodeNumber.equals(r.getEpisodeNumber()) 816 && !r.isClipped()) { 817 return r; 818 } 819 } 820 return null; 821 } 822 823 @WorkerThread removeRecordedData(Uri dataUri)824 private void removeRecordedData(Uri dataUri) { 825 try { 826 if (isFile(dataUri)) { 827 File recordedProgramPath = new File(dataUri.getPath()); 828 if (!recordedProgramPath.exists()) { 829 if (DEBUG) Log.d(TAG, "File to delete not exist: " + recordedProgramPath); 830 } else { 831 if (CommonUtils.deleteDirOrFile(recordedProgramPath)) { 832 if (DEBUG) { 833 Log.d( 834 TAG, 835 "Successfully deleted files of the recorded program: " 836 + dataUri); 837 } 838 } else { 839 Log.w(TAG, "Unable to delete recording data at " + dataUri); 840 } 841 } 842 } 843 } catch (SecurityException e) { 844 Log.w(TAG, "Unable to delete recording data at " + dataUri, e); 845 } 846 } 847 848 @AnyThread isFromBundledInput(RecordedProgram mRecordedProgram)849 public static boolean isFromBundledInput(RecordedProgram mRecordedProgram) { 850 return CommonUtils.isInBundledPackageSet(mRecordedProgram.getPackageName()); 851 } 852 853 @AnyThread isFile(Uri dataUri)854 public static boolean isFile(Uri dataUri) { 855 return dataUri != null 856 && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme()) 857 && dataUri.getPath() != null; 858 } 859 860 /** 861 * Remove all the records related to the input. 862 * 863 * <p>Note that this should be called after the input was removed. 864 */ forgetStorage(String inputId)865 public void forgetStorage(String inputId) { 866 if (mDataManager != null && mDataManager.isInitialized()) { 867 mDataManager.forgetStorage(inputId); 868 } 869 } 870 871 /** 872 * Listener to stop recording request. Should only be internally used inside dvr and its 873 * sub-package. 874 */ 875 public interface Listener { onStopRecordingRequested(ScheduledRecording scheduledRecording)876 void onStopRecordingRequested(ScheduledRecording scheduledRecording); 877 } 878 } 879