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