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