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