Home | History | Annotate | Download | only in webkit
      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