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