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