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