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 PlaybackTransportControlGlueSample(Context context, T impl)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 onCreateSecondaryActions(ArrayObjectAdapter adapter)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 onCreatePrimaryActions(ArrayObjectAdapter adapter)76 protected void onCreatePrimaryActions(ArrayObjectAdapter adapter) { 77 super.onCreatePrimaryActions(adapter); 78 adapter.add(mRepeatAction); 79 adapter.add(mClosedCaptioningAction); 80 } 81 82 @Override onActionClicked(Action action)83 public void onActionClicked(Action action) { 84 if (shouldDispatchAction(action)) { 85 dispatchAction(action); 86 return; 87 } 88 super.onActionClicked(action); 89 } 90 91 @Override onUpdateBufferedProgress()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 onUpdateProgress()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 onUpdateDuration()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 onMetadataChanged()124 protected void onMetadataChanged() { 125 super.onMetadataChanged(); 126 onMediaSessionMetaDataChanged(); 127 } 128 129 @Override onKey(View view, int keyCode, KeyEvent keyEvent)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 */ connectToMediaSession(MediaSessionCompat mediaSessionCompat)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 */ disconnectToMediaSession()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 shouldDispatchAction(Action action)164 private boolean shouldDispatchAction(Action action) { 165 return action == mRepeatAction || action == mThumbsUpAction || action == mThumbsDownAction; 166 } 167 dispatchAction(Action action)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 notifyActionChanged(PlaybackControlsRow.MultiAction action)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 getPrimaryActionsAdapter()192 private ArrayObjectAdapter getPrimaryActionsAdapter() { 193 if (getControlsRow() == null) { 194 return null; 195 } 196 return (ArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter(); 197 } 198 getSecondaryActionsAdapter()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 onPlayCompleted()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 setMode(int mode)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 */ onMediaSessionMetaDataChanged()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 play()272 public void play() { 273 super.play(); 274 } 275 276 @Override pause()277 public void pause() { 278 super.pause(); 279 } 280 281 @Override onPlayStateChanged()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 onPreparedStateChanged()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 onPlay()309 public void onPlay() { 310 play(); 311 } 312 313 @Override onPause()314 public void onPause() { 315 pause(); 316 } 317 318 @Override onSeekTo(long pos)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 */ getPlaybackStateActions()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 */ createPlaybackStateBasedOnAdapterState()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