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