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