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