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.os.Handler; 24 import android.os.Looper; 25 import android.os.Message; 26 import android.support.annotation.VisibleForTesting; 27 import android.util.Log; 28 import android.util.LongSparseArray; 29 import android.util.Range; 30 31 import com.android.tv.data.Channel; 32 import com.android.tv.data.ChannelDataManager; 33 import com.android.tv.util.Clock; 34 35 import java.util.List; 36 import java.util.concurrent.TimeUnit; 37 38 /** 39 * The core class to manage schedule and run actual recording. 40 */ 41 @VisibleForTesting 42 public class Scheduler implements DvrDataManager.ScheduledRecordingListener { 43 private static final String TAG = "Scheduler"; 44 private static final boolean DEBUG = false; 45 46 private final static long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(5); 47 @VisibleForTesting final static long MS_TO_WAKE_BEFORE_START = TimeUnit.MINUTES.toMillis(1); 48 49 /** 50 * Wraps a {@link RecordingTask} removing it from {@link #mPendingRecordings} when it is done. 51 */ 52 public final class HandlerWrapper extends Handler { 53 public static final int MESSAGE_REMOVE = 999; 54 private final long mId; 55 HandlerWrapper(Looper looper, ScheduledRecording scheduledRecording, RecordingTask recordingTask)56 HandlerWrapper(Looper looper, ScheduledRecording scheduledRecording, RecordingTask recordingTask) { 57 super(looper, recordingTask); 58 mId = scheduledRecording.getId(); 59 } 60 61 @Override handleMessage(Message msg)62 public void handleMessage(Message msg) { 63 // The RecordingTask gets a chance first. 64 // It must return false to pass this message to here. 65 if (msg.what == MESSAGE_REMOVE) { 66 if (DEBUG) Log.d(TAG, "done " + mId); 67 mPendingRecordings.remove(mId); 68 } 69 removeCallbacksAndMessages(null); 70 super.handleMessage(msg); 71 } 72 } 73 74 private final LongSparseArray<HandlerWrapper> mPendingRecordings = new LongSparseArray<>(); 75 private final Looper mLooper; 76 private final DvrSessionManager mSessionManager; 77 private final WritableDvrDataManager mDataManager; 78 private final DvrManager mDvrManager; 79 private final ChannelDataManager mChannelDataManager; 80 private final Context mContext; 81 private final Clock mClock; 82 private final AlarmManager mAlarmManager; 83 Scheduler(Looper looper, DvrManager dvrManager, DvrSessionManager sessionManager, WritableDvrDataManager dataManager, ChannelDataManager channelDataManager, Context context, Clock clock, AlarmManager alarmManager)84 public Scheduler(Looper looper, DvrManager dvrManager, DvrSessionManager sessionManager, 85 WritableDvrDataManager dataManager, ChannelDataManager channelDataManager, 86 Context context, Clock clock, 87 AlarmManager alarmManager) { 88 mLooper = looper; 89 mDvrManager = dvrManager; 90 mSessionManager = sessionManager; 91 mDataManager = dataManager; 92 mChannelDataManager = channelDataManager; 93 mContext = context; 94 mClock = clock; 95 mAlarmManager = alarmManager; 96 } 97 updatePendingRecordings()98 private void updatePendingRecordings() { 99 List<ScheduledRecording> scheduledRecordings = mDataManager.getRecordingsThatOverlapWith( 100 new Range(mClock.currentTimeMillis(), 101 mClock.currentTimeMillis() + SOON_DURATION_IN_MS)); 102 // TODO(DVR): handle removing and updating exiting recordings. 103 for (ScheduledRecording r : scheduledRecordings) { 104 scheduleRecordingSoon(r); 105 } 106 } 107 108 /** 109 * Start recording that will happen soon, and set the next alarm time. 110 */ update()111 public void update() { 112 if (DEBUG) Log.d(TAG, "update"); 113 updatePendingRecordings(); 114 updateNextAlarm(); 115 } 116 117 @Override onScheduledRecordingAdded(ScheduledRecording scheduledRecording)118 public void onScheduledRecordingAdded(ScheduledRecording scheduledRecording) { 119 if (DEBUG) Log.d(TAG, "added " + scheduledRecording); 120 if (startsWithin(scheduledRecording, SOON_DURATION_IN_MS)) { 121 scheduleRecordingSoon(scheduledRecording); 122 } else { 123 updateNextAlarm(); 124 } 125 } 126 127 @Override onScheduledRecordingRemoved(ScheduledRecording scheduledRecording)128 public void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording) { 129 long id = scheduledRecording.getId(); 130 HandlerWrapper wrapper = mPendingRecordings.get(id); 131 if (wrapper != null) { 132 wrapper.removeCallbacksAndMessages(null); 133 mPendingRecordings.remove(id); 134 } else { 135 updateNextAlarm(); 136 } 137 } 138 139 @Override onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording)140 public void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording) { 141 //TODO(DVR): implement 142 } 143 scheduleRecordingSoon(ScheduledRecording scheduledRecording)144 private void scheduleRecordingSoon(ScheduledRecording scheduledRecording) { 145 Channel channel = mChannelDataManager.getChannel(scheduledRecording.getChannelId()); 146 RecordingTask recordingTask = new RecordingTask(scheduledRecording, channel, mDvrManager, 147 mSessionManager, mDataManager, mClock); 148 HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, scheduledRecording, 149 recordingTask); 150 recordingTask.setHandler(handlerWrapper); 151 mPendingRecordings.put(scheduledRecording.getId(), handlerWrapper); 152 handlerWrapper.sendEmptyMessage(RecordingTask.MESSAGE_INIT); 153 } 154 updateNextAlarm()155 private void updateNextAlarm() { 156 long lastStartTimePending = getLastStartTimePending(); 157 long nextStartTime = mDataManager.getNextScheduledStartTimeAfter(lastStartTimePending); 158 if (nextStartTime != DvrDataManager.NEXT_START_TIME_NOT_FOUND) { 159 long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START; 160 if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt); 161 Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class); 162 PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); 163 //This will cancel the previous alarm. 164 mAlarmManager.set(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent); 165 } else { 166 if (DEBUG) Log.d(TAG, "No future recording, alarm not set"); 167 } 168 } 169 getLastStartTimePending()170 private long getLastStartTimePending() { 171 // TODO(DVR): implement 172 return mClock.currentTimeMillis(); 173 } 174 175 @VisibleForTesting startsWithin(ScheduledRecording scheduledRecording, long durationInMs)176 boolean startsWithin(ScheduledRecording scheduledRecording, long durationInMs) { 177 return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs; 178 } 179 } 180