1 /*
2  * Copyright (C) 2016 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.dvr.ui.playback;
18 
19 import android.app.Activity;
20 import android.content.Intent;
21 import android.graphics.Bitmap;
22 import android.graphics.BitmapFactory;
23 import android.media.MediaMetadata;
24 import android.media.session.MediaController;
25 import android.media.session.MediaSession;
26 import android.media.session.PlaybackState;
27 import android.media.tv.TvContract;
28 import android.os.AsyncTask;
29 import android.support.annotation.Nullable;
30 import android.text.TextUtils;
31 import com.android.tv.R;
32 import com.android.tv.TvSingletons;
33 import com.android.tv.data.ChannelDataManager;
34 import com.android.tv.data.api.Channel;
35 import com.android.tv.dvr.DvrWatchedPositionManager;
36 import com.android.tv.dvr.data.RecordedProgram;
37 import com.android.tv.util.TimeShiftUtils;
38 import com.android.tv.util.Utils;
39 import com.android.tv.util.images.ImageLoader;
40 
41 class DvrPlaybackMediaSessionHelper {
42     private int mNowPlayingCardWidth;
43     private int mNowPlayingCardHeight;
44     private int mSpeedLevel;
45     private long mProgramDurationMs;
46 
47     private Activity mActivity;
48     private DvrPlayer mDvrPlayer;
49     private MediaSession mMediaSession;
50     private final DvrWatchedPositionManager mDvrWatchedPositionManager;
51     private final ChannelDataManager mChannelDataManager;
52 
DvrPlaybackMediaSessionHelper( Activity activity, String mediaSessionTag, DvrPlayer dvrPlayer, DvrPlaybackOverlayFragment overlayFragment)53     public DvrPlaybackMediaSessionHelper(
54             Activity activity,
55             String mediaSessionTag,
56             DvrPlayer dvrPlayer,
57             DvrPlaybackOverlayFragment overlayFragment) {
58         mActivity = activity;
59         mDvrPlayer = dvrPlayer;
60         mDvrWatchedPositionManager =
61                 TvSingletons.getSingletons(activity).getDvrWatchedPositionManager();
62         mChannelDataManager = TvSingletons.getSingletons(activity).getChannelDataManager();
63         mDvrPlayer.setCallback(
64                 new DvrPlayer.DvrPlayerCallback() {
65                     @Override
66                     public void onPlaybackStateChanged(int playbackState, int playbackSpeed) {
67                         updateMediaSessionPlaybackState();
68                     }
69 
70                     @Override
71                     public void onPlaybackPositionChanged(long positionMs) {
72                         updateMediaSessionPlaybackState();
73                         if (getProgram().isPartial()) {
74                             overlayFragment.updateProgress();
75                         }
76                         if (mDvrPlayer.isPlaybackPrepared()) {
77                             mDvrWatchedPositionManager.setWatchedPosition(
78                                     mDvrPlayer.getProgram().getId(), positionMs);
79                         }
80                     }
81 
82                     @Override
83                     public void onPlaybackEnded() {
84                         // TODO: Deal with watched over recordings in DVR library
85                         RecordedProgram nextEpisode =
86                                 overlayFragment.getNextEpisode(mDvrPlayer.getProgram());
87                         if (nextEpisode == null) {
88                             mDvrPlayer.reset();
89                             mActivity.finish();
90                         } else {
91                             Intent intent = new Intent(activity, DvrPlaybackActivity.class);
92                             intent.putExtra(
93                                     Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, nextEpisode.getId());
94                             mActivity.startActivity(intent);
95                         }
96                     }
97 
98                     @Override
99                     public void onPlaybackResume() {
100                         overlayFragment.onPlaybackResume();
101                     }
102                 });
103         initializeMediaSession(mediaSessionTag);
104     }
105 
106     /** Stops DVR player and release media session. */
release()107     public void release() {
108         if (mDvrPlayer != null) {
109             mDvrPlayer.reset();
110         }
111         if (mMediaSession != null) {
112             mMediaSession.release();
113             mMediaSession = null;
114         }
115     }
116 
117     /** Updates media session's playback state and speed. */
updateMediaSessionPlaybackState()118     public void updateMediaSessionPlaybackState() {
119         mMediaSession.setPlaybackState(
120                 new PlaybackState.Builder()
121                         .setState(
122                                 mDvrPlayer.getPlaybackState(),
123                                 mDvrPlayer.getPlaybackPosition(),
124                                 mSpeedLevel)
125                         .build());
126     }
127 
128     /**
129      * Sets the recorded program for playback.
130      *
131      * @param program The recorded program to play. {@code null} to reset the DVR player.
132      */
setupPlayback(RecordedProgram program, long seekPositionMs)133     public void setupPlayback(RecordedProgram program, long seekPositionMs) {
134         if (program != null) {
135             mDvrPlayer.setProgram(program, seekPositionMs);
136             setupMediaSession(program);
137         } else {
138             mDvrPlayer.reset();
139             mMediaSession.setActive(false);
140         }
141     }
142 
143     /** Returns the recorded program now playing. */
getProgram()144     public RecordedProgram getProgram() {
145         return mDvrPlayer.getProgram();
146     }
147 
148     /** Checks if the recorded program is the same as now playing one. */
isCurrentProgram(RecordedProgram program)149     public boolean isCurrentProgram(RecordedProgram program) {
150         return program != null && program.equals(getProgram());
151     }
152 
153     /** Returns playback state. */
getPlaybackState()154     public int getPlaybackState() {
155         return mDvrPlayer.getPlaybackState();
156     }
157 
158     /** Returns the underlying DVR player. */
getDvrPlayer()159     public DvrPlayer getDvrPlayer() {
160         return mDvrPlayer;
161     }
162 
initializeMediaSession(String mediaSessionTag)163     private void initializeMediaSession(String mediaSessionTag) {
164         mMediaSession = new MediaSession(mActivity, mediaSessionTag);
165         mMediaSession.setFlags(
166                 MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
167                         | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
168         mNowPlayingCardWidth =
169                 mActivity.getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width);
170         mNowPlayingCardHeight =
171                 mActivity.getResources().getDimensionPixelSize(R.dimen.notif_card_img_height);
172         mMediaSession.setCallback(new MediaSessionCallback());
173         mActivity.setMediaController(
174                 new MediaController(mActivity, mMediaSession.getSessionToken()));
175         updateMediaSessionPlaybackState();
176     }
177 
setupMediaSession(RecordedProgram program)178     private void setupMediaSession(RecordedProgram program) {
179         mProgramDurationMs = program.getDurationMillis();
180         String cardTitleText = program.getTitle();
181         if (TextUtils.isEmpty(cardTitleText)) {
182             Channel channel = mChannelDataManager.getChannel(program.getChannelId());
183             cardTitleText =
184                     (channel != null)
185                             ? channel.getDisplayName()
186                             : mActivity.getString(R.string.no_program_information);
187         }
188         final MediaMetadata currentMetadata =
189                 updateMetadataTextInfo(
190                         program.getId(),
191                         cardTitleText,
192                         program.getDescription(),
193                         mProgramDurationMs);
194         String posterArtUri = program.getPosterArtUri();
195         if (posterArtUri == null) {
196             posterArtUri = TvContract.buildChannelLogoUri(program.getChannelId()).toString();
197         }
198         updatePosterArt(program, currentMetadata, null, posterArtUri);
199         mMediaSession.setActive(true);
200     }
201 
updatePosterArt( RecordedProgram program, MediaMetadata currentMetadata, @Nullable Bitmap posterArt, @Nullable String posterArtUri)202     private void updatePosterArt(
203             RecordedProgram program,
204             MediaMetadata currentMetadata,
205             @Nullable Bitmap posterArt,
206             @Nullable String posterArtUri) {
207         if (posterArt != null) {
208             updateMetadataImageInfo(program, currentMetadata, posterArt, 0);
209         } else if (posterArtUri != null) {
210             ImageLoader.loadBitmap(
211                     mActivity,
212                     posterArtUri,
213                     mNowPlayingCardWidth,
214                     mNowPlayingCardHeight,
215                     new ProgramPosterArtCallback(mActivity, program, currentMetadata));
216         } else {
217             updateMetadataImageInfo(program, currentMetadata, null, R.drawable.default_now_card);
218         }
219     }
220 
221     private class ProgramPosterArtCallback extends ImageLoader.ImageLoaderCallback<Activity> {
222         private final RecordedProgram mRecordedProgram;
223         private final MediaMetadata mCurrentMetadata;
224 
ProgramPosterArtCallback( Activity activity, RecordedProgram program, MediaMetadata metadata)225         public ProgramPosterArtCallback(
226                 Activity activity, RecordedProgram program, MediaMetadata metadata) {
227             super(activity);
228             mRecordedProgram = program;
229             mCurrentMetadata = metadata;
230         }
231 
232         @Override
onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt)233         public void onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt) {
234             if (isCurrentProgram(mRecordedProgram)) {
235                 updatePosterArt(mRecordedProgram, mCurrentMetadata, posterArt, null);
236             }
237         }
238     }
239 
updateMetadataTextInfo( final long programId, final String title, final String subtitle, final long duration)240     private MediaMetadata updateMetadataTextInfo(
241             final long programId, final String title, final String subtitle, final long duration) {
242         MediaMetadata.Builder builder = new MediaMetadata.Builder();
243         builder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(programId))
244                 .putString(MediaMetadata.METADATA_KEY_TITLE, title)
245                 .putLong(MediaMetadata.METADATA_KEY_DURATION, duration);
246         if (subtitle != null) {
247             builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle);
248         }
249         MediaMetadata metadata = builder.build();
250         mMediaSession.setMetadata(metadata);
251         return metadata;
252     }
253 
updateMetadataImageInfo( final RecordedProgram program, final MediaMetadata currentMetadata, final Bitmap posterArt, final int imageResId)254     private void updateMetadataImageInfo(
255             final RecordedProgram program,
256             final MediaMetadata currentMetadata,
257             final Bitmap posterArt,
258             final int imageResId) {
259         if (mMediaSession != null && (posterArt != null || imageResId != 0)) {
260             MediaMetadata.Builder builder = new MediaMetadata.Builder(currentMetadata);
261             if (posterArt != null) {
262                 builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt);
263                 mMediaSession.setMetadata(builder.build());
264             } else {
265                 new AsyncTask<Void, Void, Bitmap>() {
266                     @Override
267                     protected Bitmap doInBackground(Void... arg0) {
268                         return BitmapFactory.decodeResource(mActivity.getResources(), imageResId);
269                     }
270 
271                     @Override
272                     protected void onPostExecute(Bitmap programPosterArt) {
273                         if (mMediaSession != null
274                                 && programPosterArt != null
275                                 && isCurrentProgram(program)) {
276                             builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt);
277                             mMediaSession.setMetadata(builder.build());
278                         }
279                     }
280                 }.execute();
281             }
282         }
283     }
284 
285     // An event was triggered by MediaController.TransportControls and must be handled here.
286     // Here we update the media itself to act on the event that was triggered.
287     private class MediaSessionCallback extends MediaSession.Callback {
288         @Override
onPrepare()289         public void onPrepare() {
290             if (!mDvrPlayer.isPlaybackPrepared()) {
291                 mDvrPlayer.prepare(true);
292             }
293         }
294 
295         @Override
onPlay()296         public void onPlay() {
297             if (mDvrPlayer.isPlaybackPrepared()) {
298                 mDvrPlayer.play();
299             }
300         }
301 
302         @Override
onPause()303         public void onPause() {
304             if (mDvrPlayer.isPlaybackPrepared()) {
305                 mDvrPlayer.pause();
306             }
307         }
308 
309         @Override
onFastForward()310         public void onFastForward() {
311             if (!mDvrPlayer.isPlaybackPrepared()) {
312                 return;
313             }
314             if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING) {
315                 if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) {
316                     mSpeedLevel++;
317                 } else {
318                     return;
319                 }
320             } else {
321                 mSpeedLevel = 0;
322             }
323             mDvrPlayer.fastForward(
324                     TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs));
325         }
326 
327         @Override
onRewind()328         public void onRewind() {
329             if (!mDvrPlayer.isPlaybackPrepared()) {
330                 return;
331             }
332             if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_REWINDING) {
333                 if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) {
334                     mSpeedLevel++;
335                 } else {
336                     return;
337                 }
338             } else {
339                 mSpeedLevel = 0;
340             }
341             mDvrPlayer.rewind(TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs));
342         }
343 
344         @Override
onSeekTo(long positionMs)345         public void onSeekTo(long positionMs) {
346             if (mDvrPlayer.isPlaybackPrepared()) {
347                 mDvrPlayer.seekTo(positionMs);
348             }
349         }
350     }
351 }
352