1 /* 2 * Copyright (C) 2017 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; 18 19 import android.app.Activity; 20 import android.app.PendingIntent; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.graphics.Bitmap; 24 import android.graphics.BitmapFactory; 25 import android.media.MediaMetadata; 26 import android.media.session.MediaController; 27 import android.media.session.MediaSession; 28 import android.media.session.PlaybackState; 29 import android.media.tv.TvContract; 30 import android.media.tv.TvInputInfo; 31 import android.os.AsyncTask; 32 import android.support.annotation.NonNull; 33 import android.support.annotation.Nullable; 34 import android.support.annotation.VisibleForTesting; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import com.android.tv.data.Program; 38 import com.android.tv.data.api.Channel; 39 import com.android.tv.util.Utils; 40 import com.android.tv.util.images.ImageLoader; 41 42 /** 43 * A wrapper class for {@link MediaSession} to support common operations on media sessions for 44 * {@link MainActivity}. 45 */ 46 class MediaSessionWrapper { 47 private static final String TAG = "MediaSessionWrapper"; 48 private static final boolean DEBUG = false; 49 private static final String MEDIA_SESSION_TAG = "com.android.tv.mediasession"; 50 51 @VisibleForTesting 52 static final PlaybackState MEDIA_SESSION_STATE_PLAYING = 53 new PlaybackState.Builder() 54 .setState( 55 PlaybackState.STATE_PLAYING, 56 PlaybackState.PLAYBACK_POSITION_UNKNOWN, 57 1.0f) 58 .build(); 59 60 @VisibleForTesting 61 static final PlaybackState MEDIA_SESSION_STATE_STOPPED = 62 new PlaybackState.Builder() 63 .setState( 64 PlaybackState.STATE_STOPPED, 65 PlaybackState.PLAYBACK_POSITION_UNKNOWN, 66 0.0f) 67 .build(); 68 69 private final Context mContext; 70 private final MediaSession mMediaSession; 71 private final MediaController.Callback mMediaControllerCallback = 72 new MediaController.Callback() { 73 @Override 74 public void onPlaybackStateChanged(@Nullable PlaybackState state) { 75 super.onPlaybackStateChanged(state); 76 if (DEBUG) { 77 Log.d(TAG, "onPlaybackStateChanged: " + state); 78 } 79 if (isMediaSessionStateStop(state)) { 80 mMediaSession.setActive(false); 81 } 82 } 83 }; 84 private MediaController mMediaController; 85 private int mNowPlayingCardWidth; 86 private int mNowPlayingCardHeight; 87 MediaSessionWrapper(Context context, PendingIntent pendingIntent)88 MediaSessionWrapper(Context context, PendingIntent pendingIntent) { 89 mContext = context; 90 mMediaSession = new MediaSession(context, MEDIA_SESSION_TAG); 91 mMediaSession.setCallback( 92 new MediaSession.Callback() { 93 @Override 94 public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) { 95 // Consume the media button event here. Should not send it to other apps. 96 return true; 97 } 98 }); 99 mMediaSession.setFlags( 100 MediaSession.FLAG_HANDLES_MEDIA_BUTTONS 101 | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); 102 mMediaSession.setSessionActivity(pendingIntent); 103 104 initMediaController(); 105 mNowPlayingCardWidth = 106 mContext.getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width); 107 mNowPlayingCardHeight = 108 mContext.getResources().getDimensionPixelSize(R.dimen.notif_card_img_height); 109 } 110 111 /** 112 * Sets playback state. 113 * 114 * @param isPlaying {@code true} if TV is playing, otherwise {@code false}. 115 */ setPlaybackState(boolean isPlaying)116 void setPlaybackState(boolean isPlaying) { 117 if (isPlaying) { 118 mMediaSession.setActive(true); 119 // setPlaybackState() has to be called after calling setActive(). b/31933276 120 mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_PLAYING); 121 } else if (mMediaSession.isActive()) { 122 mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_STOPPED); 123 } 124 } 125 126 /** 127 * Updates media session according to the current TV playback status. 128 * 129 * @param blocked {@code true} if the current channel is blocked, either by user settings or the 130 * current program's content ratings. 131 * @param currentChannel The currently playing channel. 132 * @param currentProgram The currently playing program. 133 */ update(boolean blocked, Channel currentChannel, Program currentProgram)134 void update(boolean blocked, Channel currentChannel, Program currentProgram) { 135 if (currentChannel == null) { 136 setPlaybackState(false); 137 return; 138 } 139 140 // If the channel is blocked, display a lock and a short text on the Now Playing Card 141 if (blocked) { 142 Bitmap art = 143 BitmapFactory.decodeResource( 144 mContext.getResources(), R.drawable.ic_message_lock_preview); 145 updateMediaMetadata( 146 mContext.getResources().getString(R.string.channel_banner_locked_channel_title), 147 art); 148 setPlaybackState(true); 149 return; 150 } 151 152 String cardTitleText = null; 153 String posterArtUri = null; 154 if (currentProgram != null) { 155 cardTitleText = currentProgram.getTitle(); 156 posterArtUri = currentProgram.getPosterArtUri(); 157 } 158 if (TextUtils.isEmpty(cardTitleText)) { 159 cardTitleText = getChannelName(currentChannel); 160 } 161 updateMediaMetadata(cardTitleText, null); 162 if (posterArtUri == null) { 163 posterArtUri = TvContract.buildChannelLogoUri(currentChannel.getId()).toString(); 164 } 165 updatePosterArt(currentChannel, currentProgram, cardTitleText, null, posterArtUri); 166 setPlaybackState(true); 167 } 168 169 /** 170 * Releases the media session. 171 * 172 * @see MediaSession#release() 173 */ release()174 void release() { 175 unregisterMediaControllerCallback(); 176 mMediaSession.release(); 177 } 178 getChannelName(Channel channel)179 private String getChannelName(Channel channel) { 180 if (channel.isPassthrough()) { 181 TvInputInfo input = 182 TvSingletons.getSingletons(mContext) 183 .getTvInputManagerHelper() 184 .getTvInputInfo(channel.getInputId()); 185 return Utils.loadLabel(mContext, input); 186 } else { 187 return channel.getDisplayName(); 188 } 189 } 190 updatePosterArt( Channel currentChannel, Program currentProgram, String cardTitleText, @Nullable Bitmap posterArt, @Nullable String posterArtUri)191 private void updatePosterArt( 192 Channel currentChannel, 193 Program currentProgram, 194 String cardTitleText, 195 @Nullable Bitmap posterArt, 196 @Nullable String posterArtUri) { 197 if (posterArt != null) { 198 updateMediaMetadata(cardTitleText, posterArt); 199 } else if (posterArtUri != null) { 200 ImageLoader.loadBitmap( 201 mContext, 202 posterArtUri, 203 mNowPlayingCardWidth, 204 mNowPlayingCardHeight, 205 new ProgramPosterArtCallback( 206 this, currentChannel, currentProgram, cardTitleText)); 207 } else { 208 updateMediaMetadata(cardTitleText, R.drawable.default_now_card); 209 } 210 } 211 updateMediaMetadata(final String title, final Bitmap posterArt)212 private void updateMediaMetadata(final String title, final Bitmap posterArt) { 213 new AsyncTask<Void, Void, Void>() { 214 @Override 215 protected Void doInBackground(Void... arg0) { 216 MediaMetadata.Builder builder = new MediaMetadata.Builder(); 217 builder.putString(MediaMetadata.METADATA_KEY_TITLE, title); 218 if (posterArt != null) { 219 builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt); 220 } 221 mMediaSession.setMetadata(builder.build()); 222 return null; 223 } 224 }.execute(); 225 } 226 updateMediaMetadata(final String title, final int imageResId)227 private void updateMediaMetadata(final String title, final int imageResId) { 228 new AsyncTask<Void, Void, Void>() { 229 @Override 230 protected Void doInBackground(Void... arg0) { 231 MediaMetadata.Builder builder = new MediaMetadata.Builder(); 232 builder.putString(MediaMetadata.METADATA_KEY_TITLE, title); 233 Bitmap posterArt = 234 BitmapFactory.decodeResource(mContext.getResources(), imageResId); 235 if (posterArt != null) { 236 builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt); 237 } 238 mMediaSession.setMetadata(builder.build()); 239 return null; 240 } 241 }.execute(); 242 } 243 244 @VisibleForTesting getMediaSession()245 MediaSession getMediaSession() { 246 return mMediaSession; 247 } 248 249 @VisibleForTesting getMediaControllerCallback()250 MediaController.Callback getMediaControllerCallback() { 251 return mMediaControllerCallback; 252 } 253 254 @VisibleForTesting initMediaController()255 void initMediaController() { 256 mMediaController = new MediaController(mContext, mMediaSession.getSessionToken()); 257 ((Activity) mContext).setMediaController(mMediaController); 258 mMediaController.registerCallback(mMediaControllerCallback); 259 } 260 261 @VisibleForTesting unregisterMediaControllerCallback()262 void unregisterMediaControllerCallback() { 263 mMediaController.unregisterCallback(mMediaControllerCallback); 264 } 265 isMediaSessionStateStop(PlaybackState state)266 private static boolean isMediaSessionStateStop(PlaybackState state) { 267 return state != null 268 && state.getState() == MEDIA_SESSION_STATE_STOPPED.getState() 269 && state.getPosition() == MEDIA_SESSION_STATE_STOPPED.getPosition() 270 && state.getPlaybackSpeed() == MEDIA_SESSION_STATE_STOPPED.getPlaybackSpeed(); 271 } 272 273 private static class ProgramPosterArtCallback 274 extends ImageLoader.ImageLoaderCallback<MediaSessionWrapper> { 275 private final Channel mChannel; 276 private final Program mProgram; 277 private final String mCardTitleText; 278 ProgramPosterArtCallback( MediaSessionWrapper sessionWrapper, Channel channel, Program program, String cardTitleText)279 ProgramPosterArtCallback( 280 MediaSessionWrapper sessionWrapper, 281 Channel channel, 282 Program program, 283 String cardTitleText) { 284 super(sessionWrapper); 285 mChannel = channel; 286 mProgram = program; 287 mCardTitleText = cardTitleText; 288 } 289 290 @Override onBitmapLoaded(MediaSessionWrapper sessionWrapper, @Nullable Bitmap posterArt)291 public void onBitmapLoaded(MediaSessionWrapper sessionWrapper, @Nullable Bitmap posterArt) { 292 if (((MainActivity) sessionWrapper.mContext).isNowPlayingProgram(mChannel, mProgram)) { 293 sessionWrapper.updatePosterArt(mChannel, mProgram, mCardTitleText, posterArt, null); 294 } 295 } 296 } 297 } 298