1 /* 2 * Copyright (C) 2016 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.content.res.Resources; 20 import android.graphics.drawable.Drawable; 21 import android.media.tv.TvInputManager; 22 import android.os.Bundle; 23 import android.support.annotation.Nullable; 24 import android.text.TextUtils; 25 import androidx.leanback.app.DetailsFragment; 26 import androidx.leanback.widget.Action; 27 import androidx.leanback.widget.ArrayObjectAdapter; 28 import androidx.leanback.widget.ClassPresenterSelector; 29 import androidx.leanback.widget.DetailsOverviewRow; 30 import androidx.leanback.widget.DetailsOverviewRowPresenter; 31 import androidx.leanback.widget.HeaderItem; 32 import androidx.leanback.widget.ListRow; 33 import androidx.leanback.widget.OnActionClickedListener; 34 import androidx.leanback.widget.PresenterSelector; 35 import androidx.leanback.widget.SparseArrayObjectAdapter; 36 import com.android.tv.R; 37 import com.android.tv.TvSingletons; 38 import com.android.tv.data.api.BaseProgram; 39 import com.android.tv.dvr.DvrDataManager; 40 import com.android.tv.dvr.DvrWatchedPositionManager; 41 import com.android.tv.dvr.data.RecordedProgram; 42 import com.android.tv.dvr.data.SeriesRecording; 43 import com.android.tv.dvr.ui.DvrUiHelper; 44 import com.android.tv.dvr.ui.SortedArrayAdapter; 45 import com.android.tv.ui.DetailsActivity; 46 import java.util.Collections; 47 import java.util.Comparator; 48 import java.util.List; 49 50 /** {@link DetailsFragment} for series recording in DVR. */ 51 public class SeriesRecordingDetailsFragment extends DvrDetailsFragment 52 implements DvrDataManager.SeriesRecordingListener, DvrDataManager.RecordedProgramListener { 53 private static final int ACTION_WATCH = 1; 54 private static final int ACTION_SERIES_SCHEDULES = 2; 55 private static final int ACTION_DELETE = 3; 56 57 private DvrWatchedPositionManager mDvrWatchedPositionManager; 58 private DvrDataManager mDvrDataManager; 59 60 private SeriesRecording mSeries; 61 // NOTICE: mRecordedPrograms should only be used in creating details fragments. 62 // After fragments are created, it should be cleared to save resources. 63 private List<RecordedProgram> mRecordedPrograms; 64 private RecordedProgram mRecommendRecordedProgram; 65 private int mSeasonRowCount; 66 private SparseArrayObjectAdapter mActionsAdapter; 67 private Action mDeleteAction; 68 69 private boolean mPaused; 70 private long mInitialPlaybackPositionMs; 71 private String mWatchLabel; 72 private String mResumeLabel; 73 private Drawable mWatchDrawable; 74 private RecordedProgramPresenter mRecordedProgramPresenter; 75 76 @Override onCreate(Bundle savedInstanceState)77 public void onCreate(Bundle savedInstanceState) { 78 mDvrDataManager = TvSingletons.getSingletons(getActivity()).getDvrDataManager(); 79 mWatchLabel = getString(R.string.dvr_detail_watch); 80 mResumeLabel = getString(R.string.dvr_detail_series_resume); 81 mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null); 82 mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true, true); 83 super.onCreate(savedInstanceState); 84 } 85 86 @Override onCreateInternal()87 protected void onCreateInternal() { 88 mDvrWatchedPositionManager = 89 TvSingletons.getSingletons(getActivity()).getDvrWatchedPositionManager(); 90 setDetailsOverviewRow(DetailsContent.createFromSeriesRecording(getContext(), mSeries)); 91 setupRecordedProgramsRow(); 92 mDvrDataManager.addSeriesRecordingListener(this); 93 mDvrDataManager.addRecordedProgramListener(this); 94 mRecordedPrograms = null; 95 } 96 97 @Override onResume()98 public void onResume() { 99 super.onResume(); 100 if (mPaused) { 101 updateWatchAction(); 102 mPaused = false; 103 } 104 } 105 106 @Override onPause()107 public void onPause() { 108 super.onPause(); 109 mPaused = true; 110 } 111 updateWatchAction()112 private void updateWatchAction() { 113 List<RecordedProgram> programs = mDvrDataManager.getRecordedPrograms(mSeries.getId()); 114 Collections.sort(programs, RecordedProgram.EPISODE_COMPARATOR); 115 mRecommendRecordedProgram = getRecommendProgram(programs); 116 if (mRecommendRecordedProgram == null) { 117 mActionsAdapter.clear(ACTION_WATCH); 118 } else { 119 String episodeStatus; 120 if (mDvrWatchedPositionManager.getWatchedStatus(mRecommendRecordedProgram) 121 == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { 122 episodeStatus = mResumeLabel; 123 mInitialPlaybackPositionMs = 124 mDvrWatchedPositionManager.getWatchedPosition( 125 mRecommendRecordedProgram.getId()); 126 } else { 127 episodeStatus = mWatchLabel; 128 mInitialPlaybackPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; 129 } 130 String episodeDisplayNumber = 131 mRecommendRecordedProgram.getEpisodeDisplayNumber(getContext()); 132 mActionsAdapter.set( 133 ACTION_WATCH, 134 new Action(ACTION_WATCH, episodeStatus, episodeDisplayNumber, mWatchDrawable)); 135 } 136 } 137 138 @Override onLoadRecordingDetails(Bundle args)139 protected boolean onLoadRecordingDetails(Bundle args) { 140 long recordId = args.getLong(DetailsActivity.RECORDING_ID); 141 mSeries = 142 TvSingletons.getSingletons(getActivity()) 143 .getDvrDataManager() 144 .getSeriesRecording(recordId); 145 if (mSeries == null) { 146 return false; 147 } 148 mRecordedPrograms = mDvrDataManager.getRecordedPrograms(mSeries.getId()); 149 Collections.sort(mRecordedPrograms, RecordedProgram.SEASON_REVERSED_EPISODE_COMPARATOR); 150 return true; 151 } 152 153 @Override onCreatePresenterSelector( DetailsOverviewRowPresenter rowPresenter)154 protected PresenterSelector onCreatePresenterSelector( 155 DetailsOverviewRowPresenter rowPresenter) { 156 ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); 157 presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); 158 presenterSelector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); 159 return presenterSelector; 160 } 161 162 @Override onCreateActionsAdapter()163 protected SparseArrayObjectAdapter onCreateActionsAdapter() { 164 mActionsAdapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); 165 Resources res = getResources(); 166 updateWatchAction(); 167 mActionsAdapter.set( 168 ACTION_SERIES_SCHEDULES, 169 new Action( 170 ACTION_SERIES_SCHEDULES, 171 getString(R.string.dvr_detail_view_schedule), 172 null, 173 res.getDrawable(R.drawable.ic_schedule_32dp, null))); 174 mDeleteAction = 175 new Action( 176 ACTION_DELETE, 177 getString(R.string.dvr_detail_series_delete), 178 null, 179 res.getDrawable(R.drawable.ic_delete_32dp, null)); 180 if (!mRecordedPrograms.isEmpty()) { 181 mActionsAdapter.set(ACTION_DELETE, mDeleteAction); 182 } 183 return mActionsAdapter; 184 } 185 setupRecordedProgramsRow()186 private void setupRecordedProgramsRow() { 187 for (RecordedProgram program : mRecordedPrograms) { 188 addProgram(program); 189 } 190 } 191 192 @Override onDestroy()193 public void onDestroy() { 194 super.onDestroy(); 195 mDvrDataManager.removeSeriesRecordingListener(this); 196 mDvrDataManager.removeRecordedProgramListener(this); 197 if (mSeries != null) { 198 mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeries.getId()); 199 } 200 mRecordedProgramPresenter.unbindAllViewHolders(); 201 } 202 203 @Override onCreateOnActionClickedListener()204 protected OnActionClickedListener onCreateOnActionClickedListener() { 205 return new OnActionClickedListener() { 206 @Override 207 public void onActionClicked(Action action) { 208 if (action.getId() == ACTION_WATCH) { 209 startPlayback(mRecommendRecordedProgram, mInitialPlaybackPositionMs); 210 } else if (action.getId() == ACTION_SERIES_SCHEDULES) { 211 DvrUiHelper.startSchedulesActivityForSeries(getContext(), mSeries); 212 } else if (action.getId() == ACTION_DELETE) { 213 DvrUiHelper.startSeriesDeletionActivity(getContext(), mSeries.getId()); 214 } 215 } 216 }; 217 } 218 219 /** The programs are sorted by season number and episode number. */ 220 @Nullable 221 private RecordedProgram getRecommendProgram(List<RecordedProgram> programs) { 222 for (int i = programs.size() - 1; i >= 0; i--) { 223 RecordedProgram program = programs.get(i); 224 int watchedStatus = mDvrWatchedPositionManager.getWatchedStatus(program); 225 if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_NEW) { 226 continue; 227 } 228 if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { 229 return program; 230 } 231 if (i == programs.size() - 1) { 232 return program; 233 } else { 234 return programs.get(i + 1); 235 } 236 } 237 return programs.isEmpty() ? null : programs.get(0); 238 } 239 240 @Override 241 public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {} 242 243 @Override 244 public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { 245 for (SeriesRecording series : seriesRecordings) { 246 if (mSeries.getId() == series.getId()) { 247 mSeries = series; 248 } 249 } 250 } 251 252 @Override 253 public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { 254 for (SeriesRecording series : seriesRecordings) { 255 if (series.getId() == mSeries.getId()) { 256 getActivity().finish(); 257 return; 258 } 259 } 260 } 261 262 @Override 263 public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { 264 for (RecordedProgram recordedProgram : recordedPrograms) { 265 if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { 266 addProgram(recordedProgram); 267 if (mActionsAdapter.lookup(ACTION_DELETE) == null) { 268 mActionsAdapter.set(ACTION_DELETE, mDeleteAction); 269 } 270 } 271 } 272 } 273 274 @Override 275 public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { 276 // Do nothing 277 } 278 279 @Override 280 public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { 281 for (RecordedProgram recordedProgram : recordedPrograms) { 282 if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { 283 ListRow row = getSeasonRow(recordedProgram.getSeasonNumber(), false); 284 if (row != null) { 285 SeasonRowAdapter adapter = (SeasonRowAdapter) row.getAdapter(); 286 adapter.remove(recordedProgram); 287 if (adapter.isEmpty()) { 288 getRowsAdapter().remove(row); 289 if (getRowsAdapter().size() == 1) { 290 // No season rows left. Only DetailsOverviewRow 291 mActionsAdapter.clear(ACTION_DELETE); 292 } 293 } 294 } 295 if (mRecommendRecordedProgram != null 296 && recordedProgram.getId() == mRecommendRecordedProgram.getId()) { 297 updateWatchAction(); 298 } 299 } 300 } 301 } 302 303 private void addProgram(RecordedProgram program) { 304 String programSeasonNumber = 305 TextUtils.isEmpty(program.getSeasonNumber()) ? "" : program.getSeasonNumber(); 306 getOrCreateSeasonRowAdapter(programSeasonNumber).add(program); 307 } 308 309 private SeasonRowAdapter getOrCreateSeasonRowAdapter(String seasonNumber) { 310 ListRow row = getSeasonRow(seasonNumber, true); 311 return (SeasonRowAdapter) row.getAdapter(); 312 } 313 314 private ListRow getSeasonRow(String seasonNumber, boolean createNewRow) { 315 seasonNumber = TextUtils.isEmpty(seasonNumber) ? "" : seasonNumber; 316 ArrayObjectAdapter rowsAdaptor = getRowsAdapter(); 317 for (int i = rowsAdaptor.size() - 1; i >= 0; i--) { 318 Object row = rowsAdaptor.get(i); 319 if (row instanceof ListRow) { 320 int compareResult = 321 BaseProgram.numberCompare( 322 seasonNumber, 323 ((SeasonRowAdapter) ((ListRow) row).getAdapter()).mSeasonNumber); 324 if (compareResult == 0) { 325 return (ListRow) row; 326 } else if (compareResult < 0) { 327 return createNewRow ? createNewSeasonRow(seasonNumber, i + 1) : null; 328 } 329 } 330 } 331 return createNewRow ? createNewSeasonRow(seasonNumber, rowsAdaptor.size()) : null; 332 } 333 334 private ListRow createNewSeasonRow(String seasonNumber, int position) { 335 String seasonTitle = 336 seasonNumber.isEmpty() 337 ? mSeries.getTitle() 338 : getString(R.string.dvr_detail_series_season_title, seasonNumber); 339 HeaderItem header = new HeaderItem(mSeasonRowCount++, seasonTitle); 340 ClassPresenterSelector selector = new ClassPresenterSelector(); 341 selector.addClassPresenter(RecordedProgram.class, mRecordedProgramPresenter); 342 ListRow row = 343 new ListRow( 344 header, 345 new SeasonRowAdapter( 346 selector, BaseProgram.EPISODE_COMPARATOR::compare, seasonNumber)); 347 getRowsAdapter().add(position, row); 348 return row; 349 } 350 351 private class SeasonRowAdapter extends SortedArrayAdapter<RecordedProgram> { 352 private String mSeasonNumber; 353 354 SeasonRowAdapter( 355 PresenterSelector selector, 356 Comparator<RecordedProgram> comparator, 357 String seasonNumber) { 358 super(selector, comparator); 359 mSeasonNumber = seasonNumber; 360 } 361 362 @Override 363 public long getId(RecordedProgram program) { 364 return program.getId(); 365 } 366 } 367 } 368