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