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