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 */ 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 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. */ 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 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 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 215 private boolean isDbLoaded() { 216 return mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished(); 217 } 218 219 @Override 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 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 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 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 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 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 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 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 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