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