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