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