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