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.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.Context; 23 import android.database.ContentObserver; 24 import android.database.sqlite.SQLiteException; 25 import android.media.tv.TvContract.RecordedPrograms; 26 import android.media.tv.TvInputInfo; 27 import android.media.tv.TvInputManager.TvInputCallback; 28 import android.net.Uri; 29 import android.os.AsyncTask; 30 import android.os.Build; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.support.annotation.MainThread; 34 import android.support.annotation.Nullable; 35 import android.support.annotation.VisibleForTesting; 36 import android.text.TextUtils; 37 import android.util.ArraySet; 38 import android.util.Log; 39 import android.util.Range; 40 import com.android.tv.TvSingletons; 41 import com.android.tv.common.SoftPreconditions; 42 import com.android.tv.common.recording.RecordingStorageStatusManager; 43 import com.android.tv.common.recording.RecordingStorageStatusManager.OnStorageMountChangedListener; 44 import com.android.tv.common.util.Clock; 45 import com.android.tv.common.util.CommonUtils; 46 import com.android.tv.dvr.data.IdGenerator; 47 import com.android.tv.dvr.data.RecordedProgram; 48 import com.android.tv.dvr.data.ScheduledRecording; 49 import com.android.tv.dvr.data.ScheduledRecording.RecordingState; 50 import com.android.tv.dvr.data.SeriesRecording; 51 import com.android.tv.dvr.provider.DvrDbFuture.AddScheduleFuture; 52 import com.android.tv.dvr.provider.DvrDbFuture.AddSeriesRecordingFuture; 53 import com.android.tv.dvr.provider.DvrDbFuture.DeleteScheduleFuture; 54 import com.android.tv.dvr.provider.DvrDbFuture.DeleteSeriesRecordingFuture; 55 import com.android.tv.dvr.provider.DvrDbFuture.DvrQueryScheduleFuture; 56 import com.android.tv.dvr.provider.DvrDbFuture.DvrQuerySeriesRecordingFuture; 57 import com.android.tv.dvr.provider.DvrDbFuture.UpdateScheduleFuture; 58 import com.android.tv.dvr.provider.DvrDbFuture.UpdateSeriesRecordingFuture; 59 import com.android.tv.dvr.provider.DvrDbSync; 60 import com.android.tv.dvr.recorder.SeriesRecordingScheduler; 61 import com.android.tv.util.AsyncDbTask; 62 import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask; 63 import com.android.tv.util.TvInputManagerHelper; 64 import com.android.tv.util.TvUriMatcher; 65 import com.google.common.base.Predicate; 66 import com.google.common.util.concurrent.FutureCallback; 67 import com.google.common.util.concurrent.ListenableFuture; 68 import java.util.ArrayList; 69 import java.util.Collections; 70 import java.util.HashMap; 71 import java.util.HashSet; 72 import java.util.Iterator; 73 import java.util.List; 74 import java.util.Map.Entry; 75 import java.util.Set; 76 import java.util.concurrent.Executor; 77 import java.util.concurrent.Future; 78 79 /** DVR Data manager to handle recordings and schedules. */ 80 @MainThread 81 @TargetApi(Build.VERSION_CODES.N) 82 public class DvrDataManagerImpl extends BaseDvrDataManager { 83 private static final String TAG = "DvrDataManagerImpl"; 84 private static final boolean DEBUG = false; 85 86 private final TvInputManagerHelper mInputManager; 87 88 private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>(); 89 private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>(); 90 private final HashMap<Long, SeriesRecording> mSeriesRecordings = new HashMap<>(); 91 private final HashMap<Long, ScheduledRecording> mProgramId2ScheduledRecordings = 92 new HashMap<>(); 93 private final HashMap<String, SeriesRecording> mSeriesId2SeriesRecordings = new HashMap<>(); 94 95 private final HashMap<Long, ScheduledRecording> mScheduledRecordingsForRemovedInput = 96 new HashMap<>(); 97 private final HashMap<Long, RecordedProgram> mRecordedProgramsForRemovedInput = new HashMap<>(); 98 private final HashMap<Long, SeriesRecording> mSeriesRecordingsForRemovedInput = new HashMap<>(); 99 100 private final Context mContext; 101 private Executor mDbExecutor; 102 private final ContentObserver mContentObserver = 103 new ContentObserver(new Handler(Looper.getMainLooper())) { 104 @Override 105 public void onChange(boolean selfChange) { 106 onChange(selfChange, null); 107 } 108 109 @Override 110 public void onChange(boolean selfChange, final @Nullable Uri uri) { 111 RecordedProgramsQueryTask task = new RecordedProgramsQueryTask(uri); 112 task.executeOnDbThread(); 113 mPendingTasks.add(task); 114 } 115 }; 116 117 private boolean mDvrLoadFinished; 118 private boolean mRecordedProgramLoadFinished; 119 private final Set<AsyncTask> mPendingTasks = new ArraySet<>(); 120 private final Set<Future> mPendingDvrFuture = new ArraySet<>(); 121 // TODO(b/79207567) make sure Future is not stopped at writing. 122 private final Set<Future> mNoStopFuture = new ArraySet<>(); 123 private DvrDbSync mDbSync; 124 private RecordingStorageStatusManager mStorageStatusManager; 125 126 private final TvInputCallback mInputCallback = 127 new TvInputCallback() { 128 @Override 129 public void onInputAdded(String inputId) { 130 if (DEBUG) Log.d(TAG, "onInputAdded " + inputId); 131 if (!isInputAvailable(inputId)) { 132 if (DEBUG) Log.d(TAG, "Not available for recording"); 133 return; 134 } 135 unhideInput(inputId); 136 } 137 138 @Override 139 public void onInputRemoved(String inputId) { 140 if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); 141 hideInput(inputId); 142 } 143 }; 144 145 private final OnStorageMountChangedListener mStorageMountChangedListener = 146 new OnStorageMountChangedListener() { 147 @Override 148 public void onStorageMountChanged(boolean storageMounted) { 149 for (TvInputInfo input : mInputManager.getTvInputInfos(true, true)) { 150 if (CommonUtils.isBundledInput(input.getId())) { 151 if (storageMounted) { 152 unhideInput(input.getId()); 153 } else { 154 hideInput(input.getId()); 155 } 156 } 157 } 158 } 159 }; 160 161 private final FutureCallback<Void> removeFromSetOnCompletion = 162 new FutureCallback<Void>() { 163 @Override 164 public void onSuccess(Void result) { 165 mNoStopFuture.remove(this); 166 } 167 168 @Override 169 public void onFailure(Throwable t) { 170 Log.w(TAG, "Failed to execute.", t); 171 mNoStopFuture.remove(this); 172 } 173 }; 174 moveElements( HashMap<Long, T> from, HashMap<Long, T> to, Predicate<T> filter)175 private static <T> List<T> moveElements( 176 HashMap<Long, T> from, HashMap<Long, T> to, Predicate<T> filter) { 177 List<T> moved = new ArrayList<>(); 178 Iterator<Entry<Long, T>> iter = from.entrySet().iterator(); 179 while (iter.hasNext()) { 180 Entry<Long, T> entry = iter.next(); 181 if (filter.apply(entry.getValue())) { 182 to.put(entry.getKey(), entry.getValue()); 183 iter.remove(); 184 moved.add(entry.getValue()); 185 } 186 } 187 return moved; 188 } 189 DvrDataManagerImpl(Context context, Clock clock)190 public DvrDataManagerImpl(Context context, Clock clock) { 191 super(context, clock); 192 mContext = context; 193 TvSingletons tvSingletons = TvSingletons.getSingletons(context); 194 mInputManager = tvSingletons.getTvInputManagerHelper(); 195 mStorageStatusManager = tvSingletons.getRecordingStorageStatusManager(); 196 mDbExecutor = tvSingletons.getDbExecutor(); 197 } 198 start()199 public void start() { 200 mInputManager.addCallback(mInputCallback); 201 mStorageStatusManager.addListener(mStorageMountChangedListener); 202 DvrQuerySeriesRecordingFuture dvrQuerySeriesRecordingTask = 203 new DvrQuerySeriesRecordingFuture(mContext); 204 ListenableFuture<List<SeriesRecording>> dvrQuerySeriesRecordingFuture = 205 dvrQuerySeriesRecordingTask.executeOnDbThread( 206 new FutureCallback<List<SeriesRecording>>() { 207 @Override 208 public void onSuccess(List<SeriesRecording> seriesRecordings) { 209 mPendingDvrFuture.remove(this); 210 long maxId = 0; 211 HashSet<String> seriesIds = new HashSet<>(); 212 for (SeriesRecording r : seriesRecordings) { 213 if (SoftPreconditions.checkState( 214 !seriesIds.contains(r.getSeriesId()), 215 TAG, 216 "Skip loading series recording with duplicate series ID: " 217 + r)) { 218 seriesIds.add(r.getSeriesId()); 219 if (isInputAvailable(r.getInputId())) { 220 mSeriesRecordings.put(r.getId(), r); 221 mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); 222 } else { 223 mSeriesRecordingsForRemovedInput.put(r.getId(), r); 224 } 225 } 226 if (maxId < r.getId()) { 227 maxId = r.getId(); 228 } 229 } 230 IdGenerator.SERIES_RECORDING.setMaxId(maxId); 231 } 232 233 @Override 234 public void onFailure(Throwable t) { 235 Log.w(TAG, "Failed to load series recording.", t); 236 mPendingDvrFuture.remove(this); 237 } 238 }); 239 mPendingDvrFuture.add(dvrQuerySeriesRecordingFuture); 240 DvrQueryScheduleFuture dvrQueryScheduleTask = new DvrQueryScheduleFuture(mContext); 241 ListenableFuture<List<ScheduledRecording>> dvrQueryScheduleFuture = 242 dvrQueryScheduleTask.executeOnDbThread( 243 new FutureCallback<List<ScheduledRecording>>() { 244 @Override 245 public void onSuccess(List<ScheduledRecording> result) { 246 mPendingDvrFuture.remove(this); 247 long maxId = 0; 248 int reasonNotStarted = 249 ScheduledRecording 250 .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED; 251 List<ScheduledRecording> toUpdate = new ArrayList<>(); 252 List<ScheduledRecording> toDelete = new ArrayList<>(); 253 for (ScheduledRecording r : result) { 254 if (!isInputAvailable(r.getInputId())) { 255 mScheduledRecordingsForRemovedInput.put(r.getId(), r); 256 } else if (r.getState() 257 == ScheduledRecording.STATE_RECORDING_DELETED) { 258 getDeletedScheduleMap().put(r.getProgramId(), r); 259 } else { 260 mScheduledRecordings.put(r.getId(), r); 261 if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) { 262 mProgramId2ScheduledRecordings.put(r.getProgramId(), r); 263 } 264 // Adjust the state of the schedules before DB loading is 265 // finished. 266 switch (r.getState()) { 267 case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: 268 if (r.getEndTimeMs() 269 <= mClock.currentTimeMillis()) { 270 int reason = 271 ScheduledRecording 272 .FAILED_REASON_NOT_FINISHED; 273 toUpdate.add( 274 ScheduledRecording.buildFrom(r) 275 .setState( 276 ScheduledRecording 277 .STATE_RECORDING_FAILED) 278 .setFailedReason(reason) 279 .build()); 280 } else { 281 toUpdate.add( 282 ScheduledRecording.buildFrom(r) 283 .setState( 284 ScheduledRecording 285 .STATE_RECORDING_NOT_STARTED) 286 .build()); 287 } 288 break; 289 case ScheduledRecording.STATE_RECORDING_NOT_STARTED: 290 if (r.getEndTimeMs() 291 <= mClock.currentTimeMillis()) { 292 toUpdate.add( 293 ScheduledRecording.buildFrom(r) 294 .setState( 295 ScheduledRecording 296 .STATE_RECORDING_FAILED) 297 .setFailedReason( 298 reasonNotStarted) 299 .build()); 300 } 301 break; 302 case ScheduledRecording.STATE_RECORDING_CANCELED: 303 toDelete.add(r); 304 break; 305 default: // fall out 306 } 307 } 308 if (maxId < r.getId()) { 309 maxId = r.getId(); 310 } 311 } 312 if (!toUpdate.isEmpty()) { 313 updateScheduledRecording(ScheduledRecording.toArray(toUpdate)); 314 } 315 if (!toDelete.isEmpty()) { 316 removeScheduledRecording(ScheduledRecording.toArray(toDelete)); 317 } 318 IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId); 319 if (mRecordedProgramLoadFinished) { 320 validateSeriesRecordings(); 321 } 322 mDvrLoadFinished = true; 323 notifyDvrScheduleLoadFinished(); 324 if (isInitialized()) { 325 mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); 326 mDbSync.start(); 327 SeriesRecordingScheduler.getInstance(mContext).start(); 328 } 329 } 330 331 @Override 332 public void onFailure(Throwable t) { 333 Log.w(TAG, "Failed to load scheduled recording.", t); 334 mPendingDvrFuture.remove(this); 335 } 336 }); 337 mPendingDvrFuture.add(dvrQueryScheduleFuture); 338 RecordedProgramsQueryTask mRecordedProgramQueryTask = new RecordedProgramsQueryTask(null); 339 mRecordedProgramQueryTask.executeOnDbThread(); 340 ContentResolver cr = mContext.getContentResolver(); 341 cr.registerContentObserver(RecordedPrograms.CONTENT_URI, true, mContentObserver); 342 } 343 stop()344 public void stop() { 345 mInputManager.removeCallback(mInputCallback); 346 mStorageStatusManager.removeListener(mStorageMountChangedListener); 347 SeriesRecordingScheduler.getInstance(mContext).stop(); 348 if (mDbSync != null) { 349 mDbSync.stop(); 350 } 351 ContentResolver cr = mContext.getContentResolver(); 352 cr.unregisterContentObserver(mContentObserver); 353 Iterator<AsyncTask> i = mPendingTasks.iterator(); 354 while (i.hasNext()) { 355 AsyncTask task = i.next(); 356 i.remove(); 357 task.cancel(true); 358 } 359 Iterator<Future> id = mPendingDvrFuture.iterator(); 360 while (id.hasNext()) { 361 Future future = id.next(); 362 id.remove(); 363 future.cancel(true); 364 } 365 } 366 onRecordedProgramsLoadedFinished(Uri uri, List<RecordedProgram> recordedPrograms)367 private void onRecordedProgramsLoadedFinished(Uri uri, List<RecordedProgram> recordedPrograms) { 368 if (uri == null) { 369 uri = RecordedPrograms.CONTENT_URI; 370 } 371 if (recordedPrograms == null) { 372 recordedPrograms = Collections.emptyList(); 373 } 374 int match = TvUriMatcher.match(uri); 375 if (match == TvUriMatcher.MATCH_RECORDED_PROGRAM) { 376 if (!mRecordedProgramLoadFinished) { 377 for (RecordedProgram recorded : recordedPrograms) { 378 if (isInputAvailable(recorded.getInputId())) { 379 mRecordedPrograms.put(recorded.getId(), recorded); 380 } else { 381 mRecordedProgramsForRemovedInput.put(recorded.getId(), recorded); 382 } 383 } 384 mRecordedProgramLoadFinished = true; 385 notifyRecordedProgramLoadFinished(); 386 if (isInitialized()) { 387 mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); 388 mDbSync.start(); 389 } 390 } else if (recordedPrograms.isEmpty()) { 391 List<RecordedProgram> oldRecordedPrograms = 392 new ArrayList<>(mRecordedPrograms.values()); 393 mRecordedPrograms.clear(); 394 mRecordedProgramsForRemovedInput.clear(); 395 notifyRecordedProgramsRemoved(RecordedProgram.toArray(oldRecordedPrograms)); 396 } else { 397 HashMap<Long, RecordedProgram> oldRecordedPrograms = 398 new HashMap<>(mRecordedPrograms); 399 mRecordedPrograms.clear(); 400 mRecordedProgramsForRemovedInput.clear(); 401 List<RecordedProgram> addedRecordedPrograms = new ArrayList<>(); 402 List<RecordedProgram> changedRecordedPrograms = new ArrayList<>(); 403 for (RecordedProgram recorded : recordedPrograms) { 404 if (isInputAvailable(recorded.getInputId())) { 405 mRecordedPrograms.put(recorded.getId(), recorded); 406 if (oldRecordedPrograms.remove(recorded.getId()) == null) { 407 addedRecordedPrograms.add(recorded); 408 } else { 409 changedRecordedPrograms.add(recorded); 410 } 411 } else { 412 mRecordedProgramsForRemovedInput.put(recorded.getId(), recorded); 413 } 414 } 415 if (!addedRecordedPrograms.isEmpty()) { 416 notifyRecordedProgramsAdded(RecordedProgram.toArray(addedRecordedPrograms)); 417 } 418 if (!changedRecordedPrograms.isEmpty()) { 419 notifyRecordedProgramsChanged(RecordedProgram.toArray(changedRecordedPrograms)); 420 } 421 if (!oldRecordedPrograms.isEmpty()) { 422 notifyRecordedProgramsRemoved( 423 RecordedProgram.toArray(oldRecordedPrograms.values())); 424 } 425 } 426 if (isInitialized()) { 427 validateSeriesRecordings(); 428 SeriesRecordingScheduler.getInstance(mContext).start(); 429 } 430 } else if (match == TvUriMatcher.MATCH_RECORDED_PROGRAM_ID) { 431 if (!mRecordedProgramLoadFinished) { 432 return; 433 } 434 long id = ContentUris.parseId(uri); 435 if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms); 436 if (recordedPrograms.isEmpty()) { 437 mRecordedProgramsForRemovedInput.remove(id); 438 RecordedProgram old = mRecordedPrograms.remove(id); 439 if (old != null) { 440 notifyRecordedProgramsRemoved(old); 441 SeriesRecording r = mSeriesId2SeriesRecordings.get(old.getSeriesId()); 442 if (r != null && isEmptySeriesRecording(r)) { 443 removeSeriesRecording(r); 444 } 445 } 446 } else { 447 RecordedProgram recordedProgram = recordedPrograms.get(0); 448 if (isInputAvailable(recordedProgram.getInputId())) { 449 RecordedProgram old = mRecordedPrograms.put(id, recordedProgram); 450 if (old == null) { 451 notifyRecordedProgramsAdded(recordedProgram); 452 } else { 453 notifyRecordedProgramsChanged(recordedProgram); 454 } 455 } else { 456 mRecordedProgramsForRemovedInput.put(id, recordedProgram); 457 } 458 } 459 } 460 } 461 462 @Override isInitialized()463 public boolean isInitialized() { 464 return mDvrLoadFinished && mRecordedProgramLoadFinished; 465 } 466 467 @Override isDvrScheduleLoadFinished()468 public boolean isDvrScheduleLoadFinished() { 469 return mDvrLoadFinished; 470 } 471 472 @Override isRecordedProgramLoadFinished()473 public boolean isRecordedProgramLoadFinished() { 474 return mRecordedProgramLoadFinished; 475 } 476 getScheduledRecordingsPrograms()477 private List<ScheduledRecording> getScheduledRecordingsPrograms() { 478 if (!mDvrLoadFinished) { 479 return Collections.emptyList(); 480 } 481 ArrayList<ScheduledRecording> list = new ArrayList<>(mScheduledRecordings.size()); 482 list.addAll(mScheduledRecordings.values()); 483 Collections.sort(list, ScheduledRecording.START_TIME_COMPARATOR); 484 return list; 485 } 486 487 @Override getRecordedPrograms()488 public List<RecordedProgram> getRecordedPrograms() { 489 if (!mRecordedProgramLoadFinished) { 490 return Collections.emptyList(); 491 } 492 return new ArrayList<>(mRecordedPrograms.values()); 493 } 494 495 @Override getRecordedPrograms(long seriesRecordingId)496 public List<RecordedProgram> getRecordedPrograms(long seriesRecordingId) { 497 SeriesRecording seriesRecording = getSeriesRecording(seriesRecordingId); 498 if (!mRecordedProgramLoadFinished || seriesRecording == null) { 499 return Collections.emptyList(); 500 } 501 return super.getRecordedPrograms(seriesRecordingId); 502 } 503 504 @Override getAllScheduledRecordings()505 public List<ScheduledRecording> getAllScheduledRecordings() { 506 return new ArrayList<>(mScheduledRecordings.values()); 507 } 508 509 @Override getRecordingsWithState(@ecordingState int... states)510 protected List<ScheduledRecording> getRecordingsWithState(@RecordingState int... states) { 511 List<ScheduledRecording> result = new ArrayList<>(); 512 for (ScheduledRecording r : mScheduledRecordings.values()) { 513 for (int state : states) { 514 if (r.getState() == state) { 515 result.add(r); 516 break; 517 } 518 } 519 } 520 return result; 521 } 522 523 @Override getSeriesRecordings()524 public List<SeriesRecording> getSeriesRecordings() { 525 if (!mDvrLoadFinished) { 526 return Collections.emptyList(); 527 } 528 return new ArrayList<>(mSeriesRecordings.values()); 529 } 530 531 @Override getSeriesRecordings(String inputId)532 public List<SeriesRecording> getSeriesRecordings(String inputId) { 533 List<SeriesRecording> result = new ArrayList<>(); 534 for (SeriesRecording r : mSeriesRecordings.values()) { 535 if (TextUtils.equals(r.getInputId(), inputId)) { 536 result.add(r); 537 } 538 } 539 return result; 540 } 541 542 @Override getNextScheduledStartTimeAfter(long startTime)543 public long getNextScheduledStartTimeAfter(long startTime) { 544 return getNextStartTimeAfter(getScheduledRecordingsPrograms(), startTime); 545 } 546 547 @VisibleForTesting getNextStartTimeAfter( List<ScheduledRecording> scheduledRecordings, long startTime)548 static long getNextStartTimeAfter( 549 List<ScheduledRecording> scheduledRecordings, long startTime) { 550 int start = 0; 551 int end = scheduledRecordings.size() - 1; 552 while (start <= end) { 553 int mid = (start + end) / 2; 554 if (scheduledRecordings.get(mid).getStartTimeMs() <= startTime) { 555 start = mid + 1; 556 } else { 557 end = mid - 1; 558 } 559 } 560 return start < scheduledRecordings.size() 561 ? scheduledRecordings.get(start).getStartTimeMs() 562 : NEXT_START_TIME_NOT_FOUND; 563 } 564 565 @Override getScheduledRecordings( Range<Long> period, @RecordingState int state)566 public List<ScheduledRecording> getScheduledRecordings( 567 Range<Long> period, @RecordingState int state) { 568 List<ScheduledRecording> result = new ArrayList<>(); 569 for (ScheduledRecording r : mScheduledRecordings.values()) { 570 if (r.isOverLapping(period) && r.getState() == state) { 571 result.add(r); 572 } 573 } 574 return result; 575 } 576 577 @Override getScheduledRecordings(long seriesRecordingId)578 public List<ScheduledRecording> getScheduledRecordings(long seriesRecordingId) { 579 List<ScheduledRecording> result = new ArrayList<>(); 580 for (ScheduledRecording r : mScheduledRecordings.values()) { 581 if (r.getSeriesRecordingId() == seriesRecordingId) { 582 result.add(r); 583 } 584 } 585 return result; 586 } 587 588 @Override getScheduledRecordings(String inputId)589 public List<ScheduledRecording> getScheduledRecordings(String inputId) { 590 List<ScheduledRecording> result = new ArrayList<>(); 591 for (ScheduledRecording r : mScheduledRecordings.values()) { 592 if (TextUtils.equals(r.getInputId(), inputId)) { 593 result.add(r); 594 } 595 } 596 return result; 597 } 598 599 @Nullable 600 @Override getScheduledRecording(long recordingId)601 public ScheduledRecording getScheduledRecording(long recordingId) { 602 return mScheduledRecordings.get(recordingId); 603 } 604 605 @Nullable 606 @Override getScheduledRecordingForProgramId(long programId)607 public ScheduledRecording getScheduledRecordingForProgramId(long programId) { 608 return mProgramId2ScheduledRecordings.get(programId); 609 } 610 611 @Nullable 612 @Override getRecordedProgram(long recordingId)613 public RecordedProgram getRecordedProgram(long recordingId) { 614 return mRecordedPrograms.get(recordingId); 615 } 616 617 @Nullable 618 @Override getSeriesRecording(long seriesRecordingId)619 public SeriesRecording getSeriesRecording(long seriesRecordingId) { 620 return mSeriesRecordings.get(seriesRecordingId); 621 } 622 623 @Nullable 624 @Override getSeriesRecording(String seriesId)625 public SeriesRecording getSeriesRecording(String seriesId) { 626 return mSeriesId2SeriesRecordings.get(seriesId); 627 } 628 629 @Override addScheduledRecording(ScheduledRecording... schedules)630 public void addScheduledRecording(ScheduledRecording... schedules) { 631 for (ScheduledRecording r : schedules) { 632 if (r.getId() == ScheduledRecording.ID_NOT_SET) { 633 r.setId(IdGenerator.SCHEDULED_RECORDING.newId()); 634 } 635 mScheduledRecordings.put(r.getId(), r); 636 if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) { 637 mProgramId2ScheduledRecordings.put(r.getProgramId(), r); 638 } 639 } 640 if (mDvrLoadFinished) { 641 notifyScheduledRecordingAdded(schedules); 642 } 643 ListenableFuture addScheduleFuture = 644 new AddScheduleFuture(mContext) 645 .executeOnDbThread(removeFromSetOnCompletion, schedules); 646 mNoStopFuture.add(addScheduleFuture); 647 removeDeletedSchedules(schedules); 648 } 649 650 @Override addSeriesRecording(SeriesRecording... seriesRecordings)651 public void addSeriesRecording(SeriesRecording... seriesRecordings) { 652 for (SeriesRecording r : seriesRecordings) { 653 r.setId(IdGenerator.SERIES_RECORDING.newId()); 654 mSeriesRecordings.put(r.getId(), r); 655 SeriesRecording previousSeries = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); 656 SoftPreconditions.checkArgument( 657 previousSeries == null, 658 TAG, 659 "Attempt to add series" + " recording with the duplicate series ID: %s", 660 r.getSeriesId()); 661 } 662 if (mDvrLoadFinished) { 663 notifySeriesRecordingAdded(seriesRecordings); 664 } 665 ListenableFuture addSeriesRecordingFuture = 666 new AddSeriesRecordingFuture(mContext) 667 .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings); 668 mNoStopFuture.add(addSeriesRecordingFuture); 669 } 670 671 @Override removeScheduledRecording(ScheduledRecording... schedules)672 public void removeScheduledRecording(ScheduledRecording... schedules) { 673 removeScheduledRecording(false, schedules); 674 } 675 676 @Override removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules)677 public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules) { 678 List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); 679 List<ScheduledRecording> schedulesNotToDelete = new ArrayList<>(); 680 Set<Long> seriesRecordingIdsToCheck = new HashSet<>(); 681 for (ScheduledRecording r : schedules) { 682 mScheduledRecordings.remove(r.getId()); 683 getDeletedScheduleMap().remove(r.getProgramId()); 684 mProgramId2ScheduledRecordings.remove(r.getProgramId()); 685 if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET 686 && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED 687 || r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { 688 seriesRecordingIdsToCheck.add(r.getSeriesRecordingId()); 689 } 690 boolean isScheduleForRemovedInput = 691 mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null; 692 // If it belongs to the series recording and it's not started yet, just mark delete 693 // instead of deleting it. 694 if (!isScheduleForRemovedInput 695 && !forceRemove 696 && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET 697 && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED 698 || r.getState() == ScheduledRecording.STATE_RECORDING_CANCELED)) { 699 SoftPreconditions.checkState(r.getProgramId() != ScheduledRecording.ID_NOT_SET); 700 ScheduledRecording deleted = 701 ScheduledRecording.buildFrom(r) 702 .setState(ScheduledRecording.STATE_RECORDING_DELETED) 703 .build(); 704 getDeletedScheduleMap().put(deleted.getProgramId(), deleted); 705 schedulesNotToDelete.add(deleted); 706 } else { 707 schedulesToDelete.add(r); 708 } 709 } 710 if (mDvrLoadFinished) { 711 if (mRecordedProgramLoadFinished) { 712 checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck); 713 } 714 notifyScheduledRecordingRemoved(schedules); 715 } 716 Iterator<ScheduledRecording> iterator = schedulesNotToDelete.iterator(); 717 while (iterator.hasNext()) { 718 ScheduledRecording r = iterator.next(); 719 if (!mSeriesRecordings.containsKey(r.getSeriesRecordingId())) { 720 iterator.remove(); 721 schedulesToDelete.add(r); 722 } 723 } 724 if (!schedulesToDelete.isEmpty()) { 725 ListenableFuture deleteScheduleFuture = 726 new DeleteScheduleFuture(mContext) 727 .executeOnDbThread( 728 removeFromSetOnCompletion, 729 ScheduledRecording.toArray(schedulesToDelete)); 730 mNoStopFuture.add(deleteScheduleFuture); 731 } 732 if (!schedulesNotToDelete.isEmpty()) { 733 ListenableFuture updateScheduleFuture = 734 new UpdateScheduleFuture(mContext) 735 .executeOnDbThread( 736 removeFromSetOnCompletion, 737 ScheduledRecording.toArray(schedulesNotToDelete)); 738 mNoStopFuture.add(updateScheduleFuture); 739 } 740 } 741 742 @Override removeSeriesRecording(final SeriesRecording... seriesRecordings)743 public void removeSeriesRecording(final SeriesRecording... seriesRecordings) { 744 HashSet<Long> ids = new HashSet<>(); 745 for (SeriesRecording r : seriesRecordings) { 746 mSeriesRecordings.remove(r.getId()); 747 mSeriesId2SeriesRecordings.remove(r.getSeriesId()); 748 ids.add(r.getId()); 749 } 750 // Reset series recording ID of the scheduled recording. 751 List<ScheduledRecording> toUpdate = new ArrayList<>(); 752 List<ScheduledRecording> toDelete = new ArrayList<>(); 753 for (ScheduledRecording r : mScheduledRecordings.values()) { 754 if (ids.contains(r.getSeriesRecordingId())) { 755 if (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { 756 toDelete.add(r); 757 } else { 758 toUpdate.add( 759 ScheduledRecording.buildFrom(r) 760 .setSeriesRecordingId(SeriesRecording.ID_NOT_SET) 761 .build()); 762 } 763 } 764 } 765 if (!toUpdate.isEmpty()) { 766 // No need to update DB. It's handled in database automatically when the series 767 // recording is deleted. 768 updateScheduledRecording(false, ScheduledRecording.toArray(toUpdate)); 769 } 770 if (!toDelete.isEmpty()) { 771 removeScheduledRecording(true, ScheduledRecording.toArray(toDelete)); 772 } 773 if (mDvrLoadFinished) { 774 notifySeriesRecordingRemoved(seriesRecordings); 775 } 776 ListenableFuture deleteSeriesRecordingFuture = 777 new DeleteSeriesRecordingFuture(mContext) 778 .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings); 779 mNoStopFuture.add(deleteSeriesRecordingFuture); 780 removeDeletedSchedules(seriesRecordings); 781 } 782 783 @Override updateScheduledRecording(final ScheduledRecording... schedules)784 public void updateScheduledRecording(final ScheduledRecording... schedules) { 785 updateScheduledRecording(true, schedules); 786 } 787 updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules)788 private void updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules) { 789 List<ScheduledRecording> toUpdate = new ArrayList<>(); 790 Set<Long> seriesRecordingIdsToCheck = new HashSet<>(); 791 for (ScheduledRecording r : schedules) { 792 if (!SoftPreconditions.checkState( 793 mScheduledRecordings.containsKey(r.getId()), 794 TAG, 795 "Recording not found for: " + r)) { 796 continue; 797 } 798 toUpdate.add(r); 799 ScheduledRecording oldScheduledRecording = mScheduledRecordings.put(r.getId(), r); 800 // The channel ID should not be changed. 801 SoftPreconditions.checkState(r.getChannelId() == oldScheduledRecording.getChannelId()); 802 long programId = r.getProgramId(); 803 if (oldScheduledRecording.getProgramId() != programId 804 && oldScheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) { 805 ScheduledRecording oldValueForProgramId = 806 mProgramId2ScheduledRecordings.get(oldScheduledRecording.getProgramId()); 807 if (oldValueForProgramId.getId() == r.getId()) { 808 // Only remove the old ScheduledRecording if it has the same ID as the new one. 809 mProgramId2ScheduledRecordings.remove(oldScheduledRecording.getProgramId()); 810 } 811 } 812 if (programId != ScheduledRecording.ID_NOT_SET) { 813 mProgramId2ScheduledRecordings.put(programId, r); 814 } 815 if (r.getState() == ScheduledRecording.STATE_RECORDING_FAILED 816 && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { 817 // If the scheduled recording is failed, it may cause the automatically generated 818 // series recording for this schedule becomes invalid (with no future schedules and 819 // past recordings.) We should check and remove these series recordings. 820 seriesRecordingIdsToCheck.add(r.getSeriesRecordingId()); 821 } 822 } 823 if (toUpdate.isEmpty()) { 824 return; 825 } 826 ScheduledRecording[] scheduleArray = ScheduledRecording.toArray(toUpdate); 827 if (mDvrLoadFinished) { 828 notifyScheduledRecordingStatusChanged(scheduleArray); 829 } 830 if (updateDb) { 831 ListenableFuture updateScheduleFuture = 832 new UpdateScheduleFuture(mContext) 833 .executeOnDbThread(removeFromSetOnCompletion, scheduleArray); 834 mNoStopFuture.add(updateScheduleFuture); 835 } 836 checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck); 837 removeDeletedSchedules(schedules); 838 } 839 840 @Override updateSeriesRecording(final SeriesRecording... seriesRecordings)841 public void updateSeriesRecording(final SeriesRecording... seriesRecordings) { 842 for (SeriesRecording r : seriesRecordings) { 843 if (!SoftPreconditions.checkArgument( 844 mSeriesRecordings.containsKey(r.getId()), 845 TAG, 846 "Non Existing Series ID: %s", 847 r)) { 848 continue; 849 } 850 SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r); 851 SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); 852 SoftPreconditions.checkArgument( 853 old1.equals(old2), TAG, "Series ID cannot be updated: %s", r); 854 } 855 if (mDvrLoadFinished) { 856 notifySeriesRecordingChanged(seriesRecordings); 857 } 858 ListenableFuture updateSeriesRecordingFuture = 859 new UpdateSeriesRecordingFuture(mContext) 860 .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings); 861 mNoStopFuture.add(updateSeriesRecordingFuture); 862 } 863 isInputAvailable(String inputId)864 private boolean isInputAvailable(String inputId) { 865 return mInputManager.hasTvInputInfo(inputId) 866 && (!CommonUtils.isBundledInput(inputId) 867 || mStorageStatusManager.isStorageMounted()); 868 } 869 removeDeletedSchedules(ScheduledRecording... addedSchedules)870 private void removeDeletedSchedules(ScheduledRecording... addedSchedules) { 871 List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); 872 for (ScheduledRecording r : addedSchedules) { 873 ScheduledRecording deleted = getDeletedScheduleMap().remove(r.getProgramId()); 874 if (deleted != null) { 875 schedulesToDelete.add(deleted); 876 } 877 } 878 if (!schedulesToDelete.isEmpty()) { 879 ListenableFuture deleteScheduleFuture = 880 new DeleteScheduleFuture(mContext) 881 .executeOnDbThread( 882 removeFromSetOnCompletion, 883 ScheduledRecording.toArray(schedulesToDelete)); 884 mNoStopFuture.add(deleteScheduleFuture); 885 } 886 } 887 removeDeletedSchedules(SeriesRecording... removedSeriesRecordings)888 private void removeDeletedSchedules(SeriesRecording... removedSeriesRecordings) { 889 Set<Long> seriesRecordingIds = new HashSet<>(); 890 for (SeriesRecording r : removedSeriesRecordings) { 891 seriesRecordingIds.add(r.getId()); 892 } 893 List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); 894 Iterator<Entry<Long, ScheduledRecording>> iter = 895 getDeletedScheduleMap().entrySet().iterator(); 896 while (iter.hasNext()) { 897 Entry<Long, ScheduledRecording> entry = iter.next(); 898 if (seriesRecordingIds.contains(entry.getValue().getSeriesRecordingId())) { 899 schedulesToDelete.add(entry.getValue()); 900 iter.remove(); 901 } 902 } 903 if (!schedulesToDelete.isEmpty()) { 904 ListenableFuture deleteScheduleFuture = 905 new DeleteScheduleFuture(mContext) 906 .executeOnDbThread( 907 removeFromSetOnCompletion, 908 ScheduledRecording.toArray(schedulesToDelete)); 909 mNoStopFuture.add(deleteScheduleFuture); 910 } 911 } 912 unhideInput(String inputId)913 private void unhideInput(String inputId) { 914 if (DEBUG) Log.d(TAG, "unhideInput " + inputId); 915 List<ScheduledRecording> movedSchedules = 916 moveElements( 917 mScheduledRecordingsForRemovedInput, 918 mScheduledRecordings, 919 r -> r.getInputId().equals(inputId)); 920 List<RecordedProgram> movedRecordedPrograms = 921 moveElements( 922 mRecordedProgramsForRemovedInput, 923 mRecordedPrograms, 924 r -> r.getInputId().equals(inputId)); 925 List<SeriesRecording> removedSeriesRecordings = new ArrayList<>(); 926 List<SeriesRecording> movedSeriesRecordings = 927 moveElements( 928 mSeriesRecordingsForRemovedInput, 929 mSeriesRecordings, 930 r -> { 931 if (r.getInputId().equals(inputId)) { 932 if (!isEmptySeriesRecording(r)) { 933 return true; 934 } 935 removedSeriesRecordings.add(r); 936 } 937 return false; 938 }); 939 if (!movedSchedules.isEmpty()) { 940 for (ScheduledRecording schedule : movedSchedules) { 941 mProgramId2ScheduledRecordings.put(schedule.getProgramId(), schedule); 942 } 943 } 944 if (!movedSeriesRecordings.isEmpty()) { 945 for (SeriesRecording seriesRecording : movedSeriesRecordings) { 946 mSeriesId2SeriesRecordings.put(seriesRecording.getSeriesId(), seriesRecording); 947 } 948 } 949 for (SeriesRecording r : removedSeriesRecordings) { 950 mSeriesRecordingsForRemovedInput.remove(r.getId()); 951 } 952 ListenableFuture deleteSeriesRecordingFuture = 953 new DeleteSeriesRecordingFuture(mContext) 954 .executeOnDbThread( 955 removeFromSetOnCompletion, 956 SeriesRecording.toArray(removedSeriesRecordings)); 957 mNoStopFuture.add(deleteSeriesRecordingFuture); 958 // Notify after all the data are moved. 959 if (!movedSchedules.isEmpty()) { 960 notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules)); 961 } 962 if (!movedSeriesRecordings.isEmpty()) { 963 notifySeriesRecordingAdded(SeriesRecording.toArray(movedSeriesRecordings)); 964 } 965 if (!movedRecordedPrograms.isEmpty()) { 966 notifyRecordedProgramsAdded(RecordedProgram.toArray(movedRecordedPrograms)); 967 } 968 } 969 hideInput(String inputId)970 private void hideInput(String inputId) { 971 if (DEBUG) Log.d(TAG, "hideInput " + inputId); 972 List<ScheduledRecording> movedSchedules = 973 moveElements( 974 mScheduledRecordings, 975 mScheduledRecordingsForRemovedInput, 976 r -> r.getInputId().equals(inputId)); 977 List<SeriesRecording> movedSeriesRecordings = 978 moveElements( 979 mSeriesRecordings, 980 mSeriesRecordingsForRemovedInput, 981 r -> r.getInputId().equals(inputId)); 982 List<RecordedProgram> movedRecordedPrograms = 983 moveElements( 984 mRecordedPrograms, 985 mRecordedProgramsForRemovedInput, 986 r -> r.getInputId().equals(inputId)); 987 if (!movedSchedules.isEmpty()) { 988 for (ScheduledRecording schedule : movedSchedules) { 989 mProgramId2ScheduledRecordings.remove(schedule.getProgramId()); 990 } 991 } 992 if (!movedSeriesRecordings.isEmpty()) { 993 for (SeriesRecording seriesRecording : movedSeriesRecordings) { 994 mSeriesId2SeriesRecordings.remove(seriesRecording.getSeriesId()); 995 } 996 } 997 // Notify after all the data are moved. 998 if (!movedSchedules.isEmpty()) { 999 notifyScheduledRecordingRemoved(ScheduledRecording.toArray(movedSchedules)); 1000 } 1001 if (!movedSeriesRecordings.isEmpty()) { 1002 notifySeriesRecordingRemoved(SeriesRecording.toArray(movedSeriesRecordings)); 1003 } 1004 if (!movedRecordedPrograms.isEmpty()) { 1005 notifyRecordedProgramsRemoved(RecordedProgram.toArray(movedRecordedPrograms)); 1006 } 1007 } 1008 checkAndRemoveEmptySeriesRecording(Set<Long> seriesRecordingIds)1009 private void checkAndRemoveEmptySeriesRecording(Set<Long> seriesRecordingIds) { 1010 int i = 0; 1011 long[] rIds = new long[seriesRecordingIds.size()]; 1012 for (long rId : seriesRecordingIds) { 1013 rIds[i++] = rId; 1014 } 1015 checkAndRemoveEmptySeriesRecording(rIds); 1016 } 1017 1018 @Override forgetStorage(String inputId)1019 public void forgetStorage(String inputId) { 1020 List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); 1021 for (Iterator<ScheduledRecording> i = 1022 mScheduledRecordingsForRemovedInput.values().iterator(); 1023 i.hasNext(); ) { 1024 ScheduledRecording r = i.next(); 1025 if (inputId.equals(r.getInputId())) { 1026 schedulesToDelete.add(r); 1027 i.remove(); 1028 } 1029 } 1030 List<SeriesRecording> seriesRecordingsToDelete = new ArrayList<>(); 1031 for (Iterator<SeriesRecording> i = mSeriesRecordingsForRemovedInput.values().iterator(); 1032 i.hasNext(); ) { 1033 SeriesRecording r = i.next(); 1034 if (inputId.equals(r.getInputId())) { 1035 seriesRecordingsToDelete.add(r); 1036 i.remove(); 1037 } 1038 } 1039 for (Iterator<RecordedProgram> i = mRecordedProgramsForRemovedInput.values().iterator(); 1040 i.hasNext(); ) { 1041 if (inputId.equals(i.next().getInputId())) { 1042 i.remove(); 1043 } 1044 } 1045 ListenableFuture deleteScheduleFuture = 1046 new DeleteScheduleFuture(mContext) 1047 .executeOnDbThread( 1048 removeFromSetOnCompletion, 1049 ScheduledRecording.toArray(schedulesToDelete)); 1050 mNoStopFuture.add(deleteScheduleFuture); 1051 ListenableFuture deleteSeriesRecordingFuture = 1052 new DeleteSeriesRecordingFuture(mContext) 1053 .executeOnDbThread( 1054 removeFromSetOnCompletion, 1055 SeriesRecording.toArray(seriesRecordingsToDelete)); 1056 mNoStopFuture.add(deleteSeriesRecordingFuture); 1057 new AsyncDbTask<Void, Void, Void>(mDbExecutor) { 1058 @Override 1059 protected Void doInBackground(Void... params) { 1060 ContentResolver resolver = mContext.getContentResolver(); 1061 String[] args = {inputId}; 1062 try { 1063 resolver.delete( 1064 RecordedPrograms.CONTENT_URI, 1065 RecordedPrograms.COLUMN_INPUT_ID + " = ?", 1066 args); 1067 } catch (SQLiteException e) { 1068 Log.e(TAG, "Failed to delete recorded programs for inputId: " + inputId, e); 1069 } 1070 return null; 1071 } 1072 }.executeOnDbThread(); 1073 } 1074 validateSeriesRecordings()1075 private void validateSeriesRecordings() { 1076 Iterator<SeriesRecording> iter = mSeriesRecordings.values().iterator(); 1077 List<SeriesRecording> removedSeriesRecordings = new ArrayList<>(); 1078 while (iter.hasNext()) { 1079 SeriesRecording r = iter.next(); 1080 if (isEmptySeriesRecording(r)) { 1081 iter.remove(); 1082 removedSeriesRecordings.add(r); 1083 } 1084 } 1085 if (!removedSeriesRecordings.isEmpty()) { 1086 SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings); 1087 ListenableFuture deleteSeriesRecordingFuture = 1088 new DeleteSeriesRecordingFuture(mContext) 1089 .executeOnDbThread(removeFromSetOnCompletion, removed); 1090 mNoStopFuture.add(deleteSeriesRecordingFuture); 1091 if (mDvrLoadFinished) { 1092 notifySeriesRecordingRemoved(removed); 1093 } 1094 } 1095 } 1096 1097 private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask { 1098 private final Uri mUri; 1099 RecordedProgramsQueryTask(Uri uri)1100 public RecordedProgramsQueryTask(Uri uri) { 1101 super(mDbExecutor, mContext, uri == null ? RecordedPrograms.CONTENT_URI : uri); 1102 mUri = uri; 1103 } 1104 1105 @Override onCancelled(List<RecordedProgram> scheduledRecordings)1106 protected void onCancelled(List<RecordedProgram> scheduledRecordings) { 1107 mPendingTasks.remove(this); 1108 } 1109 1110 @Override onPostExecute(List<RecordedProgram> result)1111 protected void onPostExecute(List<RecordedProgram> result) { 1112 mPendingTasks.remove(this); 1113 onRecordedProgramsLoadedFinished(mUri, result); 1114 } 1115 } 1116 } 1117