Home | History | Annotate | Download | only in browse
      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
    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
    257     public void onViewCreated(View view, Bundle savedInstanceState) {
    258         super.onViewCreated(view, savedInstanceState);
    259         view.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
    260     }
    261 
    262     @Override
    263     public void onDestroyView() {
    264         getView()
    265                 .getViewTreeObserver()
    266                 .removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
    267         super.onDestroyView();
    268     }
    269 
    270     @Override
    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
    291     public void onDvrScheduleLoadFinished() {
    292         startBrowseIfDvrInitialized();
    293         mDvrDataManager.removeDvrScheduleLoadFinishedListener(this);
    294     }
    295 
    296     @Override
    297     public void onRecordedProgramLoadFinished() {
    298         startBrowseIfDvrInitialized();
    299         mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
    300     }
    301 
    302     @Override
    303     public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
    304         for (RecordedProgram recordedProgram : recordedPrograms) {
    305             handleRecordedProgramAdded(recordedProgram, true);
    306         }
    307         postUpdateRows();
    308     }
    309 
    310     @Override
    311     public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
    312         for (RecordedProgram recordedProgram : recordedPrograms) {
    313             handleRecordedProgramChanged(recordedProgram);
    314         }
    315         postUpdateRows();
    316     }
    317 
    318     @Override
    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
    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
    340     public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
    341         for (ScheduledRecording scheduleRecording : scheduledRecordings) {
    342             mScheduleAdapter.remove(scheduleRecording);
    343         }
    344     }
    345 
    346     @Override
    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 
    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
    370     public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {
    371         handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings));
    372         postUpdateRows();
    373     }
    374 
    375     @Override
    376     public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
    377         handleSeriesRecordingsRemoved(Arrays.asList(seriesRecordings));
    378         postUpdateRows();
    379     }
    380 
    381     @Override
    382     public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
    383         handleSeriesRecordingsChanged(Arrays.asList(seriesRecordings));
    384         postUpdateRows();
    385     }
    386 
    387     // Workaround of b/29108300
    388     @Override
    389     public void showTitle(int flags) {
    390         flags &= ~TitleViewAdapter.SEARCH_VIEW_VISIBLE;
    391         super.showTitle(flags);
    392     }
    393 
    394     @Override
    395     protected void onEntranceTransitionEnd() {
    396         super.onEntranceTransitionEnd();
    397         if (mShouldShowScheduleRow) {
    398             showScheduledRowInternal();
    399         }
    400         mEntranceTransitionEnded = true;
    401     }
    402 
    403     void showScheduledRow() {
    404         if (!mEntranceTransitionEnded) {
    405             setHeadersState(HEADERS_HIDDEN);
    406             mShouldShowScheduleRow = true;
    407         } else {
    408             showScheduledRowInternal();
    409         }
    410     }
    411 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    638     private void postUpdateRows() {
    639         mHandler.removeCallbacks(mUpdateRowsRunnable);
    640         mHandler.post(mUpdateRowsRunnable);
    641     }
    642 
    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 
    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 
    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> {
    698         ScheduleAdapter(int maxItemCount) {
    699             super(mPresenterSelector, SCHEDULE_COMPARATOR, maxItemCount);
    700         }
    701 
    702         @Override
    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> {
    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
    730         public long getId(SeriesRecording item) {
    731             return item.getId();
    732         }
    733     }
    734 
    735     private class RecordedProgramAdapter extends SortedArrayAdapter<Object> {
    736         RecordedProgramAdapter() {
    737             this(Integer.MAX_VALUE);
    738         }
    739 
    740         RecordedProgramAdapter(int maxItemCount) {
    741             super(mPresenterSelector, RECORDED_PROGRAM_COMPARATOR, maxItemCount);
    742         }
    743 
    744         @Override
    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> {
    758         RecentRowAdapter(int maxItemCount) {
    759             super(mPresenterSelector, RECENT_ROW_COMPARATOR, maxItemCount);
    760         }
    761 
    762         @Override
    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