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.ui.browse;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.os.Build;
22 import android.os.Bundle;
23 import android.os.Handler;
24 import android.text.TextUtils;
25 import androidx.leanback.app.BrowseFragment;
26 import androidx.leanback.widget.ArrayObjectAdapter;
27 import androidx.leanback.widget.ClassPresenterSelector;
28 import androidx.leanback.widget.HeaderItem;
29 import androidx.leanback.widget.ListRow;
30 import androidx.leanback.widget.Presenter;
31 import androidx.leanback.widget.TitleViewAdapter;
32 import android.util.Log;
33 import android.view.View;
34 import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
35 import com.android.tv.R;
36 import com.android.tv.TvSingletons;
37 import com.android.tv.data.GenreItems;
38 import com.android.tv.dvr.DvrDataManager;
39 import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
40 import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener;
41 import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
42 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
43 import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
44 import com.android.tv.dvr.DvrScheduleManager;
45 import com.android.tv.dvr.data.RecordedProgram;
46 import com.android.tv.dvr.data.ScheduledRecording;
47 import com.android.tv.dvr.data.SeriesRecording;
48 import com.android.tv.dvr.ui.SortedArrayAdapter;
49 import com.google.common.collect.ImmutableList;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.Comparator;
53 import java.util.HashMap;
54 import java.util.List;
55 
56 /** {@link BrowseFragment} for DVR functions. */
57 @TargetApi(Build.VERSION_CODES.N)
58 @SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
59 public class DvrBrowseFragment extends BrowseFragment
60         implements RecordedProgramListener,
61                 ScheduledRecordingListener,
62                 SeriesRecordingListener,
63                 OnDvrScheduleLoadFinishedListener,
64                 OnRecordedProgramLoadFinishedListener {
65     private static final String TAG = "DvrBrowseFragment";
66     private static final boolean DEBUG = false;
67 
68     private static final int MAX_RECENT_ITEM_COUNT = 4;
69     private static final int MAX_SCHEDULED_ITEM_COUNT = 4;
70 
71     private boolean mShouldShowScheduleRow;
72     private boolean mEntranceTransitionEnded;
73 
74     private RecentRowAdapter mRecentAdapter;
75     private ScheduleAdapter mScheduleAdapter;
76     private SeriesAdapter mSeriesAdapter;
77     private RecordedProgramAdapter[] mGenreAdapters =
78             new RecordedProgramAdapter[GenreItems.getGenreCount() + 1];
79     private ListRow mRecentRow;
80     private ListRow mScheduledRow;
81     private ListRow mSeriesRow;
82     private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1];
83     private List<String> mGenreLabels;
84     private DvrDataManager mDvrDataManager;
85     private DvrScheduleManager mDvrScheudleManager;
86     private ArrayObjectAdapter mRowsAdapter;
87     private ClassPresenterSelector mPresenterSelector;
88     private final HashMap<String, RecordedProgram> mSeriesId2LatestProgram = new HashMap<>();
89     private final Handler mHandler = new Handler();
90     private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener =
91             new OnGlobalFocusChangeListener() {
92                 @Override
93                 public void onGlobalFocusChanged(View oldFocus, View newFocus) {
94                     if (oldFocus instanceof RecordingCardView) {
95                         ((RecordingCardView) oldFocus).expandTitle(false, true);
96                     }
97                     if (newFocus instanceof RecordingCardView) {
98                         // If the header transition is ongoing, expand cards immediately without
99                         // animation to make a smooth transition.
100                         ((RecordingCardView) newFocus).expandTitle(true, !isInHeadersTransition());
101                     }
102                 }
103             };
104 
105     private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR =
106             (Object lhs, Object rhs) -> {
107                 if (lhs instanceof SeriesRecording) {
108                     lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId());
109                 }
110                 if (rhs instanceof SeriesRecording) {
111                     rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId());
112                 }
113                 if (lhs instanceof RecordedProgram) {
114                     if (rhs instanceof RecordedProgram) {
115                         return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
116                                 .reversed()
117                                 .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
118                     } else {
119                         return -1;
120                     }
121                 } else if (rhs instanceof RecordedProgram) {
122                     return 1;
123                 } else {
124                     return 0;
125                 }
126             };
127 
128     private static final Comparator<Object> SCHEDULE_COMPARATOR =
129             (Object lhs, Object rhs) -> {
130                 if (lhs instanceof ScheduledRecording) {
131                     if (rhs instanceof ScheduledRecording) {
132                         return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
133                                 .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
134                     } else {
135                         return -1;
136                     }
137                 } else if (rhs instanceof ScheduledRecording) {
138                     return 1;
139                 } else {
140                     return 0;
141                 }
142             };
143 
144     static final Comparator<Object> RECENT_ROW_COMPARATOR =
145             (Object lhs, Object rhs) -> {
146                 if (lhs instanceof ScheduledRecording) {
147                     if (rhs instanceof ScheduledRecording) {
148                         return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
149                                 .reversed()
150                                 .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
151                     } else if (rhs instanceof RecordedProgram) {
152                         ScheduledRecording scheduled = (ScheduledRecording) lhs;
153                         RecordedProgram recorded = (RecordedProgram) rhs;
154                         int compare =
155                                 Long.compare(
156                                         recorded.getStartTimeUtcMillis(),
157                                         scheduled.getStartTimeMs());
158                         // recorded program first when the start times are the same
159                         return compare == 0 ? 1 : compare;
160                     } else {
161                         return -1;
162                     }
163                 } else if (lhs instanceof RecordedProgram) {
164                     if (rhs instanceof RecordedProgram) {
165                         return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
166                                 .reversed()
167                                 .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
168                     } else if (rhs instanceof ScheduledRecording) {
169                         RecordedProgram recorded = (RecordedProgram) lhs;
170                         ScheduledRecording scheduled = (ScheduledRecording) rhs;
171                         int compare =
172                                 Long.compare(
173                                         scheduled.getStartTimeMs(),
174                                         recorded.getStartTimeUtcMillis());
175                         // recorded program first when the start times are the same
176                         return compare == 0 ? -1 : compare;
177                     } else {
178                         return -1;
179                     }
180                 } else {
181                     return !(rhs instanceof RecordedProgram) && !(rhs instanceof ScheduledRecording)
182                             ? 0
183                             : 1;
184                 }
185             };
186 
187     private final DvrScheduleManager.OnConflictStateChangeListener mOnConflictStateChangeListener =
188             new DvrScheduleManager.OnConflictStateChangeListener() {
189                 @Override
190                 public void onConflictStateChange(
191                         boolean conflict, ScheduledRecording... schedules) {
192                     if (mScheduleAdapter != null) {
193                         for (ScheduledRecording schedule : schedules) {
194                             onScheduledRecordingConflictStatusChanged(schedule);
195                         }
196                     }
197                 }
198             };
199 
200     private final Runnable mUpdateRowsRunnable = this::updateRows;
201 
202     @Override
onCreate(Bundle savedInstanceState)203     public void onCreate(Bundle savedInstanceState) {
204         if (DEBUG) Log.d(TAG, "onCreate");
205         super.onCreate(savedInstanceState);
206         Context context = getContext();
207         TvSingletons singletons = TvSingletons.getSingletons(context);
208         mDvrDataManager = singletons.getDvrDataManager();
209         mDvrScheudleManager = singletons.getDvrScheduleManager();
210         mPresenterSelector =
211                 new ClassPresenterSelector()
212                         .addClassPresenter(
213                                 ScheduledRecording.class, new ScheduledRecordingPresenter(context))
214                         .addClassPresenter(
215                                 RecordedProgram.class, new RecordedProgramPresenter(context))
216                         .addClassPresenter(
217                                 SeriesRecording.class, new SeriesRecordingPresenter(context))
218                         .addClassPresenter(
219                                 FullScheduleCardHolder.class,
220                                 new FullSchedulesCardPresenter(context))
221                         .addClassPresenter(
222                                 DvrHistoryCardHolder.class, new DvrHistoryCardPresenter(context));
223 
224         mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context)));
225         mGenreLabels.add(getString(R.string.dvr_main_others));
226         prepareUiElements();
227         if (!startBrowseIfDvrInitialized()) {
228             if (!mDvrDataManager.isDvrScheduleLoadFinished()) {
229                 mDvrDataManager.addDvrScheduleLoadFinishedListener(this);
230             }
231             if (!mDvrDataManager.isRecordedProgramLoadFinished()) {
232                 mDvrDataManager.addRecordedProgramLoadFinishedListener(this);
233             }
234         }
235     }
236 
237     @Override
onViewCreated(View view, Bundle savedInstanceState)238     public void onViewCreated(View view, Bundle savedInstanceState) {
239         super.onViewCreated(view, savedInstanceState);
240         view.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
241     }
242 
243     @Override
onDestroyView()244     public void onDestroyView() {
245         getView()
246                 .getViewTreeObserver()
247                 .removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
248         super.onDestroyView();
249     }
250 
251     @Override
onDestroy()252     public void onDestroy() {
253         if (DEBUG) Log.d(TAG, "onDestroy");
254         mHandler.removeCallbacks(mUpdateRowsRunnable);
255         mDvrScheudleManager.removeOnConflictStateChangeListener(mOnConflictStateChangeListener);
256         mDvrDataManager.removeRecordedProgramListener(this);
257         mDvrDataManager.removeScheduledRecordingListener(this);
258         mDvrDataManager.removeSeriesRecordingListener(this);
259         mDvrDataManager.removeDvrScheduleLoadFinishedListener(this);
260         mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
261         mRowsAdapter.clear();
262         mSeriesId2LatestProgram.clear();
263         for (Presenter presenter : mPresenterSelector.getPresenters()) {
264             if (presenter instanceof DvrItemPresenter) {
265                 ((DvrItemPresenter) presenter).unbindAllViewHolders();
266             }
267         }
268         super.onDestroy();
269     }
270 
271     @Override
onDvrScheduleLoadFinished()272     public void onDvrScheduleLoadFinished() {
273         startBrowseIfDvrInitialized();
274         mDvrDataManager.removeDvrScheduleLoadFinishedListener(this);
275     }
276 
277     @Override
onRecordedProgramLoadFinished()278     public void onRecordedProgramLoadFinished() {
279         startBrowseIfDvrInitialized();
280         mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
281     }
282 
283     @Override
onRecordedProgramsAdded(RecordedProgram... recordedPrograms)284     public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
285         for (RecordedProgram recordedProgram : recordedPrograms) {
286             handleRecordedProgramAdded(recordedProgram, true);
287         }
288         postUpdateRows();
289     }
290 
291     @Override
onRecordedProgramsChanged(RecordedProgram... recordedPrograms)292     public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
293         for (RecordedProgram recordedProgram : recordedPrograms) {
294             if (recordedProgram.isVisible()) {
295                 handleRecordedProgramChanged(recordedProgram);
296             }
297         }
298         postUpdateRows();
299     }
300 
301     @Override
onRecordedProgramsRemoved(RecordedProgram... recordedPrograms)302     public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
303         for (RecordedProgram recordedProgram : recordedPrograms) {
304             handleRecordedProgramRemoved(recordedProgram);
305         }
306         postUpdateRows();
307     }
308 
309     // No need to call updateRows() during ScheduledRecordings' change because
310     // the row for ScheduledRecordings is always displayed.
311     @Override
onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings)312     public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
313         for (ScheduledRecording scheduleRecording : scheduledRecordings) {
314             if (needToShowScheduledRecording(scheduleRecording)) {
315                 mScheduleAdapter.add(scheduleRecording);
316             } else if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
317                 mRecentAdapter.add(scheduleRecording);
318             }
319         }
320     }
321 
322     @Override
onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings)323     public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
324         for (ScheduledRecording scheduleRecording : scheduledRecordings) {
325             mScheduleAdapter.remove(scheduleRecording);
326             if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
327                 mRecentAdapter.remove(scheduleRecording);
328             }
329         }
330     }
331 
332     @Override
onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings)333     public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
334         for (ScheduledRecording scheduleRecording : scheduledRecordings) {
335             if (needToShowScheduledRecording(scheduleRecording)) {
336                 mScheduleAdapter.change(scheduleRecording);
337             } else {
338                 mScheduleAdapter.removeWithId(scheduleRecording);
339             }
340             if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
341                 mRecentAdapter.change(scheduleRecording);
342             }
343         }
344     }
345 
onScheduledRecordingConflictStatusChanged(ScheduledRecording... schedules)346     private void onScheduledRecordingConflictStatusChanged(ScheduledRecording... schedules) {
347         for (ScheduledRecording schedule : schedules) {
348             if (needToShowScheduledRecording(schedule)) {
349                 if (mScheduleAdapter.contains(schedule)) {
350                     mScheduleAdapter.change(schedule);
351                 }
352             } else {
353                 mScheduleAdapter.removeWithId(schedule);
354             }
355         }
356     }
357 
358     @Override
onSeriesRecordingAdded(SeriesRecording... seriesRecordings)359     public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {
360         handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings));
361         postUpdateRows();
362     }
363 
364     @Override
onSeriesRecordingRemoved(SeriesRecording... seriesRecordings)365     public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
366         handleSeriesRecordingsRemoved(Arrays.asList(seriesRecordings));
367         postUpdateRows();
368     }
369 
370     @Override
onSeriesRecordingChanged(SeriesRecording... seriesRecordings)371     public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
372         handleSeriesRecordingsChanged(Arrays.asList(seriesRecordings));
373         postUpdateRows();
374     }
375 
376     // Workaround of b/29108300
377     @Override
showTitle(int flags)378     public void showTitle(int flags) {
379         flags &= ~TitleViewAdapter.SEARCH_VIEW_VISIBLE;
380         super.showTitle(flags);
381     }
382 
383     @Override
onEntranceTransitionEnd()384     protected void onEntranceTransitionEnd() {
385         super.onEntranceTransitionEnd();
386         if (mShouldShowScheduleRow) {
387             showScheduledRowInternal();
388         }
389         mEntranceTransitionEnded = true;
390     }
391 
showScheduledRow()392     void showScheduledRow() {
393         if (!mEntranceTransitionEnded) {
394             setHeadersState(HEADERS_HIDDEN);
395             mShouldShowScheduleRow = true;
396         } else {
397             showScheduledRowInternal();
398         }
399     }
400 
showScheduledRowInternal()401     private void showScheduledRowInternal() {
402         setSelectedPosition(mRowsAdapter.indexOf(mScheduledRow), true, null);
403         if (getHeadersState() == HEADERS_ENABLED) {
404             startHeadersTransition(false);
405         }
406         mShouldShowScheduleRow = false;
407     }
408 
prepareUiElements()409     private void prepareUiElements() {
410         setBadgeDrawable(getActivity().getDrawable(R.drawable.ic_dvr_badge));
411         setHeadersState(HEADERS_ENABLED);
412         setHeadersTransitionOnBackEnabled(false);
413         setBrandColor(getResources().getColor(R.color.program_guide_side_panel_background, null));
414         mRowsAdapter = new ArrayObjectAdapter(new DvrListRowPresenter(getContext()));
415         setAdapter(mRowsAdapter);
416         prepareEntranceTransition();
417     }
418 
startBrowseIfDvrInitialized()419     private boolean startBrowseIfDvrInitialized() {
420         if (mDvrDataManager.isInitialized()) {
421             // Setup rows
422             mRecentAdapter = new RecentRowAdapter(MAX_RECENT_ITEM_COUNT);
423             mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT);
424             mSeriesAdapter = new SeriesAdapter();
425             for (int i = 0; i < mGenreAdapters.length; i++) {
426                 mGenreAdapters[i] = new RecordedProgramAdapter();
427             }
428             // Schedule Recordings.
429             // only get not started or in progress recordings
430             List<ScheduledRecording> schedules = mDvrDataManager.getAvailableScheduledRecordings();
431             onScheduledRecordingAdded(ScheduledRecording.toArray(schedules));
432             mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
433             // Recorded Programs.
434             for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
435                 if (recordedProgram.isVisible()) {
436                     handleRecordedProgramAdded(recordedProgram, false);
437                 }
438             }
439             // only get failed recordings
440             for (ScheduledRecording scheduledRecording :
441                     mDvrDataManager.getFailedScheduledRecordings()) {
442                 onScheduledRecordingAdded(scheduledRecording);
443             }
444             mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER);
445 
446             // Series Recordings. Series recordings should be added after recorded programs, because
447             // we build series recordings' latest program information while adding recorded
448             // programs.
449             List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings();
450             handleSeriesRecordingsAdded(recordings);
451             mRecentRow =
452                     new ListRow(
453                             new HeaderItem(getString(R.string.dvr_main_recent)), mRecentAdapter);
454             mScheduledRow =
455                     new ListRow(
456                             new HeaderItem(getString(R.string.dvr_main_scheduled)),
457                             mScheduleAdapter);
458             mSeriesRow =
459                     new ListRow(
460                             new HeaderItem(getString(R.string.dvr_main_series)), mSeriesAdapter);
461             mRowsAdapter.add(mScheduledRow);
462             updateRows();
463             // Initialize listeners
464             mDvrDataManager.addRecordedProgramListener(this);
465             mDvrDataManager.addScheduledRecordingListener(this);
466             mDvrDataManager.addSeriesRecordingListener(this);
467             mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener);
468             startEntranceTransition();
469             return true;
470         }
471         return false;
472     }
473 
handleRecordedProgramAdded( RecordedProgram recordedProgram, boolean updateSeriesRecording)474     private void handleRecordedProgramAdded(
475             RecordedProgram recordedProgram, boolean updateSeriesRecording) {
476         mRecentAdapter.add(recordedProgram);
477         String seriesId = recordedProgram.getSeriesId();
478         SeriesRecording seriesRecording = null;
479         if (!TextUtils.isEmpty(seriesId)) {
480             seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
481             RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId);
482             if (latestProgram == null
483                     || RecordedProgram.START_TIME_THEN_ID_COMPARATOR.compare(
484                                     latestProgram, recordedProgram)
485                             < 0) {
486                 mSeriesId2LatestProgram.put(seriesId, recordedProgram);
487                 if (updateSeriesRecording && seriesRecording != null) {
488                     onSeriesRecordingChanged(seriesRecording);
489                 }
490             }
491         }
492         if (seriesRecording == null) {
493             for (RecordedProgramAdapter adapter :
494                     getGenreAdapters(recordedProgram.getCanonicalGenres())) {
495                 adapter.add(recordedProgram);
496             }
497         }
498     }
499 
handleRecordedProgramRemoved(RecordedProgram recordedProgram)500     private void handleRecordedProgramRemoved(RecordedProgram recordedProgram) {
501         mRecentAdapter.remove(recordedProgram);
502         String seriesId = recordedProgram.getSeriesId();
503         if (!TextUtils.isEmpty(seriesId)) {
504             SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
505             RecordedProgram latestProgram =
506                     mSeriesId2LatestProgram.get(recordedProgram.getSeriesId());
507             if (latestProgram != null && latestProgram.getId() == recordedProgram.getId()) {
508                 if (seriesRecording != null) {
509                     updateLatestRecordedProgram(seriesRecording);
510                     onSeriesRecordingChanged(seriesRecording);
511                 }
512             }
513         }
514         for (RecordedProgramAdapter adapter :
515                 getGenreAdapters(recordedProgram.getCanonicalGenres())) {
516             adapter.remove(recordedProgram);
517         }
518     }
519 
handleRecordedProgramChanged(RecordedProgram recordedProgram)520     private void handleRecordedProgramChanged(RecordedProgram recordedProgram) {
521         mRecentAdapter.change(recordedProgram);
522         String seriesId = recordedProgram.getSeriesId();
523         SeriesRecording seriesRecording = null;
524         if (!TextUtils.isEmpty(seriesId)) {
525             seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
526             RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId);
527             if (latestProgram == null
528                     || RecordedProgram.START_TIME_THEN_ID_COMPARATOR.compare(
529                                     latestProgram, recordedProgram)
530                             <= 0) {
531                 mSeriesId2LatestProgram.put(seriesId, recordedProgram);
532                 if (seriesRecording != null) {
533                     onSeriesRecordingChanged(seriesRecording);
534                 }
535             } else if (latestProgram.getId() == recordedProgram.getId()) {
536                 if (seriesRecording != null) {
537                     updateLatestRecordedProgram(seriesRecording);
538                     onSeriesRecordingChanged(seriesRecording);
539                 }
540             }
541         }
542         if (seriesRecording == null) {
543             updateGenreAdapters(
544                     getGenreAdapters(recordedProgram.getCanonicalGenres()), recordedProgram);
545         } else {
546             updateGenreAdapters(new ArrayList<>(), recordedProgram);
547         }
548     }
549 
handleSeriesRecordingsAdded(List<SeriesRecording> seriesRecordings)550     private void handleSeriesRecordingsAdded(List<SeriesRecording> seriesRecordings) {
551         for (SeriesRecording seriesRecording : seriesRecordings) {
552             mSeriesAdapter.add(seriesRecording);
553             if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) {
554                 for (RecordedProgramAdapter adapter :
555                         getGenreAdapters(seriesRecording.getCanonicalGenreIds())) {
556                     adapter.add(seriesRecording);
557                 }
558             }
559         }
560     }
561 
handleSeriesRecordingsRemoved(List<SeriesRecording> seriesRecordings)562     private void handleSeriesRecordingsRemoved(List<SeriesRecording> seriesRecordings) {
563         for (SeriesRecording seriesRecording : seriesRecordings) {
564             mSeriesAdapter.remove(seriesRecording);
565             for (RecordedProgramAdapter adapter :
566                     getGenreAdapters(seriesRecording.getCanonicalGenreIds())) {
567                 adapter.remove(seriesRecording);
568             }
569         }
570     }
571 
handleSeriesRecordingsChanged(List<SeriesRecording> seriesRecordings)572     private void handleSeriesRecordingsChanged(List<SeriesRecording> seriesRecordings) {
573         for (SeriesRecording seriesRecording : seriesRecordings) {
574             mSeriesAdapter.change(seriesRecording);
575             if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) {
576                 updateGenreAdapters(
577                         getGenreAdapters(seriesRecording.getCanonicalGenreIds()), seriesRecording);
578             } else {
579                 // Remove series recording from all genre rows if it has no recorded program
580                 updateGenreAdapters(new ArrayList<>(), seriesRecording);
581             }
582         }
583     }
584 
getGenreAdapters(ImmutableList<String> genres)585     private List<RecordedProgramAdapter> getGenreAdapters(ImmutableList<String> genres) {
586         List<RecordedProgramAdapter> result = new ArrayList<>();
587         if (genres == null || genres.isEmpty()) {
588             result.add(mGenreAdapters[mGenreAdapters.length - 1]);
589         } else {
590             for (String genre : genres) {
591                 int genreId = GenreItems.getId(genre);
592                 if (genreId >= mGenreAdapters.length) {
593                     Log.d(TAG, "Wrong Genre ID: " + genreId);
594                 } else {
595                     result.add(mGenreAdapters[genreId]);
596                 }
597             }
598         }
599         return result;
600     }
601 
getGenreAdapters(int[] genreIds)602     private List<RecordedProgramAdapter> getGenreAdapters(int[] genreIds) {
603         List<RecordedProgramAdapter> result = new ArrayList<>();
604         if (genreIds == null || genreIds.length == 0) {
605             result.add(mGenreAdapters[mGenreAdapters.length - 1]);
606         } else {
607             for (int genreId : genreIds) {
608                 if (genreId >= mGenreAdapters.length) {
609                     Log.d(TAG, "Wrong Genre ID: " + genreId);
610                 } else {
611                     result.add(mGenreAdapters[genreId]);
612                 }
613             }
614         }
615         return result;
616     }
617 
updateGenreAdapters(List<RecordedProgramAdapter> adapters, Object r)618     private void updateGenreAdapters(List<RecordedProgramAdapter> adapters, Object r) {
619         for (RecordedProgramAdapter adapter : mGenreAdapters) {
620             if (adapters.contains(adapter)) {
621                 adapter.change(r);
622             } else {
623                 adapter.remove(r);
624             }
625         }
626     }
627 
postUpdateRows()628     private void postUpdateRows() {
629         mHandler.removeCallbacks(mUpdateRowsRunnable);
630         mHandler.post(mUpdateRowsRunnable);
631     }
632 
updateRows()633     private void updateRows() {
634         int visibleRowsCount = 1; // Schedule's Row will never be empty
635         if (mRecentAdapter.size() <= 1) {
636             // remove the row if there is only the DVR history card
637             mRowsAdapter.remove(mRecentRow);
638         } else {
639             if (mRowsAdapter.indexOf(mRecentRow) < 0) {
640                 mRowsAdapter.add(0, mRecentRow);
641             }
642             visibleRowsCount++;
643         }
644         if (mSeriesAdapter.isEmpty()) {
645             mRowsAdapter.remove(mSeriesRow);
646         } else {
647             if (mRowsAdapter.indexOf(mSeriesRow) < 0) {
648                 mRowsAdapter.add(visibleRowsCount, mSeriesRow);
649             }
650             visibleRowsCount++;
651         }
652         for (int i = 0; i < mGenreAdapters.length; i++) {
653             RecordedProgramAdapter adapter = mGenreAdapters[i];
654             if (adapter != null) {
655                 if (adapter.isEmpty()) {
656                     mRowsAdapter.remove(mGenreRows[i]);
657                 } else {
658                     if (mGenreRows[i] == null || mRowsAdapter.indexOf(mGenreRows[i]) < 0) {
659                         mGenreRows[i] = new ListRow(new HeaderItem(mGenreLabels.get(i)), adapter);
660                         mRowsAdapter.add(visibleRowsCount, mGenreRows[i]);
661                     }
662                     visibleRowsCount++;
663                 }
664             }
665         }
666         if (getSelectedPosition() >= mRowsAdapter.size()) {
667             setSelectedPosition(mRowsAdapter.size() - 1);
668         }
669     }
670 
needToShowScheduledRecording(ScheduledRecording recording)671     private boolean needToShowScheduledRecording(ScheduledRecording recording) {
672         int state = recording.getState();
673         return state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS
674                 || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED;
675     }
676 
updateLatestRecordedProgram(SeriesRecording seriesRecording)677     private void updateLatestRecordedProgram(SeriesRecording seriesRecording) {
678         RecordedProgram latestProgram = null;
679         for (RecordedProgram program :
680                 mDvrDataManager.getRecordedPrograms(seriesRecording.getId())) {
681             if (latestProgram == null
682                     || RecordedProgram.START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program)
683                             < 0) {
684                 latestProgram = program;
685             }
686         }
687         mSeriesId2LatestProgram.put(seriesRecording.getSeriesId(), latestProgram);
688     }
689 
690     private class ScheduleAdapter extends SortedArrayAdapter<Object> {
ScheduleAdapter(int maxItemCount)691         ScheduleAdapter(int maxItemCount) {
692             super(mPresenterSelector, SCHEDULE_COMPARATOR, maxItemCount);
693         }
694 
695         @Override
getId(Object item)696         public long getId(Object item) {
697             if (item instanceof ScheduledRecording) {
698                 return ((ScheduledRecording) item).getId();
699             } else {
700                 return -1;
701             }
702         }
703     }
704 
705     private class SeriesAdapter extends SortedArrayAdapter<SeriesRecording> {
SeriesAdapter()706         SeriesAdapter() {
707             super(
708                     mPresenterSelector,
709                     (SeriesRecording lhs, SeriesRecording rhs) -> {
710                         if (lhs.isStopped() && !rhs.isStopped()) {
711                             return 1;
712                         } else if (!lhs.isStopped() && rhs.isStopped()) {
713                             return -1;
714                         }
715                         return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs);
716                     });
717         }
718 
719         @Override
getId(SeriesRecording item)720         public long getId(SeriesRecording item) {
721             return item.getId();
722         }
723     }
724 
725     private class RecordedProgramAdapter extends SortedArrayAdapter<Object> {
RecordedProgramAdapter()726         RecordedProgramAdapter() {
727             this(Integer.MAX_VALUE);
728         }
729 
RecordedProgramAdapter(int maxItemCount)730         RecordedProgramAdapter(int maxItemCount) {
731             super(mPresenterSelector, RECORDED_PROGRAM_COMPARATOR, maxItemCount);
732         }
733 
734         @Override
getId(Object item)735         public long getId(Object item) {
736             // We takes the inverse number for the ID of recorded programs to make the ID stable.
737             if (item instanceof SeriesRecording) {
738                 return ((SeriesRecording) item).getId();
739             } else if (item instanceof RecordedProgram) {
740                 return -((RecordedProgram) item).getId() - 1;
741             } else {
742                 return -1;
743             }
744         }
745     }
746 
747     private class RecentRowAdapter extends SortedArrayAdapter<Object> {
RecentRowAdapter(int maxItemCount)748         RecentRowAdapter(int maxItemCount) {
749             super(mPresenterSelector, RECENT_ROW_COMPARATOR, maxItemCount);
750         }
751 
752         @Override
getId(Object item)753         public long getId(Object item) {
754             // We takes the inverse number for the ID of scheduled recordings to make the ID stable.
755             if (item instanceof ScheduledRecording) {
756                 return -((ScheduledRecording) item).getId() - 1;
757             } else if (item instanceof RecordedProgram) {
758                 return ((RecordedProgram) item).getId();
759             } else {
760                 return -1;
761             }
762         }
763     }
764 }
765