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
17package com.android.tv.dvr;
18
19import android.annotation.TargetApi;
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.Context;
23import android.database.ContentObserver;
24import android.database.Cursor;
25import android.media.tv.TvContract;
26import android.net.Uri;
27import android.os.AsyncTask;
28import android.os.Build;
29import android.os.Handler;
30import android.os.Looper;
31import android.support.annotation.MainThread;
32import android.support.annotation.Nullable;
33import android.support.annotation.VisibleForTesting;
34import android.util.ArraySet;
35import android.util.Log;
36import android.util.Range;
37
38import com.android.tv.common.SoftPreconditions;
39import com.android.tv.common.recording.RecordedProgram;
40import com.android.tv.dvr.ScheduledRecording.RecordingState;
41import com.android.tv.dvr.provider.AsyncDvrDbTask;
42import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryTask;
43import com.android.tv.util.AsyncDbTask;
44import com.android.tv.util.Clock;
45
46import java.util.ArrayList;
47import java.util.Collections;
48import java.util.HashMap;
49import java.util.Iterator;
50import java.util.List;
51import java.util.Set;
52
53/**
54 * DVR Data manager to handle recordings and schedules.
55 */
56@MainThread
57@TargetApi(Build.VERSION_CODES.N)
58public 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
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
114    public DvrDataManagerImpl(Context context, Clock clock) {
115        super(context, clock);
116        mContext = context;
117    }
118
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
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
157    public boolean isInitialized() {
158        return mDvrLoadFinished && mRecordedProgramLoadFinished;
159    }
160
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
172    public List<RecordedProgram> getRecordedPrograms() {
173        if (!mRecordedProgramLoadFinished) {
174            return Collections.emptyList();
175        }
176        return new ArrayList<>(mRecordedPrograms.values());
177    }
178
179    @Override
180    public List<ScheduledRecording> getAllScheduledRecordings() {
181        return new ArrayList<>(mScheduledRecordings.values());
182    }
183
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
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
201    public long getNextScheduledStartTimeAfter(long startTime) {
202        return getNextStartTimeAfter(getScheduledRecordingsPrograms(), startTime);
203    }
204
205    @VisibleForTesting
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
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
234    public ScheduledRecording getScheduledRecording(long recordingId) {
235        if (mDvrLoadFinished) {
236            return mScheduledRecordings.get(recordingId);
237        }
238        return null;
239    }
240
241    @Nullable
242    @Override
243    public ScheduledRecording getScheduledRecordingForProgramId(long programId) {
244        if (mDvrLoadFinished) {
245            return mProgramId2ScheduledRecordings.get(programId);
246        }
247        return null;
248    }
249
250    @Nullable
251    @Override
252    public RecordedProgram getRecordedProgram(long recordingId) {
253        return mRecordedPrograms.get(recordingId);
254    }
255
256    @Override
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
280    public void addSeasonRecording(SeasonRecording seasonRecording) { }
281
282    @Override
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
308    public void removeSeasonSchedule(SeasonRecording seasonSchedule) { }
309
310    @Override
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> {
350        public AsyncRecordedProgramsQueryTask(ContentResolver contentResolver) {
351            super(contentResolver, TvContract.RecordedPrograms.CONTENT_URI,
352                    RecordedProgram.PROJECTION, null, null, null);
353        }
354
355        @Override
356        protected RecordedProgram fromCursor(Cursor c) {
357            return RecordedProgram.fromCursor(c);
358        }
359
360        @Override
361        protected void onCancelled(List<RecordedProgram> scheduledRecordings) {
362            mPendingTasks.remove(this);
363        }
364
365        @Override
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
382        public AsyncRecordedProgramQueryTask(ContentResolver contentResolver, Uri uri) {
383            super(contentResolver, uri, RecordedProgram.PROJECTION, null, null, null);
384            mUri = uri;
385        }
386
387        @Override
388        protected RecordedProgram fromCursor(Cursor c) {
389            return RecordedProgram.fromCursor(c);
390        }
391
392        @Override
393        protected void onCancelled(RecordedProgram recordedProgram) {
394            mPendingTasks.remove(this);
395        }
396
397        @Override
398        protected void onPostExecute(RecordedProgram recordedProgram) {
399            mPendingTasks.remove(this);
400            onObservedChange(mUri, recordedProgram);
401        }
402    }
403}
404