1/*
2 * Copyright (C) 2010 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 android.webkit;
18
19import android.content.Context;
20import android.media.AudioManager;
21import android.media.MediaPlayer;
22import android.os.Handler;
23import android.os.Looper;
24import android.os.Message;
25import android.util.Log;
26
27import java.io.IOException;
28import java.util.HashMap;
29import java.util.Map;
30import java.util.Timer;
31import java.util.TimerTask;
32
33/**
34 * HTML5 support class for Audio.
35 *
36 * This class runs almost entirely on the WebCore thread. The exception is when
37 * accessing the WebView object to determine whether private browsing is
38 * enabled.
39 */
40class HTML5Audio extends Handler
41                 implements MediaPlayer.OnBufferingUpdateListener,
42                            MediaPlayer.OnCompletionListener,
43                            MediaPlayer.OnErrorListener,
44                            MediaPlayer.OnPreparedListener,
45                            MediaPlayer.OnSeekCompleteListener,
46                            AudioManager.OnAudioFocusChangeListener {
47    // Logging tag.
48    private static final String LOGTAG = "HTML5Audio";
49
50    private MediaPlayer mMediaPlayer;
51
52    // The C++ MediaPlayerPrivateAndroid object.
53    private int mNativePointer;
54    // The private status of the view that created this player
55    private IsPrivateBrowsingEnabledGetter mIsPrivateBrowsingEnabledGetter;
56
57    private static int IDLE        =  0;
58    private static int INITIALIZED =  1;
59    private static int PREPARED    =  2;
60    private static int STARTED     =  4;
61    private static int COMPLETE    =  5;
62    private static int PAUSED      =  6;
63    private static int STOPPED     = -2;
64    private static int ERROR       = -1;
65
66    private int mState = IDLE;
67
68    private String mUrl;
69    private boolean mAskToPlay = false;
70    private Context mContext;
71
72    // Timer thread -> UI thread
73    private static final int TIMEUPDATE = 100;
74
75    private static final String COOKIE = "Cookie";
76    private static final String HIDE_URL_LOGS = "x-hide-urls-from-log";
77
78    // The spec says the timer should fire every 250 ms or less.
79    private static final int TIMEUPDATE_PERIOD = 250;  // ms
80    // The timer for timeupate events.
81    // See http://www.whatwg.org/specs/web-apps/current-work/#event-media-timeupdate
82    private Timer mTimer;
83    private final class TimeupdateTask extends TimerTask {
84        public void run() {
85            HTML5Audio.this.obtainMessage(TIMEUPDATE).sendToTarget();
86        }
87    }
88
89    // Helper class to determine whether private browsing is enabled in the
90    // given WebView. Queries the WebView on the UI thread. Calls to get()
91    // block until the data is available.
92    private class IsPrivateBrowsingEnabledGetter {
93        private boolean mIsReady;
94        private boolean mIsPrivateBrowsingEnabled;
95        IsPrivateBrowsingEnabledGetter(Looper uiThreadLooper, final WebView webView) {
96            new Handler(uiThreadLooper).post(new Runnable() {
97                @Override
98                public void run() {
99                    synchronized(IsPrivateBrowsingEnabledGetter.this) {
100                        mIsPrivateBrowsingEnabled = webView.isPrivateBrowsingEnabled();
101                        mIsReady = true;
102                        IsPrivateBrowsingEnabledGetter.this.notify();
103                    }
104                }
105            });
106        }
107        synchronized boolean get() {
108            while (!mIsReady) {
109                try {
110                    wait();
111                } catch (InterruptedException e) {
112                }
113            }
114            return mIsPrivateBrowsingEnabled;
115        }
116    };
117
118    @Override
119    public void handleMessage(Message msg) {
120        switch (msg.what) {
121            case TIMEUPDATE: {
122                try {
123                    if (mState != ERROR && mMediaPlayer.isPlaying()) {
124                        int position = mMediaPlayer.getCurrentPosition();
125                        nativeOnTimeupdate(position, mNativePointer);
126                    }
127                } catch (IllegalStateException e) {
128                    mState = ERROR;
129                }
130            }
131        }
132    }
133
134    // event listeners for MediaPlayer
135    // Those are called from the same thread we created the MediaPlayer
136    // (i.e. the webviewcore thread here)
137
138    // MediaPlayer.OnBufferingUpdateListener
139    public void onBufferingUpdate(MediaPlayer mp, int percent) {
140        nativeOnBuffering(percent, mNativePointer);
141    }
142
143    // MediaPlayer.OnCompletionListener;
144    public void onCompletion(MediaPlayer mp) {
145        resetMediaPlayer();
146        mState = IDLE;
147        nativeOnEnded(mNativePointer);
148    }
149
150    // MediaPlayer.OnErrorListener
151    public boolean onError(MediaPlayer mp, int what, int extra) {
152        mState = ERROR;
153        resetMediaPlayer();
154        mState = IDLE;
155        return false;
156    }
157
158    // MediaPlayer.OnPreparedListener
159    public void onPrepared(MediaPlayer mp) {
160        mState = PREPARED;
161        if (mTimer != null) {
162            mTimer.schedule(new TimeupdateTask(),
163                            TIMEUPDATE_PERIOD, TIMEUPDATE_PERIOD);
164        }
165        nativeOnPrepared(mp.getDuration(), 0, 0, mNativePointer);
166        if (mAskToPlay) {
167            mAskToPlay = false;
168            play();
169        }
170    }
171
172    // MediaPlayer.OnSeekCompleteListener
173    public void onSeekComplete(MediaPlayer mp) {
174        nativeOnTimeupdate(mp.getCurrentPosition(), mNativePointer);
175    }
176
177
178    /**
179     * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object.
180     */
181    public HTML5Audio(WebViewCore webViewCore, int nativePtr) {
182        // Save the native ptr
183        mNativePointer = nativePtr;
184        resetMediaPlayer();
185        mContext = webViewCore.getContext();
186        mIsPrivateBrowsingEnabledGetter = new IsPrivateBrowsingEnabledGetter(
187                webViewCore.getContext().getMainLooper(), webViewCore.getWebView());
188    }
189
190    private void resetMediaPlayer() {
191        if (mMediaPlayer == null) {
192            mMediaPlayer = new MediaPlayer();
193        } else {
194            mMediaPlayer.reset();
195        }
196        mMediaPlayer.setOnBufferingUpdateListener(this);
197        mMediaPlayer.setOnCompletionListener(this);
198        mMediaPlayer.setOnErrorListener(this);
199        mMediaPlayer.setOnPreparedListener(this);
200        mMediaPlayer.setOnSeekCompleteListener(this);
201
202        if (mTimer != null) {
203            mTimer.cancel();
204        }
205        mTimer = new Timer();
206        mState = IDLE;
207    }
208
209    private void setDataSource(String url) {
210        mUrl = url;
211        try {
212            if (mState != IDLE) {
213                resetMediaPlayer();
214            }
215            String cookieValue = CookieManager.getInstance().getCookie(
216                    url, mIsPrivateBrowsingEnabledGetter.get());
217            Map<String, String> headers = new HashMap<String, String>();
218
219            if (cookieValue != null) {
220                headers.put(COOKIE, cookieValue);
221            }
222            if (mIsPrivateBrowsingEnabledGetter.get()) {
223                headers.put(HIDE_URL_LOGS, "true");
224            }
225
226            mMediaPlayer.setDataSource(url, headers);
227            mState = INITIALIZED;
228            mMediaPlayer.prepareAsync();
229        } catch (IOException e) {
230            String debugUrl = url.length() > 128 ? url.substring(0, 128) + "..." : url;
231            Log.e(LOGTAG, "couldn't load the resource: "+ debugUrl +" exc: " + e);
232            resetMediaPlayer();
233        }
234    }
235
236    @Override
237    public void onAudioFocusChange(int focusChange) {
238        switch (focusChange) {
239        case AudioManager.AUDIOFOCUS_GAIN:
240            // resume playback
241            if (mMediaPlayer == null) {
242                resetMediaPlayer();
243            } else if (mState != ERROR && !mMediaPlayer.isPlaying()) {
244                mMediaPlayer.start();
245                mState = STARTED;
246            }
247            break;
248
249        case AudioManager.AUDIOFOCUS_LOSS:
250            // Lost focus for an unbounded amount of time: stop playback.
251            if (mState != ERROR && mMediaPlayer.isPlaying()) {
252                mMediaPlayer.stop();
253                mState = STOPPED;
254            }
255            break;
256
257        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
258        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
259            // Lost focus for a short time, but we have to stop
260            // playback.
261            if (mState != ERROR && mMediaPlayer.isPlaying()) pause();
262            break;
263        }
264    }
265
266
267    private void play() {
268        if ((mState >= ERROR && mState < PREPARED) && mUrl != null) {
269            resetMediaPlayer();
270            setDataSource(mUrl);
271            mAskToPlay = true;
272        }
273
274        if (mState >= PREPARED) {
275            AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
276            int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
277                AudioManager.AUDIOFOCUS_GAIN);
278
279            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
280                mMediaPlayer.start();
281                mState = STARTED;
282            }
283        }
284    }
285
286    private void pause() {
287        if (mState == STARTED) {
288            if (mTimer != null) {
289                mTimer.purge();
290            }
291            mMediaPlayer.pause();
292            mState = PAUSED;
293        }
294    }
295
296    private void seek(int msec) {
297        if (mState >= PREPARED) {
298            mMediaPlayer.seekTo(msec);
299        }
300    }
301
302    /**
303     * Called only over JNI when WebKit is happy to
304     * destroy the media player.
305     */
306    private void teardown() {
307        mMediaPlayer.release();
308        mMediaPlayer = null;
309        mState = ERROR;
310        mNativePointer = 0;
311    }
312
313    private float getMaxTimeSeekable() {
314        return mMediaPlayer.getDuration() / 1000.0f;
315    }
316
317    private native void nativeOnBuffering(int percent, int nativePointer);
318    private native void nativeOnEnded(int nativePointer);
319    private native void nativeOnPrepared(int duration, int width, int height, int nativePointer);
320    private native void nativeOnTimeupdate(int position, int nativePointer);
321
322}
323