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