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