| /* |
| * 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; |
| |
| 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.util.AsyncDbTask.AsyncQueryProgramTask; |
| import com.android.tv.util.TvProviderUriMatcher; |
| |
| 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) |
| class DvrDbSync { |
| private static final String TAG = "DvrDbSync"; |
| private static final boolean DEBUG = false; |
| |
| private final Context mContext; |
| 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 (TvProviderUriMatcher.match(uri)) { |
| case TvProviderUriMatcher.MATCH_PROGRAM: |
| if (DEBUG) Log.d(TAG, "onProgramsUpdated"); |
| onProgramsUpdated(); |
| break; |
| case TvProviderUriMatcher.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(); |
| } |
| }; |
| |
| DvrDbSync(Context context, DvrDataManagerImpl dataManager) { |
| this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager()); |
| } |
| |
| @VisibleForTesting |
| DvrDbSync(Context context, DvrDataManagerImpl dataManager, |
| ChannelDataManager channelDataManager) { |
| mContext = context; |
| mDataManager = dataManager; |
| mChannelDataManager = channelDataManager; |
| mSeriesRecordingScheduler = SeriesRecordingScheduler.getInstance(context); |
| } |
| |
| /** |
| * 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.getSeriesId() != null) { |
| // 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 = TvApplication.getSingletons(mContext) |
| .getDvrManager().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 start time has not passed. |
| boolean needToChangeStartTime = schedule.getStartTimeMs() > currentTimeMs |
| && 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(); |
| } |
| } |
| } |