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.Intent; 21 import android.graphics.Bitmap; 22 import android.graphics.BitmapFactory; 23 import android.media.MediaMetadata; 24 import android.media.session.MediaController; 25 import android.media.session.MediaSession; 26 import android.media.session.PlaybackState; 27 import android.media.tv.TvContract; 28 import android.os.AsyncTask; 29 import android.support.annotation.Nullable; 30 import android.text.TextUtils; 31 import com.android.tv.R; 32 import com.android.tv.TvSingletons; 33 import com.android.tv.data.ChannelDataManager; 34 import com.android.tv.data.api.Channel; 35 import com.android.tv.dvr.DvrWatchedPositionManager; 36 import com.android.tv.dvr.data.RecordedProgram; 37 import com.android.tv.util.TimeShiftUtils; 38 import com.android.tv.util.Utils; 39 import com.android.tv.util.images.ImageLoader; 40 41 class DvrPlaybackMediaSessionHelper { 42 private static final String TAG = "DvrPlaybackMediaSessionHelper"; 43 private static final boolean DEBUG = false; 44 45 private int mNowPlayingCardWidth; 46 private int mNowPlayingCardHeight; 47 private int mSpeedLevel; 48 private long mProgramDurationMs; 49 50 private Activity mActivity; 51 private DvrPlayer mDvrPlayer; 52 private MediaSession mMediaSession; 53 private final DvrWatchedPositionManager mDvrWatchedPositionManager; 54 private final ChannelDataManager mChannelDataManager; 55 DvrPlaybackMediaSessionHelper( Activity activity, String mediaSessionTag, DvrPlayer dvrPlayer, DvrPlaybackOverlayFragment overlayFragment)56 public DvrPlaybackMediaSessionHelper( 57 Activity activity, 58 String mediaSessionTag, 59 DvrPlayer dvrPlayer, 60 DvrPlaybackOverlayFragment overlayFragment) { 61 mActivity = activity; 62 mDvrPlayer = dvrPlayer; 63 mDvrWatchedPositionManager = 64 TvSingletons.getSingletons(activity).getDvrWatchedPositionManager(); 65 mChannelDataManager = TvSingletons.getSingletons(activity).getChannelDataManager(); 66 mDvrPlayer.setCallback( 67 new DvrPlayer.DvrPlayerCallback() { 68 @Override 69 public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { 70 updateMediaSessionPlaybackState(); 71 } 72 73 @Override 74 public void onPlaybackPositionChanged(long positionMs) { 75 updateMediaSessionPlaybackState(); 76 if (mDvrPlayer.isPlaybackPrepared()) { 77 mDvrWatchedPositionManager.setWatchedPosition( 78 mDvrPlayer.getProgram().getId(), positionMs); 79 } 80 } 81 82 @Override 83 public void onPlaybackEnded() { 84 // TODO: Deal with watched over recordings in DVR library 85 RecordedProgram nextEpisode = 86 overlayFragment.getNextEpisode(mDvrPlayer.getProgram()); 87 if (nextEpisode == null) { 88 mDvrPlayer.reset(); 89 mActivity.finish(); 90 } else { 91 Intent intent = new Intent(activity, DvrPlaybackActivity.class); 92 intent.putExtra( 93 Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, nextEpisode.getId()); 94 mActivity.startActivity(intent); 95 } 96 } 97 }); 98 initializeMediaSession(mediaSessionTag); 99 } 100 101 /** Stops DVR player and release media session. */ release()102 public void release() { 103 if (mDvrPlayer != null) { 104 mDvrPlayer.reset(); 105 } 106 if (mMediaSession != null) { 107 mMediaSession.release(); 108 mMediaSession = null; 109 } 110 } 111 112 /** Updates media session's playback state and speed. */ updateMediaSessionPlaybackState()113 public void updateMediaSessionPlaybackState() { 114 mMediaSession.setPlaybackState( 115 new PlaybackState.Builder() 116 .setState( 117 mDvrPlayer.getPlaybackState(), 118 mDvrPlayer.getPlaybackPosition(), 119 mSpeedLevel) 120 .build()); 121 } 122 123 /** 124 * Sets the recorded program for playback. 125 * 126 * @param program The recorded program to play. {@code null} to reset the DVR player. 127 */ setupPlayback(RecordedProgram program, long seekPositionMs)128 public void setupPlayback(RecordedProgram program, long seekPositionMs) { 129 if (program != null) { 130 mDvrPlayer.setProgram(program, seekPositionMs); 131 setupMediaSession(program); 132 } else { 133 mDvrPlayer.reset(); 134 mMediaSession.setActive(false); 135 } 136 } 137 138 /** Returns the recorded program now playing. */ getProgram()139 public RecordedProgram getProgram() { 140 return mDvrPlayer.getProgram(); 141 } 142 143 /** Checks if the recorded program is the same as now playing one. */ isCurrentProgram(RecordedProgram program)144 public boolean isCurrentProgram(RecordedProgram program) { 145 return program != null && program.equals(getProgram()); 146 } 147 148 /** Returns playback state. */ getPlaybackState()149 public int getPlaybackState() { 150 return mDvrPlayer.getPlaybackState(); 151 } 152 153 /** Returns the underlying DVR player. */ getDvrPlayer()154 public DvrPlayer getDvrPlayer() { 155 return mDvrPlayer; 156 } 157 initializeMediaSession(String mediaSessionTag)158 private void initializeMediaSession(String mediaSessionTag) { 159 mMediaSession = new MediaSession(mActivity, mediaSessionTag); 160 mMediaSession.setFlags( 161 MediaSession.FLAG_HANDLES_MEDIA_BUTTONS 162 | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); 163 mNowPlayingCardWidth = 164 mActivity.getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width); 165 mNowPlayingCardHeight = 166 mActivity.getResources().getDimensionPixelSize(R.dimen.notif_card_img_height); 167 mMediaSession.setCallback(new MediaSessionCallback()); 168 mActivity.setMediaController( 169 new MediaController(mActivity, mMediaSession.getSessionToken())); 170 updateMediaSessionPlaybackState(); 171 } 172 setupMediaSession(RecordedProgram program)173 private void setupMediaSession(RecordedProgram program) { 174 mProgramDurationMs = program.getDurationMillis(); 175 String cardTitleText = program.getTitle(); 176 if (TextUtils.isEmpty(cardTitleText)) { 177 Channel channel = mChannelDataManager.getChannel(program.getChannelId()); 178 cardTitleText = 179 (channel != null) 180 ? channel.getDisplayName() 181 : mActivity.getString(R.string.no_program_information); 182 } 183 final MediaMetadata currentMetadata = 184 updateMetadataTextInfo( 185 program.getId(), 186 cardTitleText, 187 program.getDescription(), 188 mProgramDurationMs); 189 String posterArtUri = program.getPosterArtUri(); 190 if (posterArtUri == null) { 191 posterArtUri = TvContract.buildChannelLogoUri(program.getChannelId()).toString(); 192 } 193 updatePosterArt(program, currentMetadata, null, posterArtUri); 194 mMediaSession.setActive(true); 195 } 196 updatePosterArt( RecordedProgram program, MediaMetadata currentMetadata, @Nullable Bitmap posterArt, @Nullable String posterArtUri)197 private void updatePosterArt( 198 RecordedProgram program, 199 MediaMetadata currentMetadata, 200 @Nullable Bitmap posterArt, 201 @Nullable String posterArtUri) { 202 if (posterArt != null) { 203 updateMetadataImageInfo(program, currentMetadata, posterArt, 0); 204 } else if (posterArtUri != null) { 205 ImageLoader.loadBitmap( 206 mActivity, 207 posterArtUri, 208 mNowPlayingCardWidth, 209 mNowPlayingCardHeight, 210 new ProgramPosterArtCallback(mActivity, program, currentMetadata)); 211 } else { 212 updateMetadataImageInfo(program, currentMetadata, null, R.drawable.default_now_card); 213 } 214 } 215 216 private class ProgramPosterArtCallback extends ImageLoader.ImageLoaderCallback<Activity> { 217 private final RecordedProgram mRecordedProgram; 218 private final MediaMetadata mCurrentMetadata; 219 ProgramPosterArtCallback( Activity activity, RecordedProgram program, MediaMetadata metadata)220 public ProgramPosterArtCallback( 221 Activity activity, RecordedProgram program, MediaMetadata metadata) { 222 super(activity); 223 mRecordedProgram = program; 224 mCurrentMetadata = metadata; 225 } 226 227 @Override onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt)228 public void onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt) { 229 if (isCurrentProgram(mRecordedProgram)) { 230 updatePosterArt(mRecordedProgram, mCurrentMetadata, posterArt, null); 231 } 232 } 233 } 234 updateMetadataTextInfo( final long programId, final String title, final String subtitle, final long duration)235 private MediaMetadata updateMetadataTextInfo( 236 final long programId, final String title, final String subtitle, final long duration) { 237 MediaMetadata.Builder builder = new MediaMetadata.Builder(); 238 builder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(programId)) 239 .putString(MediaMetadata.METADATA_KEY_TITLE, title) 240 .putLong(MediaMetadata.METADATA_KEY_DURATION, duration); 241 if (subtitle != null) { 242 builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); 243 } 244 MediaMetadata metadata = builder.build(); 245 mMediaSession.setMetadata(metadata); 246 return metadata; 247 } 248 updateMetadataImageInfo( final RecordedProgram program, final MediaMetadata currentMetadata, final Bitmap posterArt, final int imageResId)249 private void updateMetadataImageInfo( 250 final RecordedProgram program, 251 final MediaMetadata currentMetadata, 252 final Bitmap posterArt, 253 final int imageResId) { 254 if (mMediaSession != null && (posterArt != null || imageResId != 0)) { 255 MediaMetadata.Builder builder = new MediaMetadata.Builder(currentMetadata); 256 if (posterArt != null) { 257 builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt); 258 mMediaSession.setMetadata(builder.build()); 259 } else { 260 new AsyncTask<Void, Void, Bitmap>() { 261 @Override 262 protected Bitmap doInBackground(Void... arg0) { 263 return BitmapFactory.decodeResource(mActivity.getResources(), imageResId); 264 } 265 266 @Override 267 protected void onPostExecute(Bitmap programPosterArt) { 268 if (mMediaSession != null 269 && programPosterArt != null 270 && isCurrentProgram(program)) { 271 builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt); 272 mMediaSession.setMetadata(builder.build()); 273 } 274 } 275 }.execute(); 276 } 277 } 278 } 279 280 // An event was triggered by MediaController.TransportControls and must be handled here. 281 // Here we update the media itself to act on the event that was triggered. 282 private class MediaSessionCallback extends MediaSession.Callback { 283 @Override onPrepare()284 public void onPrepare() { 285 if (!mDvrPlayer.isPlaybackPrepared()) { 286 mDvrPlayer.prepare(true); 287 } 288 } 289 290 @Override onPlay()291 public void onPlay() { 292 if (mDvrPlayer.isPlaybackPrepared()) { 293 mDvrPlayer.play(); 294 } 295 } 296 297 @Override onPause()298 public void onPause() { 299 if (mDvrPlayer.isPlaybackPrepared()) { 300 mDvrPlayer.pause(); 301 } 302 } 303 304 @Override onFastForward()305 public void onFastForward() { 306 if (!mDvrPlayer.isPlaybackPrepared()) { 307 return; 308 } 309 if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING) { 310 if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { 311 mSpeedLevel++; 312 } else { 313 return; 314 } 315 } else { 316 mSpeedLevel = 0; 317 } 318 mDvrPlayer.fastForward( 319 TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs)); 320 } 321 322 @Override onRewind()323 public void onRewind() { 324 if (!mDvrPlayer.isPlaybackPrepared()) { 325 return; 326 } 327 if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_REWINDING) { 328 if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { 329 mSpeedLevel++; 330 } else { 331 return; 332 } 333 } else { 334 mSpeedLevel = 0; 335 } 336 mDvrPlayer.rewind(TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs)); 337 } 338 339 @Override onSeekTo(long positionMs)340 public void onSeekTo(long positionMs) { 341 if (mDvrPlayer.isPlaybackPrepared()) { 342 mDvrPlayer.seekTo(positionMs); 343 } 344 } 345 } 346 } 347