Home | History | Annotate | Download | only in dvr
      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