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 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 {
     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;
     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         }
    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
    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
    141     public void onBufferingUpdate(MediaPlayer mp, int percent) {
    142         nativeOnBuffering(percent, mNativePointer);
    143     }
    144 
    145     // MediaPlayer.OnCompletionListener;
    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
    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
    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
    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      */
    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 
    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 
    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
    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 
    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 
    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 
    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      */
    323     private void teardown() {
    324         mMediaPlayer.release();
    325         mMediaPlayer = null;
    326         mState = ERROR;
    327         mNativePointer = 0;
    328     }
    329 
    330     private float getMaxTimeSeekable() {
    331         if (mState >= PREPARED) {
    332             return mMediaPlayer.getDuration() / 1000.0f;
    333         } else {
    334             return 0;
    335         }
    336     }
    337 
    338     private native void nativeOnBuffering(int percent, int nativePointer);
    339     private native void nativeOnEnded(int nativePointer);
    340     private native void nativeOnRequestPlay(int nativePointer);
    341     private native void nativeOnPrepared(int duration, int width, int height, int nativePointer);
    342     private native void nativeOnTimeupdate(int position, int nativePointer);
    343 
    344 }
    345