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.annotation.TargetApi;
     20 import android.content.ContentUris;
     21 import android.media.tv.TvContract;
     22 import android.net.Uri;
     23 import android.os.Build;
     24 import android.os.Message;
     25 import android.support.annotation.MainThread;
     26 import android.support.annotation.NonNull;
     27 import android.support.annotation.Nullable;
     28 import android.util.ArraySet;
     29 import android.util.Log;
     30 import com.android.tv.InputSessionManager;
     31 import com.android.tv.InputSessionManager.OnTvViewChannelChangeListener;
     32 import com.android.tv.MainActivity;
     33 import com.android.tv.TvSingletons;
     34 import com.android.tv.common.WeakHandler;
     35 import com.android.tv.data.ChannelDataManager;
     36 import com.android.tv.data.api.Channel;
     37 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
     38 import com.android.tv.dvr.DvrScheduleManager;
     39 import com.android.tv.dvr.data.ScheduledRecording;
     40 import com.android.tv.dvr.ui.DvrUiHelper;
     41 import java.util.ArrayList;
     42 import java.util.Arrays;
     43 import java.util.HashMap;
     44 import java.util.List;
     45 import java.util.Map;
     46 import java.util.Set;
     47 import java.util.concurrent.TimeUnit;
     48 
     49 /**
     50  * Checking the runtime conflict of DVR recording.
     51  *
     52  * <p>This class runs only while the {@link MainActivity} is resumed and holds the upcoming
     53  * conflicts.
     54  */
     55 @TargetApi(Build.VERSION_CODES.N)
     56 @MainThread
     57 public class ConflictChecker {
     58     private static final String TAG = "ConflictChecker";
     59     private static final boolean DEBUG = false;
     60 
     61     private static final int MSG_CHECK_CONFLICT = 1;
     62 
     63     private static final long CHECK_RETRY_PERIOD_MS = TimeUnit.SECONDS.toMillis(30);
     64 
     65     /**
     66      * To show watch conflict dialog, the start time of the earliest conflicting schedule should be
     67      * less than or equal to this time.
     68      */
     69     private static final long MAX_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.MINUTES.toMillis(5);
     70     /**
     71      * To show watch conflict dialog, the start time of the earliest conflicting schedule should be
     72      * greater than or equal to this time.
     73      */
     74     private static final long MIN_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.SECONDS.toMillis(30);
     75 
     76     private final MainActivity mMainActivity;
     77     private final ChannelDataManager mChannelDataManager;
     78     private final DvrScheduleManager mScheduleManager;
     79     private final InputSessionManager mSessionManager;
     80     private final ConflictCheckerHandler mHandler = new ConflictCheckerHandler(this);
     81 
     82     private final List<ScheduledRecording> mUpcomingConflicts = new ArrayList<>();
     83     private final Set<OnUpcomingConflictChangeListener> mOnUpcomingConflictChangeListeners =
     84             new ArraySet<>();
     85     private final Map<Long, List<ScheduledRecording>> mCheckedConflictsMap = new HashMap<>();
     86 
     87     private final ScheduledRecordingListener mScheduledRecordingListener =
     88             new ScheduledRecordingListener() {
     89                 @Override
     90                 public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
     91                     if (DEBUG) {
     92                         Log.d(
     93                                 TAG,
     94                                 "onScheduledRecordingAdded: "
     95                                         + Arrays.toString(scheduledRecordings));
     96                     }
     97                     mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
     98                 }
     99 
    100                 @Override
    101                 public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
    102                     if (DEBUG) {
    103                         Log.d(
    104                                 TAG,
    105                                 "onScheduledRecordingRemoved: "
    106                                         + Arrays.toString(scheduledRecordings));
    107                     }
    108                     mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
    109                 }
    110 
    111                 @Override
    112                 public void onScheduledRecordingStatusChanged(
    113                         ScheduledRecording... scheduledRecordings) {
    114                     if (DEBUG) {
    115                         Log.d(
    116                                 TAG,
    117                                 "onScheduledRecordingStatusChanged: "
    118                                         + Arrays.toString(scheduledRecordings));
    119                     }
    120                     mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
    121                 }
    122             };
    123 
    124     private final OnTvViewChannelChangeListener mOnTvViewChannelChangeListener =
    125             new OnTvViewChannelChangeListener() {
    126                 @Override
    127                 public void onTvViewChannelChange(@Nullable Uri channelUri) {
    128                     mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
    129                 }
    130             };
    131 
    132     private boolean mStarted;
    133 
    134     public ConflictChecker(MainActivity mainActivity) {
    135         mMainActivity = mainActivity;
    136         TvSingletons tvSingletons = TvSingletons.getSingletons(mainActivity);
    137         mChannelDataManager = tvSingletons.getChannelDataManager();
    138         mScheduleManager = tvSingletons.getDvrScheduleManager();
    139         mSessionManager = tvSingletons.getInputSessionManager();
    140     }
    141 
    142     /** Starts checking the conflict. */
    143     public void start() {
    144         if (mStarted) {
    145             return;
    146         }
    147         mStarted = true;
    148         mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
    149         mScheduleManager.addScheduledRecordingListener(mScheduledRecordingListener);
    150         mSessionManager.addOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener);
    151     }
    152 
    153     /** Stops checking the conflict. */
    154     public void stop() {
    155         if (!mStarted) {
    156             return;
    157         }
    158         mStarted = false;
    159         mSessionManager.removeOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener);
    160         mScheduleManager.removeScheduledRecordingListener(mScheduledRecordingListener);
    161         mHandler.removeCallbacksAndMessages(null);
    162     }
    163 
    164     /** Returns the upcoming conflicts. */
    165     public List<ScheduledRecording> getUpcomingConflicts() {
    166         return new ArrayList<>(mUpcomingConflicts);
    167     }
    168 
    169     /** Adds a {@link OnUpcomingConflictChangeListener}. */
    170     public void addOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) {
    171         mOnUpcomingConflictChangeListeners.add(listener);
    172     }
    173 
    174     /** Removes the {@link OnUpcomingConflictChangeListener}. */
    175     public void removeOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) {
    176         mOnUpcomingConflictChangeListeners.remove(listener);
    177     }
    178 
    179     private void notifyUpcomingConflictChanged() {
    180         for (OnUpcomingConflictChangeListener l : mOnUpcomingConflictChangeListeners) {
    181             l.onUpcomingConflictChange();
    182         }
    183     }
    184 
    185     /** Remembers the user's decision to record while watching the channel. */
    186     public void setCheckedConflictsForChannel(long mChannelId, List<ScheduledRecording> conflicts) {
    187         mCheckedConflictsMap.put(mChannelId, new ArrayList<>(conflicts));
    188     }
    189 
    190     void onCheckConflict() {
    191         // Checks the conflicting schedules and setup the next re-check time.
    192         // If there are upcoming conflicts soon, it opens the conflict dialog.
    193         if (DEBUG) Log.d(TAG, "Handling MSG_CHECK_CONFLICT");
    194         mHandler.removeMessages(MSG_CHECK_CONFLICT);
    195         mUpcomingConflicts.clear();
    196         if (!mScheduleManager.isInitialized() || !mChannelDataManager.isDbLoadFinished()) {
    197             mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, CHECK_RETRY_PERIOD_MS);
    198             notifyUpcomingConflictChanged();
    199             return;
    200         }
    201         if (mSessionManager.getCurrentTvViewChannelUri() == null) {
    202             // As MainActivity is not using a tuner, no need to check the conflict.
    203             notifyUpcomingConflictChanged();
    204             return;
    205         }
    206         Uri channelUri = mSessionManager.getCurrentTvViewChannelUri();
    207         if (TvContract.isChannelUriForPassthroughInput(channelUri)) {
    208             notifyUpcomingConflictChanged();
    209             return;
    210         }
    211         long channelId = ContentUris.parseId(channelUri);
    212         Channel channel = mChannelDataManager.getChannel(channelId);
    213         // The conflicts caused by watching the channel.
    214         List<ScheduledRecording> conflicts =
    215                 mScheduleManager.getConflictingSchedulesForWatching(channel.getId());
    216         long earliestToCheck = Long.MAX_VALUE;
    217         long currentTimeMs = System.currentTimeMillis();
    218         for (ScheduledRecording schedule : conflicts) {
    219             long startTimeMs = schedule.getStartTimeMs();
    220             if (startTimeMs < currentTimeMs + MIN_WATCH_CONFLICT_CHECK_TIME_MS) {
    221                 // The start time of the upcoming conflict remains less than the minimum
    222                 // check time.
    223                 continue;
    224             }
    225             if (startTimeMs > currentTimeMs + MAX_WATCH_CONFLICT_CHECK_TIME_MS) {
    226                 // The start time of the upcoming conflict remains greater than the
    227                 // maximum check time. Setup the next re-check time.
    228                 long nextCheckTimeMs = startTimeMs - MAX_WATCH_CONFLICT_CHECK_TIME_MS;
    229                 if (earliestToCheck > nextCheckTimeMs) {
    230                     earliestToCheck = nextCheckTimeMs;
    231                 }
    232             } else {
    233                 // Found upcoming conflicts which will start soon.
    234                 mUpcomingConflicts.add(schedule);
    235                 // The schedule will be removed from the "upcoming conflict" when the
    236                 // recording is almost started.
    237                 long nextCheckTimeMs = startTimeMs - MIN_WATCH_CONFLICT_CHECK_TIME_MS;
    238                 if (earliestToCheck > nextCheckTimeMs) {
    239                     earliestToCheck = nextCheckTimeMs;
    240                 }
    241             }
    242         }
    243         if (earliestToCheck != Long.MAX_VALUE) {
    244             mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, earliestToCheck - currentTimeMs);
    245         }
    246         if (DEBUG) Log.d(TAG, "upcoming conflicts: " + mUpcomingConflicts);
    247         notifyUpcomingConflictChanged();
    248         if (!mUpcomingConflicts.isEmpty()
    249                 && !DvrUiHelper.isChannelWatchConflictDialogShown(mMainActivity)) {
    250             // Don't show the conflict dialog if the user already knows.
    251             List<ScheduledRecording> checkedConflicts = mCheckedConflictsMap.get(channel.getId());
    252             if (checkedConflicts == null || !checkedConflicts.containsAll(mUpcomingConflicts)) {
    253                 DvrUiHelper.showChannelWatchConflictDialog(mMainActivity, channel);
    254             }
    255         }
    256     }
    257 
    258     private static class ConflictCheckerHandler extends WeakHandler<ConflictChecker> {
    259         ConflictCheckerHandler(ConflictChecker conflictChecker) {
    260             super(conflictChecker);
    261         }
    262 
    263         @Override
    264         protected void handleMessage(Message msg, @NonNull ConflictChecker conflictChecker) {
    265             switch (msg.what) {
    266                 case MSG_CHECK_CONFLICT:
    267                     conflictChecker.onCheckConflict();
    268                     break;
    269             }
    270         }
    271     }
    272 
    273     /** A listener for the change of upcoming conflicts. */
    274     public interface OnUpcomingConflictChangeListener {
    275         void onUpcomingConflictChange();
    276     }
    277 }
    278