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.playback;
18 
19 import android.app.Fragment;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.graphics.Point;
23 import android.hardware.display.DisplayManager;
24 import android.media.session.PlaybackState;
25 import android.media.tv.TvContentRating;
26 import android.media.tv.TvInputManager;
27 import android.media.tv.TvTrackInfo;
28 import android.os.Bundle;
29 import android.util.Log;
30 import android.view.Display;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.widget.Toast;
34 import androidx.leanback.app.PlaybackFragment;
35 import androidx.leanback.app.PlaybackFragmentGlueHost;
36 import androidx.leanback.widget.ArrayObjectAdapter;
37 import androidx.leanback.widget.BaseOnItemViewClickedListener;
38 import androidx.leanback.widget.ClassPresenterSelector;
39 import androidx.leanback.widget.HeaderItem;
40 import androidx.leanback.widget.ListRow;
41 import androidx.leanback.widget.Presenter;
42 import androidx.leanback.widget.RowPresenter;
43 import androidx.leanback.widget.SinglePresenterSelector;
44 import com.android.tv.R;
45 import com.android.tv.audio.AudioManagerHelper;
46 import com.android.tv.common.buildtype.HasBuildType.BuildType;
47 import com.android.tv.data.api.BaseProgram;
48 import com.android.tv.dialog.PinDialogFragment;
49 import com.android.tv.dvr.DvrDataManager;
50 import com.android.tv.dvr.data.RecordedProgram;
51 import com.android.tv.dvr.data.SeriesRecording;
52 import com.android.tv.dvr.ui.SortedArrayAdapter;
53 import com.android.tv.dvr.ui.browse.DvrListRowPresenter;
54 import com.android.tv.dvr.ui.browse.RecordingCardView;
55 import com.android.tv.ui.AppLayerTvView;
56 import com.android.tv.util.TvSettings;
57 import com.android.tv.util.TvTrackInfoUtils;
58 import com.android.tv.util.Utils;
59 import dagger.android.AndroidInjection;
60 import com.android.tv.common.flags.LegacyFlags;
61 import java.util.ArrayList;
62 import java.util.List;
63 import javax.inject.Inject;
64 
65 public class DvrPlaybackOverlayFragment extends PlaybackFragment {
66     // TODO: Handles audio focus. Deals with block and ratings.
67     private static final String TAG = "DvrPlaybackOverlayFrag";
68     private static final boolean DEBUG = false;
69 
70     private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession";
71     private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f;
72     private static final long INVALID_TIME = -1;
73 
74     // mProgram is only used to store program from intent. Don't use it elsewhere.
75     private RecordedProgram mProgram;
76     private DvrPlayer mDvrPlayer;
77     private DvrPlaybackMediaSessionHelper mMediaSessionHelper;
78     private DvrPlaybackControlHelper mPlaybackControlHelper;
79     private ArrayObjectAdapter mRowsAdapter;
80     private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter;
81     private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter;
82     private AudioManagerHelper mAudioManagerHelper;
83     private AppLayerTvView mTvView;
84     private View mBlockScreenView;
85     private ListRow mRelatedRecordingsRow;
86     private int mVerticalPaddingBase;
87     private int mPaddingWithoutRelatedRow;
88     private int mPaddingWithoutSecondaryRow;
89     private int mWindowWidth;
90     private int mWindowHeight;
91     private float mAppliedAspectRatio;
92     private float mWindowAspectRatio;
93     private boolean mPinChecked;
94     private boolean mStarted;
95     private DvrPlayer.OnTrackSelectedListener mOnSubtitleTrackSelectedListener =
96             new DvrPlayer.OnTrackSelectedListener() {
97                 @Override
98                 public void onTrackSelected(String selectedTrackId) {
99                     mPlaybackControlHelper.onSubtitleTrackStateChanged(selectedTrackId != null);
100                     mRowsAdapter.notifyArrayItemRangeChanged(0, 1);
101                 }
102             };
103 
104     @Inject DvrDataManager mDvrDataManager;
105     @Inject LegacyFlags mLegacyFlags;
106     @Inject BuildType buildType;
107 
108     @Override
onAttach(Context context)109     public void onAttach(Context context) {
110         if (DEBUG) {
111             Log.d(TAG, "onAttach");
112         }
113         AndroidInjection.inject(this);
114         super.onAttach(context);
115     }
116 
117     @Override
onCreate(Bundle savedInstanceState)118     public void onCreate(Bundle savedInstanceState) {
119         if (DEBUG) {
120             Log.d(TAG, "onCreate");
121         }
122         super.onCreate(savedInstanceState);
123         mVerticalPaddingBase =
124                 getActivity()
125                         .getResources()
126                         .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_base);
127         mPaddingWithoutRelatedRow =
128                 getActivity()
129                         .getResources()
130                         .getDimensionPixelOffset(
131                                 R.dimen.dvr_playback_overlay_padding_top_no_related_row);
132         mPaddingWithoutSecondaryRow =
133                 getActivity()
134                         .getResources()
135                         .getDimensionPixelOffset(
136                                 R.dimen.dvr_playback_overlay_padding_top_no_secondary_row);
137         if (!mDvrDataManager.isRecordedProgramLoadFinished()) {
138             mDvrDataManager.addRecordedProgramLoadFinishedListener(
139                     new DvrDataManager.OnRecordedProgramLoadFinishedListener() {
140                         @Override
141                         public void onRecordedProgramLoadFinished() {
142                             mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
143                             if (handleIntent(getActivity().getIntent(), true)) {
144                                 setUpRows();
145                                 preparePlayback(getActivity().getIntent());
146                             }
147                         }
148                     });
149         } else if (!handleIntent(getActivity().getIntent(), true)) {
150             return;
151         }
152         Point size = new Point();
153         ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE))
154                 .getDisplay(Display.DEFAULT_DISPLAY)
155                 .getSize(size);
156         mWindowWidth = size.x;
157         mWindowHeight = size.y;
158         mWindowAspectRatio = mAppliedAspectRatio = (float) mWindowWidth / mWindowHeight;
159         setBackgroundType(PlaybackFragment.BG_LIGHT);
160         setFadingEnabled(true);
161     }
162 
163     @Override
onStart()164     public void onStart() {
165         super.onStart();
166         mStarted = true;
167         updateVerticalPosition();
168     }
169 
170     @Override
onActivityCreated(Bundle savedInstanceState)171     public void onActivityCreated(Bundle savedInstanceState) {
172         super.onActivityCreated(savedInstanceState);
173         mTvView = getActivity().findViewById(R.id.dvr_tv_view);
174         mTvView.setUseSecureSurface(
175                 buildType != BuildType.ENG && !mLegacyFlags.enableDeveloperFeatures());
176         mBlockScreenView = getActivity().findViewById(R.id.block_screen);
177         mDvrPlayer = new DvrPlayer(mTvView, getActivity());
178         mMediaSessionHelper =
179                 new DvrPlaybackMediaSessionHelper(
180                         getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this);
181         mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this);
182         mRelatedRecordingsRow = getRelatedRecordingsRow();
183         mDvrPlayer.setOnTracksAvailabilityChangedListener(
184                 new DvrPlayer.OnTracksAvailabilityChangedListener() {
185                     @Override
186                     public void onTracksAvailabilityChanged(
187                             boolean hasClosedCaption, boolean hasMultiAudio) {
188                         mPlaybackControlHelper.updateSecondaryRow(hasClosedCaption, hasMultiAudio);
189                         if (hasClosedCaption) {
190                             mDvrPlayer.setOnTrackSelectedListener(
191                                     TvTrackInfo.TYPE_SUBTITLE, mOnSubtitleTrackSelectedListener);
192                             selectBestMatchedTrack(TvTrackInfo.TYPE_SUBTITLE);
193                         } else {
194                             mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, null);
195                         }
196                         if (hasMultiAudio) {
197                             selectBestMatchedTrack(TvTrackInfo.TYPE_AUDIO);
198                         }
199                         updateVerticalPosition();
200                         mPlaybackControlHelper.getHost().notifyPlaybackRowChanged();
201                     }
202                 });
203         mDvrPlayer.setOnAspectRatioChangedListener(
204                 new DvrPlayer.OnAspectRatioChangedListener() {
205                     @Override
206                     public void onAspectRatioChanged(float videoAspectRatio) {
207                         updateAspectRatio(videoAspectRatio);
208                     }
209                 });
210         mPinChecked =
211                 getActivity()
212                         .getIntent()
213                         .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false);
214         mDvrPlayer.setOnContentBlockedListener(
215                 new DvrPlayer.OnContentBlockedListener() {
216                     @Override
217                     public void onContentBlocked(TvContentRating contentRating) {
218                         if (mPinChecked) {
219                             mTvView.unblockContent(contentRating);
220                             return;
221                         }
222                         mBlockScreenView.setVisibility(View.VISIBLE);
223                         getActivity().getMediaController().getTransportControls().pause();
224                         ((DvrPlaybackActivity) getActivity())
225                                 .setOnPinCheckListener(
226                                         new PinDialogFragment.OnPinCheckedListener() {
227                                             @Override
228                                             public void onPinChecked(
229                                                     boolean checked, int type, String rating) {
230                                                 ((DvrPlaybackActivity) getActivity())
231                                                         .setOnPinCheckListener(null);
232                                                 if (checked) {
233                                                     mPinChecked = true;
234                                                     mTvView.unblockContent(contentRating);
235                                                     mBlockScreenView.setVisibility(View.GONE);
236                                                     getActivity()
237                                                             .getMediaController()
238                                                             .getTransportControls()
239                                                             .play();
240                                                 }
241                                             }
242                                         });
243                         PinDialogFragment.create(
244                                         PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_DVR,
245                                         contentRating.flattenToString())
246                                 .show(
247                                         getActivity().getFragmentManager(),
248                                         PinDialogFragment.DIALOG_TAG);
249                     }
250                 });
251         setOnItemViewClickedListener(
252                 new BaseOnItemViewClickedListener() {
253                     @Override
254                     public void onItemClicked(
255                             Presenter.ViewHolder itemViewHolder,
256                             Object item,
257                             RowPresenter.ViewHolder rowViewHolder,
258                             Object row) {
259                         if (itemViewHolder.view instanceof RecordingCardView) {
260                             setFadingEnabled(false);
261                             long programId =
262                                     ((RecordedProgram) itemViewHolder.view.getTag()).getId();
263                             if (DEBUG) {
264                                 Log.d(TAG, "Play Related Recording:" + programId);
265                             }
266                             Intent intent = new Intent(getContext(), DvrPlaybackActivity.class);
267                             intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId);
268                             getContext().startActivity(intent);
269                         }
270                     }
271                 });
272         mAudioManagerHelper = new AudioManagerHelper(getActivity(), mDvrPlayer.getView());
273         if (mProgram != null) {
274             setUpRows();
275             preparePlayback(getActivity().getIntent());
276         }
277     }
278 
279     @Override
onPause()280     public void onPause() {
281         if (DEBUG) {
282             Log.d(TAG, "onPause");
283         }
284         super.onPause();
285         if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING
286                 || mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_REWINDING) {
287             getActivity().getMediaController().getTransportControls().pause();
288         }
289         if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_NONE) {
290             getActivity().requestVisibleBehind(false);
291         } else {
292             getActivity().requestVisibleBehind(true);
293         }
294     }
295 
296     @Override
onDestroy()297     public void onDestroy() {
298         if (DEBUG) {
299             Log.d(TAG, "onDestroy");
300         }
301         mPlaybackControlHelper.unregisterCallback();
302         mMediaSessionHelper.release();
303         mAudioManagerHelper.abandonAudioFocus();
304         mRelatedRecordingCardPresenter.unbindAllViewHolders();
305         mDvrPlayer.release();
306         super.onDestroy();
307     }
308 
309     /** Passes the intent to the fragment. */
onNewIntent(Intent intent)310     public void onNewIntent(Intent intent) {
311         if (mDvrDataManager.isRecordedProgramLoadFinished() && handleIntent(intent, false)) {
312             preparePlayback(intent);
313         }
314     }
315 
316     /**
317      * Should be called when windows' size is changed in order to notify DVR player to update it's
318      * view width/height and position.
319      */
onWindowSizeChanged(final int windowWidth, final int windowHeight)320     public void onWindowSizeChanged(final int windowWidth, final int windowHeight) {
321         mWindowWidth = windowWidth;
322         mWindowHeight = windowHeight;
323         mWindowAspectRatio = (float) mWindowWidth / mWindowHeight;
324         updateAspectRatio(mAppliedAspectRatio);
325     }
326 
327     /** Returns next recorded episode in the same series as now playing program. */
getNextEpisode(RecordedProgram program)328     public RecordedProgram getNextEpisode(RecordedProgram program) {
329         int position = mRelatedRecordingsRowAdapter.findInsertPosition(program);
330         if (position == mRelatedRecordingsRowAdapter.size()) {
331             return null;
332         } else {
333             return (RecordedProgram) mRelatedRecordingsRowAdapter.get(position);
334         }
335     }
336 
337     /**
338      * Returns the tracks of the give type of the current playback.
339      *
340      * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} or {@link
341      *     TvTrackInfo#TYPE_AUDIO}. Or returns {@code null}.
342      */
getTracks(int trackType)343     public ArrayList<TvTrackInfo> getTracks(int trackType) {
344         if (trackType == TvTrackInfo.TYPE_AUDIO) {
345             return mDvrPlayer.getAudioTracks();
346         } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
347             return mDvrPlayer.getSubtitleTracks();
348         }
349         return null;
350     }
351 
352     /** Returns the ID of the selected track of the given type. */
getSelectedTrackId(int trackType)353     public String getSelectedTrackId(int trackType) {
354         return mDvrPlayer.getSelectedTrackId(trackType);
355     }
356 
357     /**
358      * Returns the language setting of the given track type.
359      *
360      * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} or {@link
361      *     TvTrackInfo#TYPE_AUDIO}.
362      * @return {@code null} if no language has been set for the given track type.
363      */
getTrackSetting(int trackType)364     TvTrackInfo getTrackSetting(int trackType) {
365         return TvSettings.getDvrPlaybackTrackSettings(getContext(), trackType);
366     }
367 
368     /**
369      * Selects the given audio or subtitle track for DVR playback.
370      *
371      * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} or {@link
372      *     TvTrackInfo#TYPE_AUDIO}.
373      * @param selectedTrack {@code null} to disable the audio or subtitle track according to
374      *     trackType.
375      */
selectTrack(int trackType, TvTrackInfo selectedTrack)376     void selectTrack(int trackType, TvTrackInfo selectedTrack) {
377         if (mDvrPlayer.isPlaybackPrepared()) {
378             mDvrPlayer.selectTrack(trackType, selectedTrack);
379         }
380     }
381 
handleIntent(Intent intent, boolean finishActivity)382     private boolean handleIntent(Intent intent, boolean finishActivity) {
383         mProgram = getProgramFromIntent(intent);
384         if (mProgram == null) {
385             Toast.makeText(
386                             getActivity(),
387                             getString(R.string.dvr_program_not_found),
388                             Toast.LENGTH_SHORT)
389                     .show();
390             if (finishActivity) {
391                 getActivity().finish();
392             }
393             return false;
394         }
395         return true;
396     }
397 
selectBestMatchedTrack(int trackType)398     private void selectBestMatchedTrack(int trackType) {
399         TvTrackInfo selectedTrack = getTrackSetting(trackType);
400         if (selectedTrack != null) {
401             TvTrackInfo bestMatchedTrack =
402                     TvTrackInfoUtils.getBestTrackInfo(
403                             getTracks(trackType),
404                             selectedTrack.getId(),
405                             selectedTrack.getLanguage(),
406                             trackType == TvTrackInfo.TYPE_AUDIO
407                                     ? selectedTrack.getAudioChannelCount()
408                                     : 0);
409             if (bestMatchedTrack != null
410                     && (trackType == TvTrackInfo.TYPE_AUDIO
411                             || Utils.isEqualLanguage(
412                                     bestMatchedTrack.getLanguage(), selectedTrack.getLanguage()))) {
413                 selectTrack(trackType, bestMatchedTrack);
414                 return;
415             }
416         }
417         if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
418             // Disables closed captioning if there's no matched language.
419             selectTrack(TvTrackInfo.TYPE_SUBTITLE, null);
420         }
421     }
422 
updateAspectRatio(float videoAspectRatio)423     private void updateAspectRatio(float videoAspectRatio) {
424         if (videoAspectRatio <= 0) {
425             // We don't have video's width or height information, use window's aspect ratio.
426             videoAspectRatio = mWindowAspectRatio;
427         }
428         if (Math.abs(mAppliedAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) {
429             // No need to change
430             return;
431         }
432         if (Math.abs(mWindowAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) {
433             ((ViewGroup) mTvView.getParent()).setPadding(0, 0, 0, 0);
434         } else if (videoAspectRatio < mWindowAspectRatio) {
435             int newPadding = (mWindowWidth - Math.round(mWindowHeight * videoAspectRatio)) / 2;
436             ((ViewGroup) mTvView.getParent()).setPadding(newPadding, 0, newPadding, 0);
437         } else {
438             int newPadding = (mWindowHeight - Math.round(mWindowWidth / videoAspectRatio)) / 2;
439             ((ViewGroup) mTvView.getParent()).setPadding(0, newPadding, 0, newPadding);
440         }
441         mAppliedAspectRatio = videoAspectRatio;
442     }
443 
preparePlayback(Intent intent)444     private void preparePlayback(Intent intent) {
445         mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent));
446         mPlaybackControlHelper.updateSecondaryRow(false, false);
447         mAudioManagerHelper.requestAudioFocus();
448         getActivity().getMediaController().getTransportControls().prepare();
449         updateRelatedRecordingsRow();
450     }
451 
updateRelatedRecordingsRow()452     private void updateRelatedRecordingsRow() {
453         boolean wasEmpty = (mRelatedRecordingsRowAdapter.size() == 0);
454         mRelatedRecordingsRowAdapter.clear();
455         long programId = mProgram.getId();
456         String seriesId = mProgram.getSeriesId();
457         SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
458         if (seriesRecording != null) {
459             if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId);
460             List<RecordedProgram> relatedPrograms =
461                     mDvrDataManager.getRecordedPrograms(seriesRecording.getId());
462             for (RecordedProgram program : relatedPrograms) {
463                 if (programId != program.getId()) {
464                     mRelatedRecordingsRowAdapter.add(program);
465                 }
466             }
467         }
468         if (mRelatedRecordingsRowAdapter.size() == 0) {
469             mRowsAdapter.remove(mRelatedRecordingsRow);
470         } else if (wasEmpty) {
471             mRowsAdapter.add(mRelatedRecordingsRow);
472         }
473         updateVerticalPosition();
474         mRowsAdapter.notifyArrayItemRangeChanged(1, 1);
475     }
476 
setUpRows()477     private void setUpRows() {
478         mPlaybackControlHelper.createControlsRow();
479         mPlaybackControlHelper.setHost(new PlaybackFragmentGlueHost(this));
480         mRowsAdapter = (ArrayObjectAdapter) getAdapter();
481         ClassPresenterSelector selector =
482                 (ClassPresenterSelector) mRowsAdapter.getPresenterSelector();
483         selector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext()));
484         mRowsAdapter.setPresenterSelector(selector);
485         if (mStarted) {
486             // If it's started before setting up rows, vertical position has not been updated and
487             // should be updated here.
488             updateVerticalPosition();
489         }
490     }
491 
getRelatedRecordingsRow()492     private ListRow getRelatedRecordingsRow() {
493         mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity());
494         mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter);
495         HeaderItem header =
496                 new HeaderItem(
497                         0, getActivity().getString(R.string.dvr_playback_related_recordings));
498         return new ListRow(header, mRelatedRecordingsRowAdapter);
499     }
500 
getProgramFromIntent(Intent intent)501     private RecordedProgram getProgramFromIntent(Intent intent) {
502         long programId = intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, -1);
503         return mDvrDataManager.getRecordedProgram(programId);
504     }
505 
getSeekTimeFromIntent(Intent intent)506     private long getSeekTimeFromIntent(Intent intent) {
507         return intent.getLongExtra(
508                 Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, TvInputManager.TIME_SHIFT_INVALID_TIME);
509     }
510 
updateVerticalPosition()511     private void updateVerticalPosition() {
512         Boolean hasSecondaryRow = mPlaybackControlHelper.hasSecondaryRow();
513         if (hasSecondaryRow == null) {
514             return;
515         }
516 
517         int verticalPadding = mVerticalPaddingBase;
518         if (mRelatedRecordingsRowAdapter.size() == 0) {
519             verticalPadding += mPaddingWithoutRelatedRow;
520         }
521         if (!hasSecondaryRow) {
522             verticalPadding += mPaddingWithoutSecondaryRow;
523         }
524         Fragment fragment = getChildFragmentManager().findFragmentById(R.id.playback_controls_dock);
525         View view = fragment == null ? null : fragment.getView();
526         if (view != null) {
527             view.setTranslationY(verticalPadding);
528         }
529     }
530 
onPlaybackResume()531     public void onPlaybackResume() {
532         mPlaybackControlHelper.onPlaybackResume();
533     }
534 
getProgramStartTimeMs()535     public long getProgramStartTimeMs() {
536         return (mProgram != null && mProgram.isPartial())
537                 ? mProgram.getStartTimeUtcMillis()
538                 : INVALID_TIME;
539     }
540 
updateProgress()541     public void updateProgress() {
542         mPlaybackControlHelper.updateProgress();
543     }
544 
545     private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> {
RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter)546         RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) {
547             super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR);
548         }
549 
550         @Override
getId(BaseProgram item)551         public long getId(BaseProgram item) {
552             return item.getId();
553         }
554     }
555 }
556