1/*
2 * Copyright (C) 2014 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 */
16package com.android.onemedia.playback;
17
18import org.apache.http.Header;
19import org.apache.http.HttpResponse;
20import org.apache.http.client.methods.HttpGet;
21
22import android.content.Context;
23import android.media.AudioManager;
24import android.media.AudioManager.OnAudioFocusChangeListener;
25import android.media.MediaPlayer;
26import android.media.MediaPlayer.OnBufferingUpdateListener;
27import android.media.MediaPlayer.OnCompletionListener;
28import android.media.MediaPlayer.OnErrorListener;
29import android.media.MediaPlayer.OnPreparedListener;
30import android.net.Uri;
31import android.net.http.AndroidHttpClient;
32import android.os.AsyncTask;
33import android.os.Bundle;
34import android.os.Handler;
35import android.util.Log;
36import android.view.SurfaceHolder;
37
38import java.io.IOException;
39import java.util.Map;
40
41/**
42 * Helper class for wrapping a MediaPlayer and doing a lot of the default work
43 * to play audio. This class is not currently thread safe and all calls to it
44 * should be made on the same thread.
45 */
46public class LocalRenderer extends Renderer implements OnPreparedListener,
47        OnBufferingUpdateListener, OnCompletionListener, OnErrorListener,
48        OnAudioFocusChangeListener {
49    private static final String TAG = "MediaPlayerManager";
50    private static final boolean DEBUG = false;
51    private static long sDebugInstanceId = 0;
52
53    private static final String[] SUPPORTED_FEATURES = {
54            FEATURE_SET_CONTENT,
55            FEATURE_SET_NEXT_CONTENT,
56            FEATURE_PLAY,
57            FEATURE_PAUSE,
58            FEATURE_NEXT,
59            FEATURE_PREVIOUS,
60            FEATURE_SEEK_TO,
61            FEATURE_STOP
62    };
63
64    /**
65     * These are the states where it is valid to call play directly on the
66     * MediaPlayer.
67     */
68    private static final int CAN_PLAY = STATE_READY | STATE_PAUSED | STATE_ENDED;
69    /**
70     * These are the states where we expect the MediaPlayer to be ready in the
71     * future, so we can set a flag to start playing when it is.
72     */
73    private static final int CAN_READY_PLAY = STATE_INIT | STATE_PREPARING;
74    /**
75     * The states when it is valid to call pause on the MediaPlayer.
76     */
77    private static final int CAN_PAUSE = STATE_PLAYING;
78    /**
79     * The states where it is valid to call seek on the MediaPlayer.
80     */
81    private static final int CAN_SEEK = STATE_READY | STATE_PLAYING | STATE_PAUSED | STATE_ENDED;
82    /**
83     * The states where we expect the MediaPlayer to be ready in the future and
84     * can store a seek position to set later.
85     */
86    private static final int CAN_READY_SEEK = STATE_INIT | STATE_PREPARING;
87    /**
88     * The states where it is valid to call stop on the MediaPlayer.
89     */
90    private static final int CAN_STOP = STATE_READY | STATE_PLAYING | STATE_PAUSED | STATE_ENDED;
91    /**
92     * The states where it is valid to get the current play position and the
93     * duration from the MediaPlayer.
94     */
95    private static final int CAN_GET_POSITION = STATE_READY | STATE_PLAYING | STATE_PAUSED;
96
97
98
99    private class PlayerContent {
100        public final String source;
101        public final Map<String, String> headers;
102
103        public PlayerContent(String source, Map<String, String> headers) {
104            this.source = source;
105            this.headers = headers;
106        }
107    }
108
109    private class AsyncErrorRetriever extends AsyncTask<HttpGet, Void, Void> {
110        private final long errorId;
111        private boolean closeHttpClient;
112
113        public AsyncErrorRetriever(long errorId) {
114            this.errorId = errorId;
115            closeHttpClient = false;
116        }
117
118        public boolean cancelRequestLocked(boolean closeHttp) {
119            closeHttpClient = closeHttp;
120            return this.cancel(false);
121        }
122
123        @Override
124        protected Void doInBackground(HttpGet[] params) {
125            synchronized (mErrorLock) {
126                if (isCancelled() || mHttpClient == null) {
127                    if (mErrorRetriever == this) {
128                        mErrorRetriever = null;
129                    }
130                    return null;
131                }
132                mSafeToCloseClient = false;
133            }
134            final PlaybackError error = new PlaybackError();
135            try {
136                HttpResponse response = mHttpClient.execute(params[0]);
137                synchronized (mErrorLock) {
138                    if (mErrorId != errorId || mError == null) {
139                        // A new error has occurred, abort
140                        return null;
141                    }
142                    error.type = mError.type;
143                    error.extra = mError.extra;
144                    error.errorMessage = mError.errorMessage;
145                }
146                final int code = response.getStatusLine().getStatusCode();
147                if (code >= 300) {
148                    error.extra = code;
149                }
150                final Bundle errorExtras = new Bundle();
151                Header[] headers = response.getAllHeaders();
152                if (headers != null && headers.length > 0) {
153                    for (Header header : headers) {
154                        errorExtras.putString(header.getName(), header.getValue());
155                    }
156                    error.errorExtras = errorExtras;
157                }
158            } catch (IOException e) {
159                Log.e(TAG, "IOException requesting from server, unable to get more exact error");
160            } finally {
161                synchronized (mErrorLock) {
162                    mSafeToCloseClient = true;
163                    if (mErrorRetriever == this) {
164                        mErrorRetriever = null;
165                    }
166                    if (isCancelled()) {
167                        if (closeHttpClient) {
168                            mHttpClient.close();
169                            mHttpClient = null;
170                        }
171                        return null;
172                    }
173                }
174            }
175            mHandler.post(new Runnable() {
176                    @Override
177                public void run() {
178                    synchronized (mErrorLock) {
179                        if (mErrorId == errorId) {
180                            setError(error.type, error.extra, error.errorExtras, null);
181                        }
182                    }
183                }
184            });
185            return null;
186        }
187    }
188
189    private int mState = STATE_INIT;
190
191    private AudioManager mAudioManager;
192    private MediaPlayer mPlayer;
193    private PlayerContent mContent;
194    private MediaPlayer mNextPlayer;
195    private PlayerContent mNextContent;
196    private SurfaceHolder mHolder;
197    private SurfaceHolder.Callback mHolderCB;
198    private Context mContext;
199
200    private Handler mHandler = new Handler();
201
202    private AndroidHttpClient mHttpClient = AndroidHttpClient.newInstance("TUQ");
203    // The ongoing error request thread if there is one. This should only be
204    // modified while mErrorLock is held.
205    private AsyncErrorRetriever mErrorRetriever;
206    // This is set to false while a server request is being made to retrieve
207    // the current error. It should only be set while mErrorLock is held.
208    private boolean mSafeToCloseClient = true;
209    private final Object mErrorLock = new Object();
210    // A tracking id for the current error. This should only be modified while
211    // mErrorLock is held.
212    private long mErrorId = 0;
213    // The current error state of this player. This is cleared when the state
214    // leaves an error state and set when it enters one. This should only be
215    // modified when mErrorLock is held.
216    private PlaybackError mError;
217
218    private boolean mPlayOnReady;
219    private int mSeekOnReady;
220    private boolean mHasAudioFocus;
221    private long mDebugId = sDebugInstanceId++;
222
223    public LocalRenderer(Context context, Bundle params) {
224        super(context, params);
225        mContext = context;
226        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
227    }
228
229    @Override
230    protected void initFeatures(Bundle params) {
231        for (String feature : SUPPORTED_FEATURES) {
232            mFeatures.add(feature);
233        }
234    }
235
236    /**
237     * Call this when completely finished with the MediaPlayerManager to have it
238     * clean up. The instance may not be used again after this is called.
239     */
240    @Override
241    public void onDestroy() {
242        synchronized (mErrorLock) {
243            if (DEBUG) {
244                Log.d(TAG, "onDestroy, error retriever? " + mErrorRetriever + " safe to close? "
245                        + mSafeToCloseClient + " client? " + mHttpClient);
246            }
247            if (mErrorRetriever != null) {
248                mErrorRetriever.cancelRequestLocked(true);
249                mErrorRetriever = null;
250            }
251            // Increment the error id to ensure no errors are sent after this
252            // point.
253            mErrorId++;
254            if (mSafeToCloseClient) {
255                mHttpClient.close();
256                mHttpClient = null;
257            }
258        }
259    }
260
261    @Override
262    public void onPrepared(MediaPlayer player) {
263        if (!isCurrentPlayer(player)) {
264            return;
265        }
266        setState(STATE_READY);
267        if (DEBUG) {
268            Log.d(TAG, mDebugId + ": Finished preparing, seekOnReady is " + mSeekOnReady);
269        }
270        if (mSeekOnReady >= 0) {
271            onSeekTo(mSeekOnReady);
272            mSeekOnReady = -1;
273        }
274        if (mPlayOnReady) {
275            player.start();
276            setState(STATE_PLAYING);
277        }
278    }
279
280    @Override
281    public void onBufferingUpdate(MediaPlayer player, int percent) {
282        if (!isCurrentPlayer(player)) {
283            return;
284        }
285        pushOnBufferingUpdate(percent);
286    }
287
288    @Override
289    public void onCompletion(MediaPlayer player) {
290        if (!isCurrentPlayer(player)) {
291            return;
292        }
293        if (DEBUG) {
294            Log.d(TAG, mDebugId + ": Completed item. Have next item? " + (mNextPlayer != null));
295        }
296        if (mNextPlayer != null) {
297            if (mPlayer != null) {
298                mPlayer.release();
299            }
300            mPlayer = mNextPlayer;
301            mContent = mNextContent;
302            mNextPlayer = null;
303            mNextContent = null;
304            pushOnNextStarted();
305            return;
306        }
307        setState(STATE_ENDED);
308    }
309
310    @Override
311    public boolean onError(MediaPlayer player, int what, int extra) {
312        if (!isCurrentPlayer(player)) {
313            return false;
314        }
315        if (DEBUG) {
316            Log.d(TAG, mDebugId + ": Entered error state, what: " + what + " extra: " + extra);
317        }
318        synchronized (mErrorLock) {
319            ++mErrorId;
320            mError = new PlaybackError();
321            mError.type = what;
322            mError.extra = extra;
323        }
324
325        if (what == MediaPlayer.MEDIA_ERROR_UNKNOWN && extra == MediaPlayer.MEDIA_ERROR_IO
326                && mContent != null && mContent.source.startsWith("http")) {
327            HttpGet request = new HttpGet(mContent.source);
328            if (mContent.headers != null) {
329                for (String key : mContent.headers.keySet()) {
330                    request.addHeader(key, mContent.headers.get(key));
331                }
332            }
333            synchronized (mErrorLock) {
334                if (mErrorRetriever != null) {
335                    mErrorRetriever.cancelRequestLocked(false);
336                }
337                mErrorRetriever = new AsyncErrorRetriever(mErrorId);
338                mErrorRetriever.execute(request);
339            }
340        } else {
341            setError(what, extra, null, null);
342        }
343        return true;
344    }
345
346    @Override
347    public void onAudioFocusChange(int focusChange) {
348        // TODO figure out appropriate logic for handling focus loss at the TUQ
349        // level.
350        switch (focusChange) {
351            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
352            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
353                if (mState == STATE_PLAYING) {
354                    onPause();
355                    mPlayOnReady = true;
356                }
357                mHasAudioFocus = false;
358                break;
359            case AudioManager.AUDIOFOCUS_LOSS:
360                if (mState == STATE_PLAYING) {
361                    onPause();
362                    mPlayOnReady = false;
363                }
364                pushOnFocusLost();
365                mHasAudioFocus = false;
366                break;
367            case AudioManager.AUDIOFOCUS_GAIN:
368                mHasAudioFocus = true;
369                if (mPlayOnReady) {
370                    onPlay();
371                }
372                break;
373            default:
374                Log.d(TAG, "Unknown focus change event " + focusChange);
375                break;
376        }
377    }
378
379    @Override
380    public void setContent(Bundle request) {
381        setContent(request, null);
382    }
383
384    /**
385     * Prepares the player for the given playback request. If the holder is null
386     * it is assumed this is an audio only source. If playOnReady is set to true
387     * the media will begin playing as soon as it can.
388     *
389     * @see RequestUtils for the set of valid keys.
390     */
391    public void setContent(Bundle request, SurfaceHolder holder) {
392        String source = request.getString(RequestUtils.EXTRA_KEY_SOURCE);
393        Map<String, String> headers = null; // request.mHeaders;
394        boolean playOnReady = true; // request.mPlayOnReady;
395        if (DEBUG) {
396            Log.d(TAG, mDebugId + ": Settings new content. Have a player? " + (mPlayer != null)
397                    + " have a next player? " + (mNextPlayer != null));
398        }
399        cleanUpPlayer();
400        setState(STATE_PREPARING);
401        mPlayOnReady = playOnReady;
402        mSeekOnReady = -1;
403        final MediaPlayer newPlayer = new MediaPlayer();
404
405        requestAudioFocus();
406
407        mPlayer = newPlayer;
408        mContent = new PlayerContent(source, headers);
409        try {
410            if (headers != null) {
411                Uri sourceUri = Uri.parse(source);
412                newPlayer.setDataSource(mContext, sourceUri, headers);
413            } else {
414                newPlayer.setDataSource(source);
415            }
416        } catch (Exception e) {
417            setError(Listener.ERROR_LOAD_FAILED, 0, null, e);
418            return;
419        }
420        if (isHolderReady(holder, newPlayer)) {
421            preparePlayer(newPlayer, true);
422        }
423    }
424
425    @Override
426    public void setNextContent(Bundle request) {
427        String source = request.getString(RequestUtils.EXTRA_KEY_SOURCE);
428        Map<String, String> headers = null; // request.mHeaders;
429
430        // TODO support video
431
432        if (DEBUG) {
433            Log.d(TAG, mDebugId + ": Setting next content. Have player? " + (mPlayer != null)
434                    + " have next player? " + (mNextPlayer != null));
435        }
436
437        if (mPlayer == null) {
438            // The manager isn't being used to play anything, don't try to
439            // set a next.
440            return;
441        }
442        if (mNextPlayer != null) {
443            // Before setting up the new one clear out the old one and release
444            // it to ensure it doesn't play.
445            mPlayer.setNextMediaPlayer(null);
446            mNextPlayer.release();
447            mNextPlayer = null;
448            mNextContent = null;
449        }
450        if (source == null) {
451            // If there's no new content we're done
452            return;
453        }
454        final MediaPlayer newPlayer = new MediaPlayer();
455
456        try {
457            if (headers != null) {
458                Uri sourceUri = Uri.parse(source);
459                newPlayer.setDataSource(mContext, sourceUri, headers);
460            } else {
461                newPlayer.setDataSource(source);
462            }
463        } catch (Exception e) {
464            newPlayer.release();
465            // Don't return an error until we get to this item in playback
466            return;
467        }
468
469        if (preparePlayer(newPlayer, false)) {
470            mPlayer.setNextMediaPlayer(newPlayer);
471            mNextPlayer = newPlayer;
472            mNextContent = new PlayerContent(source, headers);
473        }
474    }
475
476    private void requestAudioFocus() {
477        int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
478                AudioManager.AUDIOFOCUS_GAIN);
479        mHasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
480    }
481
482    /**
483     * Start the player if possible or queue it to play when ready. If the
484     * player is in a state where it will never be ready returns false.
485     *
486     * @return true if the content was started or will be started later
487     */
488    @Override
489    public boolean onPlay() {
490        MediaPlayer player = mPlayer;
491        if (player != null && mState == STATE_PLAYING) {
492            // already playing, just return
493            return true;
494        }
495        if (!mHasAudioFocus) {
496            requestAudioFocus();
497        }
498        if (player != null && canPlay()) {
499            player.start();
500            setState(STATE_PLAYING);
501        } else if (canReadyPlay()) {
502            mPlayOnReady = true;
503        } else if (!isPlaying()) {
504            return false;
505        }
506        return true;
507    }
508
509    /**
510     * Pause the player if possible or set it to not play when ready. If the
511     * player is in a state where it will never be ready returns false.
512     *
513     * @return true if the content was paused or will wait to play when ready
514     *         later
515     */
516    @Override
517    public boolean onPause() {
518        MediaPlayer player = mPlayer;
519        // If the user paused us make sure we won't start playing again until
520        // asked to
521        mPlayOnReady = false;
522        if (player != null && (mState & CAN_PAUSE) != 0) {
523            player.pause();
524            setState(STATE_PAUSED);
525        } else if (!isPaused()) {
526            return false;
527        }
528        return true;
529    }
530
531    /**
532     * Seek to a given position in the media. If the seek succeeded or will be
533     * performed when loading is complete returns true. If the position is not
534     * in range or the player will never be ready returns false.
535     *
536     * @param position The position to seek to in milliseconds
537     * @return true if playback was moved or will be moved when ready
538     */
539    @Override
540    public boolean onSeekTo(int position) {
541        MediaPlayer player = mPlayer;
542        if (player != null && (mState & CAN_SEEK) != 0) {
543            if (position < 0 || position >= getDuration()) {
544                return false;
545            } else {
546                if (mState == STATE_ENDED) {
547                    player.start();
548                    player.pause();
549                    setState(STATE_PAUSED);
550                }
551                player.seekTo(position);
552            }
553        } else if ((mState & CAN_READY_SEEK) != 0) {
554            mSeekOnReady = position;
555        } else {
556            return false;
557        }
558        return true;
559    }
560
561    /**
562     * Stop the player. It cannot be used again until
563     * {@link #setContent(String, boolean)} is called.
564     *
565     * @return true if stopping the player succeeded
566     */
567    @Override
568    public boolean onStop() {
569        cleanUpPlayer();
570        setState(STATE_STOPPED);
571        return true;
572    }
573
574    public boolean isPlaying() {
575        return mState == STATE_PLAYING;
576    }
577
578    public boolean isPaused() {
579        return mState == STATE_PAUSED;
580    }
581
582    @Override
583    public long getSeekPosition() {
584        return ((mState & CAN_GET_POSITION) == 0) ? -1 : mPlayer.getCurrentPosition();
585    }
586
587    @Override
588    public long getDuration() {
589        return ((mState & CAN_GET_POSITION) == 0) ? -1 : mPlayer.getDuration();
590    }
591
592    private boolean canPlay() {
593        return ((mState & CAN_PLAY) != 0) && mHasAudioFocus;
594    }
595
596    private boolean canReadyPlay() {
597        return (mState & CAN_PLAY) != 0 || (mState & CAN_READY_PLAY) != 0;
598    }
599
600    /**
601     * Sends a state update if the listener exists
602     */
603    private void setState(int state) {
604        if (state == mState) {
605            return;
606        }
607        Log.d(TAG, "Entering state " + state + " from state " + mState);
608        mState = state;
609        if (state != STATE_ERROR) {
610            // Don't notify error here, it'll get sent via onError
611            pushOnStateChanged(state);
612        }
613    }
614
615    private boolean preparePlayer(final MediaPlayer player, boolean current) {
616        player.setOnPreparedListener(this);
617        player.setOnBufferingUpdateListener(this);
618        player.setOnCompletionListener(this);
619        player.setOnErrorListener(this);
620        try {
621            player.prepareAsync();
622            if (current) {
623                setState(STATE_PREPARING);
624            }
625        } catch (IllegalStateException e) {
626            if (current) {
627                setError(Listener.ERROR_PREPARE_ERROR, 0, null, e);
628            }
629            return false;
630        }
631        return true;
632    }
633
634    /**
635     * @param extra
636     * @param e
637     */
638    private void setError(int type, int extra, Bundle extras, Exception e) {
639        setState(STATE_ERROR);
640        pushOnError(type, extra, extras, e);
641        cleanUpPlayer();
642        return;
643    }
644
645    /**
646     * Checks if the holder is ready and either sets up a callback to wait for
647     * it or sets it directly. If
648     *
649     * @param holder
650     * @param player
651     * @return
652     */
653    private boolean isHolderReady(final SurfaceHolder holder, final MediaPlayer player) {
654        mHolder = holder;
655        if (holder != null) {
656            if (holder.getSurface() != null && holder.getSurface().isValid()) {
657                player.setDisplay(holder);
658                return true;
659            } else {
660                Log.w(TAG, "Holder not null, waiting for it to be ready");
661                // If the holder isn't ready yet add a callback to set the
662                // holder when it's ready.
663                SurfaceHolder.Callback cb = new SurfaceHolder.Callback() {
664                        @Override
665                    public void surfaceDestroyed(SurfaceHolder arg0) {
666                    }
667
668                        @Override
669                    public void surfaceCreated(SurfaceHolder arg0) {
670                        if (player.equals(mPlayer)) {
671                            player.setDisplay(arg0);
672                            preparePlayer(player, true);
673                        }
674                    }
675
676                        @Override
677                    public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
678                    }
679                };
680                mHolderCB = cb;
681                holder.addCallback(cb);
682                return false;
683            }
684        }
685        return true;
686    }
687
688    private void cleanUpPlayer() {
689        if (DEBUG) {
690            Log.d(TAG, mDebugId + ": Cleaning up current player");
691        }
692        synchronized (mErrorLock) {
693            mError = null;
694            if (mErrorRetriever != null) {
695                mErrorRetriever.cancelRequestLocked(false);
696                // Don't set to null as we may need to cancel again with true if
697                // the object gets destroyed.
698            }
699        }
700        mAudioManager.abandonAudioFocus(this);
701
702        SurfaceHolder.Callback cb = mHolderCB;
703        mHolderCB = null;
704        SurfaceHolder holder = mHolder;
705        mHolder = null;
706        if (holder != null && cb != null) {
707            holder.removeCallback(cb);
708        }
709
710        MediaPlayer player = mPlayer;
711        mPlayer = null;
712        if (player != null) {
713            player.reset();
714            player.release();
715        }
716    }
717
718    private boolean isCurrentPlayer(MediaPlayer player) {
719        return player.equals(mPlayer);
720    }
721}
722