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