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