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