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