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