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