/* * Copyright (C) 2015 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.recorder; import android.annotation.TargetApi; import android.content.ContentUris; import android.media.tv.TvContract; import android.net.Uri; import android.os.Build; import android.os.Message; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.ArraySet; import android.util.Log; import com.android.tv.ApplicationSingletons; import com.android.tv.InputSessionManager; import com.android.tv.InputSessionManager.OnTvViewChannelChangeListener; import com.android.tv.MainActivity; import com.android.tv.TvApplication; import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrUiHelper; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Checking the runtime conflict of DVR recording. *

* This class runs only while the {@link MainActivity} is resumed and holds the upcoming conflicts. */ @TargetApi(Build.VERSION_CODES.N) @MainThread public class ConflictChecker { private static final String TAG = "ConflictChecker"; private static final boolean DEBUG = false; private static final int MSG_CHECK_CONFLICT = 1; private static final long CHECK_RETRY_PERIOD_MS = TimeUnit.SECONDS.toMillis(30); /** * To show watch conflict dialog, the start time of the earliest conflicting schedule should be * less than or equal to this time. */ private static final long MAX_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.MINUTES.toMillis(5); /** * To show watch conflict dialog, the start time of the earliest conflicting schedule should be * greater than or equal to this time. */ private static final long MIN_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.SECONDS.toMillis(30); private final MainActivity mMainActivity; private final ChannelDataManager mChannelDataManager; private final DvrScheduleManager mScheduleManager; private final InputSessionManager mSessionManager; private final ConflictCheckerHandler mHandler = new ConflictCheckerHandler(this); private final List mUpcomingConflicts = new ArrayList<>(); private final Set mOnUpcomingConflictChangeListeners = new ArraySet<>(); private final Map> mCheckedConflictsMap = new HashMap<>(); private final ScheduledRecordingListener mScheduledRecordingListener = new ScheduledRecordingListener() { @Override public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + scheduledRecordings); mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); } @Override public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + scheduledRecordings); mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); } @Override public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { if (DEBUG) Log.d(TAG, "onScheduledRecordingStatusChanged: " + scheduledRecordings); mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); } }; private final OnTvViewChannelChangeListener mOnTvViewChannelChangeListener = new OnTvViewChannelChangeListener() { @Override public void onTvViewChannelChange(@Nullable Uri channelUri) { mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); } }; private boolean mStarted; public ConflictChecker(MainActivity mainActivity) { mMainActivity = mainActivity; ApplicationSingletons appSingletons = TvApplication.getSingletons(mainActivity); mChannelDataManager = appSingletons.getChannelDataManager(); mScheduleManager = appSingletons.getDvrScheduleManager(); mSessionManager = appSingletons.getInputSessionManager(); } /** * Starts checking the conflict. */ public void start() { if (mStarted) { return; } mStarted = true; mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); mScheduleManager.addScheduledRecordingListener(mScheduledRecordingListener); mSessionManager.addOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener); } /** * Stops checking the conflict. */ public void stop() { if (!mStarted) { return; } mStarted = false; mSessionManager.removeOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener); mScheduleManager.removeScheduledRecordingListener(mScheduledRecordingListener); mHandler.removeCallbacksAndMessages(null); } /** * Returns the upcoming conflicts. */ public List getUpcomingConflicts() { return new ArrayList<>(mUpcomingConflicts); } /** * Adds a {@link OnUpcomingConflictChangeListener}. */ public void addOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) { mOnUpcomingConflictChangeListeners.add(listener); } /** * Removes the {@link OnUpcomingConflictChangeListener}. */ public void removeOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) { mOnUpcomingConflictChangeListeners.remove(listener); } private void notifyUpcomingConflictChanged() { for (OnUpcomingConflictChangeListener l : mOnUpcomingConflictChangeListeners) { l.onUpcomingConflictChange(); } } /** * Remembers the user's decision to record while watching the channel. */ public void setCheckedConflictsForChannel(long mChannelId, List conflicts) { mCheckedConflictsMap.put(mChannelId, new ArrayList<>(conflicts)); } void onCheckConflict() { // Checks the conflicting schedules and setup the next re-check time. // If there are upcoming conflicts soon, it opens the conflict dialog. if (DEBUG) Log.d(TAG, "Handling MSG_CHECK_CONFLICT"); mHandler.removeMessages(MSG_CHECK_CONFLICT); mUpcomingConflicts.clear(); if (!mScheduleManager.isInitialized() || !mChannelDataManager.isDbLoadFinished()) { mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, CHECK_RETRY_PERIOD_MS); notifyUpcomingConflictChanged(); return; } if (mSessionManager.getCurrentTvViewChannelUri() == null) { // As MainActivity is not using a tuner, no need to check the conflict. notifyUpcomingConflictChanged(); return; } Uri channelUri = mSessionManager.getCurrentTvViewChannelUri(); if (TvContract.isChannelUriForPassthroughInput(channelUri)) { notifyUpcomingConflictChanged(); return; } long channelId = ContentUris.parseId(channelUri); Channel channel = mChannelDataManager.getChannel(channelId); // The conflicts caused by watching the channel. List conflicts = mScheduleManager .getConflictingSchedulesForWatching(channel.getId()); long earliestToCheck = Long.MAX_VALUE; long currentTimeMs = System.currentTimeMillis(); for (ScheduledRecording schedule : conflicts) { long startTimeMs = schedule.getStartTimeMs(); if (startTimeMs < currentTimeMs + MIN_WATCH_CONFLICT_CHECK_TIME_MS) { // The start time of the upcoming conflict remains less than the minimum // check time. continue; } if (startTimeMs > currentTimeMs + MAX_WATCH_CONFLICT_CHECK_TIME_MS) { // The start time of the upcoming conflict remains greater than the // maximum check time. Setup the next re-check time. long nextCheckTimeMs = startTimeMs - MAX_WATCH_CONFLICT_CHECK_TIME_MS; if (earliestToCheck > nextCheckTimeMs) { earliestToCheck = nextCheckTimeMs; } } else { // Found upcoming conflicts which will start soon. mUpcomingConflicts.add(schedule); // The schedule will be removed from the "upcoming conflict" when the // recording is almost started. long nextCheckTimeMs = startTimeMs - MIN_WATCH_CONFLICT_CHECK_TIME_MS; if (earliestToCheck > nextCheckTimeMs) { earliestToCheck = nextCheckTimeMs; } } } if (earliestToCheck != Long.MAX_VALUE) { mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, earliestToCheck - currentTimeMs); } if (DEBUG) Log.d(TAG, "upcoming conflicts: " + mUpcomingConflicts); notifyUpcomingConflictChanged(); if (!mUpcomingConflicts.isEmpty() && !DvrUiHelper.isChannelWatchConflictDialogShown(mMainActivity)) { // Don't show the conflict dialog if the user already knows. List checkedConflicts = mCheckedConflictsMap.get( channel.getId()); if (checkedConflicts == null || !checkedConflicts.containsAll(mUpcomingConflicts)) { DvrUiHelper.showChannelWatchConflictDialog(mMainActivity, channel); } } } private static class ConflictCheckerHandler extends WeakHandler { ConflictCheckerHandler(ConflictChecker conflictChecker) { super(conflictChecker); } @Override protected void handleMessage(Message msg, @NonNull ConflictChecker conflictChecker) { switch (msg.what) { case MSG_CHECK_CONFLICT: conflictChecker.onCheckConflict(); break; } } } /** * A listener for the change of upcoming conflicts. */ public interface OnUpcomingConflictChangeListener { void onUpcomingConflictChange(); } }