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
17package com.android.tv.dvr;
18
19import android.app.AlarmManager;
20import android.app.PendingIntent;
21import android.content.Context;
22import android.content.Intent;
23import android.os.Handler;
24import android.os.Looper;
25import android.os.Message;
26import android.support.annotation.VisibleForTesting;
27import android.util.Log;
28import android.util.LongSparseArray;
29import android.util.Range;
30
31import com.android.tv.data.Channel;
32import com.android.tv.data.ChannelDataManager;
33import com.android.tv.util.Clock;
34
35import java.util.List;
36import java.util.concurrent.TimeUnit;
37
38/**
39 * The core class to manage schedule and run actual recording.
40 */
41@VisibleForTesting
42public 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