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