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