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.annotation.SuppressLint;
     20 import android.annotation.TargetApi;
     21 import android.content.Context;
     22 import android.content.SharedPreferences;
     23 import android.os.AsyncTask;
     24 import android.os.Build;
     25 import android.support.annotation.MainThread;
     26 import android.text.TextUtils;
     27 import android.util.ArraySet;
     28 import android.util.Log;
     29 import android.util.LongSparseArray;
     30 import com.android.tv.TvSingletons;
     31 import com.android.tv.common.SoftPreconditions;
     32 import com.android.tv.common.experiments.Experiments;
     33 import com.android.tv.common.util.CollectionUtils;
     34 import com.android.tv.common.util.SharedPreferencesUtils;
     35 import com.android.tv.data.Program;
     36 import com.android.tv.data.epg.EpgReader;
     37 import com.android.tv.dvr.DvrDataManager;
     38 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
     39 import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
     40 import com.android.tv.dvr.DvrManager;
     41 import com.android.tv.dvr.WritableDvrDataManager;
     42 import com.android.tv.dvr.data.ScheduledRecording;
     43 import com.android.tv.dvr.data.SeasonEpisodeNumber;
     44 import com.android.tv.dvr.data.SeriesInfo;
     45 import com.android.tv.dvr.data.SeriesRecording;
     46 import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
     47 import java.util.ArrayList;
     48 import java.util.Arrays;
     49 import java.util.Collection;
     50 import java.util.Collections;
     51 import java.util.Comparator;
     52 import java.util.HashMap;
     53 import java.util.HashSet;
     54 import java.util.Iterator;
     55 import java.util.List;
     56 import java.util.Map;
     57 import java.util.Map.Entry;
     58 import java.util.Set;
     59 import javax.inject.Provider;
     60 
     61 /**
     62  * Creates the {@link com.android.tv.dvr.data.ScheduledRecording}s for the {@link
     63  * com.android.tv.dvr.data.SeriesRecording}.
     64  *
     65  * <p>The current implementation assumes that the series recordings are scheduled only for one
     66  * channel.
     67  */
     68 @TargetApi(Build.VERSION_CODES.N)
     69 public class SeriesRecordingScheduler {
     70     private static final String TAG = "SeriesRecordingSchd";
     71     private static final boolean DEBUG = false;
     72 
     73     private static final String KEY_FETCHED_SERIES_IDS =
     74             "SeriesRecordingScheduler.fetched_series_ids";
     75 
     76     @SuppressLint("StaticFieldLeak")
     77     private static SeriesRecordingScheduler sInstance;
     78 
     79     /** Creates and returns the {@link SeriesRecordingScheduler}. */
     80     public static synchronized SeriesRecordingScheduler getInstance(Context context) {
     81         if (sInstance == null) {
     82             sInstance = new SeriesRecordingScheduler(context);
     83         }
     84         return sInstance;
     85     }
     86 
     87     private final Context mContext;
     88     private final DvrManager mDvrManager;
     89     private final WritableDvrDataManager mDataManager;
     90     private final List<SeriesRecordingUpdateTask> mScheduleTasks = new ArrayList<>();
     91     private final LongSparseArray<FetchSeriesInfoTask> mFetchSeriesInfoTasks =
     92             new LongSparseArray<>();
     93     private final Set<String> mFetchedSeriesIds = new ArraySet<>();
     94     private final SharedPreferences mSharedPreferences;
     95     private boolean mStarted;
     96     private boolean mPaused;
     97     private final Set<Long> mPendingSeriesRecordings = new ArraySet<>();
     98 
     99     private final SeriesRecordingListener mSeriesRecordingListener =
    100             new SeriesRecordingListener() {
    101                 @Override
    102                 public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {
    103                     for (SeriesRecording seriesRecording : seriesRecordings) {
    104                         executeFetchSeriesInfoTask(seriesRecording);
    105                     }
    106                 }
    107 
    108                 @Override
    109                 public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
    110                     // Cancel the update.
    111                     for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator();
    112                             iter.hasNext(); ) {
    113                         SeriesRecordingUpdateTask task = iter.next();
    114                         if (CollectionUtils.subtract(
    115                                         task.getSeriesRecordings(),
    116                                         seriesRecordings,
    117                                         SeriesRecording.ID_COMPARATOR)
    118                                 .isEmpty()) {
    119                             task.cancel(true);
    120                             iter.remove();
    121                         }
    122                     }
    123                     for (SeriesRecording seriesRecording : seriesRecordings) {
    124                         FetchSeriesInfoTask task =
    125                                 mFetchSeriesInfoTasks.get(seriesRecording.getId());
    126                         if (task != null) {
    127                             task.cancel(true);
    128                             mFetchSeriesInfoTasks.remove(seriesRecording.getId());
    129                         }
    130                     }
    131                 }
    132 
    133                 @Override
    134                 public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
    135                     List<SeriesRecording> stopped = new ArrayList<>();
    136                     List<SeriesRecording> normal = new ArrayList<>();
    137                     for (SeriesRecording r : seriesRecordings) {
    138                         if (r.isStopped()) {
    139                             stopped.add(r);
    140                         } else {
    141                             normal.add(r);
    142                         }
    143                     }
    144                     if (!stopped.isEmpty()) {
    145                         onSeriesRecordingRemoved(SeriesRecording.toArray(stopped));
    146                     }
    147                     if (!normal.isEmpty()) {
    148                         updateSchedules(normal);
    149                     }
    150                 }
    151             };
    152 
    153     private final ScheduledRecordingListener mScheduledRecordingListener =
    154             new ScheduledRecordingListener() {
    155                 @Override
    156                 public void onScheduledRecordingAdded(ScheduledRecording... schedules) {
    157                     // No need to update series recordings when the new schedule is added.
    158                 }
    159 
    160                 @Override
    161                 public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
    162                     handleScheduledRecordingChange(Arrays.asList(schedules));
    163                 }
    164 
    165                 @Override
    166                 public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
    167                     List<ScheduledRecording> schedulesForUpdate = new ArrayList<>();
    168                     for (ScheduledRecording r : schedules) {
    169                         if ((r.getState() == ScheduledRecording.STATE_RECORDING_FAILED
    170                                         || r.getState()
    171                                                 == ScheduledRecording.STATE_RECORDING_CLIPPED)
    172                                 && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET
    173                                 && !TextUtils.isEmpty(r.getSeasonNumber())
    174                                 && !TextUtils.isEmpty(r.getEpisodeNumber())) {
    175                             schedulesForUpdate.add(r);
    176                         }
    177                     }
    178                     if (!schedulesForUpdate.isEmpty()) {
    179                         handleScheduledRecordingChange(schedulesForUpdate);
    180                     }
    181                 }
    182 
    183                 private void handleScheduledRecordingChange(List<ScheduledRecording> schedules) {
    184                     if (schedules.isEmpty()) {
    185                         return;
    186                     }
    187                     Set<Long> seriesRecordingIds = new HashSet<>();
    188                     for (ScheduledRecording r : schedules) {
    189                         if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) {
    190                             seriesRecordingIds.add(r.getSeriesRecordingId());
    191                         }
    192                     }
    193                     if (!seriesRecordingIds.isEmpty()) {
    194                         List<SeriesRecording> seriesRecordings = new ArrayList<>();
    195                         for (Long id : seriesRecordingIds) {
    196                             SeriesRecording seriesRecording = mDataManager.getSeriesRecording(id);
    197                             if (seriesRecording != null) {
    198                                 seriesRecordings.add(seriesRecording);
    199                             }
    200                         }
    201                         if (!seriesRecordings.isEmpty()) {
    202                             updateSchedules(seriesRecordings);
    203                         }
    204                     }
    205                 }
    206             };
    207 
    208     private SeriesRecordingScheduler(Context context) {
    209         mContext = context.getApplicationContext();
    210         TvSingletons tvSingletons = TvSingletons.getSingletons(context);
    211         mDvrManager = tvSingletons.getDvrManager();
    212         mDataManager = (WritableDvrDataManager) tvSingletons.getDvrDataManager();
    213         mSharedPreferences =
    214                 context.getSharedPreferences(
    215                         SharedPreferencesUtils.SHARED_PREF_SERIES_RECORDINGS, Context.MODE_PRIVATE);
    216         mFetchedSeriesIds.addAll(
    217                 mSharedPreferences.getStringSet(KEY_FETCHED_SERIES_IDS, Collections.emptySet()));
    218     }
    219 
    220     /** Starts the scheduler. */
    221     @MainThread
    222     public void start() {
    223         SoftPreconditions.checkState(mDataManager.isInitialized());
    224         if (mStarted) {
    225             return;
    226         }
    227         if (DEBUG) Log.d(TAG, "start");
    228         mStarted = true;
    229         mDataManager.addSeriesRecordingListener(mSeriesRecordingListener);
    230         mDataManager.addScheduledRecordingListener(mScheduledRecordingListener);
    231         startFetchingSeriesInfo();
    232         updateSchedules(mDataManager.getSeriesRecordings());
    233     }
    234 
    235     @MainThread
    236     public void stop() {
    237         if (!mStarted) {
    238             return;
    239         }
    240         if (DEBUG) Log.d(TAG, "stop");
    241         mStarted = false;
    242         for (int i = 0; i < mFetchSeriesInfoTasks.size(); i++) {
    243             FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(mFetchSeriesInfoTasks.keyAt(i));
    244             task.cancel(true);
    245         }
    246         mFetchSeriesInfoTasks.clear();
    247         for (SeriesRecordingUpdateTask task : mScheduleTasks) {
    248             task.cancel(true);
    249         }
    250         mScheduleTasks.clear();
    251         mDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
    252         mDataManager.removeSeriesRecordingListener(mSeriesRecordingListener);
    253     }
    254 
    255     private void startFetchingSeriesInfo() {
    256         for (SeriesRecording seriesRecording : mDataManager.getSeriesRecordings()) {
    257             if (!mFetchedSeriesIds.contains(seriesRecording.getSeriesId())) {
    258                 executeFetchSeriesInfoTask(seriesRecording);
    259             }
    260         }
    261     }
    262 
    263     private void executeFetchSeriesInfoTask(SeriesRecording seriesRecording) {
    264         if (Experiments.CLOUD_EPG.get()) {
    265             FetchSeriesInfoTask task =
    266                     new FetchSeriesInfoTask(
    267                             seriesRecording,
    268                             TvSingletons.getSingletons(mContext).providesEpgReader());
    269             task.execute();
    270             mFetchSeriesInfoTasks.put(seriesRecording.getId(), task);
    271         }
    272     }
    273 
    274     /** Pauses the updates of the series recordings. */
    275     public void pauseUpdate() {
    276         if (DEBUG) Log.d(TAG, "Schedule paused");
    277         if (mPaused) {
    278             return;
    279         }
    280         mPaused = true;
    281         if (!mStarted) {
    282             return;
    283         }
    284         for (SeriesRecordingUpdateTask task : mScheduleTasks) {
    285             for (SeriesRecording r : task.getSeriesRecordings()) {
    286                 mPendingSeriesRecordings.add(r.getId());
    287             }
    288             task.cancel(true);
    289         }
    290     }
    291 
    292     /** Resumes the updates of the series recordings. */
    293     public void resumeUpdate() {
    294         if (DEBUG) Log.d(TAG, "Schedule resumed");
    295         if (!mPaused) {
    296             return;
    297         }
    298         mPaused = false;
    299         if (!mStarted) {
    300             return;
    301         }
    302         if (!mPendingSeriesRecordings.isEmpty()) {
    303             List<SeriesRecording> seriesRecordings = new ArrayList<>();
    304             for (long seriesRecordingId : mPendingSeriesRecordings) {
    305                 SeriesRecording seriesRecording =
    306                         mDataManager.getSeriesRecording(seriesRecordingId);
    307                 if (seriesRecording != null) {
    308                     seriesRecordings.add(seriesRecording);
    309                 }
    310             }
    311             if (!seriesRecordings.isEmpty()) {
    312                 updateSchedules(seriesRecordings);
    313             }
    314         }
    315     }
    316 
    317     /**
    318      * Update schedules for the given series recordings. If it's paused, the update will be done
    319      * after it's resumed.
    320      */
    321     public void updateSchedules(Collection<SeriesRecording> seriesRecordings) {
    322         if (DEBUG) Log.d(TAG, "updateSchedules:" + seriesRecordings);
    323         if (!mStarted) {
    324             if (DEBUG) Log.d(TAG, "Not started yet.");
    325             return;
    326         }
    327         if (mPaused) {
    328             for (SeriesRecording r : seriesRecordings) {
    329                 mPendingSeriesRecordings.add(r.getId());
    330             }
    331             if (DEBUG) {
    332                 Log.d(
    333                         TAG,
    334                         "The scheduler has been paused. Adding to the pending list. size="
    335                                 + mPendingSeriesRecordings.size());
    336             }
    337             return;
    338         }
    339         Set<SeriesRecording> previousSeriesRecordings = new HashSet<>();
    340         for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator();
    341                 iter.hasNext(); ) {
    342             SeriesRecordingUpdateTask task = iter.next();
    343             if (CollectionUtils.containsAny(
    344                     task.getSeriesRecordings(), seriesRecordings, SeriesRecording.ID_COMPARATOR)) {
    345                 // The task is affected by the seriesRecordings
    346                 task.cancel(true);
    347                 previousSeriesRecordings.addAll(task.getSeriesRecordings());
    348                 iter.remove();
    349             }
    350         }
    351         List<SeriesRecording> seriesRecordingsToUpdate =
    352                 CollectionUtils.union(
    353                         seriesRecordings, previousSeriesRecordings, SeriesRecording.ID_COMPARATOR);
    354         for (Iterator<SeriesRecording> iter = seriesRecordingsToUpdate.iterator();
    355                 iter.hasNext(); ) {
    356             SeriesRecording seriesRecording = mDataManager.getSeriesRecording(iter.next().getId());
    357             if (seriesRecording == null || seriesRecording.isStopped()) {
    358                 // Series recording has been removed or stopped.
    359                 iter.remove();
    360             }
    361         }
    362         if (seriesRecordingsToUpdate.isEmpty()) {
    363             return;
    364         }
    365         if (needToReadAllChannels(seriesRecordingsToUpdate)) {
    366             SeriesRecordingUpdateTask task =
    367                     new SeriesRecordingUpdateTask(seriesRecordingsToUpdate);
    368             mScheduleTasks.add(task);
    369             if (DEBUG) Log.d(TAG, "Added schedule task: " + task);
    370             task.execute();
    371         } else {
    372             for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) {
    373                 SeriesRecordingUpdateTask task =
    374                         new SeriesRecordingUpdateTask(Collections.singletonList(seriesRecording));
    375                 mScheduleTasks.add(task);
    376                 if (DEBUG) Log.d(TAG, "Added schedule task: " + task);
    377                 task.execute();
    378             }
    379         }
    380     }
    381 
    382     private boolean needToReadAllChannels(List<SeriesRecording> seriesRecordingsToUpdate) {
    383         for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) {
    384             if (seriesRecording.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ALL) {
    385                 return true;
    386             }
    387         }
    388         return false;
    389     }
    390 
    391     /**
    392      * Pick one program per an episode.
    393      *
    394      * <p>Note that the programs which has been already scheduled have the highest priority, and all
    395      * of them are added even though they are the same episodes. That's because the schedules should
    396      * be added to the series recording.
    397      *
    398      * <p>If there are no existing schedules for an episode, one program which starts earlier is
    399      * picked.
    400      */
    401     private LongSparseArray<List<Program>> pickOneProgramPerEpisode(
    402             List<SeriesRecording> seriesRecordings, List<Program> programs) {
    403         return pickOneProgramPerEpisode(mDataManager, seriesRecordings, programs);
    404     }
    405 
    406     /** @see #pickOneProgramPerEpisode(List, List) */
    407     public static LongSparseArray<List<Program>> pickOneProgramPerEpisode(
    408             DvrDataManager dataManager,
    409             List<SeriesRecording> seriesRecordings,
    410             List<Program> programs) {
    411         // Initialize.
    412         LongSparseArray<List<Program>> result = new LongSparseArray<>();
    413         Map<String, Long> seriesRecordingIds = new HashMap<>();
    414         for (SeriesRecording seriesRecording : seriesRecordings) {
    415             result.put(seriesRecording.getId(), new ArrayList<>());
    416             seriesRecordingIds.put(seriesRecording.getSeriesId(), seriesRecording.getId());
    417         }
    418         // Group programs by the episode.
    419         Map<SeasonEpisodeNumber, List<Program>> programsForEpisodeMap = new HashMap<>();
    420         for (Program program : programs) {
    421             long seriesRecordingId = seriesRecordingIds.get(program.getSeriesId());
    422             if (TextUtils.isEmpty(program.getSeasonNumber())
    423                     || TextUtils.isEmpty(program.getEpisodeNumber())) {
    424                 // Add all the programs if it doesn't have season number or episode number.
    425                 result.get(seriesRecordingId).add(program);
    426                 continue;
    427             }
    428             SeasonEpisodeNumber seasonEpisodeNumber =
    429                     new SeasonEpisodeNumber(
    430                             seriesRecordingId,
    431                             program.getSeasonNumber(),
    432                             program.getEpisodeNumber());
    433             List<Program> programsForEpisode = programsForEpisodeMap.get(seasonEpisodeNumber);
    434             if (programsForEpisode == null) {
    435                 programsForEpisode = new ArrayList<>();
    436                 programsForEpisodeMap.put(seasonEpisodeNumber, programsForEpisode);
    437             }
    438             programsForEpisode.add(program);
    439         }
    440         // Pick one program.
    441         for (Entry<SeasonEpisodeNumber, List<Program>> entry : programsForEpisodeMap.entrySet()) {
    442             List<Program> programsForEpisode = entry.getValue();
    443             Collections.sort(
    444                     programsForEpisode,
    445                     new Comparator<Program>() {
    446                         @Override
    447                         public int compare(Program lhs, Program rhs) {
    448                             // Place the existing schedule first.
    449                             boolean lhsScheduled = isProgramScheduled(dataManager, lhs);
    450                             boolean rhsScheduled = isProgramScheduled(dataManager, rhs);
    451                             if (lhsScheduled && !rhsScheduled) {
    452                                 return -1;
    453                             }
    454                             if (!lhsScheduled && rhsScheduled) {
    455                                 return 1;
    456                             }
    457                             // Sort by the start time in ascending order.
    458                             return lhs.compareTo(rhs);
    459                         }
    460                     });
    461             boolean added = false;
    462             // Add all the scheduled programs
    463             List<Program> programsForSeries = result.get(entry.getKey().seriesRecordingId);
    464             for (Program program : programsForEpisode) {
    465                 if (isProgramScheduled(dataManager, program)) {
    466                     programsForSeries.add(program);
    467                     added = true;
    468                 } else if (!added) {
    469                     programsForSeries.add(program);
    470                     break;
    471                 }
    472             }
    473         }
    474         return result;
    475     }
    476 
    477     private static boolean isProgramScheduled(DvrDataManager dataManager, Program program) {
    478         ScheduledRecording schedule =
    479                 dataManager.getScheduledRecordingForProgramId(program.getId());
    480         return schedule != null
    481                 && schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED;
    482     }
    483 
    484     private void updateFetchedSeries() {
    485         mSharedPreferences.edit().putStringSet(KEY_FETCHED_SERIES_IDS, mFetchedSeriesIds).apply();
    486     }
    487 
    488     /**
    489      * This works only for the existing series recordings. Do not use this task for the "adding
    490      * series recording" UI.
    491      */
    492     private class SeriesRecordingUpdateTask extends EpisodicProgramLoadTask {
    493         SeriesRecordingUpdateTask(List<SeriesRecording> seriesRecordings) {
    494             super(mContext, seriesRecordings);
    495         }
    496 
    497         @Override
    498         protected void onPostExecute(List<Program> programs) {
    499             if (DEBUG) Log.d(TAG, "onPostExecute: updating schedules with programs:" + programs);
    500             mScheduleTasks.remove(this);
    501             if (programs == null) {
    502                 Log.e(
    503                         TAG,
    504                         "Creating schedules for series recording failed: " + getSeriesRecordings());
    505                 return;
    506             }
    507             LongSparseArray<List<Program>> seriesProgramMap =
    508                     pickOneProgramPerEpisode(getSeriesRecordings(), programs);
    509             for (SeriesRecording seriesRecording : getSeriesRecordings()) {
    510                 // Check the series recording is still valid.
    511                 SeriesRecording actualSeriesRecording =
    512                         mDataManager.getSeriesRecording(seriesRecording.getId());
    513                 if (actualSeriesRecording == null || actualSeriesRecording.isStopped()) {
    514                     continue;
    515                 }
    516                 List<Program> programsToSchedule = seriesProgramMap.get(seriesRecording.getId());
    517                 if (mDataManager.getSeriesRecording(seriesRecording.getId()) != null
    518                         && !programsToSchedule.isEmpty()) {
    519                     mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule);
    520                 }
    521             }
    522         }
    523 
    524         @Override
    525         protected void onCancelled(List<Program> programs) {
    526             mScheduleTasks.remove(this);
    527         }
    528 
    529         @Override
    530         public String toString() {
    531             return "SeriesRecordingUpdateTask:{"
    532                     + "series_recordings="
    533                     + getSeriesRecordings()
    534                     + "}";
    535         }
    536     }
    537 
    538     private class FetchSeriesInfoTask extends AsyncTask<Void, Void, SeriesInfo> {
    539         private final SeriesRecording mSeriesRecording;
    540         private final Provider<EpgReader> mEpgReaderProvider;
    541 
    542         FetchSeriesInfoTask(
    543                 SeriesRecording seriesRecording, Provider<EpgReader> epgReaderProvider) {
    544             mSeriesRecording = seriesRecording;
    545             mEpgReaderProvider = epgReaderProvider;
    546         }
    547 
    548         @Override
    549         protected SeriesInfo doInBackground(Void... voids) {
    550             return mEpgReaderProvider.get().getSeriesInfo(mSeriesRecording.getSeriesId());
    551         }
    552 
    553         @Override
    554         protected void onPostExecute(SeriesInfo seriesInfo) {
    555             if (seriesInfo != null) {
    556                 mDataManager.updateSeriesRecording(
    557                         SeriesRecording.buildFrom(mSeriesRecording)
    558                                 .setTitle(seriesInfo.getTitle())
    559                                 .setDescription(seriesInfo.getDescription())
    560                                 .setLongDescription(seriesInfo.getLongDescription())
    561                                 .setCanonicalGenreIds(seriesInfo.getCanonicalGenreIds())
    562                                 .setPosterUri(seriesInfo.getPosterUri())
    563                                 .setPhotoUri(seriesInfo.getPhotoUri())
    564                                 .build());
    565                 mFetchedSeriesIds.add(seriesInfo.getId());
    566                 updateFetchedSeries();
    567             }
    568             mFetchSeriesInfoTasks.remove(mSeriesRecording.getId());
    569         }
    570 
    571         @Override
    572         protected void onCancelled(SeriesInfo seriesInfo) {
    573             mFetchSeriesInfoTasks.remove(mSeriesRecording.getId());
    574         }
    575     }
    576 }
    577