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