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