Home | History | Annotate | Download | only in leanback
      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.example.android.leanback;
     18 
     19 import android.content.Context;
     20 import android.graphics.Bitmap;
     21 import android.graphics.drawable.Drawable;
     22 import android.os.Handler;
     23 import android.support.v4.media.MediaMetadataCompat;
     24 import android.support.v4.media.session.MediaSessionCompat;
     25 import android.support.v4.media.session.PlaybackStateCompat;
     26 import android.util.Log;
     27 import android.view.KeyEvent;
     28 import android.view.View;
     29 import android.widget.Toast;
     30 
     31 import androidx.leanback.media.PlaybackBaseControlGlue;
     32 import androidx.leanback.media.PlayerAdapter;
     33 import androidx.leanback.widget.Action;
     34 import androidx.leanback.widget.ArrayObjectAdapter;
     35 import androidx.leanback.widget.PlaybackControlsRow;
     36 
     37 class PlaybackTransportControlGlueSample<T extends PlayerAdapter> extends
     38         androidx.leanback.media.PlaybackTransportControlGlue<T> {
     39 
     40 
     41     // In this glue, we don't support fast forward/ rewind/ repeat/ shuffle action
     42     private static final float NORMAL_SPEED = 1.0f;
     43 
     44     // for debugging purpose
     45     private static final Boolean DEBUG = false;
     46     private static final String TAG = "PlaybackTransportControlGlue";
     47 
     48     private PlaybackControlsRow.RepeatAction mRepeatAction;
     49     private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
     50     private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
     51     private PlaybackControlsRow.PictureInPictureAction mPipAction;
     52     private PlaybackControlsRow.ClosedCaptioningAction mClosedCaptioningAction;
     53     private MediaSessionCompat mMediaSessionCompat;
     54 
     55     PlaybackTransportControlGlueSample(Context context, T impl) {
     56         super(context, impl);
     57         mClosedCaptioningAction = new PlaybackControlsRow.ClosedCaptioningAction(context);
     58         mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
     59         mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.INDEX_OUTLINE);
     60         mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
     61         mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.INDEX_OUTLINE);
     62         mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
     63         mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
     64     }
     65 
     66     @Override
     67     protected void onCreateSecondaryActions(ArrayObjectAdapter adapter) {
     68         adapter.add(mThumbsUpAction);
     69         adapter.add(mThumbsDownAction);
     70         if (android.os.Build.VERSION.SDK_INT > 23) {
     71             adapter.add(mPipAction);
     72         }
     73     }
     74 
     75     @Override
     76     protected void onCreatePrimaryActions(ArrayObjectAdapter adapter) {
     77         super.onCreatePrimaryActions(adapter);
     78         adapter.add(mRepeatAction);
     79         adapter.add(mClosedCaptioningAction);
     80     }
     81 
     82     @Override
     83     public void onActionClicked(Action action) {
     84         if (shouldDispatchAction(action)) {
     85             dispatchAction(action);
     86             return;
     87         }
     88         super.onActionClicked(action);
     89     }
     90 
     91     @Override
     92     protected void onUpdateBufferedProgress() {
     93         super.onUpdateBufferedProgress();
     94 
     95         // if the media session is not connected, don't update playback state information
     96         if (mMediaSessionCompat == null) {
     97             return;
     98         }
     99 
    100         mMediaSessionCompat.setPlaybackState(createPlaybackStateBasedOnAdapterState());
    101     }
    102 
    103     @Override
    104     protected void onUpdateProgress() {
    105         super.onUpdateProgress();
    106 
    107         // if the media session is not connected, don't update playback state information
    108         if (mMediaSessionCompat == null) {
    109             return;
    110         }
    111 
    112         mMediaSessionCompat.setPlaybackState(createPlaybackStateBasedOnAdapterState());
    113     }
    114 
    115 
    116     @Override
    117     protected void onUpdateDuration() {
    118         super.onUpdateDuration();
    119         onMediaSessionMetaDataChanged();
    120     }
    121 
    122     // when meta data is changed, the metadata for media session will also be updated
    123     @Override
    124     protected void onMetadataChanged() {
    125         super.onMetadataChanged();
    126         onMediaSessionMetaDataChanged();
    127     }
    128 
    129     @Override
    130     public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
    131         if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
    132             Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode());
    133             if (shouldDispatchAction(action)) {
    134                 dispatchAction(action);
    135                 return true;
    136             }
    137         }
    138         return super.onKey(view, keyCode, keyEvent);
    139     }
    140 
    141     /**
    142      * Public api to connect media session to this glue
    143      */
    144     public void connectToMediaSession(MediaSessionCompat mediaSessionCompat) {
    145         mMediaSessionCompat = mediaSessionCompat;
    146         mMediaSessionCompat.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
    147                 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
    148         mMediaSessionCompat.setActive(true);
    149         mMediaSessionCompat.setCallback(new MediaSessionCallback());
    150         onMediaSessionMetaDataChanged();
    151     }
    152 
    153     /**
    154      * Public api to disconnect media session from this glue
    155      */
    156     public void disconnectToMediaSession() {
    157         if (DEBUG) {
    158             Log.e(TAG, "disconnectToMediaSession: Media session disconnected");
    159         }
    160         mMediaSessionCompat.setActive(false);
    161         mMediaSessionCompat.release();
    162     }
    163 
    164     private boolean shouldDispatchAction(Action action) {
    165         return action == mRepeatAction || action == mThumbsUpAction || action == mThumbsDownAction;
    166     }
    167 
    168     private void dispatchAction(Action action) {
    169         Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show();
    170         PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action;
    171         multiAction.nextIndex();
    172         notifyActionChanged(multiAction);
    173     }
    174 
    175     private void notifyActionChanged(PlaybackControlsRow.MultiAction action) {
    176         int index = -1;
    177         if (getPrimaryActionsAdapter() != null) {
    178             index = getPrimaryActionsAdapter().indexOf(action);
    179         }
    180         if (index >= 0) {
    181             getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
    182         } else {
    183             if (getSecondaryActionsAdapter() != null) {
    184                 index = getSecondaryActionsAdapter().indexOf(action);
    185                 if (index >= 0) {
    186                     getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
    187                 }
    188             }
    189         }
    190     }
    191 
    192     private ArrayObjectAdapter getPrimaryActionsAdapter() {
    193         if (getControlsRow() == null) {
    194             return null;
    195         }
    196         return (ArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
    197     }
    198 
    199     private ArrayObjectAdapter getSecondaryActionsAdapter() {
    200         if (getControlsRow() == null) {
    201             return null;
    202         }
    203         return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter();
    204     }
    205 
    206     Handler mHandler = new Handler();
    207 
    208     @Override
    209     protected void onPlayCompleted() {
    210         super.onPlayCompleted();
    211         mHandler.post(new Runnable() {
    212             @Override
    213             public void run() {
    214                 if (mRepeatAction.getIndex() != PlaybackControlsRow.RepeatAction.INDEX_NONE) {
    215                     play();
    216                 }
    217             }
    218         });
    219     }
    220 
    221     public void setMode(int mode) {
    222         mRepeatAction.setIndex(mode);
    223         if (getPrimaryActionsAdapter() == null) {
    224             return;
    225         }
    226         notifyActionChanged(mRepeatAction);
    227     }
    228 
    229     /**
    230      * Callback function when media session's meta data is changed.
    231      * When this function is returned, the callback function onMetaDataChanged will be
    232      * executed to address the new playback state.
    233      */
    234     private void onMediaSessionMetaDataChanged() {
    235 
    236         /**
    237          * Only update the media session's meta data when the media session is connected
    238          */
    239         if (mMediaSessionCompat == null) {
    240             return;
    241         }
    242 
    243         MediaMetadataCompat.Builder metaDataBuilder = new MediaMetadataCompat.Builder();
    244 
    245         // update media title
    246         if (getTitle() != null) {
    247             metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE,
    248                     getTitle().toString());
    249         }
    250 
    251         if (getSubtitle() != null) {
    252             // update media subtitle
    253             metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
    254                     getSubtitle().toString());
    255         }
    256 
    257         if (getArt() != null) {
    258             // update media art bitmap
    259             Drawable artDrawable = getArt();
    260             metaDataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART,
    261                     Bitmap.createBitmap(
    262                             artDrawable.getIntrinsicWidth(), artDrawable.getIntrinsicHeight(),
    263                             Bitmap.Config.ARGB_8888));
    264         }
    265 
    266         metaDataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, getDuration());
    267 
    268         mMediaSessionCompat.setMetadata(metaDataBuilder.build());
    269     }
    270 
    271     @Override
    272     public void play() {
    273         super.play();
    274     }
    275 
    276     @Override
    277     public void pause() {
    278         super.pause();
    279     }
    280 
    281     @Override
    282     protected void onPlayStateChanged() {
    283         super.onPlayStateChanged();
    284 
    285         // return when the media session compat is null
    286         if (mMediaSessionCompat == null) {
    287             return;
    288         }
    289 
    290         mMediaSessionCompat.setPlaybackState(createPlaybackStateBasedOnAdapterState());
    291     }
    292 
    293     @Override
    294     protected void onPreparedStateChanged() {
    295         super.onPreparedStateChanged();
    296 
    297         // return when the media session compat is null
    298         if (mMediaSessionCompat == null) {
    299             return;
    300         }
    301 
    302         mMediaSessionCompat.setPlaybackState(createPlaybackStateBasedOnAdapterState());
    303     }
    304 
    305     // associate media session event with player action
    306     private class MediaSessionCallback extends MediaSessionCompat.Callback {
    307 
    308         @Override
    309         public void onPlay() {
    310             play();
    311         }
    312 
    313         @Override
    314         public void onPause() {
    315             pause();
    316         }
    317 
    318         @Override
    319         public void onSeekTo(long pos) {
    320             seekTo(pos);
    321         }
    322     }
    323 
    324     /**
    325      * Get supported actions from player adapter then translate it into playback state compat
    326      * related actions
    327      */
    328     private long getPlaybackStateActions() {
    329         long supportedActions = 0L;
    330         long actionsFromPlayerAdapter = getPlayerAdapter().getSupportedActions();
    331         if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_SKIP_TO_PREVIOUS) != 0) {
    332             supportedActions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
    333         } else if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_SKIP_TO_NEXT) != 0) {
    334             supportedActions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
    335         } else if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_REWIND) != 0) {
    336             supportedActions |= PlaybackStateCompat.ACTION_REWIND;
    337         } else if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_FAST_FORWARD) != 0) {
    338             supportedActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
    339         } else if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_PLAY_PAUSE) != 0) {
    340             supportedActions |= PlaybackStateCompat.ACTION_PLAY_PAUSE;
    341         } else if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_REPEAT) != 0) {
    342             supportedActions |= PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
    343         } else if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_SHUFFLE) != 0) {
    344             supportedActions |= PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
    345         }
    346         return supportedActions;
    347     }
    348 
    349     /**
    350      * Helper function to create a playback state based on current adapter's state.
    351      *
    352      * @return playback state compat builder
    353      */
    354     private PlaybackStateCompat createPlaybackStateBasedOnAdapterState() {
    355 
    356         PlaybackStateCompat.Builder playbackStateCompatBuilder = new PlaybackStateCompat.Builder();
    357         long currentPosition = getCurrentPosition();
    358         long bufferedPosition = getBufferedPosition();
    359 
    360         // In this glue we only support normal speed
    361         float playbackSpeed = NORMAL_SPEED;
    362 
    363         // Translate player adapter's state to play back state compat
    364         // If player adapter is not prepared
    365         // ==> STATE_STOPPED
    366         //     (Launcher can only visualize the media session under playing state,
    367         //     it makes more sense to map this state to PlaybackStateCompat.STATE_STOPPED)
    368         // If player adapter is prepared
    369         //     If player is playing
    370         //     ==> STATE_PLAYING
    371         //     If player is not playing
    372         //     ==> STATE_PAUSED
    373         if (!getPlayerAdapter().isPrepared()) {
    374             playbackStateCompatBuilder
    375                     .setState(PlaybackStateCompat.STATE_STOPPED, currentPosition, playbackSpeed)
    376                     .setActions(getPlaybackStateActions());
    377         } else if (getPlayerAdapter().isPlaying()) {
    378             playbackStateCompatBuilder
    379                     .setState(PlaybackStateCompat.STATE_PLAYING, currentPosition, playbackSpeed)
    380                     .setActions(getPlaybackStateActions());
    381         } else {
    382             playbackStateCompatBuilder
    383                     .setState(PlaybackStateCompat.STATE_PAUSED, currentPosition, playbackSpeed)
    384                     .setActions(getPlaybackStateActions());
    385         }
    386 
    387         // always fill buffered position
    388         return playbackStateCompatBuilder.setBufferedPosition(bufferedPosition).build();
    389     }
    390 }
    391