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