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