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