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.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.Build; 26 import android.os.HandlerThread; 27 import android.os.Looper; 28 import android.support.annotation.MainThread; 29 import android.support.annotation.RequiresApi; 30 import android.support.annotation.VisibleForTesting; 31 import android.util.ArrayMap; 32 import android.util.Log; 33 import android.util.Range; 34 import com.android.tv.InputSessionManager; 35 import com.android.tv.TvSingletons; 36 import com.android.tv.common.SoftPreconditions; 37 import com.android.tv.common.util.Clock; 38 import com.android.tv.data.ChannelDataManager; 39 import com.android.tv.data.ChannelDataManager.Listener; 40 import com.android.tv.dvr.DvrDataManager; 41 import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; 42 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; 43 import com.android.tv.dvr.DvrManager; 44 import com.android.tv.dvr.WritableDvrDataManager; 45 import com.android.tv.dvr.data.ScheduledRecording; 46 import com.android.tv.util.TvInputManagerHelper; 47 import com.android.tv.util.Utils; 48 import java.util.Arrays; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.concurrent.TimeUnit; 52 53 /** 54 * The core class to manage DVR schedule and run recording task. * 55 * 56 * <p>This class is responsible for: 57 * 58 * <ul> 59 * <li>Sending record commands to TV inputs 60 * <li>Resolving conflicting schedules, handling overlapping recording time durations, etc. 61 * </ul> 62 * 63 * <p>This should be a singleton associated with application's main process. 64 */ 65 @RequiresApi(Build.VERSION_CODES.N) 66 @MainThread 67 public class RecordingScheduler extends TvInputCallback implements ScheduledRecordingListener { 68 private static final String TAG = "RecordingScheduler"; 69 private static final boolean DEBUG = false; 70 71 private static final String HANDLER_THREAD_NAME = "RecordingScheduler"; 72 private static final long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(1); 73 @VisibleForTesting static final long MS_TO_WAKE_BEFORE_START = TimeUnit.SECONDS.toMillis(30); 74 75 private final Looper mLooper; 76 private final InputSessionManager mSessionManager; 77 private final WritableDvrDataManager mDataManager; 78 private final DvrManager mDvrManager; 79 private final ChannelDataManager mChannelDataManager; 80 private final TvInputManagerHelper mInputManager; 81 private final Context mContext; 82 private final Clock mClock; 83 private final AlarmManager mAlarmManager; 84 85 private final Map<String, InputTaskScheduler> mInputSchedulerMap = new ArrayMap<>(); 86 private long mLastStartTimePendingMs; 87 88 private OnDvrScheduleLoadFinishedListener mDvrScheduleLoadListener = 89 new OnDvrScheduleLoadFinishedListener() { 90 @Override 91 public void onDvrScheduleLoadFinished() { 92 mDataManager.removeDvrScheduleLoadFinishedListener(this); 93 if (isDbLoaded()) { 94 updateInternal(); 95 } 96 } 97 }; 98 99 private Listener mChannelDataLoadListener = 100 new Listener() { 101 @Override 102 public void onLoadFinished() { 103 mChannelDataManager.removeListener(this); 104 if (isDbLoaded()) { 105 updateInternal(); 106 } 107 } 108 109 @Override 110 public void onChannelListUpdated() {} 111 112 @Override 113 public void onChannelBrowsableChanged() {} 114 }; 115 116 /** 117 * Creates a scheduler to schedule alarms for scheduled recordings and create recording tasks. 118 * This method should be only called once in the life-cycle of the application. 119 */ createScheduler(Context context)120 public static RecordingScheduler createScheduler(Context context) { 121 SoftPreconditions.checkState( 122 TvSingletons.getSingletons(context).getRecordingScheduler() == null); 123 HandlerThread handlerThread = new HandlerThread(HANDLER_THREAD_NAME); 124 handlerThread.start(); 125 TvSingletons singletons = TvSingletons.getSingletons(context); 126 return new RecordingScheduler( 127 handlerThread.getLooper(), 128 singletons.getDvrManager(), 129 singletons.getInputSessionManager(), 130 (WritableDvrDataManager) singletons.getDvrDataManager(), 131 singletons.getChannelDataManager(), 132 singletons.getTvInputManagerHelper(), 133 context, 134 Clock.SYSTEM, 135 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE)); 136 } 137 138 @VisibleForTesting RecordingScheduler( Looper looper, DvrManager dvrManager, InputSessionManager sessionManager, WritableDvrDataManager dataManager, ChannelDataManager channelDataManager, TvInputManagerHelper inputManager, Context context, Clock clock, AlarmManager alarmManager)139 RecordingScheduler( 140 Looper looper, 141 DvrManager dvrManager, 142 InputSessionManager sessionManager, 143 WritableDvrDataManager dataManager, 144 ChannelDataManager channelDataManager, 145 TvInputManagerHelper inputManager, 146 Context context, 147 Clock clock, 148 AlarmManager alarmManager) { 149 mLooper = looper; 150 mDvrManager = dvrManager; 151 mSessionManager = sessionManager; 152 mDataManager = dataManager; 153 mChannelDataManager = channelDataManager; 154 mInputManager = inputManager; 155 mContext = context; 156 mClock = clock; 157 mAlarmManager = alarmManager; 158 mDataManager.addScheduledRecordingListener(this); 159 mInputManager.addCallback(this); 160 if (isDbLoaded()) { 161 updateInternal(); 162 } else { 163 if (!mDataManager.isDvrScheduleLoadFinished()) { 164 mDataManager.addDvrScheduleLoadFinishedListener(mDvrScheduleLoadListener); 165 } 166 if (!mChannelDataManager.isDbLoadFinished()) { 167 mChannelDataManager.addListener(mChannelDataLoadListener); 168 } 169 } 170 } 171 172 /** Start recording that will happen soon, and set the next alarm time. */ updateAndStartServiceIfNeeded()173 public void updateAndStartServiceIfNeeded() { 174 if (DEBUG) Log.d(TAG, "update and start service if needed"); 175 if (isDbLoaded()) { 176 updateInternal(); 177 } else { 178 // updateInternal will be called when DB is loaded. Start DvrRecordingService to 179 // prevent process being killed before that. 180 DvrRecordingService.startForegroundService(mContext, false); 181 } 182 } 183 updateInternal()184 private void updateInternal() { 185 boolean recordingSoon = updatePendingRecordings(); 186 updateNextAlarm(); 187 if (recordingSoon) { 188 // Start DvrRecordingService to protect upcoming recording task from being killed. 189 DvrRecordingService.startForegroundService(mContext, true); 190 } else { 191 DvrRecordingService.stopForegroundIfNotRecording(); 192 } 193 } 194 updatePendingRecordings()195 private boolean updatePendingRecordings() { 196 List<ScheduledRecording> scheduledRecordings = 197 mDataManager.getScheduledRecordings( 198 new Range<>( 199 mLastStartTimePendingMs, 200 mClock.currentTimeMillis() + SOON_DURATION_IN_MS), 201 ScheduledRecording.STATE_RECORDING_NOT_STARTED); 202 for (ScheduledRecording r : scheduledRecordings) { 203 scheduleRecordingSoon(r); 204 } 205 // update() may be called multiple times, under this situation, pending recordings may be 206 // already updated thus scheduledRecordings may have a size of 0. Therefore we also have to 207 // check mLastStartTimePendingMs to check if we have upcoming recordings and prevent the 208 // recording service being wrongly pushed back to background in updateInternal(). 209 return scheduledRecordings.size() > 0 210 || (mLastStartTimePendingMs > mClock.currentTimeMillis() 211 && mLastStartTimePendingMs 212 < mClock.currentTimeMillis() + SOON_DURATION_IN_MS); 213 } 214 isDbLoaded()215 private boolean isDbLoaded() { 216 return mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished(); 217 } 218 219 @Override onScheduledRecordingAdded(ScheduledRecording... schedules)220 public void onScheduledRecordingAdded(ScheduledRecording... schedules) { 221 if (DEBUG) Log.d(TAG, "added " + Arrays.asList(schedules)); 222 if (!isDbLoaded()) { 223 return; 224 } 225 handleScheduleChange(schedules); 226 } 227 228 @Override onScheduledRecordingRemoved(ScheduledRecording... schedules)229 public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { 230 if (DEBUG) Log.d(TAG, "removed " + Arrays.asList(schedules)); 231 if (!isDbLoaded()) { 232 return; 233 } 234 boolean needToUpdateAlarm = false; 235 for (ScheduledRecording schedule : schedules) { 236 InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(schedule.getInputId()); 237 if (inputTaskScheduler != null) { 238 inputTaskScheduler.removeSchedule(schedule); 239 needToUpdateAlarm = true; 240 } 241 } 242 if (needToUpdateAlarm) { 243 updateNextAlarm(); 244 } 245 } 246 247 @Override onScheduledRecordingStatusChanged(ScheduledRecording... schedules)248 public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { 249 if (DEBUG) Log.d(TAG, "state changed " + Arrays.asList(schedules)); 250 if (!isDbLoaded()) { 251 return; 252 } 253 // Update the recordings. 254 for (ScheduledRecording schedule : schedules) { 255 InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(schedule.getInputId()); 256 if (inputTaskScheduler != null) { 257 inputTaskScheduler.updateSchedule(schedule); 258 } 259 } 260 handleScheduleChange(schedules); 261 } 262 handleScheduleChange(ScheduledRecording... schedules)263 private void handleScheduleChange(ScheduledRecording... schedules) { 264 boolean needToUpdateAlarm = false; 265 for (ScheduledRecording schedule : schedules) { 266 if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { 267 if (startsWithin(schedule, SOON_DURATION_IN_MS)) { 268 scheduleRecordingSoon(schedule); 269 } else { 270 needToUpdateAlarm = true; 271 } 272 } 273 } 274 if (needToUpdateAlarm) { 275 updateNextAlarm(); 276 } 277 } 278 scheduleRecordingSoon(ScheduledRecording schedule)279 private void scheduleRecordingSoon(ScheduledRecording schedule) { 280 TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); 281 if (input == null) { 282 Log.e(TAG, "Can't find input for " + schedule); 283 mDataManager.changeState( 284 schedule, 285 ScheduledRecording.STATE_RECORDING_FAILED, 286 ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE); 287 return; 288 } 289 if (!input.canRecord() || input.getTunerCount() <= 0) { 290 Log.e(TAG, "TV input doesn't support recording: " + input); 291 mDataManager.changeState( 292 schedule, 293 ScheduledRecording.STATE_RECORDING_FAILED, 294 ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED); 295 return; 296 } 297 InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(input.getId()); 298 if (inputTaskScheduler == null) { 299 inputTaskScheduler = 300 new InputTaskScheduler( 301 mContext, 302 input, 303 mLooper, 304 mChannelDataManager, 305 mDvrManager, 306 mDataManager, 307 mSessionManager, 308 mClock); 309 mInputSchedulerMap.put(input.getId(), inputTaskScheduler); 310 } 311 inputTaskScheduler.addSchedule(schedule); 312 if (mLastStartTimePendingMs < schedule.getStartTimeMs()) { 313 mLastStartTimePendingMs = schedule.getStartTimeMs(); 314 } 315 } 316 updateNextAlarm()317 private void updateNextAlarm() { 318 long nextStartTime = 319 mDataManager.getNextScheduledStartTimeAfter( 320 Math.max(mLastStartTimePendingMs, mClock.currentTimeMillis())); 321 if (nextStartTime != DvrDataManager.NEXT_START_TIME_NOT_FOUND) { 322 long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START; 323 if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt); 324 Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class); 325 PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); 326 // This will cancel the previous alarm. 327 mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent); 328 } else { 329 if (DEBUG) Log.d(TAG, "No future recording, alarm not set"); 330 } 331 } 332 333 @VisibleForTesting startsWithin(ScheduledRecording scheduledRecording, long durationInMs)334 boolean startsWithin(ScheduledRecording scheduledRecording, long durationInMs) { 335 return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs; 336 } 337 338 // No need to remove input task schedule worker when the input is removed. If the input is 339 // removed temporarily, the scheduler should keep the non-started schedules. 340 @Override onInputUpdated(String inputId)341 public void onInputUpdated(String inputId) { 342 InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(inputId); 343 if (inputTaskScheduler != null) { 344 inputTaskScheduler.updateTvInputInfo(Utils.getTvInputInfoForInputId(mContext, inputId)); 345 } 346 } 347 348 @Override onTvInputInfoUpdated(TvInputInfo input)349 public void onTvInputInfoUpdated(TvInputInfo input) { 350 InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(input.getId()); 351 if (inputTaskScheduler != null) { 352 inputTaskScheduler.updateTvInputInfo(input); 353 } 354 } 355 } 356