Inline HTML5 Video support

Use the HTML5VideoView to make inline HTML5 video possible.
Full screen support will be the next step.

The native side change is at 101310.

bug:3506407, 2126902
Change-Id: I012f33a4d0c7b83d37b184fceb3923e1fb277b80
diff --git a/core/java/android/webkit/HTML5VideoView.java b/core/java/android/webkit/HTML5VideoView.java
new file mode 100644
index 0000000..2312160
--- /dev/null
+++ b/core/java/android/webkit/HTML5VideoView.java
@@ -0,0 +1,211 @@
+
+package android.webkit;
+
+import android.graphics.SurfaceTexture;
+import android.media.MediaPlayer;
+import android.util.Log;
+import android.webkit.HTML5VideoViewProxy;
+import android.widget.MediaController;
+import android.opengl.GLES20;
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * @hide This is only used by the browser
+ */
+public class HTML5VideoView implements MediaPlayer.OnPreparedListener{
+    // Due to the fact that SurfaceTexture consume a lot of memory, we make it
+    // as static. m_textureNames is the texture bound with this SurfaceTexture.
+    private static SurfaceTexture mSurfaceTexture = null;
+    private static int[] mTextureNames;
+
+    // Only when the video is prepared, we render using SurfaceTexture.
+    // This in fact is used to avoid showing the obsolete content when
+    // switching videos.
+    private static boolean mReadyToUseSurfTex = false;
+
+    // For handling the seekTo before prepared, we need to know whether or not
+    // the video is prepared. Therefore, we differentiate the state between
+    // prepared and not prepared.
+    // When the video is not prepared, we will have to save the seekTo time,
+    // and use it when prepared to play.
+    private static final int STATE_NOTPREPARED        = 0;
+    private static final int STATE_PREPARED           = 1;
+
+    // We only need state for handling seekTo
+    private int mCurrentState;
+
+    // Basically for calling back the OnPrepared in the proxy
+    private HTML5VideoViewProxy mProxy;
+
+    // Save the seek time when not prepared. This can happen when switching
+    // video besides initial load.
+    private int mSaveSeekTime;
+
+    // This is used to find the VideoLayer on the native side.
+    private int mVideoLayerId;
+
+    // Every video will have one MediaPlayer. Given the fact we only have one
+    // SurfaceTexture, there is only one MediaPlayer in action. Every time we
+    // switch videos, a new instance of MediaPlayer will be created in reset().
+    private MediaPlayer mPlayer;
+
+    private static HTML5VideoView mInstance = new HTML5VideoView();
+
+    // Video control FUNCTIONS:
+    public void start() {
+        if (mCurrentState == STATE_PREPARED) {
+            mPlayer.start();
+            mReadyToUseSurfTex = true;
+        }
+    }
+
+    public void pause() {
+        mPlayer.pause();
+    }
+
+    public int getDuration() {
+        return mPlayer.getDuration();
+    }
+
+    public int getCurrentPosition() {
+        return mPlayer.getCurrentPosition();
+    }
+
+    public void seekTo(int pos) {
+        if (mCurrentState == STATE_PREPARED)
+            mPlayer.seekTo(pos);
+        else
+            mSaveSeekTime = pos;
+    }
+
+    public boolean isPlaying() {
+        return mPlayer.isPlaying();
+    }
+
+    public void release() {
+        mPlayer.release();
+    }
+
+    public void stopPlayback() {
+        mPlayer.stop();
+    }
+
+    private void reset(int videoLayerId) {
+        mPlayer = new MediaPlayer();
+        mCurrentState = STATE_NOTPREPARED;
+        mProxy = null;
+        mVideoLayerId = videoLayerId;
+        mReadyToUseSurfTex = false;
+    }
+
+    public static HTML5VideoView getInstance(int videoLayerId) {
+        // Every time we switch between the videos, a new MediaPlayer will be
+        // created. Make sure we call the m_player.release() when it is done.
+        mInstance.reset(videoLayerId);
+        return mInstance;
+    }
+
+    private HTML5VideoView() {
+        // This is a singleton across WebViews (i.e. Tabs).
+        // HTML5VideoViewProxy will reset the internal state every time a new
+        // video start.
+    }
+
+    public void setMediaController(MediaController m) {
+        this.setMediaController(m);
+    }
+
+    public void setVideoURI(String uri, Map<String, String> headers) {
+        // When switching players, surface texture will be reused.
+        mPlayer.setTexture(getSurfaceTextureInstance());
+
+        // When there is exception, we could just bail out silently.
+        // No Video will be played though. Write the stack for debug
+        try {
+            mPlayer.setDataSource(uri, headers);
+            mPlayer.prepareAsync();
+        } catch (IllegalArgumentException e) {
+            e.printStackTrace();
+        } catch (IllegalStateException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    // TODO [FULL SCREEN SUPPORT]
+
+    // Listeners setup FUNCTIONS:
+    public void setOnCompletionListener(HTML5VideoViewProxy proxy) {
+        mPlayer.setOnCompletionListener(proxy);
+    }
+
+    public void setOnErrorListener(HTML5VideoViewProxy proxy) {
+        mPlayer.setOnErrorListener(proxy);
+    }
+
+    public void setOnPreparedListener(HTML5VideoViewProxy proxy) {
+        mProxy = proxy;
+        mPlayer.setOnPreparedListener(this);
+    }
+
+    // Inline Video specific FUNCTIONS:
+
+    public SurfaceTexture getSurfaceTexture() {
+        return mSurfaceTexture;
+    }
+
+    public void deleteSurfaceTexture() {
+        mSurfaceTexture = null;
+        return;
+    }
+
+    // SurfaceTexture is a singleton here , too
+    private SurfaceTexture getSurfaceTextureInstance() {
+        // Create the surface texture.
+        if (mSurfaceTexture == null)
+        {
+            mTextureNames = new int[1];
+            GLES20.glGenTextures(1, mTextureNames, 0);
+            mSurfaceTexture = new SurfaceTexture(mTextureNames[0]);
+        }
+        return mSurfaceTexture;
+    }
+
+    public int getTextureName() {
+        return mTextureNames[0];
+    }
+
+    public int getVideoLayerId() {
+        return mVideoLayerId;
+    }
+
+    public boolean getReadyToUseSurfTex() {
+        return mReadyToUseSurfTex;
+    }
+
+    public void setFrameAvailableListener(SurfaceTexture.OnFrameAvailableListener l) {
+        mSurfaceTexture.setOnFrameAvailableListener(l);
+    }
+
+    @Override
+    public void onPrepared(MediaPlayer mp) {
+        mCurrentState = STATE_PREPARED;
+        seekTo(mSaveSeekTime);
+        if (mProxy != null)
+            mProxy.onPrepared(mp);
+    }
+
+    // Pause the play and update the play/pause button
+    public void pauseAndDispatch(HTML5VideoViewProxy proxy) {
+        if (isPlaying()) {
+            pause();
+            if (proxy != null) {
+                proxy.dispatchOnPaused();
+            }
+        }
+        mReadyToUseSurfTex = false;
+    }
+
+}
diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java
index 85763da..b614d8f 100644
--- a/core/java/android/webkit/HTML5VideoViewProxy.java
+++ b/core/java/android/webkit/HTML5VideoViewProxy.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.SurfaceTexture;
 import android.media.MediaPlayer;
 import android.media.MediaPlayer.OnPreparedListener;
 import android.media.MediaPlayer.OnCompletionListener;
@@ -59,7 +60,8 @@
 class HTML5VideoViewProxy extends Handler
                           implements MediaPlayer.OnPreparedListener,
                           MediaPlayer.OnCompletionListener,
-                          MediaPlayer.OnErrorListener {
+                          MediaPlayer.OnErrorListener,
+                          SurfaceTexture.OnFrameAvailableListener {
     // Logging tag.
     private static final String LOGTAG = "HTML5VideoViewProxy";
 
@@ -101,7 +103,7 @@
         private static HTML5VideoViewProxy mCurrentProxy;
         // The VideoView instance. This is a singleton for now, at least until
         // http://b/issue?id=1973663 is fixed.
-        private static VideoView mVideoView;
+        private static HTML5VideoView mHTML5VideoView;
         // The progress view.
         private static View mProgressView;
         // The container for the progress view and video view
@@ -122,131 +124,149 @@
         }
         // The spec says the timer should fire every 250 ms or less.
         private static final int TIMEUPDATE_PERIOD = 250;  // ms
-        static boolean isVideoSelfEnded = false;
+        private static boolean isVideoSelfEnded = false;
+        // By using the baseLayer and the current video Layer ID, we can
+        // identify the exact layer on the UI thread to use the SurfaceTexture.
+        private static int mBaseLayer = 0;
 
-        private static final WebChromeClient.CustomViewCallback mCallback =
-            new WebChromeClient.CustomViewCallback() {
-                public void onCustomViewHidden() {
-                    // At this point the videoview is pretty much destroyed.
-                    // It listens to SurfaceHolder.Callback.SurfaceDestroyed event
-                    // which happens when the video view is detached from its parent
-                    // view. This happens in the WebChromeClient before this method
-                    // is invoked.
-                    mTimer.cancel();
-                    mTimer = null;
-                    if (mVideoView.isPlaying()) {
-                        mVideoView.stopPlayback();
+        // TODO: [FULL SCREEN SUPPORT]
+
+        // Every time webView setBaseLayer, this will be called.
+        // When we found the Video layer, then we set the Surface Texture to it.
+        // Otherwise, we may want to delete the Surface Texture to save memory.
+        public static void setBaseLayer(int layer) {
+            if (mHTML5VideoView != null) {
+                mBaseLayer = layer;
+                SurfaceTexture surfTexture = mHTML5VideoView.getSurfaceTexture();
+                int textureName = mHTML5VideoView.getTextureName();
+
+                int currentVideoLayerId = mHTML5VideoView.getVideoLayerId();
+                if (layer != 0 && surfTexture != null && currentVideoLayerId != -1) {
+                    boolean readyToUseSurfTex =
+                        mHTML5VideoView.getReadyToUseSurfTex();
+                    boolean foundInTree = nativeSendSurfaceTexture(surfTexture,
+                            layer, currentVideoLayerId, textureName,
+                            readyToUseSurfTex);
+                    if (readyToUseSurfTex && !foundInTree) {
+                        mHTML5VideoView.pauseAndDispatch(mCurrentProxy);
+                        mHTML5VideoView.deleteSurfaceTexture();
                     }
-                    if (isVideoSelfEnded)
-                        mCurrentProxy.dispatchOnEnded();
-                    else
-                        mCurrentProxy.dispatchOnPaused();
-
-                    // Re enable plugin views.
-                    mCurrentProxy.getWebView().getViewManager().showAll();
-
-                    isVideoSelfEnded = false;
-                    mCurrentProxy = null;
-                    mLayout.removeView(mVideoView);
-                    mVideoView = null;
-                    if (mProgressView != null) {
-                        mLayout.removeView(mProgressView);
-                        mProgressView = null;
-                    }
-                    mLayout = null;
                 }
-            };
-
-        public static void play(String url, int time, HTML5VideoViewProxy proxy,
-                WebChromeClient client) {
-            if (mCurrentProxy == proxy) {
-                if (!mVideoView.isPlaying()) {
-                    mVideoView.start();
-                }
-                return;
             }
+        }
 
-            if (mCurrentProxy != null) {
+        // When a WebView is paused, we also want to pause the video in it.
+        public static void pauseAndDispatch() {
+            if (mHTML5VideoView != null) {
+                mHTML5VideoView.pauseAndDispatch(mCurrentProxy);
+                // When switching out, clean the video content on the old page
+                // by telling the layer not readyToUseSurfTex.
+                setBaseLayer(mBaseLayer);
+            }
+        }
+
+        // This is on the UI thread.
+        // When native tell Java to play, we need to check whether or not it is
+        // still the same video by using videoLayerId and treat it differently.
+        public static void play(String url, int time, HTML5VideoViewProxy proxy,
+                WebChromeClient client, int videoLayerId) {
+            int currentVideoLayerId = -1;
+            if (mHTML5VideoView != null)
+                currentVideoLayerId = mHTML5VideoView.getVideoLayerId();
+
+            if (currentVideoLayerId != videoLayerId
+                || mHTML5VideoView.getSurfaceTexture() == null) {
+                // Here, we handle the case when switching to a new video,
+                // either inside a WebView or across WebViews
+                // For switching videos within a WebView or across the WebView,
+                // we need to pause the old one and re-create a new media player
+                // inside the HTML5VideoView.
+                if (mHTML5VideoView != null) {
+                    mHTML5VideoView.pauseAndDispatch(mCurrentProxy);
+                    // release the media player to avoid finalize error
+                    mHTML5VideoView.release();
+                }
+                // HTML5VideoView is singleton, however, the internal state will
+                // be reset since we are switching from one video to another.
+                // Then we need to set up all the source/listener etc...
+                mHTML5VideoView = HTML5VideoView.getInstance(videoLayerId);
+
+                mCurrentProxy = proxy;
+
+                // TODO: [FULL SCREEN SUPPORT]
+
+                boolean isPrivate = mCurrentProxy.getWebView().isPrivateBrowsingEnabled();
+                String cookieValue = CookieManager.getInstance().getCookie(url, isPrivate);
+                Map<String, String> headers = new HashMap<String, String>();
+                if (cookieValue != null) {
+                    headers.put(COOKIE, cookieValue);
+                }
+                if (isPrivate) {
+                    headers.put(HIDE_URL_LOGS, "true");
+                }
+
+                mHTML5VideoView.setVideoURI(url, headers);
+                mHTML5VideoView.setOnCompletionListener(proxy);
+                mHTML5VideoView.setOnPreparedListener(proxy);
+                mHTML5VideoView.setOnErrorListener(proxy);
+                mHTML5VideoView.setFrameAvailableListener(proxy);
+
+                mHTML5VideoView.seekTo(time);
+
+                mTimer = new Timer();
+
+            } else if (mCurrentProxy == proxy) {
+                // Here, we handle the case when we keep playing with one video
+                if (!mHTML5VideoView.isPlaying()) {
+                    mHTML5VideoView.seekTo(time);
+                    mHTML5VideoView.start();
+                }
+            } else if (mCurrentProxy != null) {
                 // Some other video is already playing. Notify the caller that its playback ended.
                 proxy.dispatchOnEnded();
-                return;
             }
-
-            mCurrentProxy = proxy;
-            // Create a FrameLayout that will contain the VideoView and the
-            // progress view (if any).
-            mLayout = new FrameLayout(proxy.getContext());
-            FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
-                    ViewGroup.LayoutParams.WRAP_CONTENT,
-                    ViewGroup.LayoutParams.WRAP_CONTENT,
-                    Gravity.CENTER);
-            mVideoView = new VideoView(proxy.getContext());
-            mVideoView.setWillNotDraw(false);
-            mVideoView.setMediaController(new MediaController(proxy.getContext()));
-
-            boolean isPrivate = mCurrentProxy.getWebView().isPrivateBrowsingEnabled();
-            String cookieValue = CookieManager.getInstance().getCookie(url, isPrivate);
-            Map<String, String> headers = new HashMap<String, String>();
-            if (cookieValue != null) {
-                headers.put(COOKIE, cookieValue);
-            }
-            if (isPrivate) {
-                headers.put(HIDE_URL_LOGS, "true");
-            }
-
-            mVideoView.setVideoURI(Uri.parse(url), headers);
-            mVideoView.setOnCompletionListener(proxy);
-            mVideoView.setOnPreparedListener(proxy);
-            mVideoView.setOnErrorListener(proxy);
-            mVideoView.seekTo(time);
-            mLayout.addView(mVideoView, layoutParams);
-            mProgressView = client.getVideoLoadingProgressView();
-            if (mProgressView != null) {
-                mLayout.addView(mProgressView, layoutParams);
-                mProgressView.setVisibility(View.VISIBLE);
-            }
-            mLayout.setVisibility(View.VISIBLE);
-            mTimer = new Timer();
-            mVideoView.start();
-            client.onShowCustomView(mLayout, mCallback);
-            // Plugins like Flash will draw over the video so hide
-            // them while we're playing.
-            mCurrentProxy.getWebView().getViewManager().hideAll();
         }
 
         public static boolean isPlaying(HTML5VideoViewProxy proxy) {
-            return (mCurrentProxy == proxy && mVideoView != null && mVideoView.isPlaying());
+            return (mCurrentProxy == proxy && mHTML5VideoView != null
+                    && mHTML5VideoView.isPlaying());
         }
 
         public static int getCurrentPosition() {
             int currentPosMs = 0;
-            if (mVideoView != null) {
-                currentPosMs = mVideoView.getCurrentPosition();
+            if (mHTML5VideoView != null) {
+                currentPosMs = mHTML5VideoView.getCurrentPosition();
             }
             return currentPosMs;
         }
 
         public static void seek(int time, HTML5VideoViewProxy proxy) {
-            if (mCurrentProxy == proxy && time >= 0 && mVideoView != null) {
-                mVideoView.seekTo(time);
+            if (mCurrentProxy == proxy && time >= 0 && mHTML5VideoView != null) {
+                mHTML5VideoView.seekTo(time);
             }
         }
 
         public static void pause(HTML5VideoViewProxy proxy) {
-            if (mCurrentProxy == proxy && mVideoView != null) {
-                mVideoView.pause();
+            if (mCurrentProxy == proxy && mHTML5VideoView != null) {
+                mHTML5VideoView.pause();
                 mTimer.purge();
             }
         }
 
         public static void onPrepared() {
-            if (mProgressView == null || mLayout == null) {
-                return;
-            }
+            mHTML5VideoView.start();
             mTimer.schedule(new TimeupdateTask(mCurrentProxy), TIMEUPDATE_PERIOD, TIMEUPDATE_PERIOD);
-            mProgressView.setVisibility(View.GONE);
-            mLayout.removeView(mProgressView);
-            mProgressView = null;
+            // TODO: [FULL SCREEN SUPPORT]
+        }
+
+        public static void end() {
+            if (mCurrentProxy != null) {
+                if (isVideoSelfEnded)
+                    mCurrentProxy.dispatchOnEnded();
+                else
+                    mCurrentProxy.dispatchOnPaused();
+            }
+            isVideoSelfEnded = false;
         }
     }
 
@@ -292,6 +312,14 @@
         sendMessage(obtainMessage(TIMEUPDATE));
     }
 
+    // When there is a frame ready from surface texture, we should tell WebView
+    // to refresh.
+    @Override
+    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+        // TODO: This should support partial invalidation too.
+        mWebView.invalidate();
+    }
+
     // Handler for the messages from WebCore or Timer thread to the UI thread.
     @Override
     public void handleMessage(Message msg) {
@@ -300,8 +328,9 @@
             case PLAY: {
                 String url = (String) msg.obj;
                 WebChromeClient client = mWebView.getWebChromeClient();
+                int videoLayerID = msg.arg1;
                 if (client != null) {
-                    VideoPlayer.play(url, mSeekPosition, this, client);
+                    VideoPlayer.play(url, mSeekPosition, this, client, videoLayerID);
                 }
                 break;
             }
@@ -318,6 +347,10 @@
             case ENDED:
                 if (msg.arg1 == 1)
                     VideoPlayer.isVideoSelfEnded = true;
+                VideoPlayer.end();
+                break;
+                // TODO: [FULL SCREEN SUPPORT]
+                // For full screen case, end may need hide the view.
             case ERROR: {
                 WebChromeClient client = mWebView.getWebChromeClient();
                 if (client != null) {
@@ -500,6 +533,10 @@
         super(Looper.getMainLooper());
         // Save the WebView object.
         mWebView = webView;
+        // Pass Proxy into webview, such that every time we have a setBaseLayer
+        // call, we tell this Proxy to call the native to update the layer tree
+        // for the Video Layer's surface texture info
+        mWebView.setHTML5VideoViewProxy(this);
         // Save the native ptr
         mNativePointer = nativePtr;
         // create the message handler for this thread
@@ -565,7 +602,7 @@
      * Play a video stream.
      * @param url is the URL of the video stream.
      */
-    public void play(String url, int position) {
+    public void play(String url, int position, int videoLayerID) {
         if (url == null) {
             return;
         }
@@ -573,8 +610,8 @@
         if (position > 0) {
             seek(position);
         }
-
         Message message = obtainMessage(PLAY);
+        message.arg1 = videoLayerID;
         message.obj = url;
         sendMessage(message);
     }
@@ -628,6 +665,14 @@
         mPosterDownloader.start();
     }
 
+    // These two function are called from UI thread only by WebView.
+    public void setBaseLayer(int layer) {
+        VideoPlayer.setBaseLayer(layer);
+    }
+
+    public void pauseAndDispatch() {
+        VideoPlayer.pauseAndDispatch();
+    }
     /**
      * The factory for HTML5VideoViewProxy instances.
      * @param webViewCore is the WebViewCore that is requesting the proxy.
@@ -647,4 +692,7 @@
     private native void nativeOnPaused(int nativePointer);
     private native void nativeOnPosterFetched(Bitmap poster, int nativePointer);
     private native void nativeOnTimeupdate(int position, int nativePointer);
+    private native static boolean nativeSendSurfaceTexture(SurfaceTexture texture,
+            int baseLayer, int videoLayerId, int textureName,
+            boolean updateTexture);
 }
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index 7bf61ab..2960cbd 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -43,6 +43,7 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Region;
+import android.graphics.SurfaceTexture;
 import android.graphics.Shader;
 import android.graphics.drawable.Drawable;
 import android.net.Proxy;
@@ -608,6 +609,10 @@
     private int mTouchHighlightX;
     private int mTouchHighlightY;
 
+    // Basically this proxy is used to tell the Video to update layer tree at
+    // SetBaseLayer time and to pause when WebView paused.
+    private HTML5VideoViewProxy mHTML5VideoViewProxy;
+
     /*
      * Private message ids
      */
@@ -1184,6 +1189,7 @@
         // Initially use a size of two, since the user is likely to only hold
         // down two keys at a time (shift + another key)
         mKeysPressed = new Vector<Integer>(2);
+        mHTML5VideoViewProxy = null ;
     }
 
     /**
@@ -2900,6 +2906,11 @@
         if (!mIsPaused) {
             mIsPaused = true;
             mWebViewCore.sendMessage(EventHub.ON_PAUSE);
+            // We want to pause the current playing video when switching out
+            // from the current WebView/tab.
+            if (mHTML5VideoViewProxy != null) {
+                mHTML5VideoViewProxy.pauseAndDispatch();
+            }
         }
     }
 
@@ -4034,6 +4045,9 @@
         if (mNativeClass == 0)
             return;
         nativeSetBaseLayer(layer, invalRegion, showVisualIndicator);
+        if (mHTML5VideoViewProxy != null) {
+            mHTML5VideoViewProxy.setBaseLayer(layer);
+        }
     }
 
     private void onZoomAnimationStart() {
@@ -8454,6 +8468,15 @@
     }
 
     /**
+     * Enable the communication b/t the webView and VideoViewProxy
+     *
+     * @hide only used by the Browser
+     */
+    public void setHTML5VideoViewProxy(HTML5VideoViewProxy proxy) {
+        mHTML5VideoViewProxy = proxy;
+    }
+
+    /**
      * Enable expanded tiles bound for smoother scrolling.
      *
      * @hide only used by the Browser