Home | History | Annotate | Download | only in recorder
      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.recorder;
     18 
     19 import android.app.AlarmManager;
     20 import android.app.PendingIntent;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.media.tv.TvInputInfo;
     24 import android.media.tv.TvInputManager.TvInputCallback;
     25 import android.os.Build;
     26 import android.os.HandlerThread;
     27 import android.os.Looper;
     28 import android.support.annotation.MainThread;
     29 import android.support.annotation.RequiresApi;
     30 import android.support.annotation.VisibleForTesting;
     31 import android.util.ArrayMap;
     32 import android.util.Log;
     33 import android.util.Range;
     34 import com.android.tv.InputSessionManager;
     35 import com.android.tv.TvSingletons;
     36 import com.android.tv.common.SoftPreconditions;
     37 import com.android.tv.common.util.Clock;
     38 import com.android.tv.data.ChannelDataManager;
     39 import com.android.tv.data.ChannelDataManager.Listener;
     40 import com.android.tv.dvr.DvrDataManager;
     41 import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
     42 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
     43 import com.android.tv.dvr.DvrManager;
     44 import com.android.tv.dvr.WritableDvrDataManager;
     45 import com.android.tv.dvr.data.ScheduledRecording;
     46 import com.android.tv.util.TvInputManagerHelper;
     47 import com.android.tv.util.Utils;
     48 import java.util.Arrays;
     49 import java.util.List;
     50 import java.util.Map;
     51 import java.util.concurrent.TimeUnit;
     52 
     53 /**
     54  * The core class to manage DVR schedule and run recording task. *
     55  *
     56  * <p>This class is responsible for:
     57  *
     58  * <ul>
     59  *   <li>Sending record commands to TV inputs
     60  *   <li>Resolving conflicting schedules, handling overlapping recording time durations, etc.
     61  * </ul>
     62  *
     63  * <p>This should be a singleton associated with application's main process.
     64  */
     65 @RequiresApi(Build.VERSION_CODES.N)
     66 @MainThread
     67 public class RecordingScheduler extends TvInputCallback implements ScheduledRecordingListener {
     68     private static final String TAG = "RecordingScheduler";
     69     private static final boolean DEBUG = false;
     70 
     71     private static final String HANDLER_THREAD_NAME = "RecordingScheduler";
     72     private static final long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(1);
     73     @VisibleForTesting static final long MS_TO_WAKE_BEFORE_START = TimeUnit.SECONDS.toMillis(30);
     74 
     75     private final Looper mLooper;
     76     private final InputSessionManager mSessionManager;
     77     private final WritableDvrDataManager mDataManager;
     78     private final DvrManager mDvrManager;
     79     private final ChannelDataManager mChannelDataManager;
     80     private final TvInputManagerHelper mInputManager;
     81     private final Context mContext;
     82     private final Clock mClock;
     83     private final AlarmManager mAlarmManager;
     84 
     85     private final Map<String, InputTaskScheduler> mInputSchedulerMap = new ArrayMap<>();
     86     private long mLastStartTimePendingMs;
     87 
     88     private OnDvrScheduleLoadFinishedListener mDvrScheduleLoadListener =
     89             new OnDvrScheduleLoadFinishedListener() {
     90                 @Override
     91                 public void onDvrScheduleLoadFinished() {
     92                     mDataManager.removeDvrScheduleLoadFinishedListener(this);
     93                     if (isDbLoaded()) {
     94                         updateInternal();
     95                     }
     96                 }
     97             };
     98 
     99     private Listener mChannelDataLoadListener =
    100             new Listener() {
    101                 @Override
    102                 public void onLoadFinished() {
    103                     mChannelDataManager.removeListener(this);
    104                     if (isDbLoaded()) {
    105                         updateInternal();
    106                     }
    107                 }
    108 
    109                 @Override
    110                 public void onChannelListUpdated() {}
    111 
    112                 @Override
    113                 public void onChannelBrowsableChanged() {}
    114             };
    115 
    116     /**
    117      * Creates a scheduler to schedule alarms for scheduled recordings and create recording tasks.
    118      * This method should be only called once in the life-cycle of the application.
    119      */
    120     public static RecordingScheduler createScheduler(Context context) {
    121         SoftPreconditions.checkState(
    122                 TvSingletons.getSingletons(context).getRecordingScheduler() == null);
    123         HandlerThread handlerThread = new HandlerThread(HANDLER_THREAD_NAME);
    124         handlerThread.start();
    125         TvSingletons singletons = TvSingletons.getSingletons(context);
    126         return new RecordingScheduler(
    127                 handlerThread.getLooper(),
    128                 singletons.getDvrManager(),
    129                 singletons.getInputSessionManager(),
    130                 (WritableDvrDataManager) singletons.getDvrDataManager(),
    131                 singletons.getChannelDataManager(),
    132                 singletons.getTvInputManagerHelper(),
    133                 context,
    134                 Clock.SYSTEM,
    135                 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE));
    136     }
    137 
    138     @VisibleForTesting
    139     RecordingScheduler(
    140             Looper looper,
    141             DvrManager dvrManager,
    142             InputSessionManager sessionManager,
    143             WritableDvrDataManager dataManager,
    144             ChannelDataManager channelDataManager,
    145             TvInputManagerHelper inputManager,
    146             Context context,
    147             Clock clock,
    148             AlarmManager alarmManager) {
    149         mLooper = looper;
    150         mDvrManager = dvrManager;
    151         mSessionManager = sessionManager;
    152         mDataManager = dataManager;
    153         mChannelDataManager = channelDataManager;
    154         mInputManager = inputManager;
    155         mContext = context;
    156         mClock = clock;
    157         mAlarmManager = alarmManager;
    158         mDataManager.addScheduledRecordingListener(this);
    159         mInputManager.addCallback(this);
    160         if (isDbLoaded()) {
    161             updateInternal();
    162         } else {
    163             if (!mDataManager.isDvrScheduleLoadFinished()) {
    164                 mDataManager.addDvrScheduleLoadFinishedListener(mDvrScheduleLoadListener);
    165             }
    166             if (!mChannelDataManager.isDbLoadFinished()) {
    167                 mChannelDataManager.addListener(mChannelDataLoadListener);
    168             }
    169         }
    170     }
    171 
    172     /** Start recording that will happen soon, and set the next alarm time. */
    173     public void updateAndStartServiceIfNeeded() {
    174         if (DEBUG) Log.d(TAG, "update and start service if needed");
    175         if (isDbLoaded()) {
    176             updateInternal();
    177         } else {
    178             // updateInternal will be called when DB is loaded. Start DvrRecordingService to
    179             // prevent process being killed before that.
    180             DvrRecordingService.startForegroundService(mContext, false);
    181         }
    182     }
    183 
    184     private void updateInternal() {
    185         boolean recordingSoon = updatePendingRecordings();
    186         updateNextAlarm();
    187         if (recordingSoon) {
    188             // Start DvrRecordingService to protect upcoming recording task from being killed.
    189             DvrRecordingService.startForegroundService(mContext, true);
    190         } else {
    191             DvrRecordingService.stopForegroundIfNotRecording();
    192         }
    193     }
    194 
    195     private boolean updatePendingRecordings() {
    196         List<ScheduledRecording> scheduledRecordings =
    197                 mDataManager.getScheduledRecordings(
    198                         new Range<>(
    199                                 mLastStartTimePendingMs,
    200                                 mClock.currentTimeMillis() + SOON_DURATION_IN_MS),
    201                         ScheduledRecording.STATE_RECORDING_NOT_STARTED);
    202         for (ScheduledRecording r : scheduledRecordings) {
    203             scheduleRecordingSoon(r);
    204         }
    205         // update() may be called multiple times, under this situation, pending recordings may be
    206         // already updated thus scheduledRecordings may have a size of 0. Therefore we also have to
    207         // check mLastStartTimePendingMs to check if we have upcoming recordings and prevent the
    208         // recording service being wrongly pushed back to background in updateInternal().
    209         return scheduledRecordings.size() > 0
    210                 || (mLastStartTimePendingMs > mClock.currentTimeMillis()
    211                         && mLastStartTimePendingMs
    212                                 < mClock.currentTimeMillis() + SOON_DURATION_IN_MS);
    213     }
    214 
    215     private boolean isDbLoaded() {
    216         return mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished();
    217     }
    218 
    219     @Override
    220     public void onScheduledRecordingAdded(ScheduledRecording... schedules) {
    221         if (DEBUG) Log.d(TAG, "added " + Arrays.asList(schedules));
    222         if (!isDbLoaded()) {
    223             return;
    224         }
    225         handleScheduleChange(schedules);
    226     }
    227 
    228     @Override
    229     public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
    230         if (DEBUG) Log.d(TAG, "removed " + Arrays.asList(schedules));
    231         if (!isDbLoaded()) {
    232             return;
    233         }
    234         boolean needToUpdateAlarm = false;
    235         for (ScheduledRecording schedule : schedules) {
    236             InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(schedule.getInputId());
    237             if (inputTaskScheduler != null) {
    238                 inputTaskScheduler.removeSchedule(schedule);
    239                 needToUpdateAlarm = true;
    240             }
    241         }
    242         if (needToUpdateAlarm) {
    243             updateNextAlarm();
    244         }
    245     }
    246 
    247     @Override
    248     public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
    249         if (DEBUG) Log.d(TAG, "state changed " + Arrays.asList(schedules));
    250         if (!isDbLoaded()) {
    251             return;
    252         }
    253         // Update the recordings.
    254         for (ScheduledRecording schedule : schedules) {
    255             InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(schedule.getInputId());
    256             if (inputTaskScheduler != null) {
    257                 inputTaskScheduler.updateSchedule(schedule);
    258             }
    259         }
    260         handleScheduleChange(schedules);
    261     }
    262 
    263     private void handleScheduleChange(ScheduledRecording... schedules) {
    264         boolean needToUpdateAlarm = false;
    265         for (ScheduledRecording schedule : schedules) {
    266             if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
    267                 if (startsWithin(schedule, SOON_DURATION_IN_MS)) {
    268                     scheduleRecordingSoon(schedule);
    269                 } else {
    270                     needToUpdateAlarm = true;
    271                 }
    272             }
    273         }
    274         if (needToUpdateAlarm) {
    275             updateNextAlarm();
    276         }
    277     }
    278 
    279     private void scheduleRecordingSoon(ScheduledRecording schedule) {
    280         TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
    281         if (input == null) {
    282             Log.e(TAG, "Can't find input for " + schedule);
    283             mDataManager.changeState(
    284                     schedule,
    285                     ScheduledRecording.STATE_RECORDING_FAILED,
    286                     ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE);
    287             return;
    288         }
    289         if (!input.canRecord() || input.getTunerCount() <= 0) {
    290             Log.e(TAG, "TV input doesn't support recording: " + input);
    291             mDataManager.changeState(
    292                     schedule,
    293                     ScheduledRecording.STATE_RECORDING_FAILED,
    294                     ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED);
    295             return;
    296         }
    297         InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(input.getId());
    298         if (inputTaskScheduler == null) {
    299             inputTaskScheduler =
    300                     new InputTaskScheduler(
    301                             mContext,
    302                             input,
    303                             mLooper,
    304                             mChannelDataManager,
    305                             mDvrManager,
    306                             mDataManager,
    307                             mSessionManager,
    308                             mClock);
    309             mInputSchedulerMap.put(input.getId(), inputTaskScheduler);
    310         }
    311         inputTaskScheduler.addSchedule(schedule);
    312         if (mLastStartTimePendingMs < schedule.getStartTimeMs()) {
    313             mLastStartTimePendingMs = schedule.getStartTimeMs();
    314         }
    315     }
    316 
    317     private void updateNextAlarm() {
    318         long nextStartTime =
    319                 mDataManager.getNextScheduledStartTimeAfter(
    320                         Math.max(mLastStartTimePendingMs, mClock.currentTimeMillis()));
    321         if (nextStartTime != DvrDataManager.NEXT_START_TIME_NOT_FOUND) {
    322             long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START;
    323             if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt);
    324             Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class);
    325             PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
    326             // This will cancel the previous alarm.
    327             mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent);
    328         } else {
    329             if (DEBUG) Log.d(TAG, "No future recording, alarm not set");
    330         }
    331     }
    332 
    333     @VisibleForTesting
    334     boolean startsWithin(ScheduledRecording scheduledRecording, long durationInMs) {
    335         return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs;
    336     }
    337 
    338     // No need to remove input task schedule worker when the input is removed. If the input is
    339     // removed temporarily, the scheduler should keep the non-started schedules.
    340     @Override
    341     public void onInputUpdated(String inputId) {
    342         InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(inputId);
    343         if (inputTaskScheduler != null) {
    344             inputTaskScheduler.updateTvInputInfo(Utils.getTvInputInfoForInputId(mContext, inputId));
    345         }
    346     }
    347 
    348     @Override
    349     public void onTvInputInfoUpdated(TvInputInfo input) {
    350         InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(input.getId());
    351         if (inputTaskScheduler != null) {
    352             inputTaskScheduler.updateTvInputInfo(input);
    353         }
    354     }
    355 }
    356