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