Home | History | Annotate | Download | only in recorder
      1 /*
      2  * Copyright (C) 2016 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.recorder;
     18 
     19 import android.content.Context;
     20 import android.media.tv.TvInputInfo;
     21 import android.os.Handler;
     22 import android.os.Looper;
     23 import android.os.Message;
     24 import android.support.annotation.VisibleForTesting;
     25 import android.util.ArrayMap;
     26 import android.util.Log;
     27 import android.util.LongSparseArray;
     28 
     29 import com.android.tv.InputSessionManager;
     30 import com.android.tv.data.Channel;
     31 import com.android.tv.data.ChannelDataManager;
     32 import com.android.tv.dvr.DvrDataManager;
     33 import com.android.tv.dvr.DvrManager;
     34 import com.android.tv.dvr.WritableDvrDataManager;
     35 import com.android.tv.dvr.data.ScheduledRecording;
     36 import com.android.tv.util.Clock;
     37 import com.android.tv.util.CompositeComparator;
     38 
     39 import java.util.ArrayList;
     40 import java.util.Collections;
     41 import java.util.Comparator;
     42 import java.util.Iterator;
     43 import java.util.List;
     44 import java.util.Map;
     45 
     46 /**
     47  * The scheduler for a TV input.
     48  */
     49 public class InputTaskScheduler {
     50     private static final String TAG = "InputTaskScheduler";
     51     private static final boolean DEBUG = false;
     52 
     53     private static final int MSG_ADD_SCHEDULED_RECORDING = 1;
     54     private static final int MSG_REMOVE_SCHEDULED_RECORDING = 2;
     55     private static final int MSG_UPDATE_SCHEDULED_RECORDING = 3;
     56     private static final int MSG_BUILD_SCHEDULE = 4;
     57     private static final int MSG_STOP_SCHEDULE = 5;
     58 
     59     private static final float MIN_REMAIN_DURATION_PERCENT = 0.05f;
     60 
     61     // The candidate comparator should be the consistent with
     62     // DvrScheduleManager#CANDIDATE_COMPARATOR.
     63     private static final Comparator<RecordingTask> CANDIDATE_COMPARATOR =
     64             new CompositeComparator<>(
     65                     RecordingTask.PRIORITY_COMPARATOR,
     66                     RecordingTask.END_TIME_COMPARATOR,
     67                     RecordingTask.ID_COMPARATOR);
     68 
     69     /**
     70      * Returns the comparator which the schedules are sorted with when executed.
     71      */
     72     public static Comparator<ScheduledRecording> getRecordingOrderComparator() {
     73         return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR;
     74     }
     75 
     76     /**
     77      * Wraps a {@link RecordingTask} removing it from {@link #mPendingRecordings} when it is done.
     78      */
     79     public final class HandlerWrapper extends Handler {
     80         public static final int MESSAGE_REMOVE = 999;
     81         private final long mId;
     82         private final RecordingTask mTask;
     83 
     84         HandlerWrapper(Looper looper, ScheduledRecording scheduledRecording,
     85                 RecordingTask recordingTask) {
     86             super(looper, recordingTask);
     87             mId = scheduledRecording.getId();
     88             mTask = recordingTask;
     89             mTask.setHandler(this);
     90         }
     91 
     92         @Override
     93         public void handleMessage(Message msg) {
     94             // The RecordingTask gets a chance first.
     95             // It must return false to pass this message to here.
     96             if (msg.what == MESSAGE_REMOVE) {
     97                 if (DEBUG)  Log.d(TAG, "done " + mId);
     98                 mPendingRecordings.remove(mId);
     99             }
    100             removeCallbacksAndMessages(null);
    101             mHandler.removeMessages(MSG_BUILD_SCHEDULE);
    102             mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE);
    103             super.handleMessage(msg);
    104         }
    105     }
    106 
    107     private TvInputInfo mInput;
    108     private final Looper mLooper;
    109     private final ChannelDataManager mChannelDataManager;
    110     private final DvrManager mDvrManager;
    111     private final WritableDvrDataManager mDataManager;
    112     private final InputSessionManager mSessionManager;
    113     private final Clock mClock;
    114     private final Context mContext;
    115 
    116     private final LongSparseArray<HandlerWrapper> mPendingRecordings = new LongSparseArray<>();
    117     private final Map<Long, ScheduledRecording> mWaitingSchedules = new ArrayMap<>();
    118     private final Handler mMainThreadHandler;
    119     private final Handler mHandler;
    120     private final Object mInputLock = new Object();
    121     private final RecordingTaskFactory mRecordingTaskFactory;
    122 
    123     public InputTaskScheduler(Context context, TvInputInfo input, Looper looper,
    124             ChannelDataManager channelDataManager, DvrManager dvrManager,
    125             DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock) {
    126         this(context, input, looper, channelDataManager, dvrManager, dataManager, sessionManager,
    127                 clock, null);
    128     }
    129 
    130     @VisibleForTesting
    131     InputTaskScheduler(Context context, TvInputInfo input, Looper looper,
    132             ChannelDataManager channelDataManager, DvrManager dvrManager,
    133             DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock,
    134             RecordingTaskFactory recordingTaskFactory) {
    135         if (DEBUG) Log.d(TAG, "Creating scheduler for " + input);
    136         mContext = context;
    137         mInput = input;
    138         mLooper = looper;
    139         mChannelDataManager = channelDataManager;
    140         mDvrManager = dvrManager;
    141         mDataManager = (WritableDvrDataManager) dataManager;
    142         mSessionManager = sessionManager;
    143         mClock = clock;
    144         mMainThreadHandler = new Handler(Looper.getMainLooper());
    145         mRecordingTaskFactory = recordingTaskFactory != null ? recordingTaskFactory
    146                 : new RecordingTaskFactory() {
    147             @Override
    148             public RecordingTask createRecordingTask(ScheduledRecording schedule, Channel channel,
    149                     DvrManager dvrManager, InputSessionManager sessionManager,
    150                     WritableDvrDataManager dataManager, Clock clock) {
    151                 return new RecordingTask(mContext, schedule, channel, mDvrManager, mSessionManager,
    152                         mDataManager, mClock);
    153             }
    154         };
    155         mHandler = new WorkerThreadHandler(looper);
    156     }
    157 
    158     /**
    159      * Adds a {@link ScheduledRecording}.
    160      */
    161     public void addSchedule(ScheduledRecording schedule) {
    162         mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_SCHEDULED_RECORDING, schedule));
    163     }
    164 
    165     @VisibleForTesting
    166     void handleAddSchedule(ScheduledRecording schedule) {
    167         if (mPendingRecordings.get(schedule.getId()) != null
    168                 || mWaitingSchedules.containsKey(schedule.getId())) {
    169             return;
    170         }
    171         mWaitingSchedules.put(schedule.getId(), schedule);
    172         mHandler.removeMessages(MSG_BUILD_SCHEDULE);
    173         mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE);
    174     }
    175 
    176     /**
    177      * Removes the {@link ScheduledRecording}.
    178      */
    179     public void removeSchedule(ScheduledRecording schedule) {
    180         mHandler.sendMessage(mHandler.obtainMessage(MSG_REMOVE_SCHEDULED_RECORDING, schedule));
    181     }
    182 
    183     @VisibleForTesting
    184     void handleRemoveSchedule(ScheduledRecording schedule) {
    185         HandlerWrapper wrapper = mPendingRecordings.get(schedule.getId());
    186         if (wrapper != null) {
    187             wrapper.mTask.cancel();
    188             return;
    189         }
    190         if (mWaitingSchedules.containsKey(schedule.getId())) {
    191             mWaitingSchedules.remove(schedule.getId());
    192             mHandler.removeMessages(MSG_BUILD_SCHEDULE);
    193             mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE);
    194         }
    195     }
    196 
    197     /**
    198      * Updates the {@link ScheduledRecording}.
    199      */
    200     public void updateSchedule(ScheduledRecording schedule) {
    201         mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SCHEDULED_RECORDING, schedule));
    202     }
    203 
    204     @VisibleForTesting
    205     void handleUpdateSchedule(ScheduledRecording schedule) {
    206         HandlerWrapper wrapper = mPendingRecordings.get(schedule.getId());
    207         if (wrapper != null) {
    208             if (schedule.getStartTimeMs() > mClock.currentTimeMillis()
    209                     && schedule.getStartTimeMs() > wrapper.mTask.getStartTimeMs()) {
    210                 // It shouldn't have started. Cancel and put to the waiting list.
    211                 // The schedules will be rebuilt when the task is removed.
    212                 // The reschedule is called in RecordingScheduler.
    213                 wrapper.mTask.cancel();
    214                 mWaitingSchedules.put(schedule.getId(), schedule);
    215                 return;
    216             }
    217             wrapper.sendMessage(wrapper.obtainMessage(RecordingTask.MSG_UDPATE_SCHEDULE, schedule));
    218             return;
    219         }
    220         if (mWaitingSchedules.containsKey(schedule.getId())) {
    221             mWaitingSchedules.put(schedule.getId(), schedule);
    222             mHandler.removeMessages(MSG_BUILD_SCHEDULE);
    223             mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE);
    224         }
    225     }
    226 
    227     /**
    228      * Updates the TV input.
    229      */
    230     public void updateTvInputInfo(TvInputInfo input) {
    231         synchronized (mInputLock) {
    232             mInput = input;
    233         }
    234     }
    235 
    236     /**
    237      * Stops the input task scheduler.
    238      */
    239     public void stop() {
    240         mHandler.removeCallbacksAndMessages(null);
    241         mHandler.sendEmptyMessage(MSG_STOP_SCHEDULE);
    242     }
    243 
    244     private void handleStopSchedule() {
    245         mWaitingSchedules.clear();
    246         int size = mPendingRecordings.size();
    247         for (int i = 0; i < size; ++i) {
    248             RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask;
    249             task.cleanUp();
    250         }
    251     }
    252 
    253     @VisibleForTesting
    254     void handleBuildSchedule() {
    255         if (mWaitingSchedules.isEmpty()) {
    256             return;
    257         }
    258         long currentTimeMs = mClock.currentTimeMillis();
    259         // Remove past schedules.
    260         for (Iterator<ScheduledRecording> iter = mWaitingSchedules.values().iterator();
    261                 iter.hasNext(); ) {
    262             ScheduledRecording schedule = iter.next();
    263             if (schedule.getEndTimeMs() - currentTimeMs
    264                     <= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) {
    265                 fail(schedule);
    266                 iter.remove();
    267             }
    268         }
    269         if (mWaitingSchedules.isEmpty()) {
    270             return;
    271         }
    272         // Record the schedules which should start now.
    273         List<ScheduledRecording> schedulesToStart = new ArrayList<>();
    274         for (ScheduledRecording schedule : mWaitingSchedules.values()) {
    275             if (schedule.getState() != ScheduledRecording.STATE_RECORDING_CANCELED
    276                     && schedule.getStartTimeMs() - RecordingTask.RECORDING_EARLY_START_OFFSET_MS
    277                     <= currentTimeMs && schedule.getEndTimeMs() > currentTimeMs) {
    278                 schedulesToStart.add(schedule);
    279             }
    280         }
    281         // The schedules will be executed with the following order.
    282         // 1. The schedule which starts early. It can be replaced later when the schedule with the
    283         //    higher priority needs to start.
    284         // 2. The schedule with the higher priority. It can be replaced later when the schedule with
    285         //    the higher priority needs to start.
    286         // 3. The schedule which was created recently.
    287         Collections.sort(schedulesToStart, getRecordingOrderComparator());
    288         int tunerCount;
    289         synchronized (mInputLock) {
    290             tunerCount = mInput.canRecord() ? mInput.getTunerCount() : 0;
    291         }
    292         for (ScheduledRecording schedule : schedulesToStart) {
    293             if (hasTaskWhichFinishEarlier(schedule)) {
    294                 // If there is a schedule which finishes earlier than the new schedule, rebuild the
    295                 // schedules after it finishes.
    296                 return;
    297             }
    298             if (mPendingRecordings.size() < tunerCount) {
    299                 // Tuners available.
    300                 createRecordingTask(schedule).start();
    301                 mWaitingSchedules.remove(schedule.getId());
    302             } else {
    303                 // No available tuners.
    304                 RecordingTask task = getReplacableTask(schedule);
    305                 if (task != null) {
    306                     task.stop();
    307                     // Just return. The schedules will be rebuilt after the task is stopped.
    308                     return;
    309                 }
    310             }
    311         }
    312         if (mWaitingSchedules.isEmpty()) {
    313             return;
    314         }
    315         // Set next scheduling.
    316         long earliest = Long.MAX_VALUE;
    317         for (ScheduledRecording schedule : mWaitingSchedules.values()) {
    318             // The conflicting schedules will be removed if they end before conflicting resolved.
    319             if (schedulesToStart.contains(schedule)) {
    320                 if (earliest > schedule.getEndTimeMs()) {
    321                     earliest = schedule.getEndTimeMs();
    322                 }
    323             } else {
    324                 if (earliest > schedule.getStartTimeMs()
    325                         - RecordingTask.RECORDING_EARLY_START_OFFSET_MS) {
    326                     earliest = schedule.getStartTimeMs()
    327                             - RecordingTask.RECORDING_EARLY_START_OFFSET_MS;
    328                 }
    329             }
    330         }
    331         mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest - currentTimeMs);
    332     }
    333 
    334     private RecordingTask createRecordingTask(ScheduledRecording schedule) {
    335         Channel channel = mChannelDataManager.getChannel(schedule.getChannelId());
    336         RecordingTask recordingTask = mRecordingTaskFactory.createRecordingTask(schedule, channel,
    337                 mDvrManager, mSessionManager, mDataManager, mClock);
    338         HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, schedule, recordingTask);
    339         mPendingRecordings.put(schedule.getId(), handlerWrapper);
    340         return recordingTask;
    341     }
    342 
    343     private boolean hasTaskWhichFinishEarlier(ScheduledRecording schedule) {
    344         int size = mPendingRecordings.size();
    345         for (int i = 0; i < size; ++i) {
    346             RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask;
    347             if (task.getEndTimeMs() <= schedule.getStartTimeMs()) {
    348                 return true;
    349             }
    350         }
    351         return false;
    352     }
    353 
    354     private RecordingTask getReplacableTask(ScheduledRecording schedule) {
    355         // Returns the recording with the following priority.
    356         // 1. The recording with the lowest priority is returned.
    357         // 2. If the priorities are the same, the recording which finishes early is returned.
    358         // 3. If 1) and 2) are the same, the early created schedule is returned.
    359         int size = mPendingRecordings.size();
    360         RecordingTask candidate = null;
    361         for (int i = 0; i < size; ++i) {
    362             RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask;
    363             if (schedule.getPriority() > task.getPriority()) {
    364                 if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, task) > 0) {
    365                     candidate = task;
    366                 }
    367             }
    368         }
    369         return candidate;
    370     }
    371 
    372     private void fail(ScheduledRecording schedule) {
    373         // It's called when the scheduling has been failed without creating RecordingTask.
    374         runOnMainHandler(new Runnable() {
    375             @Override
    376             public void run() {
    377                 ScheduledRecording scheduleInManager =
    378                         mDataManager.getScheduledRecording(schedule.getId());
    379                 if (scheduleInManager != null) {
    380                     // The schedule should be updated based on the object from DataManager in case
    381                     // when it has been updated.
    382                     mDataManager.changeState(scheduleInManager,
    383                             ScheduledRecording.STATE_RECORDING_FAILED);
    384                 }
    385             }
    386         });
    387     }
    388 
    389     private void runOnMainHandler(Runnable runnable) {
    390         if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
    391             runnable.run();
    392         } else {
    393             mMainThreadHandler.post(runnable);
    394         }
    395     }
    396 
    397     @VisibleForTesting
    398     interface RecordingTaskFactory {
    399         RecordingTask createRecordingTask(ScheduledRecording scheduledRecording, Channel channel,
    400                 DvrManager dvrManager, InputSessionManager sessionManager,
    401                 WritableDvrDataManager dataManager, Clock clock);
    402     }
    403 
    404     private class WorkerThreadHandler extends Handler {
    405         public WorkerThreadHandler(Looper looper) {
    406             super(looper);
    407         }
    408 
    409         @Override
    410         public void handleMessage(Message msg) {
    411             switch (msg.what) {
    412                 case MSG_ADD_SCHEDULED_RECORDING:
    413                     handleAddSchedule((ScheduledRecording) msg.obj);
    414                     break;
    415                 case MSG_REMOVE_SCHEDULED_RECORDING:
    416                     handleRemoveSchedule((ScheduledRecording) msg.obj);
    417                     break;
    418                 case MSG_UPDATE_SCHEDULED_RECORDING:
    419                     handleUpdateSchedule((ScheduledRecording) msg.obj);
    420                 case MSG_BUILD_SCHEDULE:
    421                     handleBuildSchedule();
    422                     break;
    423                 case MSG_STOP_SCHEDULE:
    424                     handleStopSchedule();
    425                     break;
    426             }
    427         }
    428     }
    429 }
    430