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