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 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