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.app.AlarmManager; 20 import android.app.PendingIntent; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.media.tv.TvInputInfo; 24 import android.media.tv.TvInputManager.TvInputCallback; 25 import android.os.Looper; 26 import android.support.annotation.MainThread; 27 import android.support.annotation.VisibleForTesting; 28 import android.util.ArrayMap; 29 import android.util.Log; 30 import android.util.Range; 31 32 import com.android.tv.InputSessionManager; 33 import com.android.tv.data.ChannelDataManager; 34 import com.android.tv.data.ChannelDataManager.Listener; 35 import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; 36 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; 37 import com.android.tv.util.Clock; 38 import com.android.tv.util.TvInputManagerHelper; 39 import com.android.tv.util.Utils; 40 41 import java.util.Arrays; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.concurrent.TimeUnit; 45 46 /** 47 * The core class to manage schedule and run actual recording. 48 */ 49 @MainThread 50 public class Scheduler extends TvInputCallback implements ScheduledRecordingListener { 51 private static final String TAG = "Scheduler"; 52 private static final boolean DEBUG = false; 53 54 private final static long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(5); 55 @VisibleForTesting final static long MS_TO_WAKE_BEFORE_START = TimeUnit.MINUTES.toMillis(1); 56 57 private final Looper mLooper; 58 private final InputSessionManager mSessionManager; 59 private final WritableDvrDataManager mDataManager; 60 private final DvrManager mDvrManager; 61 private final ChannelDataManager mChannelDataManager; 62 private final TvInputManagerHelper mInputManager; 63 private final Context mContext; 64 private final Clock mClock; 65 private final AlarmManager mAlarmManager; 66 67 private final Map<String, InputTaskScheduler> mInputSchedulerMap = new ArrayMap<>(); 68 private long mLastStartTimePendingMs; 69 Scheduler(Looper looper, DvrManager dvrManager, InputSessionManager sessionManager, WritableDvrDataManager dataManager, ChannelDataManager channelDataManager, TvInputManagerHelper inputManager, Context context, Clock clock, AlarmManager alarmManager)70 public Scheduler(Looper looper, DvrManager dvrManager, InputSessionManager sessionManager, 71 WritableDvrDataManager dataManager, ChannelDataManager channelDataManager, 72 TvInputManagerHelper inputManager, Context context, Clock clock, 73 AlarmManager alarmManager) { 74 mLooper = looper; 75 mDvrManager = dvrManager; 76 mSessionManager = sessionManager; 77 mDataManager = dataManager; 78 mChannelDataManager = channelDataManager; 79 mInputManager = inputManager; 80 mContext = context; 81 mClock = clock; 82 mAlarmManager = alarmManager; 83 } 84 85 /** 86 * Starts the scheduler. 87 */ start()88 public void start() { 89 mDataManager.addScheduledRecordingListener(this); 90 mInputManager.addCallback(this); 91 if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) { 92 updateInternal(); 93 } else { 94 if (!mDataManager.isDvrScheduleLoadFinished()) { 95 mDataManager.addDvrScheduleLoadFinishedListener( 96 new OnDvrScheduleLoadFinishedListener() { 97 @Override 98 public void onDvrScheduleLoadFinished() { 99 mDataManager.removeDvrScheduleLoadFinishedListener(this); 100 updateInternal(); 101 } 102 }); 103 } 104 if (!mChannelDataManager.isDbLoadFinished()) { 105 mChannelDataManager.addListener(new Listener() { 106 @Override 107 public void onLoadFinished() { 108 mChannelDataManager.removeListener(this); 109 updateInternal(); 110 } 111 112 @Override 113 public void onChannelListUpdated() { } 114 115 @Override 116 public void onChannelBrowsableChanged() { } 117 }); 118 } 119 } 120 } 121 122 /** 123 * Stops the scheduler. 124 */ stop()125 public void stop() { 126 for (InputTaskScheduler inputTaskScheduler : mInputSchedulerMap.values()) { 127 inputTaskScheduler.stop(); 128 } 129 mInputManager.removeCallback(this); 130 mDataManager.removeScheduledRecordingListener(this); 131 } 132 updatePendingRecordings()133 private void updatePendingRecordings() { 134 List<ScheduledRecording> scheduledRecordings = mDataManager 135 .getScheduledRecordings(new Range<>(mLastStartTimePendingMs, 136 mClock.currentTimeMillis() + SOON_DURATION_IN_MS), 137 ScheduledRecording.STATE_RECORDING_NOT_STARTED); 138 for (ScheduledRecording r : scheduledRecordings) { 139 scheduleRecordingSoon(r); 140 } 141 } 142 143 /** 144 * Start recording that will happen soon, and set the next alarm time. 145 */ update()146 public void update() { 147 if (DEBUG) Log.d(TAG, "update"); 148 updateInternal(); 149 } 150 updateInternal()151 private void updateInternal() { 152 if (isInitialized()) { 153 updatePendingRecordings(); 154 updateNextAlarm(); 155 } 156 } 157 isInitialized()158 private boolean isInitialized() { 159 return mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished(); 160 } 161 162 @Override onScheduledRecordingAdded(ScheduledRecording... schedules)163 public void onScheduledRecordingAdded(ScheduledRecording... schedules) { 164 if (DEBUG) Log.d(TAG, "added " + Arrays.asList(schedules)); 165 if (!isInitialized()) { 166 return; 167 } 168 handleScheduleChange(schedules); 169 } 170 171 @Override onScheduledRecordingRemoved(ScheduledRecording... schedules)172 public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { 173 if (DEBUG) Log.d(TAG, "removed " + Arrays.asList(schedules)); 174 if (!isInitialized()) { 175 return; 176 } 177 boolean needToUpdateAlarm = false; 178 for (ScheduledRecording schedule : schedules) { 179 InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId()); 180 if (scheduler != null) { 181 scheduler.removeSchedule(schedule); 182 needToUpdateAlarm = true; 183 } 184 } 185 if (needToUpdateAlarm) { 186 updateNextAlarm(); 187 } 188 } 189 190 @Override onScheduledRecordingStatusChanged(ScheduledRecording... schedules)191 public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { 192 if (DEBUG) Log.d(TAG, "state changed " + Arrays.asList(schedules)); 193 if (!isInitialized()) { 194 return; 195 } 196 // Update the recordings. 197 for (ScheduledRecording schedule : schedules) { 198 InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId()); 199 if (scheduler != null) { 200 scheduler.updateSchedule(schedule); 201 } 202 } 203 handleScheduleChange(schedules); 204 } 205 handleScheduleChange(ScheduledRecording... schedules)206 private void handleScheduleChange(ScheduledRecording... schedules) { 207 boolean needToUpdateAlarm = false; 208 for (ScheduledRecording schedule : schedules) { 209 if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { 210 if (startsWithin(schedule, SOON_DURATION_IN_MS)) { 211 scheduleRecordingSoon(schedule); 212 } else { 213 needToUpdateAlarm = true; 214 } 215 } 216 } 217 if (needToUpdateAlarm) { 218 updateNextAlarm(); 219 } 220 } 221 scheduleRecordingSoon(ScheduledRecording schedule)222 private void scheduleRecordingSoon(ScheduledRecording schedule) { 223 TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); 224 if (input == null) { 225 Log.e(TAG, "Can't find input for " + schedule); 226 mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); 227 return; 228 } 229 if (!input.canRecord() || input.getTunerCount() <= 0) { 230 Log.e(TAG, "TV input doesn't support recording: " + input); 231 mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); 232 return; 233 } 234 InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId()); 235 if (scheduler == null) { 236 scheduler = new InputTaskScheduler(mContext, input, mLooper, mChannelDataManager, 237 mDvrManager, mDataManager, mSessionManager, mClock); 238 mInputSchedulerMap.put(input.getId(), scheduler); 239 } 240 scheduler.addSchedule(schedule); 241 if (mLastStartTimePendingMs < schedule.getStartTimeMs()) { 242 mLastStartTimePendingMs = schedule.getStartTimeMs(); 243 } 244 } 245 updateNextAlarm()246 private void updateNextAlarm() { 247 long nextStartTime = mDataManager.getNextScheduledStartTimeAfter( 248 Math.max(mLastStartTimePendingMs, mClock.currentTimeMillis())); 249 if (nextStartTime != DvrDataManager.NEXT_START_TIME_NOT_FOUND) { 250 long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START; 251 if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt); 252 Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class); 253 PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); 254 // This will cancel the previous alarm. 255 mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent); 256 } else { 257 if (DEBUG) Log.d(TAG, "No future recording, alarm not set"); 258 } 259 } 260 261 @VisibleForTesting startsWithin(ScheduledRecording scheduledRecording, long durationInMs)262 boolean startsWithin(ScheduledRecording scheduledRecording, long durationInMs) { 263 return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs; 264 } 265 266 // No need to remove input task scheduler when the input is removed. If the input is removed 267 // temporarily, the scheduler should keep the non-started schedules. 268 @Override onInputUpdated(String inputId)269 public void onInputUpdated(String inputId) { 270 InputTaskScheduler scheduler = mInputSchedulerMap.get(inputId); 271 if (scheduler != null) { 272 scheduler.updateTvInputInfo(Utils.getTvInputInfoForInputId(mContext, inputId)); 273 } 274 } 275 276 @Override onTvInputInfoUpdated(TvInputInfo input)277 public void onTvInputInfoUpdated(TvInputInfo input) { 278 InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId()); 279 if (scheduler != null) { 280 scheduler.updateTvInputInfo(input); 281 } 282 } 283 } 284