/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.webkit; import android.content.Context; import android.media.AudioManager; import android.media.MediaPlayer; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Timer; import java.util.TimerTask; /** * HTML5 support class for Audio. * * This class runs almost entirely on the WebCore thread. The exception is when * accessing the WebView object to determine whether private browsing is * enabled. */ class HTML5Audio extends Handler implements MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnSeekCompleteListener, AudioManager.OnAudioFocusChangeListener { // Logging tag. private static final String LOGTAG = "HTML5Audio"; private MediaPlayer mMediaPlayer; // The C++ MediaPlayerPrivateAndroid object. private int mNativePointer; // The private status of the view that created this player private IsPrivateBrowsingEnabledGetter mIsPrivateBrowsingEnabledGetter; private static int IDLE = 0; private static int INITIALIZED = 1; private static int PREPARED = 2; private static int STARTED = 4; private static int COMPLETE = 5; private static int PAUSED = 6; private static int STOPPED = -2; private static int ERROR = -1; private int mState = IDLE; private String mUrl; private boolean mAskToPlay = false; private boolean mLoopEnabled = false; private boolean mProcessingOnEnd = false; private Context mContext; // Timer thread -> UI thread private static final int TIMEUPDATE = 100; private static final String COOKIE = "Cookie"; private static final String HIDE_URL_LOGS = "x-hide-urls-from-log"; // The spec says the timer should fire every 250 ms or less. private static final int TIMEUPDATE_PERIOD = 250; // ms // The timer for timeupate events. // See http://www.whatwg.org/specs/web-apps/current-work/#event-media-timeupdate private Timer mTimer; private final class TimeupdateTask extends TimerTask { public void run() { HTML5Audio.this.obtainMessage(TIMEUPDATE).sendToTarget(); } } // Helper class to determine whether private browsing is enabled in the // given WebView. Queries the WebView on the UI thread. Calls to get() // block until the data is available. private class IsPrivateBrowsingEnabledGetter { private boolean mIsReady; private boolean mIsPrivateBrowsingEnabled; IsPrivateBrowsingEnabledGetter(Looper uiThreadLooper, final WebViewClassic webView) { new Handler(uiThreadLooper).post(new Runnable() { @Override public void run() { synchronized(IsPrivateBrowsingEnabledGetter.this) { mIsPrivateBrowsingEnabled = webView.isPrivateBrowsingEnabled(); mIsReady = true; IsPrivateBrowsingEnabledGetter.this.notify(); } } }); } synchronized boolean get() { while (!mIsReady) { try { wait(); } catch (InterruptedException e) { } } return mIsPrivateBrowsingEnabled; } }; @Override public void handleMessage(Message msg) { switch (msg.what) { case TIMEUPDATE: { try { if (mState != ERROR && mMediaPlayer.isPlaying()) { int position = mMediaPlayer.getCurrentPosition(); nativeOnTimeupdate(position, mNativePointer); } } catch (IllegalStateException e) { mState = ERROR; } } } } // event listeners for MediaPlayer // Those are called from the same thread we created the MediaPlayer // (i.e. the webviewcore thread here) // MediaPlayer.OnBufferingUpdateListener public void onBufferingUpdate(MediaPlayer mp, int percent) { nativeOnBuffering(percent, mNativePointer); } // MediaPlayer.OnCompletionListener; public void onCompletion(MediaPlayer mp) { mState = COMPLETE; mProcessingOnEnd = true; nativeOnEnded(mNativePointer); mProcessingOnEnd = false; if (mLoopEnabled == true) { nativeOnRequestPlay(mNativePointer); mLoopEnabled = false; } } // MediaPlayer.OnErrorListener public boolean onError(MediaPlayer mp, int what, int extra) { mState = ERROR; resetMediaPlayer(); mState = IDLE; return false; } // MediaPlayer.OnPreparedListener public void onPrepared(MediaPlayer mp) { mState = PREPARED; if (mTimer != null) { mTimer.schedule(new TimeupdateTask(), TIMEUPDATE_PERIOD, TIMEUPDATE_PERIOD); } nativeOnPrepared(mp.getDuration(), 0, 0, mNativePointer); if (mAskToPlay) { mAskToPlay = false; play(); } } // MediaPlayer.OnSeekCompleteListener public void onSeekComplete(MediaPlayer mp) { nativeOnTimeupdate(mp.getCurrentPosition(), mNativePointer); } /** * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object. */ public HTML5Audio(WebViewCore webViewCore, int nativePtr) { // Save the native ptr mNativePointer = nativePtr; resetMediaPlayer(); mContext = webViewCore.getContext(); mIsPrivateBrowsingEnabledGetter = new IsPrivateBrowsingEnabledGetter( webViewCore.getContext().getMainLooper(), webViewCore.getWebViewClassic()); } private void resetMediaPlayer() { if (mMediaPlayer == null) { mMediaPlayer = new MediaPlayer(); } else { mMediaPlayer.reset(); } mMediaPlayer.setOnBufferingUpdateListener(this); mMediaPlayer.setOnCompletionListener(this); mMediaPlayer.setOnErrorListener(this); mMediaPlayer.setOnPreparedListener(this); mMediaPlayer.setOnSeekCompleteListener(this); if (mTimer != null) { mTimer.cancel(); } mTimer = new Timer(); mState = IDLE; } private void setDataSource(String url) { mUrl = url; try { if (mState != IDLE) { resetMediaPlayer(); } String cookieValue = CookieManager.getInstance().getCookie( url, mIsPrivateBrowsingEnabledGetter.get()); Map headers = new HashMap(); if (cookieValue != null) { headers.put(COOKIE, cookieValue); } if (mIsPrivateBrowsingEnabledGetter.get()) { headers.put(HIDE_URL_LOGS, "true"); } mMediaPlayer.setDataSource(url, headers); mState = INITIALIZED; mMediaPlayer.prepareAsync(); } catch (IOException e) { String debugUrl = url.length() > 128 ? url.substring(0, 128) + "..." : url; Log.e(LOGTAG, "couldn't load the resource: "+ debugUrl +" exc: " + e); resetMediaPlayer(); } } @Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: // resume playback if (mMediaPlayer == null) { resetMediaPlayer(); } else if (mState != ERROR && !mMediaPlayer.isPlaying()) { mMediaPlayer.start(); mState = STARTED; } break; case AudioManager.AUDIOFOCUS_LOSS: // Lost focus for an unbounded amount of time: stop playback. if (mState != ERROR && mMediaPlayer.isPlaying()) { mMediaPlayer.stop(); mState = STOPPED; } break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // Lost focus for a short time, but we have to stop // playback. if (mState != ERROR && mMediaPlayer.isPlaying()) pause(); break; } } private void play() { if (mState == COMPLETE && mLoopEnabled == true) { // Play it again, Sam mMediaPlayer.start(); mState = STARTED; return; } if (((mState >= ERROR && mState < PREPARED)) && mUrl != null) { resetMediaPlayer(); setDataSource(mUrl); mAskToPlay = true; } if (mState >= PREPARED) { AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { mMediaPlayer.start(); mState = STARTED; } } } private void pause() { if (mState == STARTED) { if (mTimer != null) { mTimer.purge(); } mMediaPlayer.pause(); mState = PAUSED; } } private void seek(int msec) { if (mProcessingOnEnd == true && mState == COMPLETE && msec == 0) { mLoopEnabled = true; } if (mState >= PREPARED) { mMediaPlayer.seekTo(msec); } } /** * Called only over JNI when WebKit is happy to * destroy the media player. */ private void teardown() { mMediaPlayer.release(); mMediaPlayer = null; mState = ERROR; mNativePointer = 0; } private float getMaxTimeSeekable() { if (mState >= PREPARED) { return mMediaPlayer.getDuration() / 1000.0f; } else { return 0; } } private native void nativeOnBuffering(int percent, int nativePointer); private native void nativeOnEnded(int nativePointer); private native void nativeOnRequestPlay(int nativePointer); private native void nativeOnPrepared(int duration, int width, int height, int nativePointer); private native void nativeOnTimeupdate(int position, int nativePointer); }