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