| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tv; |
| |
| import android.app.Activity; |
| import android.app.PendingIntent; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.media.MediaMetadata; |
| import android.media.session.MediaController; |
| import android.media.session.MediaSession; |
| import android.media.session.PlaybackState; |
| import android.media.tv.TvContract; |
| import android.media.tv.TvInputInfo; |
| import android.os.AsyncTask; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.support.annotation.VisibleForTesting; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import com.android.tv.data.api.Channel; |
| import com.android.tv.data.api.Program; |
| import com.android.tv.util.Utils; |
| import com.android.tv.util.images.ImageLoader; |
| |
| /** |
| * A wrapper class for {@link MediaSession} to support common operations on media sessions for |
| * {@link MainActivity}. |
| */ |
| class MediaSessionWrapper { |
| private static final String TAG = "MediaSessionWrapper"; |
| private static final boolean DEBUG = false; |
| private static final String MEDIA_SESSION_TAG = "com.android.tv.mediasession"; |
| |
| @VisibleForTesting |
| static final PlaybackState MEDIA_SESSION_STATE_PLAYING = |
| new PlaybackState.Builder() |
| .setState( |
| PlaybackState.STATE_PLAYING, |
| PlaybackState.PLAYBACK_POSITION_UNKNOWN, |
| 1.0f) |
| .build(); |
| |
| @VisibleForTesting |
| static final PlaybackState MEDIA_SESSION_STATE_STOPPED = |
| new PlaybackState.Builder() |
| .setState( |
| PlaybackState.STATE_STOPPED, |
| PlaybackState.PLAYBACK_POSITION_UNKNOWN, |
| 0.0f) |
| .build(); |
| |
| private final Context mContext; |
| private final MediaSession mMediaSession; |
| private final MediaController.Callback mMediaControllerCallback = |
| new MediaController.Callback() { |
| @Override |
| public void onPlaybackStateChanged(@Nullable PlaybackState state) { |
| super.onPlaybackStateChanged(state); |
| if (DEBUG) { |
| Log.d(TAG, "onPlaybackStateChanged: " + state); |
| } |
| if (isMediaSessionStateStop(state)) { |
| mMediaSession.setActive(false); |
| } |
| } |
| }; |
| private MediaController mMediaController; |
| private int mNowPlayingCardWidth; |
| private int mNowPlayingCardHeight; |
| |
| MediaSessionWrapper(Context context, PendingIntent pendingIntent) { |
| mContext = context; |
| mMediaSession = new MediaSession(context, MEDIA_SESSION_TAG); |
| mMediaSession.setCallback( |
| new MediaSession.Callback() { |
| @Override |
| public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) { |
| // Consume the media button event here. Should not send it to other apps. |
| return true; |
| } |
| }); |
| mMediaSession.setFlags( |
| MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
| | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); |
| mMediaSession.setSessionActivity(pendingIntent); |
| |
| initMediaController(); |
| mNowPlayingCardWidth = |
| mContext.getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width); |
| mNowPlayingCardHeight = |
| mContext.getResources().getDimensionPixelSize(R.dimen.notif_card_img_height); |
| } |
| |
| /** |
| * Sets playback state. |
| * |
| * @param isPlaying {@code true} if TV is playing, otherwise {@code false}. |
| */ |
| void setPlaybackState(boolean isPlaying) { |
| if (isPlaying) { |
| mMediaSession.setActive(true); |
| // setPlaybackState() has to be called after calling setActive(). b/31933276 |
| mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_PLAYING); |
| } else if (mMediaSession.isActive()) { |
| mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_STOPPED); |
| } |
| } |
| |
| /** |
| * Updates media session according to the current TV playback status. |
| * |
| * @param blocked {@code true} if the current channel is blocked, either by user settings or the |
| * current program's content ratings. |
| * @param currentChannel The currently playing channel. |
| * @param currentProgram The currently playing program. |
| */ |
| void update(boolean blocked, Channel currentChannel, Program currentProgram) { |
| if (currentChannel == null) { |
| setPlaybackState(false); |
| return; |
| } |
| |
| // If the channel is blocked, display a lock and a short text on the Now Playing Card |
| if (blocked) { |
| Bitmap art = |
| BitmapFactory.decodeResource( |
| mContext.getResources(), R.drawable.ic_message_lock_preview); |
| updateMediaMetadata( |
| mContext.getResources().getString(R.string.channel_banner_locked_channel_title), |
| art); |
| setPlaybackState(true); |
| return; |
| } |
| |
| String cardTitleText = null; |
| String posterArtUri = null; |
| if (currentProgram != null) { |
| cardTitleText = currentProgram.getTitle(); |
| posterArtUri = currentProgram.getPosterArtUri(); |
| } |
| if (TextUtils.isEmpty(cardTitleText)) { |
| cardTitleText = getChannelName(currentChannel); |
| } |
| updateMediaMetadata(cardTitleText, null); |
| if (posterArtUri == null) { |
| posterArtUri = TvContract.buildChannelLogoUri(currentChannel.getId()).toString(); |
| } |
| updatePosterArt(currentChannel, currentProgram, cardTitleText, null, posterArtUri); |
| setPlaybackState(true); |
| } |
| |
| /** |
| * Releases the media session. |
| * |
| * @see MediaSession#release() |
| */ |
| void release() { |
| unregisterMediaControllerCallback(); |
| mMediaSession.release(); |
| } |
| |
| private String getChannelName(Channel channel) { |
| if (channel.isPassthrough()) { |
| TvInputInfo input = |
| TvSingletons.getSingletons(mContext) |
| .getTvInputManagerHelper() |
| .getTvInputInfo(channel.getInputId()); |
| return Utils.loadLabel(mContext, input); |
| } else { |
| return channel.getDisplayName(); |
| } |
| } |
| |
| private void updatePosterArt( |
| Channel currentChannel, |
| Program currentProgram, |
| String cardTitleText, |
| @Nullable Bitmap posterArt, |
| @Nullable String posterArtUri) { |
| if (posterArt != null) { |
| updateMediaMetadata(cardTitleText, posterArt); |
| } else if (posterArtUri != null) { |
| ImageLoader.loadBitmap( |
| mContext, |
| posterArtUri, |
| mNowPlayingCardWidth, |
| mNowPlayingCardHeight, |
| new ProgramPosterArtCallback( |
| this, currentChannel, currentProgram, cardTitleText)); |
| } else { |
| updateMediaMetadata(cardTitleText, R.drawable.default_now_card); |
| } |
| } |
| |
| private void updateMediaMetadata(final String title, final Bitmap posterArt) { |
| new AsyncTask<Void, Void, Void>() { |
| @Override |
| protected Void doInBackground(Void... arg0) { |
| MediaMetadata.Builder builder = new MediaMetadata.Builder(); |
| builder.putString(MediaMetadata.METADATA_KEY_TITLE, title); |
| if (posterArt != null) { |
| builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt); |
| } |
| mMediaSession.setMetadata(builder.build()); |
| return null; |
| } |
| }.execute(); |
| } |
| |
| private void updateMediaMetadata(final String title, final int imageResId) { |
| new AsyncTask<Void, Void, Void>() { |
| @Override |
| protected Void doInBackground(Void... arg0) { |
| MediaMetadata.Builder builder = new MediaMetadata.Builder(); |
| builder.putString(MediaMetadata.METADATA_KEY_TITLE, title); |
| Bitmap posterArt = |
| BitmapFactory.decodeResource(mContext.getResources(), imageResId); |
| if (posterArt != null) { |
| builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt); |
| } |
| mMediaSession.setMetadata(builder.build()); |
| return null; |
| } |
| }.execute(); |
| } |
| |
| @VisibleForTesting |
| MediaSession getMediaSession() { |
| return mMediaSession; |
| } |
| |
| @VisibleForTesting |
| MediaController.Callback getMediaControllerCallback() { |
| return mMediaControllerCallback; |
| } |
| |
| @VisibleForTesting |
| void initMediaController() { |
| mMediaController = new MediaController(mContext, mMediaSession.getSessionToken()); |
| ((Activity) mContext).setMediaController(mMediaController); |
| mMediaController.registerCallback(mMediaControllerCallback); |
| } |
| |
| @VisibleForTesting |
| void unregisterMediaControllerCallback() { |
| mMediaController.unregisterCallback(mMediaControllerCallback); |
| } |
| |
| private static boolean isMediaSessionStateStop(PlaybackState state) { |
| return state != null |
| && state.getState() == MEDIA_SESSION_STATE_STOPPED.getState() |
| && state.getPosition() == MEDIA_SESSION_STATE_STOPPED.getPosition() |
| && state.getPlaybackSpeed() == MEDIA_SESSION_STATE_STOPPED.getPlaybackSpeed(); |
| } |
| |
| private static class ProgramPosterArtCallback |
| extends ImageLoader.ImageLoaderCallback<MediaSessionWrapper> { |
| private final Channel mChannel; |
| private final Program mProgram; |
| private final String mCardTitleText; |
| |
| ProgramPosterArtCallback( |
| MediaSessionWrapper sessionWrapper, |
| Channel channel, |
| Program program, |
| String cardTitleText) { |
| super(sessionWrapper); |
| mChannel = channel; |
| mProgram = program; |
| mCardTitleText = cardTitleText; |
| } |
| |
| @Override |
| public void onBitmapLoaded(MediaSessionWrapper sessionWrapper, @Nullable Bitmap posterArt) { |
| if (((MainActivity) sessionWrapper.mContext).isNowPlayingProgram(mChannel, mProgram)) { |
| sessionWrapper.updatePosterArt(mChannel, mProgram, mCardTitleText, posterArt, null); |
| } |
| } |
| } |
| } |