1 /* 2 * Copyright (C) 2015 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.TargetApi; 20 import android.content.ContentUris; 21 import android.media.tv.TvContract; 22 import android.net.Uri; 23 import android.os.Build; 24 import android.os.Message; 25 import android.support.annotation.MainThread; 26 import android.support.annotation.NonNull; 27 import android.support.annotation.Nullable; 28 import android.util.ArraySet; 29 import android.util.Log; 30 31 import com.android.tv.ApplicationSingletons; 32 import com.android.tv.InputSessionManager; 33 import com.android.tv.InputSessionManager.OnTvViewChannelChangeListener; 34 import com.android.tv.MainActivity; 35 import com.android.tv.TvApplication; 36 import com.android.tv.common.WeakHandler; 37 import com.android.tv.data.Channel; 38 import com.android.tv.data.ChannelDataManager; 39 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; 40 41 import java.util.ArrayList; 42 import java.util.HashMap; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Set; 46 import java.util.concurrent.TimeUnit; 47 48 /** 49 * Checking the runtime conflict of DVR recording. 50 * <p> 51 * This class runs only while the {@link MainActivity} is resumed and holds the upcoming conflicts. 52 */ 53 @TargetApi(Build.VERSION_CODES.N) 54 @MainThread 55 public class ConflictChecker { 56 private static final String TAG = "ConflictChecker"; 57 private static final boolean DEBUG = false; 58 59 private static final int MSG_CHECK_CONFLICT = 1; 60 61 private static final long CHECK_RETRY_PERIOD_MS = TimeUnit.SECONDS.toMillis(30); 62 63 /** 64 * To show watch conflict dialog, the start time of the earliest conflicting schedule should be 65 * less than or equal to this time. 66 */ 67 private static final long MAX_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.MINUTES.toMillis(5); 68 /** 69 * To show watch conflict dialog, the start time of the earliest conflicting schedule should be 70 * greater than or equal to this time. 71 */ 72 private static final long MIN_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.SECONDS.toMillis(30); 73 74 private final MainActivity mMainActivity; 75 private final ChannelDataManager mChannelDataManager; 76 private final DvrScheduleManager mScheduleManager; 77 private final InputSessionManager mSessionManager; 78 private final ConflictCheckerHandler mHandler = new ConflictCheckerHandler(this); 79 80 private final List<ScheduledRecording> mUpcomingConflicts = new ArrayList<>(); 81 private final Set<OnUpcomingConflictChangeListener> mOnUpcomingConflictChangeListeners = 82 new ArraySet<>(); 83 private final Map<Long, List<ScheduledRecording>> mCheckedConflictsMap = new HashMap<>(); 84 85 private final ScheduledRecordingListener mScheduledRecordingListener = 86 new ScheduledRecordingListener() { 87 @Override 88 public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { 89 if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + scheduledRecordings); 90 mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); 91 } 92 93 @Override 94 public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { 95 if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + scheduledRecordings); 96 mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); 97 } 98 99 @Override 100 public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { 101 if (DEBUG) Log.d(TAG, "onScheduledRecordingStatusChanged: " + scheduledRecordings); 102 mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); 103 } 104 }; 105 106 private final OnTvViewChannelChangeListener mOnTvViewChannelChangeListener = 107 new OnTvViewChannelChangeListener() { 108 @Override 109 public void onTvViewChannelChange(@Nullable Uri channelUri) { 110 mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); 111 } 112 }; 113 114 private boolean mStarted; 115 ConflictChecker(MainActivity mainActivity)116 public ConflictChecker(MainActivity mainActivity) { 117 mMainActivity = mainActivity; 118 ApplicationSingletons appSingletons = TvApplication.getSingletons(mainActivity); 119 mChannelDataManager = appSingletons.getChannelDataManager(); 120 mScheduleManager = appSingletons.getDvrScheduleManager(); 121 mSessionManager = appSingletons.getInputSessionManager(); 122 } 123 124 /** 125 * Starts checking the conflict. 126 */ start()127 public void start() { 128 if (mStarted) { 129 return; 130 } 131 mStarted = true; 132 mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); 133 mScheduleManager.addScheduledRecordingListener(mScheduledRecordingListener); 134 mSessionManager.addOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener); 135 } 136 137 /** 138 * Stops checking the conflict. 139 */ stop()140 public void stop() { 141 if (!mStarted) { 142 return; 143 } 144 mStarted = false; 145 mSessionManager.removeOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener); 146 mScheduleManager.removeScheduledRecordingListener(mScheduledRecordingListener); 147 mHandler.removeCallbacksAndMessages(null); 148 } 149 150 /** 151 * Returns the upcoming conflicts. 152 */ getUpcomingConflicts()153 public List<ScheduledRecording> getUpcomingConflicts() { 154 return new ArrayList<>(mUpcomingConflicts); 155 } 156 157 /** 158 * Adds a {@link OnUpcomingConflictChangeListener}. 159 */ addOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener)160 public void addOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) { 161 mOnUpcomingConflictChangeListeners.add(listener); 162 } 163 164 /** 165 * Removes the {@link OnUpcomingConflictChangeListener}. 166 */ removeOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener)167 public void removeOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) { 168 mOnUpcomingConflictChangeListeners.remove(listener); 169 } 170 notifyUpcomingConflictChanged()171 private void notifyUpcomingConflictChanged() { 172 for (OnUpcomingConflictChangeListener l : mOnUpcomingConflictChangeListeners) { 173 l.onUpcomingConflictChange(); 174 } 175 } 176 177 /** 178 * Remembers the user's decision to record while watching the channel. 179 */ setCheckedConflictsForChannel(long mChannelId, List<ScheduledRecording> conflicts)180 public void setCheckedConflictsForChannel(long mChannelId, List<ScheduledRecording> conflicts) { 181 mCheckedConflictsMap.put(mChannelId, new ArrayList<>(conflicts)); 182 } 183 onCheckConflict()184 void onCheckConflict() { 185 // Checks the conflicting schedules and setup the next re-check time. 186 // If there are upcoming conflicts soon, it opens the conflict dialog. 187 if (DEBUG) Log.d(TAG, "Handling MSG_CHECK_CONFLICT"); 188 mHandler.removeMessages(MSG_CHECK_CONFLICT); 189 mUpcomingConflicts.clear(); 190 if (!mScheduleManager.isInitialized() 191 || !mChannelDataManager.isDbLoadFinished()) { 192 mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, CHECK_RETRY_PERIOD_MS); 193 notifyUpcomingConflictChanged(); 194 return; 195 } 196 if (mSessionManager.getCurrentTvViewChannelUri() == null) { 197 // As MainActivity is not using a tuner, no need to check the conflict. 198 notifyUpcomingConflictChanged(); 199 return; 200 } 201 Uri channelUri = mSessionManager.getCurrentTvViewChannelUri(); 202 if (TvContract.isChannelUriForPassthroughInput(channelUri)) { 203 notifyUpcomingConflictChanged(); 204 return; 205 } 206 long channelId = ContentUris.parseId(channelUri); 207 Channel channel = mChannelDataManager.getChannel(channelId); 208 // The conflicts caused by watching the channel. 209 List<ScheduledRecording> conflicts = mScheduleManager 210 .getConflictingSchedulesForWatching(channel.getId()); 211 long earliestToCheck = Long.MAX_VALUE; 212 long currentTimeMs = System.currentTimeMillis(); 213 for (ScheduledRecording schedule : conflicts) { 214 long startTimeMs = schedule.getStartTimeMs(); 215 if (startTimeMs < currentTimeMs + MIN_WATCH_CONFLICT_CHECK_TIME_MS) { 216 // The start time of the upcoming conflict remains less than the minimum 217 // check time. 218 continue; 219 } 220 if (startTimeMs > currentTimeMs + MAX_WATCH_CONFLICT_CHECK_TIME_MS) { 221 // The start time of the upcoming conflict remains greater than the 222 // maximum check time. Setup the next re-check time. 223 long nextCheckTimeMs = startTimeMs - MAX_WATCH_CONFLICT_CHECK_TIME_MS; 224 if (earliestToCheck > nextCheckTimeMs) { 225 earliestToCheck = nextCheckTimeMs; 226 } 227 } else { 228 // Found upcoming conflicts which will start soon. 229 mUpcomingConflicts.add(schedule); 230 // The schedule will be removed from the "upcoming conflict" when the 231 // recording is almost started. 232 long nextCheckTimeMs = startTimeMs - MIN_WATCH_CONFLICT_CHECK_TIME_MS; 233 if (earliestToCheck > nextCheckTimeMs) { 234 earliestToCheck = nextCheckTimeMs; 235 } 236 } 237 } 238 if (earliestToCheck != Long.MAX_VALUE) { 239 mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, 240 earliestToCheck - currentTimeMs); 241 } 242 if (DEBUG) Log.d(TAG, "upcoming conflicts: " + mUpcomingConflicts); 243 notifyUpcomingConflictChanged(); 244 if (!mUpcomingConflicts.isEmpty() 245 && !DvrUiHelper.isChannelWatchConflictDialogShown(mMainActivity)) { 246 // Don't show the conflict dialog if the user already knows. 247 List<ScheduledRecording> checkedConflicts = mCheckedConflictsMap.get( 248 channel.getId()); 249 if (checkedConflicts == null 250 || !checkedConflicts.containsAll(mUpcomingConflicts)) { 251 DvrUiHelper.showChannelWatchConflictDialog(mMainActivity, channel); 252 } 253 } 254 } 255 256 private static class ConflictCheckerHandler extends WeakHandler<ConflictChecker> { ConflictCheckerHandler(ConflictChecker conflictChecker)257 ConflictCheckerHandler(ConflictChecker conflictChecker) { 258 super(conflictChecker); 259 } 260 261 @Override handleMessage(Message msg, @NonNull ConflictChecker conflictChecker)262 protected void handleMessage(Message msg, @NonNull ConflictChecker conflictChecker) { 263 switch (msg.what) { 264 case MSG_CHECK_CONFLICT: 265 conflictChecker.onCheckConflict(); 266 break; 267 } 268 } 269 } 270 271 /** 272 * A listener for the change of upcoming conflicts. 273 */ 274 public interface OnUpcomingConflictChangeListener { onUpcomingConflictChange()275 void onUpcomingConflictChange(); 276 } 277 } 278