1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.dvr;
18 
19 import android.annotation.TargetApi;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.Context;
23 import android.database.ContentObserver;
24 import android.database.Cursor;
25 import android.media.tv.TvContract;
26 import android.net.Uri;
27 import android.os.AsyncTask;
28 import android.os.Build;
29 import android.os.Handler;
30 import android.os.Looper;
31 import android.support.annotation.MainThread;
32 import android.support.annotation.Nullable;
33 import android.support.annotation.VisibleForTesting;
34 import android.util.ArraySet;
35 import android.util.Log;
36 import android.util.Range;
37 
38 import com.android.tv.common.SoftPreconditions;
39 import com.android.tv.common.recording.RecordedProgram;
40 import com.android.tv.dvr.ScheduledRecording.RecordingState;
41 import com.android.tv.dvr.provider.AsyncDvrDbTask;
42 import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryTask;
43 import com.android.tv.util.AsyncDbTask;
44 import com.android.tv.util.Clock;
45 
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.HashMap;
49 import java.util.Iterator;
50 import java.util.List;
51 import java.util.Set;
52 
53 /**
54  * DVR Data manager to handle recordings and schedules.
55  */
56 @MainThread
57 @TargetApi(Build.VERSION_CODES.N)
58 public class DvrDataManagerImpl extends BaseDvrDataManager {
59     private static final String TAG = "DvrDataManagerImpl";
60     private static final boolean DEBUG = false;
61 
62     private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>();
63     private final HashMap<Long, ScheduledRecording> mProgramId2ScheduledRecordings =
64             new HashMap<>();
65     private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>();
66 
67     private final Context mContext;
68     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
69     private final ContentObserver mContentObserver = new ContentObserver(mMainThreadHandler) {
70 
71         @Override
72         public void onChange(boolean selfChange) {
73             onChange(selfChange, null);
74         }
75 
76         @Override
77         public void onChange(boolean selfChange, @Nullable final Uri uri) {
78             if (uri == null) {
79                 // TODO reload everything.
80             }
81             AsyncRecordedProgramQueryTask task = new AsyncRecordedProgramQueryTask(
82                     mContext.getContentResolver(), uri);
83             task.executeOnDbThread();
84             mPendingTasks.add(task);
85         }
86     };
87 
onObservedChange(Uri uri, RecordedProgram recordedProgram)88     private void onObservedChange(Uri uri, RecordedProgram recordedProgram) {
89         long id = ContentUris.parseId(uri);
90         if (DEBUG) {
91             Log.d(TAG, "changed recorded program #" + id + " to " + recordedProgram);
92         }
93         if (recordedProgram == null) {
94             RecordedProgram old = mRecordedPrograms.remove(id);
95             if (old != null) {
96                 notifyRecordedProgramRemoved(old);
97             } else {
98                 Log.w(TAG, "Could not find old version of deleted program #" + id);
99             }
100         } else {
101             RecordedProgram old = mRecordedPrograms.put(id, recordedProgram);
102             if (old == null) {
103                 notifyRecordedProgramAdded(recordedProgram);
104             } else {
105                 notifyRecordedProgramChanged(recordedProgram);
106             }
107         }
108     }
109 
110     private boolean mDvrLoadFinished;
111     private boolean mRecordedProgramLoadFinished;
112     private final Set<AsyncTask> mPendingTasks = new ArraySet<>();
113 
DvrDataManagerImpl(Context context, Clock clock)114     public DvrDataManagerImpl(Context context, Clock clock) {
115         super(context, clock);
116         mContext = context;
117     }
118 
start()119     public void start() {
120         AsyncDvrQueryTask mDvrQueryTask = new AsyncDvrQueryTask(mContext) {
121 
122             @Override
123             protected void onCancelled(List<ScheduledRecording> scheduledRecordings) {
124                 mPendingTasks.remove(this);
125             }
126 
127             @Override
128             protected void onPostExecute(List<ScheduledRecording> result) {
129                 mPendingTasks.remove(this);
130                 mDvrLoadFinished = true;
131                 for (ScheduledRecording r : result) {
132                     mScheduledRecordings.put(r.getId(), r);
133                 }
134             }
135         };
136         mDvrQueryTask.executeOnDbThread();
137         mPendingTasks.add(mDvrQueryTask);
138         AsyncRecordedProgramsQueryTask mRecordedProgramQueryTask =
139                 new AsyncRecordedProgramsQueryTask(mContext.getContentResolver());
140         mRecordedProgramQueryTask.executeOnDbThread();
141         ContentResolver cr = mContext.getContentResolver();
142         cr.registerContentObserver(TvContract.RecordedPrograms.CONTENT_URI, true, mContentObserver);
143     }
144 
stop()145     public void stop() {
146         ContentResolver cr = mContext.getContentResolver();
147         cr.unregisterContentObserver(mContentObserver);
148         Iterator<AsyncTask> i = mPendingTasks.iterator();
149         while (i.hasNext()) {
150             AsyncTask task = i.next();
151             i.remove();
152             task.cancel(true);
153         }
154     }
155 
156     @Override
isInitialized()157     public boolean isInitialized() {
158         return mDvrLoadFinished && mRecordedProgramLoadFinished;
159     }
160 
getScheduledRecordingsPrograms()161     private List<ScheduledRecording> getScheduledRecordingsPrograms() {
162         if (!mDvrLoadFinished) {
163             return Collections.emptyList();
164         }
165         ArrayList<ScheduledRecording> list = new ArrayList<>(mScheduledRecordings.size());
166         list.addAll(mScheduledRecordings.values());
167         Collections.sort(list, ScheduledRecording.START_TIME_COMPARATOR);
168         return list;
169     }
170 
171     @Override
getRecordedPrograms()172     public List<RecordedProgram> getRecordedPrograms() {
173         if (!mRecordedProgramLoadFinished) {
174             return Collections.emptyList();
175         }
176         return new ArrayList<>(mRecordedPrograms.values());
177     }
178 
179     @Override
getAllScheduledRecordings()180     public List<ScheduledRecording> getAllScheduledRecordings() {
181         return new ArrayList<>(mScheduledRecordings.values());
182     }
183 
getRecordingsWithState(@ecordingState int state)184     protected List<ScheduledRecording> getRecordingsWithState(@RecordingState int state) {
185         List<ScheduledRecording> result = new ArrayList<>();
186         for (ScheduledRecording r : mScheduledRecordings.values()) {
187             if (r.getState() == state) {
188                 result.add(r);
189             }
190         }
191         return result;
192     }
193 
194     @Override
getSeasonRecordings()195     public List<SeasonRecording> getSeasonRecordings() {
196         // If we return dummy data here, we can implement UI part independently.
197         return Collections.emptyList();
198     }
199 
200     @Override
getNextScheduledStartTimeAfter(long startTime)201     public long getNextScheduledStartTimeAfter(long startTime) {
202         return getNextStartTimeAfter(getScheduledRecordingsPrograms(), startTime);
203     }
204 
205     @VisibleForTesting
getNextStartTimeAfter(List<ScheduledRecording> scheduledRecordings, long startTime)206     static long getNextStartTimeAfter(List<ScheduledRecording> scheduledRecordings, long startTime) {
207         int start = 0;
208         int end = scheduledRecordings.size() - 1;
209         while (start <= end) {
210             int mid = (start + end) / 2;
211             if (scheduledRecordings.get(mid).getStartTimeMs() <= startTime) {
212                 start = mid + 1;
213             } else {
214                 end = mid - 1;
215             }
216         }
217         return start < scheduledRecordings.size() ? scheduledRecordings.get(start).getStartTimeMs()
218                 : NEXT_START_TIME_NOT_FOUND;
219     }
220 
221     @Override
getRecordingsThatOverlapWith(Range<Long> period)222     public List<ScheduledRecording> getRecordingsThatOverlapWith(Range<Long> period) {
223         List<ScheduledRecording> result = new ArrayList<>();
224         for (ScheduledRecording r : mScheduledRecordings.values()) {
225             if (r.isOverLapping(period)) {
226                 result.add(r);
227             }
228         }
229         return result;
230     }
231 
232     @Nullable
233     @Override
getScheduledRecording(long recordingId)234     public ScheduledRecording getScheduledRecording(long recordingId) {
235         if (mDvrLoadFinished) {
236             return mScheduledRecordings.get(recordingId);
237         }
238         return null;
239     }
240 
241     @Nullable
242     @Override
getScheduledRecordingForProgramId(long programId)243     public ScheduledRecording getScheduledRecordingForProgramId(long programId) {
244         if (mDvrLoadFinished) {
245             return mProgramId2ScheduledRecordings.get(programId);
246         }
247         return null;
248     }
249 
250     @Nullable
251     @Override
getRecordedProgram(long recordingId)252     public RecordedProgram getRecordedProgram(long recordingId) {
253         return mRecordedPrograms.get(recordingId);
254     }
255 
256     @Override
addScheduledRecording(final ScheduledRecording scheduledRecording)257     public void addScheduledRecording(final ScheduledRecording scheduledRecording) {
258         new AsyncDvrDbTask.AsyncAddRecordingTask(mContext) {
259             @Override
260             protected void onPostExecute(List<ScheduledRecording> scheduledRecordings) {
261                 super.onPostExecute(scheduledRecordings);
262                 SoftPreconditions.checkArgument(scheduledRecordings.size() == 1);
263                 for (ScheduledRecording r : scheduledRecordings) {
264                     if (r.getId() != -1) {
265                         mScheduledRecordings.put(r.getId(), r);
266                         if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
267                             mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
268                         }
269                         notifyScheduledRecordingAdded(r);
270                     } else {
271                         Log.w(TAG, "Error adding " + r);
272                     }
273                 }
274 
275             }
276         }.executeOnDbThread(scheduledRecording);
277     }
278 
279     @Override
addSeasonRecording(SeasonRecording seasonRecording)280     public void addSeasonRecording(SeasonRecording seasonRecording) { }
281 
282     @Override
removeScheduledRecording(final ScheduledRecording scheduledRecording)283     public void removeScheduledRecording(final ScheduledRecording scheduledRecording) {
284         new AsyncDvrDbTask.AsyncDeleteRecordingTask(mContext) {
285             @Override
286             protected void onPostExecute(List<Integer> counts) {
287                 super.onPostExecute(counts);
288                 SoftPreconditions.checkArgument(counts.size() == 1);
289                 for (Integer c : counts) {
290                     if (c == 1) {
291                         mScheduledRecordings.remove(scheduledRecording.getId());
292                         if (scheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) {
293                             mProgramId2ScheduledRecordings
294                                     .remove(scheduledRecording.getProgramId());
295                         }
296                         //TODO change to notifyRecordingUpdated
297                         notifyScheduledRecordingRemoved(scheduledRecording);
298                     } else {
299                         Log.w(TAG, "Error removing " + scheduledRecording);
300                     }
301                 }
302 
303             }
304         }.executeOnDbThread(scheduledRecording);
305     }
306 
307     @Override
removeSeasonSchedule(SeasonRecording seasonSchedule)308     public void removeSeasonSchedule(SeasonRecording seasonSchedule) { }
309 
310     @Override
updateScheduledRecording(final ScheduledRecording scheduledRecording)311     public void updateScheduledRecording(final ScheduledRecording scheduledRecording) {
312         new AsyncDvrDbTask.AsyncUpdateRecordingTask(mContext) {
313             @Override
314             protected void onPostExecute(List<Integer> counts) {
315                 super.onPostExecute(counts);
316                 SoftPreconditions.checkArgument(counts.size() == 1);
317                 for (Integer c : counts) {
318                     if (c == 1) {
319                         ScheduledRecording oldScheduledRecording = mScheduledRecordings
320                                 .put(scheduledRecording.getId(), scheduledRecording);
321                         long programId = scheduledRecording.getProgramId();
322                         if (oldScheduledRecording != null
323                                 && oldScheduledRecording.getProgramId() != programId
324                                 && oldScheduledRecording.getProgramId()
325                                 != ScheduledRecording.ID_NOT_SET) {
326                             ScheduledRecording oldValueForProgramId = mProgramId2ScheduledRecordings
327                                     .get(oldScheduledRecording.getProgramId());
328                             if (oldValueForProgramId.getId() == scheduledRecording.getId()) {
329                                 //Only remove the old ScheduledRecording if it has the same ID as
330                                 // the new one.
331                                 mProgramId2ScheduledRecordings
332                                         .remove(oldScheduledRecording.getProgramId());
333                             }
334                         }
335                         if (programId != ScheduledRecording.ID_NOT_SET) {
336                             mProgramId2ScheduledRecordings.put(programId, scheduledRecording);
337                         }
338                         //TODO change to notifyRecordingUpdated
339                         notifyScheduledRecordingStatusChanged(scheduledRecording);
340                     } else {
341                         Log.w(TAG, "Error updating " + scheduledRecording);
342                     }
343                 }
344             }
345         }.executeOnDbThread(scheduledRecording);
346     }
347 
348     private final class AsyncRecordedProgramsQueryTask
349             extends AsyncDbTask.AsyncQueryListTask<RecordedProgram> {
AsyncRecordedProgramsQueryTask(ContentResolver contentResolver)350         public AsyncRecordedProgramsQueryTask(ContentResolver contentResolver) {
351             super(contentResolver, TvContract.RecordedPrograms.CONTENT_URI,
352                     RecordedProgram.PROJECTION, null, null, null);
353         }
354 
355         @Override
fromCursor(Cursor c)356         protected RecordedProgram fromCursor(Cursor c) {
357             return RecordedProgram.fromCursor(c);
358         }
359 
360         @Override
onCancelled(List<RecordedProgram> scheduledRecordings)361         protected void onCancelled(List<RecordedProgram> scheduledRecordings) {
362             mPendingTasks.remove(this);
363         }
364 
365         @Override
onPostExecute(List<RecordedProgram> result)366         protected void onPostExecute(List<RecordedProgram> result) {
367             mPendingTasks.remove(this);
368             mRecordedProgramLoadFinished = true;
369             if (result != null) {
370                 for (RecordedProgram r : result) {
371                     mRecordedPrograms.put(r.getId(), r);
372                 }
373             }
374         }
375     }
376 
377     private final class AsyncRecordedProgramQueryTask
378             extends AsyncDbTask.AsyncQueryItemTask<RecordedProgram> {
379 
380         private final Uri mUri;
381 
AsyncRecordedProgramQueryTask(ContentResolver contentResolver, Uri uri)382         public AsyncRecordedProgramQueryTask(ContentResolver contentResolver, Uri uri) {
383             super(contentResolver, uri, RecordedProgram.PROJECTION, null, null, null);
384             mUri = uri;
385         }
386 
387         @Override
fromCursor(Cursor c)388         protected RecordedProgram fromCursor(Cursor c) {
389             return RecordedProgram.fromCursor(c);
390         }
391 
392         @Override
onCancelled(RecordedProgram recordedProgram)393         protected void onCancelled(RecordedProgram recordedProgram) {
394             mPendingTasks.remove(this);
395         }
396 
397         @Override
onPostExecute(RecordedProgram recordedProgram)398         protected void onPostExecute(RecordedProgram recordedProgram) {
399             mPendingTasks.remove(this);
400             onObservedChange(mUri, recordedProgram);
401         }
402     }
403 }
404