• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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