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