• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.provider;
18 
19 import android.annotation.SuppressLint;
20 import android.annotation.TargetApi;
21 import android.content.ContentUris;
22 import android.content.Context;
23 import android.database.ContentObserver;
24 import android.media.tv.TvContract.Programs;
25 import android.net.Uri;
26 import android.os.Build;
27 import android.os.Handler;
28 import android.os.Looper;
29 import android.support.annotation.MainThread;
30 import android.support.annotation.VisibleForTesting;
31 import android.util.Log;
32 
33 import com.android.tv.TvApplication;
34 import com.android.tv.data.ChannelDataManager;
35 import com.android.tv.data.Program;
36 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
37 import com.android.tv.dvr.DvrDataManagerImpl;
38 import com.android.tv.dvr.DvrManager;
39 import com.android.tv.dvr.data.ScheduledRecording;
40 import com.android.tv.dvr.data.SeriesRecording;
41 import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
42 import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask;
43 import com.android.tv.util.TvUriMatcher;
44 
45 import java.util.ArrayList;
46 import java.util.Collections;
47 import java.util.HashSet;
48 import java.util.LinkedList;
49 import java.util.List;
50 import java.util.Objects;
51 import java.util.Queue;
52 import java.util.Set;
53 
54 /**
55  * A class to synchronizes DVR DB with TvProvider.
56  *
57  * <p>The current implementation of AsyncDbTask allows only one task to run at a time, and all the
58  * other tasks are blocked until the current one finishes. As this class performs the low priority
59  * jobs which take long time, it should not block others if possible. For this reason, only one
60  * program is queried at a time and others are queued and will be executed on the other
61  * AsyncDbTask's after the current one finishes to minimize the execution time of one AsyncDbTask.
62  */
63 @MainThread
64 @TargetApi(Build.VERSION_CODES.N)
65 public class DvrDbSync {
66     private static final String TAG = "DvrDbSync";
67     private static final boolean DEBUG = false;
68 
69     private final Context mContext;
70     private final DvrManager mDvrManager;
71     private final DvrDataManagerImpl mDataManager;
72     private final ChannelDataManager mChannelDataManager;
73     private final Queue<Long> mProgramIdQueue = new LinkedList<>();
74     private QueryProgramTask mQueryProgramTask;
75     private final SeriesRecordingScheduler mSeriesRecordingScheduler;
76     private final ContentObserver mContentObserver = new ContentObserver(new Handler(
77             Looper.getMainLooper())) {
78         @SuppressLint("SwitchIntDef")
79         @Override
80         public void onChange(boolean selfChange, Uri uri) {
81             switch (TvUriMatcher.match(uri)) {
82                 case TvUriMatcher.MATCH_PROGRAM:
83                     if (DEBUG) Log.d(TAG, "onProgramsUpdated");
84                     onProgramsUpdated();
85                     break;
86                 case TvUriMatcher.MATCH_PROGRAM_ID:
87                     if (DEBUG) {
88                         Log.d(TAG, "onProgramUpdated: programId=" + ContentUris.parseId(uri));
89                     }
90                     onProgramUpdated(ContentUris.parseId(uri));
91                     break;
92             }
93         }
94     };
95 
96     private final ChannelDataManager.Listener mChannelDataManagerListener =
97             new ChannelDataManager.Listener() {
98                 @Override
99                 public void onLoadFinished() {
100                     start();
101                 }
102 
103                 @Override
104                 public void onChannelListUpdated() {
105                     onChannelsUpdated();
106                 }
107 
108                 @Override
109                 public void onChannelBrowsableChanged() { }
110             };
111 
112     private final ScheduledRecordingListener mScheduleListener = new ScheduledRecordingListener() {
113         @Override
114         public void onScheduledRecordingAdded(ScheduledRecording... schedules) {
115             for (ScheduledRecording schedule : schedules) {
116                 addProgramIdToCheckIfNeeded(schedule);
117             }
118             startNextUpdateIfNeeded();
119         }
120 
121         @Override
122         public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
123             for (ScheduledRecording schedule : schedules) {
124                 mProgramIdQueue.remove(schedule.getProgramId());
125             }
126         }
127 
128         @Override
129         public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
130             for (ScheduledRecording schedule : schedules) {
131                 mProgramIdQueue.remove(schedule.getProgramId());
132                 addProgramIdToCheckIfNeeded(schedule);
133             }
134             startNextUpdateIfNeeded();
135         }
136     };
137 
DvrDbSync(Context context, DvrDataManagerImpl dataManager)138     public DvrDbSync(Context context, DvrDataManagerImpl dataManager) {
139         this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager(),
140                 TvApplication.getSingletons(context).getDvrManager(),
141                 SeriesRecordingScheduler.getInstance(context));
142     }
143 
144     @VisibleForTesting
DvrDbSync(Context context, DvrDataManagerImpl dataManager, ChannelDataManager channelDataManager, DvrManager dvrManager, SeriesRecordingScheduler seriesRecordingScheduler)145     DvrDbSync(Context context, DvrDataManagerImpl dataManager,
146             ChannelDataManager channelDataManager, DvrManager dvrManager,
147             SeriesRecordingScheduler seriesRecordingScheduler) {
148         mContext = context;
149         mDvrManager = dvrManager;
150         mDataManager = dataManager;
151         mChannelDataManager = channelDataManager;
152         mSeriesRecordingScheduler = seriesRecordingScheduler;
153     }
154 
155     /**
156      * Starts the DB sync.
157      */
start()158     public void start() {
159         if (!mChannelDataManager.isDbLoadFinished()) {
160             mChannelDataManager.addListener(mChannelDataManagerListener);
161             return;
162         }
163         mContext.getContentResolver().registerContentObserver(Programs.CONTENT_URI, true,
164                 mContentObserver);
165         mDataManager.addScheduledRecordingListener(mScheduleListener);
166         onChannelsUpdated();
167         onProgramsUpdated();
168     }
169 
170     /**
171      * Stops the DB sync.
172      */
stop()173     public void stop() {
174         mProgramIdQueue.clear();
175         if (mQueryProgramTask != null) {
176             mQueryProgramTask.cancel(true);
177         }
178         mChannelDataManager.removeListener(mChannelDataManagerListener);
179         mDataManager.removeScheduledRecordingListener(mScheduleListener);
180         mContext.getContentResolver().unregisterContentObserver(mContentObserver);
181     }
182 
onChannelsUpdated()183     private void onChannelsUpdated() {
184         List<SeriesRecording> seriesRecordingsToUpdate = new ArrayList<>();
185         for (SeriesRecording r : mDataManager.getSeriesRecordings()) {
186             if (r.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
187                     && !mChannelDataManager.doesChannelExistInDb(r.getChannelId())) {
188                 seriesRecordingsToUpdate.add(SeriesRecording.buildFrom(r)
189                         .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL)
190                         .setState(SeriesRecording.STATE_SERIES_STOPPED).build());
191             }
192         }
193         if (!seriesRecordingsToUpdate.isEmpty()) {
194             mDataManager.updateSeriesRecording(
195                     SeriesRecording.toArray(seriesRecordingsToUpdate));
196         }
197         List<ScheduledRecording> schedulesToRemove = new ArrayList<>();
198         for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) {
199             if (!mChannelDataManager.doesChannelExistInDb(r.getChannelId())) {
200                 schedulesToRemove.add(r);
201                 mProgramIdQueue.remove(r.getProgramId());
202             }
203         }
204         if (!schedulesToRemove.isEmpty()) {
205             mDataManager.removeScheduledRecording(
206                     ScheduledRecording.toArray(schedulesToRemove));
207         }
208     }
209 
onProgramsUpdated()210     private void onProgramsUpdated() {
211         for (ScheduledRecording schedule : mDataManager.getAvailableScheduledRecordings()) {
212             addProgramIdToCheckIfNeeded(schedule);
213         }
214         startNextUpdateIfNeeded();
215     }
216 
onProgramUpdated(long programId)217     private void onProgramUpdated(long programId) {
218         addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId(programId));
219         startNextUpdateIfNeeded();
220     }
221 
addProgramIdToCheckIfNeeded(ScheduledRecording schedule)222     private void addProgramIdToCheckIfNeeded(ScheduledRecording schedule) {
223         if (schedule == null) {
224             return;
225         }
226         long programId = schedule.getProgramId();
227         if (programId != ScheduledRecording.ID_NOT_SET
228                 && !mProgramIdQueue.contains(programId)
229                 && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
230                 || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
231             if (DEBUG) Log.d(TAG, "Program ID enqueued: " + programId);
232             mProgramIdQueue.offer(programId);
233             // There are schedules to be updated. Pause the SeriesRecordingScheduler until all the
234             // schedule updates finish.
235             // Note that the SeriesRecordingScheduler should be paused even though the program to
236             // check is not episodic because it can be changed to the episodic program after the
237             // update, which affect the SeriesRecordingScheduler.
238             mSeriesRecordingScheduler.pauseUpdate();
239         }
240     }
241 
startNextUpdateIfNeeded()242     private void startNextUpdateIfNeeded() {
243         if (mQueryProgramTask != null && !mQueryProgramTask.isCancelled()) {
244             return;
245         }
246         if (!mProgramIdQueue.isEmpty()) {
247             if (DEBUG) Log.d(TAG, "Program ID dequeued: " + mProgramIdQueue.peek());
248             mQueryProgramTask = new QueryProgramTask(mProgramIdQueue.poll());
249             mQueryProgramTask.executeOnDbThread();
250         } else {
251             mSeriesRecordingScheduler.resumeUpdate();
252         }
253     }
254 
255     @VisibleForTesting
handleUpdateProgram(Program program, long programId)256     void handleUpdateProgram(Program program, long programId) {
257         Set<SeriesRecording> seriesRecordingsToUpdate = new HashSet<>();
258         ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(programId);
259         if (schedule != null
260                 && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
261                 || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
262             if (program == null) {
263                 mDataManager.removeScheduledRecording(schedule);
264                 if (schedule.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) {
265                     SeriesRecording seriesRecording =
266                             mDataManager.getSeriesRecording(schedule.getSeriesRecordingId());
267                     if (seriesRecording != null) {
268                         seriesRecordingsToUpdate.add(seriesRecording);
269                     }
270                 }
271             } else {
272                 long currentTimeMs = System.currentTimeMillis();
273                 ScheduledRecording.Builder builder = ScheduledRecording.buildFrom(schedule)
274                         .setEndTimeMs(program.getEndTimeUtcMillis())
275                         .setSeasonNumber(program.getSeasonNumber())
276                         .setEpisodeNumber(program.getEpisodeNumber())
277                         .setEpisodeTitle(program.getEpisodeTitle())
278                         .setProgramDescription(program.getDescription())
279                         .setProgramLongDescription(program.getLongDescription())
280                         .setProgramPosterArtUri(program.getPosterArtUri())
281                         .setProgramThumbnailUri(program.getThumbnailUri());
282                 boolean needUpdate = false;
283                 // Check the series recording.
284                 SeriesRecording seriesRecordingForOldSchedule =
285                         mDataManager.getSeriesRecording(schedule.getSeriesRecordingId());
286                 if (program.isEpisodic()) {
287                     // New program belongs to a series.
288                     SeriesRecording seriesRecording =
289                             mDataManager.getSeriesRecording(program.getSeriesId());
290                     if (seriesRecording == null) {
291                         // The new program is episodic while the previous one isn't.
292                         SeriesRecording newSeriesRecording = mDvrManager.addSeriesRecording(
293                                 program, Collections.singletonList(program),
294                                 SeriesRecording.STATE_SERIES_STOPPED);
295                         builder.setSeriesRecordingId(newSeriesRecording.getId());
296                         needUpdate = true;
297                     } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) {
298                         // The new program belongs to the other series.
299                         builder.setSeriesRecordingId(seriesRecording.getId());
300                         needUpdate = true;
301                         seriesRecordingsToUpdate.add(seriesRecording);
302                         if (seriesRecordingForOldSchedule != null) {
303                             seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
304                         }
305                     } else if (!Objects.equals(schedule.getSeasonNumber(),
306                                     program.getSeasonNumber())
307                             || !Objects.equals(schedule.getEpisodeNumber(),
308                                     program.getEpisodeNumber())) {
309                         // The episode number has been changed.
310                         if (seriesRecordingForOldSchedule != null) {
311                             seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
312                         }
313                     }
314                 } else if (seriesRecordingForOldSchedule != null) {
315                     // Old program belongs to a series but the new one doesn't.
316                     seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
317                 }
318                 // Change start time only when the recording is not started yet.
319                 boolean needToChangeStartTime =
320                         schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS
321                         && program.getStartTimeUtcMillis() != schedule.getStartTimeMs();
322                 if (needToChangeStartTime) {
323                     builder.setStartTimeMs(program.getStartTimeUtcMillis());
324                     needUpdate = true;
325                 }
326                 if (needUpdate || schedule.getEndTimeMs() != program.getEndTimeUtcMillis()
327                         || !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber())
328                         || !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber())
329                         || !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle())
330                         || !Objects.equals(schedule.getProgramDescription(),
331                         program.getDescription())
332                         || !Objects.equals(schedule.getProgramLongDescription(),
333                         program.getLongDescription())
334                         || !Objects.equals(schedule.getProgramPosterArtUri(),
335                         program.getPosterArtUri())
336                         || !Objects.equals(schedule.getProgramThumbnailUri(),
337                         program.getThumbnailUri())) {
338                     mDataManager.updateScheduledRecording(builder.build());
339                 }
340                 if (!seriesRecordingsToUpdate.isEmpty()) {
341                     // The series recordings will be updated after it's resumed.
342                     mSeriesRecordingScheduler.updateSchedules(seriesRecordingsToUpdate);
343                 }
344             }
345         }
346     }
347 
348     private class QueryProgramTask extends AsyncQueryProgramTask {
349         private final long mProgramId;
350 
QueryProgramTask(long programId)351         QueryProgramTask(long programId) {
352             super(mContext.getContentResolver(), programId);
353             mProgramId = programId;
354         }
355 
356         @Override
onCancelled(Program program)357         protected void onCancelled(Program program) {
358             if (mQueryProgramTask == this) {
359                 mQueryProgramTask = null;
360             }
361             startNextUpdateIfNeeded();
362         }
363 
364         @Override
onPostExecute(Program program)365         protected void onPostExecute(Program program) {
366             if (mQueryProgramTask == this) {
367                 mQueryProgramTask = null;
368             }
369             handleUpdateProgram(program, mProgramId);
370             startNextUpdateIfNeeded();
371         }
372     }
373 }
374