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.Activity;
20 import android.content.Context;
21 import android.graphics.drawable.Drawable;
22 import android.media.MediaMetadata;
23 import android.media.session.MediaController;
24 import android.media.session.MediaController.TransportControls;
25 import android.media.session.PlaybackState;
26 import android.media.tv.TvTrackInfo;
27 import android.os.Bundle;
28 import android.support.annotation.Nullable;
29 import androidx.leanback.media.PlaybackControlGlue;
30 import androidx.leanback.widget.AbstractDetailsDescriptionPresenter;
31 import androidx.leanback.widget.Action;
32 import androidx.leanback.widget.ArrayObjectAdapter;
33 import androidx.leanback.widget.PlaybackControlsRow;
34 import androidx.leanback.widget.PlaybackControlsRow.ClosedCaptioningAction;
35 import androidx.leanback.widget.PlaybackControlsRow.MultiAction;
36 import androidx.leanback.widget.PlaybackControlsRowPresenter;
37 import androidx.leanback.widget.RowPresenter;
38 import android.text.TextUtils;
39 import android.util.Log;
40 import android.view.KeyEvent;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import com.android.tv.R;
44 import com.android.tv.util.TimeShiftUtils;
45 import java.util.ArrayList;
46 
47 /**
48  * A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and send
49  * command to the media controller. It also helps to update playback states displayed in the
50  * fragment according to information the media session provides.
51  */
52 class DvrPlaybackControlHelper extends PlaybackControlGlue {
53     private static final String TAG = "DvrPlaybackControlHelpr";
54     private static final boolean DEBUG = false;
55 
56     private static final int AUDIO_ACTION_ID = 1001;
57     private static final long INVALID_TIME = -1;
58 
59     private int mPlaybackState = PlaybackState.STATE_NONE;
60     private int mPlaybackSpeedLevel;
61     private int mPlaybackSpeedId;
62     private long mProgramStartTimeMs = INVALID_TIME;
63     private boolean mEnableBuffering = false;
64     private boolean mReadyToControl;
65 
66     private final DvrPlaybackOverlayFragment mFragment;
67     private final MediaController mMediaController;
68     private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback();
69     private final TransportControls mTransportControls;
70     private final int mExtraPaddingTopForNoDescription;
71     private final MultiAction mClosedCaptioningAction;
72     private final MultiAction mMultiAudioAction;
73     private ArrayObjectAdapter mSecondaryActionsAdapter;
74     private PlaybackControlsRow mPlaybackControlsRow;
75     @Nullable private View mPlayPauseButton;
76 
DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment)77     DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) {
78         super(activity, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]);
79         mFragment = overlayFragment;
80         mMediaController = activity.getMediaController();
81         mMediaController.registerCallback(mMediaControllerCallback);
82         mTransportControls = mMediaController.getTransportControls();
83         mExtraPaddingTopForNoDescription =
84                 activity.getResources()
85                         .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top);
86         mClosedCaptioningAction = new ClosedCaptioningAction(activity);
87         mMultiAudioAction = new MultiAudioAction(activity);
88         mProgramStartTimeMs = overlayFragment.getProgramStartTimeMs();
89         if (mProgramStartTimeMs != INVALID_TIME) {
90             mEnableBuffering = true;
91         }
92         createControlsRowPresenter();
93     }
94 
createControlsRow()95     void createControlsRow() {
96         mPlaybackControlsRow = new PlaybackControlsRow(this);
97         setControlsRow(mPlaybackControlsRow);
98         mSecondaryActionsAdapter =
99                 (ArrayObjectAdapter) mPlaybackControlsRow.getSecondaryActionsAdapter();
100     }
101 
createControlsRowPresenter()102     private void createControlsRowPresenter() {
103         AbstractDetailsDescriptionPresenter detailsPresenter =
104                 new AbstractDetailsDescriptionPresenter() {
105                     @Override
106                     protected void onBindDescription(
107                             AbstractDetailsDescriptionPresenter.ViewHolder viewHolder,
108                             Object object) {
109                         PlaybackControlGlue glue = (PlaybackControlGlue) object;
110                         if (glue.hasValidMedia()) {
111                             viewHolder.getTitle().setText(glue.getMediaTitle());
112                             viewHolder.getSubtitle().setText(glue.getMediaSubtitle());
113                         } else {
114                             viewHolder.getTitle().setText("");
115                             viewHolder.getSubtitle().setText("");
116                         }
117                         if (TextUtils.isEmpty(viewHolder.getSubtitle().getText())) {
118                             viewHolder.view.setPadding(
119                                     viewHolder.view.getPaddingLeft(),
120                                     mExtraPaddingTopForNoDescription,
121                                     viewHolder.view.getPaddingRight(),
122                                     viewHolder.view.getPaddingBottom());
123                         }
124                     }
125                 };
126         PlaybackControlsRowPresenter presenter =
127                 new PlaybackControlsRowPresenter(detailsPresenter) {
128                     @Override
129                     protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
130                         super.onBindRowViewHolder(vh, item);
131                         vh.setOnKeyListener(DvrPlaybackControlHelper.this);
132                         ViewGroup controlBar = (ViewGroup) vh.view.findViewById(R.id.control_bar);
133                         mPlayPauseButton = controlBar.getChildAt(1);
134                     }
135 
136                     @Override
137                     protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) {
138                         super.onUnbindRowViewHolder(vh);
139                         vh.setOnKeyListener(null);
140                     }
141                 };
142         presenter.setProgressColor(
143                 getContext().getResources().getColor(R.color.play_controls_progress_bar_watched));
144         presenter.setBackgroundColor(
145                 getContext()
146                         .getResources()
147                         .getColor(R.color.play_controls_body_background_enabled));
148         setControlsRowPresenter(presenter);
149     }
150 
151     @Override
onActionClicked(Action action)152     public void onActionClicked(Action action) {
153         if (mReadyToControl) {
154             int trackType;
155             if (action.getId() == mClosedCaptioningAction.getId()) {
156                 trackType = TvTrackInfo.TYPE_SUBTITLE;
157             } else if (action.getId() == AUDIO_ACTION_ID) {
158                 trackType = TvTrackInfo.TYPE_AUDIO;
159             } else {
160                 super.onActionClicked(action);
161                 return;
162             }
163             ArrayList<TvTrackInfo> trackInfos = mFragment.getTracks(trackType);
164             if (!trackInfos.isEmpty()) {
165                 showSideFragment(trackInfos, mFragment.getSelectedTrackId(trackType));
166             }
167         }
168     }
169 
170     @Override
onKey(View v, int keyCode, KeyEvent event)171     public boolean onKey(View v, int keyCode, KeyEvent event) {
172         return mReadyToControl && super.onKey(v, keyCode, event);
173     }
174 
175     @Override
hasValidMedia()176     public boolean hasValidMedia() {
177         PlaybackState playbackState = mMediaController.getPlaybackState();
178         return playbackState != null;
179     }
180 
181     @Override
isMediaPlaying()182     public boolean isMediaPlaying() {
183         PlaybackState playbackState = mMediaController.getPlaybackState();
184         if (playbackState == null) {
185             return false;
186         }
187         int state = playbackState.getState();
188         return state != PlaybackState.STATE_NONE
189                 && state != PlaybackState.STATE_CONNECTING
190                 && state != PlaybackState.STATE_PAUSED;
191     }
192 
193     /** Returns the ID of the media under playback. */
getMediaId()194     public String getMediaId() {
195         MediaMetadata mediaMetadata = mMediaController.getMetadata();
196         return mediaMetadata == null
197                 ? null
198                 : mediaMetadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
199     }
200 
201     @Override
getMediaTitle()202     public CharSequence getMediaTitle() {
203         MediaMetadata mediaMetadata = mMediaController.getMetadata();
204         return mediaMetadata == null
205                 ? ""
206                 : mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
207     }
208 
209     @Override
getMediaSubtitle()210     public CharSequence getMediaSubtitle() {
211         MediaMetadata mediaMetadata = mMediaController.getMetadata();
212         return mediaMetadata == null
213                 ? ""
214                 : mediaMetadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE);
215     }
216 
217     @Override
getMediaDuration()218     public int getMediaDuration() {
219         MediaMetadata mediaMetadata = mMediaController.getMetadata();
220         return mediaMetadata == null
221                 ? 0
222                 : (int) mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
223     }
224 
225     @Override
getMediaArt()226     public Drawable getMediaArt() {
227         // Do not show the poster art on control row.
228         return null;
229     }
230 
231     @Override
getSupportedActions()232     public long getSupportedActions() {
233         return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND;
234     }
235 
236     @Override
getCurrentSpeedId()237     public int getCurrentSpeedId() {
238         return mPlaybackSpeedId;
239     }
240 
241     @Override
getCurrentPosition()242     public int getCurrentPosition() {
243         PlaybackState playbackState = mMediaController.getPlaybackState();
244         if (playbackState == null) {
245             return 0;
246         }
247         return (int) playbackState.getPosition();
248     }
249 
250     /** Unregister media controller's callback. */
unregisterCallback()251     void unregisterCallback() {
252         mMediaController.unregisterCallback(mMediaControllerCallback);
253     }
254 
255     /**
256      * Update the secondary controls row.
257      *
258      * @param hasClosedCaption {@code true} to show the closed caption selection button, {@code
259      *     false} to hide it.
260      * @param hasMultiAudio {@code true} to show the audio track selection button, {@code false} to
261      *     hide it.
262      */
updateSecondaryRow(boolean hasClosedCaption, boolean hasMultiAudio)263     void updateSecondaryRow(boolean hasClosedCaption, boolean hasMultiAudio) {
264         if (hasClosedCaption) {
265             if (mSecondaryActionsAdapter.indexOf(mClosedCaptioningAction) < 0) {
266                 mSecondaryActionsAdapter.add(0, mClosedCaptioningAction);
267             }
268         } else {
269             mSecondaryActionsAdapter.remove(mClosedCaptioningAction);
270         }
271         if (hasMultiAudio) {
272             if (mSecondaryActionsAdapter.indexOf(mMultiAudioAction) < 0) {
273                 mSecondaryActionsAdapter.add(mMultiAudioAction);
274             }
275         } else {
276             mSecondaryActionsAdapter.remove(mMultiAudioAction);
277         }
278         getHost().notifyPlaybackRowChanged();
279     }
280 
281     /** Update the focus to play pause button. */
onPlaybackResume()282     public void onPlaybackResume() {
283         if (mPlayPauseButton != null) {
284             mPlayPauseButton.requestFocus();
285         }
286     }
287 
288     @Nullable
hasSecondaryRow()289     Boolean hasSecondaryRow() {
290         if (mSecondaryActionsAdapter == null) {
291             return null;
292         }
293         return mSecondaryActionsAdapter.size() != 0;
294     }
295 
296     @Override
play(int speedId)297     public void play(int speedId) {
298         if (getCurrentSpeedId() == speedId) {
299             return;
300         }
301         if (speedId == PLAYBACK_SPEED_NORMAL) {
302             mTransportControls.play();
303         } else if (speedId <= -PLAYBACK_SPEED_FAST_L0) {
304             mTransportControls.rewind();
305         } else if (speedId >= PLAYBACK_SPEED_FAST_L0) {
306             mTransportControls.fastForward();
307         }
308     }
309 
310     @Override
pause()311     public void pause() {
312         mTransportControls.pause();
313     }
314 
315     @Override
updateProgress()316     public void updateProgress() {
317         if (mEnableBuffering) {
318             super.updateProgress();
319             long bufferedTimeMs = System.currentTimeMillis() - mProgramStartTimeMs;
320             mPlaybackControlsRow.setBufferedPosition(bufferedTimeMs);
321         }
322     }
323 
324     /** Notifies closed caption being enabled/disabled to update related UI. */
onSubtitleTrackStateChanged(boolean enabled)325     void onSubtitleTrackStateChanged(boolean enabled) {
326         mClosedCaptioningAction.setIndex(
327                 enabled ? ClosedCaptioningAction.ON : ClosedCaptioningAction.OFF);
328     }
329 
onStateChanged(int state, long positionMs, int speedLevel)330     private void onStateChanged(int state, long positionMs, int speedLevel) {
331         if (DEBUG) Log.d(TAG, "onStateChanged");
332         getControlsRow().setCurrentTime((int) positionMs);
333         if (state == mPlaybackState && mPlaybackSpeedLevel == speedLevel) {
334             // Only position is changed, no need to update controls row
335             return;
336         }
337         // NOTICE: The below two variables should only be used in this method.
338         // The only usage of them is to confirm if the state is changed or not.
339         mPlaybackState = state;
340         mPlaybackSpeedLevel = speedLevel;
341         switch (state) {
342             case PlaybackState.STATE_PLAYING:
343                 mPlaybackSpeedId = PLAYBACK_SPEED_NORMAL;
344                 setFadingEnabled(true);
345                 mReadyToControl = true;
346                 break;
347             case PlaybackState.STATE_PAUSED:
348                 mPlaybackSpeedId = PLAYBACK_SPEED_PAUSED;
349                 setFadingEnabled(true);
350                 mReadyToControl = true;
351                 break;
352             case PlaybackState.STATE_FAST_FORWARDING:
353                 mPlaybackSpeedId = PLAYBACK_SPEED_FAST_L0 + speedLevel;
354                 setFadingEnabled(false);
355                 mReadyToControl = true;
356                 break;
357             case PlaybackState.STATE_REWINDING:
358                 mPlaybackSpeedId = -PLAYBACK_SPEED_FAST_L0 - speedLevel;
359                 setFadingEnabled(false);
360                 mReadyToControl = true;
361                 break;
362             case PlaybackState.STATE_CONNECTING:
363                 setFadingEnabled(false);
364                 mReadyToControl = false;
365                 break;
366             case PlaybackState.STATE_NONE:
367                 mReadyToControl = false;
368                 break;
369             default:
370                 setFadingEnabled(true);
371                 break;
372         }
373         onStateChanged();
374     }
375 
showSideFragment(ArrayList<TvTrackInfo> trackInfos, String selectedTrackId)376     private void showSideFragment(ArrayList<TvTrackInfo> trackInfos, String selectedTrackId) {
377         Bundle args = new Bundle();
378         args.putParcelableArrayList(DvrPlaybackSideFragment.TRACK_INFOS, trackInfos);
379         args.putString(DvrPlaybackSideFragment.SELECTED_TRACK_ID, selectedTrackId);
380         DvrPlaybackSideFragment sideFragment = new DvrPlaybackSideFragment();
381         sideFragment.setArguments(args);
382         mFragment
383                 .getFragmentManager()
384                 .beginTransaction()
385                 .hide(mFragment)
386                 .replace(R.id.dvr_playback_side_fragment, sideFragment)
387                 .addToBackStack(null)
388                 .commit();
389     }
390 
391     private class MediaControllerCallback extends MediaController.Callback {
392         @Override
onPlaybackStateChanged(PlaybackState state)393         public void onPlaybackStateChanged(PlaybackState state) {
394             if (DEBUG) Log.d(TAG, "Playback state changed: " + state.getState());
395             onStateChanged(state.getState(), state.getPosition(), (int) state.getPlaybackSpeed());
396         }
397 
398         @Override
onMetadataChanged(MediaMetadata metadata)399         public void onMetadataChanged(MediaMetadata metadata) {
400             DvrPlaybackControlHelper.this.onMetadataChanged();
401         }
402     }
403 
404     private static class MultiAudioAction extends MultiAction {
MultiAudioAction(Context context)405         MultiAudioAction(Context context) {
406             super(AUDIO_ACTION_ID);
407             setDrawables(new Drawable[] {context.getDrawable(R.drawable.ic_tvoption_multi_track)});
408         }
409     }
410 }
411