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 56 HandlerWrapper(Looper looper, ScheduledRecording scheduledRecording, RecordingTask recordingTask) { 57 super(looper, recordingTask); 58 mId = scheduledRecording.getId(); 59 } 60 61 @Override 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 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 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 */ 111 public void update() { 112 if (DEBUG) Log.d(TAG, "update"); 113 updatePendingRecordings(); 114 updateNextAlarm(); 115 } 116 117 @Override 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 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 140 public void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording) { 141 //TODO(DVR): implement 142 } 143 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 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 170 private long getLastStartTimePending() { 171 // TODO(DVR): implement 172 return mClock.currentTimeMillis(); 173 } 174 175 @VisibleForTesting 176 boolean startsWithin(ScheduledRecording scheduledRecording, long durationInMs) { 177 return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs; 178 } 179 } 180