blob: df886391b1cebee6989f8f687c083417a2c1d5e4 [file] [log] [blame]
/*
* 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);
}
}
}
}