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