aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/dvr/provider/DvrDbSync.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/dvr/provider/DvrDbSync.java')
-rw-r--r--src/com/android/tv/dvr/provider/DvrDbSync.java373
1 files changed, 373 insertions, 0 deletions
diff --git a/src/com/android/tv/dvr/provider/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java
new file mode 100644
index 00000000..ff391959
--- /dev/null
+++ b/src/com/android/tv/dvr/provider/DvrDbSync.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.provider;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.media.tv.TvContract.Programs;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.MainThread;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import com.android.tv.TvApplication;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.dvr.DvrDataManagerImpl;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
+import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask;
+import com.android.tv.util.TvUriMatcher;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * A class to synchronizes DVR DB with TvProvider.
+ *
+ * <p>The current implementation of AsyncDbTask allows only one task to run at a time, and all the
+ * other tasks are blocked until the current one finishes. As this class performs the low priority
+ * jobs which take long time, it should not block others if possible. For this reason, only one
+ * program is queried at a time and others are queued and will be executed on the other
+ * AsyncDbTask's after the current one finishes to minimize the execution time of one AsyncDbTask.
+ */
+@MainThread
+@TargetApi(Build.VERSION_CODES.N)
+public class DvrDbSync {
+ private static final String TAG = "DvrDbSync";
+ private static final boolean DEBUG = false;
+
+ private final Context mContext;
+ private final DvrManager mDvrManager;
+ private final DvrDataManagerImpl mDataManager;
+ private final ChannelDataManager mChannelDataManager;
+ private final Queue<Long> mProgramIdQueue = new LinkedList<>();
+ private QueryProgramTask mQueryProgramTask;
+ private final SeriesRecordingScheduler mSeriesRecordingScheduler;
+ private final ContentObserver mContentObserver = new ContentObserver(new Handler(
+ Looper.getMainLooper())) {
+ @SuppressLint("SwitchIntDef")
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ switch (TvUriMatcher.match(uri)) {
+ case TvUriMatcher.MATCH_PROGRAM:
+ if (DEBUG) Log.d(TAG, "onProgramsUpdated");
+ onProgramsUpdated();
+ break;
+ case TvUriMatcher.MATCH_PROGRAM_ID:
+ if (DEBUG) {
+ Log.d(TAG, "onProgramUpdated: programId=" + ContentUris.parseId(uri));
+ }
+ onProgramUpdated(ContentUris.parseId(uri));
+ break;
+ }
+ }
+ };
+
+ private final ChannelDataManager.Listener mChannelDataManagerListener =
+ new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ start();
+ }
+
+ @Override
+ public void onChannelListUpdated() {
+ onChannelsUpdated();
+ }
+
+ @Override
+ public void onChannelBrowsableChanged() { }
+ };
+
+ private final ScheduledRecordingListener mScheduleListener = new ScheduledRecordingListener() {
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ addProgramIdToCheckIfNeeded(schedule);
+ }
+ startNextUpdateIfNeeded();
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ mProgramIdQueue.remove(schedule.getProgramId());
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ mProgramIdQueue.remove(schedule.getProgramId());
+ addProgramIdToCheckIfNeeded(schedule);
+ }
+ startNextUpdateIfNeeded();
+ }
+ };
+
+ public DvrDbSync(Context context, DvrDataManagerImpl dataManager) {
+ this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager(),
+ TvApplication.getSingletons(context).getDvrManager(),
+ SeriesRecordingScheduler.getInstance(context));
+ }
+
+ @VisibleForTesting
+ DvrDbSync(Context context, DvrDataManagerImpl dataManager,
+ ChannelDataManager channelDataManager, DvrManager dvrManager,
+ SeriesRecordingScheduler seriesRecordingScheduler) {
+ mContext = context;
+ mDvrManager = dvrManager;
+ mDataManager = dataManager;
+ mChannelDataManager = channelDataManager;
+ mSeriesRecordingScheduler = seriesRecordingScheduler;
+ }
+
+ /**
+ * Starts the DB sync.
+ */
+ public void start() {
+ if (!mChannelDataManager.isDbLoadFinished()) {
+ mChannelDataManager.addListener(mChannelDataManagerListener);
+ return;
+ }
+ mContext.getContentResolver().registerContentObserver(Programs.CONTENT_URI, true,
+ mContentObserver);
+ mDataManager.addScheduledRecordingListener(mScheduleListener);
+ onChannelsUpdated();
+ onProgramsUpdated();
+ }
+
+ /**
+ * Stops the DB sync.
+ */
+ public void stop() {
+ mProgramIdQueue.clear();
+ if (mQueryProgramTask != null) {
+ mQueryProgramTask.cancel(true);
+ }
+ mChannelDataManager.removeListener(mChannelDataManagerListener);
+ mDataManager.removeScheduledRecordingListener(mScheduleListener);
+ mContext.getContentResolver().unregisterContentObserver(mContentObserver);
+ }
+
+ private void onChannelsUpdated() {
+ List<SeriesRecording> seriesRecordingsToUpdate = new ArrayList<>();
+ for (SeriesRecording r : mDataManager.getSeriesRecordings()) {
+ if (r.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
+ && !mChannelDataManager.doesChannelExistInDb(r.getChannelId())) {
+ seriesRecordingsToUpdate.add(SeriesRecording.buildFrom(r)
+ .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL)
+ .setState(SeriesRecording.STATE_SERIES_STOPPED).build());
+ }
+ }
+ if (!seriesRecordingsToUpdate.isEmpty()) {
+ mDataManager.updateSeriesRecording(
+ SeriesRecording.toArray(seriesRecordingsToUpdate));
+ }
+ List<ScheduledRecording> schedulesToRemove = new ArrayList<>();
+ for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) {
+ if (!mChannelDataManager.doesChannelExistInDb(r.getChannelId())) {
+ schedulesToRemove.add(r);
+ mProgramIdQueue.remove(r.getProgramId());
+ }
+ }
+ if (!schedulesToRemove.isEmpty()) {
+ mDataManager.removeScheduledRecording(
+ ScheduledRecording.toArray(schedulesToRemove));
+ }
+ }
+
+ private void onProgramsUpdated() {
+ for (ScheduledRecording schedule : mDataManager.getAvailableScheduledRecordings()) {
+ addProgramIdToCheckIfNeeded(schedule);
+ }
+ startNextUpdateIfNeeded();
+ }
+
+ private void onProgramUpdated(long programId) {
+ addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId(programId));
+ startNextUpdateIfNeeded();
+ }
+
+ private void addProgramIdToCheckIfNeeded(ScheduledRecording schedule) {
+ if (schedule == null) {
+ return;
+ }
+ long programId = schedule.getProgramId();
+ if (programId != ScheduledRecording.ID_NOT_SET
+ && !mProgramIdQueue.contains(programId)
+ && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
+ || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
+ if (DEBUG) Log.d(TAG, "Program ID enqueued: " + programId);
+ mProgramIdQueue.offer(programId);
+ // There are schedules to be updated. Pause the SeriesRecordingScheduler until all the
+ // schedule updates finish.
+ // Note that the SeriesRecordingScheduler should be paused even though the program to
+ // check is not episodic because it can be changed to the episodic program after the
+ // update, which affect the SeriesRecordingScheduler.
+ mSeriesRecordingScheduler.pauseUpdate();
+ }
+ }
+
+ private void startNextUpdateIfNeeded() {
+ if (mQueryProgramTask != null && !mQueryProgramTask.isCancelled()) {
+ return;
+ }
+ if (!mProgramIdQueue.isEmpty()) {
+ if (DEBUG) Log.d(TAG, "Program ID dequeued: " + mProgramIdQueue.peek());
+ mQueryProgramTask = new QueryProgramTask(mProgramIdQueue.poll());
+ mQueryProgramTask.executeOnDbThread();
+ } else {
+ mSeriesRecordingScheduler.resumeUpdate();
+ }
+ }
+
+ @VisibleForTesting
+ void handleUpdateProgram(Program program, long programId) {
+ Set<SeriesRecording> seriesRecordingsToUpdate = new HashSet<>();
+ ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(programId);
+ if (schedule != null
+ && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
+ || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
+ if (program == null) {
+ mDataManager.removeScheduledRecording(schedule);
+ if (schedule.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) {
+ SeriesRecording seriesRecording =
+ mDataManager.getSeriesRecording(schedule.getSeriesRecordingId());
+ if (seriesRecording != null) {
+ seriesRecordingsToUpdate.add(seriesRecording);
+ }
+ }
+ } else {
+ long currentTimeMs = System.currentTimeMillis();
+ ScheduledRecording.Builder builder = ScheduledRecording.buildFrom(schedule)
+ .setEndTimeMs(program.getEndTimeUtcMillis())
+ .setSeasonNumber(program.getSeasonNumber())
+ .setEpisodeNumber(program.getEpisodeNumber())
+ .setEpisodeTitle(program.getEpisodeTitle())
+ .setProgramDescription(program.getDescription())
+ .setProgramLongDescription(program.getLongDescription())
+ .setProgramPosterArtUri(program.getPosterArtUri())
+ .setProgramThumbnailUri(program.getThumbnailUri());
+ boolean needUpdate = false;
+ // Check the series recording.
+ SeriesRecording seriesRecordingForOldSchedule =
+ mDataManager.getSeriesRecording(schedule.getSeriesRecordingId());
+ if (program.isEpisodic()) {
+ // New program belongs to a series.
+ SeriesRecording seriesRecording =
+ mDataManager.getSeriesRecording(program.getSeriesId());
+ if (seriesRecording == null) {
+ // The new program is episodic while the previous one isn't.
+ SeriesRecording newSeriesRecording = mDvrManager.addSeriesRecording(
+ program, Collections.singletonList(program),
+ SeriesRecording.STATE_SERIES_STOPPED);
+ builder.setSeriesRecordingId(newSeriesRecording.getId());
+ needUpdate = true;
+ } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) {
+ // The new program belongs to the other series.
+ builder.setSeriesRecordingId(seriesRecording.getId());
+ needUpdate = true;
+ seriesRecordingsToUpdate.add(seriesRecording);
+ if (seriesRecordingForOldSchedule != null) {
+ seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
+ }
+ } else if (!Objects.equals(schedule.getSeasonNumber(),
+ program.getSeasonNumber())
+ || !Objects.equals(schedule.getEpisodeNumber(),
+ program.getEpisodeNumber())) {
+ // The episode number has been changed.
+ if (seriesRecordingForOldSchedule != null) {
+ seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
+ }
+ }
+ } else if (seriesRecordingForOldSchedule != null) {
+ // Old program belongs to a series but the new one doesn't.
+ seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
+ }
+ // Change start time only when the recording is not started yet.
+ boolean needToChangeStartTime =
+ schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS
+ && program.getStartTimeUtcMillis() != schedule.getStartTimeMs();
+ if (needToChangeStartTime) {
+ builder.setStartTimeMs(program.getStartTimeUtcMillis());
+ needUpdate = true;
+ }
+ if (needUpdate || schedule.getEndTimeMs() != program.getEndTimeUtcMillis()
+ || !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber())
+ || !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber())
+ || !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle())
+ || !Objects.equals(schedule.getProgramDescription(),
+ program.getDescription())
+ || !Objects.equals(schedule.getProgramLongDescription(),
+ program.getLongDescription())
+ || !Objects.equals(schedule.getProgramPosterArtUri(),
+ program.getPosterArtUri())
+ || !Objects.equals(schedule.getProgramThumbnailUri(),
+ program.getThumbnailUri())) {
+ mDataManager.updateScheduledRecording(builder.build());
+ }
+ if (!seriesRecordingsToUpdate.isEmpty()) {
+ // The series recordings will be updated after it's resumed.
+ mSeriesRecordingScheduler.updateSchedules(seriesRecordingsToUpdate);
+ }
+ }
+ }
+ }
+
+ private class QueryProgramTask extends AsyncQueryProgramTask {
+ private final long mProgramId;
+
+ QueryProgramTask(long programId) {
+ super(mContext.getContentResolver(), programId);
+ mProgramId = programId;
+ }
+
+ @Override
+ protected void onCancelled(Program program) {
+ if (mQueryProgramTask == this) {
+ mQueryProgramTask = null;
+ }
+ startNextUpdateIfNeeded();
+ }
+
+ @Override
+ protected void onPostExecute(Program program) {
+ if (mQueryProgramTask == this) {
+ mQueryProgramTask = null;
+ }
+ handleUpdateProgram(program, mProgramId);
+ startNextUpdateIfNeeded();
+ }
+ }
+}