Bug 1530402 - Provide an API to use TextureView in GeckoView. r=snorp,droeh
authorAgi Sferro <agi@sferro.dev>
Mon, 18 Nov 2019 16:48:46 +0000
changeset 502429 48cd0a34cab7b4c7dc4aee49c4bac1797b4b7aba
parent 502428 6d66e846ad4634bd49523a9ea592fc792199994c
child 502430 ebce35d83c874e821dcfac210ed217fca5f74a04
push id114172
push userdluca@mozilla.com
push dateTue, 19 Nov 2019 11:31:10 +0000
treeherdermozilla-inbound@b5c5ba07d3db [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp, droeh
bugs1530402
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1530402 - Provide an API to use TextureView in GeckoView. r=snorp,droeh Differential Revision: https://phabricator.services.mozilla.com/D51916
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/main/java/org/mozilla/gecko/SurfaceViewWrapper.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -21,17 +21,16 @@ import android.support.annotation.UiThre
 import android.util.AttributeSet;
 import android.util.SparseArray;
 import android.view.ActionMode;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.Surface;
-import android.view.SurfaceView;
 import android.view.View;
 import android.view.ViewStructure;
 import android.view.autofill.AutofillValue;
 import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
@@ -1155,20 +1154,22 @@ package org.mozilla.geckoview {
     method @AnyThread @Nullable public GeckoSession getSession();
     method public int onGenericMotionEventForResult(@NonNull MotionEvent);
     method public int onTouchEventForResult(@NonNull MotionEvent);
     method @UiThread @Nullable public GeckoSession releaseSession();
     method public void setAutofillEnabled(boolean);
     method public void setDynamicToolbarMaxHeight(int);
     method @UiThread public void setSession(@NonNull GeckoSession);
     method public void setVerticalClipping(int);
+    method public void setViewBackend(int);
     method public boolean shouldPinOnScreen();
+    field public static final int BACKEND_SURFACE_VIEW = 1;
+    field public static final int BACKEND_TEXTURE_VIEW = 2;
     field @NonNull protected final GeckoView.Display mDisplay;
     field @Nullable protected GeckoSession mSession;
-    field @Nullable protected SurfaceView mSurfaceView;
   }
 
   @AnyThread public class GeckoWebExecutor {
     ctor public GeckoWebExecutor(@NonNull GeckoRuntime);
     method @NonNull public GeckoResult<WebResponse> fetch(@NonNull WebRequest);
     method @NonNull public GeckoResult<WebResponse> fetch(@NonNull WebRequest, int);
     method @NonNull public GeckoResult<InetAddress[]> resolve(@NonNull String);
     method public void speculativeConnect(@NonNull String);
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SurfaceViewWrapper.java
@@ -0,0 +1,174 @@
+package org.mozilla.gecko;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+
+/** Provides transparent access to either a SurfaceView or TextureView */
+public class SurfaceViewWrapper {
+    private static final String LOGTAG = "SurfaceViewWrapper";
+
+    private ListenerWrapper mListenerWrapper;
+    private View mView;
+
+    // Only one of these will be non-null at any point in time
+    SurfaceView mSurfaceView;
+    TextureView mTextureView;
+
+    public SurfaceViewWrapper(final Context context) {
+        // By default, use SurfaceView
+        mListenerWrapper = new ListenerWrapper();
+        mSurfaceView = new SurfaceView(context);
+        mView = mSurfaceView;
+    }
+
+    public void useSurfaceView(final Context context) {
+        if (mTextureView != null) {
+            mListenerWrapper.onSurfaceTextureDestroyed(
+                    mTextureView.getSurfaceTexture());
+            mTextureView = null;
+        }
+        mListenerWrapper.reset();
+        mSurfaceView = new SurfaceView(context);
+        mSurfaceView.getHolder().addCallback(mListenerWrapper);
+        mView = mSurfaceView;
+    }
+
+    public void useTextureView(final Context context) {
+        if (mSurfaceView != null) {
+            mListenerWrapper.surfaceDestroyed(mSurfaceView.getHolder());
+            mSurfaceView = null;
+        }
+        mListenerWrapper.reset();
+        mTextureView = new TextureView(context);
+        mTextureView.setSurfaceTextureListener(mListenerWrapper);
+        mView = mTextureView;
+    }
+
+    public void setBackgroundColor(final int color) {
+        if (mSurfaceView != null) {
+            mSurfaceView.setBackgroundColor(color);
+        } else {
+            Log.e(LOGTAG, "TextureView doesn't support background color.");
+        }
+    }
+
+    public void setListener(final Listener listener) {
+        mListenerWrapper.mListener = listener;
+        mSurfaceView.getHolder().addCallback(mListenerWrapper);
+    }
+
+    public int getWidth() {
+        if (mSurfaceView != null) {
+            return mSurfaceView.getHolder().getSurfaceFrame().right;
+        }
+        return mListenerWrapper.mWidth;
+    }
+
+    public int getHeight() {
+        if (mSurfaceView != null) {
+            return mSurfaceView.getHolder().getSurfaceFrame().bottom;
+        }
+        return mListenerWrapper.mHeight;
+    }
+
+    public Surface getSurface() {
+        if (mSurfaceView != null) {
+            return mSurfaceView.getHolder().getSurface();
+        }
+
+        return mListenerWrapper.mSurface;
+    }
+
+    public View getView() {
+        return mView;
+    }
+
+    /**
+     * Translates SurfaceTextureListener and SurfaceHolder.Callback into a common interface
+     * SurfaceViewWrapper.Listener
+     */
+    private static class ListenerWrapper implements TextureView.SurfaceTextureListener,
+            SurfaceHolder.Callback {
+        private Listener mListener;
+
+        // TextureView doesn't provide getters for these so we keep track of them here
+        private Surface mSurface;
+        private int mWidth;
+        private int mHeight;
+
+        public void reset() {
+            mWidth = 0;
+            mHeight = 0;
+            mSurface = null;
+        }
+
+        // TextureView
+        @Override
+        public void onSurfaceTextureAvailable(final SurfaceTexture surface, final int width,
+                                              final int height) {
+            mSurface = new Surface(surface);
+            mWidth = width;
+            mHeight = height;
+            if (mListener != null) {
+                mListener.onSurfaceChanged(mSurface, width, height);
+            }
+        }
+
+        @Override
+        public void onSurfaceTextureSizeChanged(final SurfaceTexture surface, final int width,
+                                                final int height) {
+            mWidth = width;
+            mHeight = height;
+            if (mListener != null) {
+                mListener.onSurfaceChanged(mSurface, mWidth, mHeight);
+            }
+        }
+
+        @Override
+        public boolean onSurfaceTextureDestroyed(final SurfaceTexture surface) {
+            if (mListener != null) {
+                mListener.onSurfaceDestroyed();
+            }
+            mSurface = null;
+            return false;
+        }
+
+        @Override
+        public void onSurfaceTextureUpdated(final SurfaceTexture surface) {
+            mSurface = new Surface(surface);
+            if (mListener != null) {
+                mListener.onSurfaceChanged(mSurface, mWidth, mHeight);
+            }
+        }
+
+        // SurfaceView
+        @Override
+        public void surfaceCreated(final SurfaceHolder holder) {}
+
+        @Override
+        public void surfaceChanged(final SurfaceHolder holder, final int format, final int width,
+                                   final int height) {
+            if (mListener != null) {
+                mListener.onSurfaceChanged(holder.getSurface(), width, height);
+            }
+        }
+
+        @Override
+        public void surfaceDestroyed(final SurfaceHolder holder) {
+            if (mListener != null) {
+                mListener.onSurfaceDestroyed();
+            }
+        }
+    }
+
+    public interface Listener {
+        void onSurfaceChanged(Surface surface, int width, int height);
+        void onSurfaceDestroyed();
+    }
+}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java
@@ -4,16 +4,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.geckoview;
 
 import org.mozilla.gecko.AndroidGamepadManager;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.InputMethods;
+import org.mozilla.gecko.SurfaceViewWrapper;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -24,48 +25,53 @@ import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Region;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.support.annotation.AnyThread;
+import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 import android.support.v4.view.ViewCompat;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.SparseArray;
 import android.util.TypedValue;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
-import android.view.SurfaceHolder;
+import android.view.Surface;
 import android.view.SurfaceView;
+import android.view.TextureView;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStructure;
 import android.view.autofill.AutofillManager;
 import android.view.autofill.AutofillValue;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.FrameLayout;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 @UiThread
 public class GeckoView extends FrameLayout {
     private static final String LOGTAG = "GeckoView";
     private static final boolean DEBUG = false;
 
     protected final @NonNull Display mDisplay = new Display();
     protected @Nullable GeckoSession mSession;
     private boolean mStateSaved;
 
-    protected @Nullable SurfaceView mSurfaceView;
+    private @Nullable SurfaceViewWrapper mSurfaceWrapper;
 
     private boolean mIsResettingFocus;
 
     private boolean mAutofillEnabled = true;
 
     private GeckoSession.SelectionActionDelegate mSelectionActionDelegate;
     private Autofill.Delegate mAutofillDelegate;
 
@@ -96,17 +102,17 @@ public class GeckoView extends FrameLayo
 
             @Override
             public SavedState[] newArray(final int size) {
                 return new SavedState[size];
             }
         };
     }
 
-    private class Display implements SurfaceHolder.Callback {
+    private class Display implements SurfaceViewWrapper.Listener {
         private final int[] mOrigin = new int[2];
 
         private GeckoDisplay mDisplay;
         private boolean mValid;
 
         private int mClippingHeight;
         private int mDynamicToolbarMaxHeight;
 
@@ -116,20 +122,20 @@ public class GeckoView extends FrameLayo
             if (!mValid) {
                 return;
             }
 
             setVerticalClipping(mClippingHeight);
 
             // Tell display there is already a surface.
             onGlobalLayout();
-            if (GeckoView.this.mSurfaceView != null) {
-                final SurfaceHolder holder = GeckoView.this.mSurfaceView.getHolder();
-                final Rect frame = holder.getSurfaceFrame();
-                mDisplay.surfaceChanged(holder.getSurface(), frame.right, frame.bottom);
+            if (GeckoView.this.mSurfaceWrapper != null) {
+                final SurfaceViewWrapper wrapper = GeckoView.this.mSurfaceWrapper;
+                mDisplay.surfaceChanged(wrapper.getSurface(),
+                        wrapper.getWidth(), wrapper.getHeight());
                 mDisplay.setDynamicToolbarMaxHeight(mDynamicToolbarMaxHeight);
                 GeckoView.this.setActive(true);
             }
         }
 
         public GeckoDisplay release() {
             if (mValid) {
                 if (mDisplay != null) {
@@ -138,48 +144,44 @@ public class GeckoView extends FrameLayo
                 GeckoView.this.setActive(false);
             }
 
             final GeckoDisplay display = mDisplay;
             mDisplay = null;
             return display;
         }
 
-        @Override // SurfaceHolder.Callback
-        public void surfaceCreated(final SurfaceHolder holder) {
-        }
-
-        @Override // SurfaceHolder.Callback
-        public void surfaceChanged(final SurfaceHolder holder, final int format,
+        @Override // SurfaceListener
+        public void onSurfaceChanged(final Surface surface,
                                    final int width, final int height) {
             if (mDisplay != null) {
-                mDisplay.surfaceChanged(holder.getSurface(), width, height);
+                mDisplay.surfaceChanged(surface, width, height);
                 mDisplay.setDynamicToolbarMaxHeight(mDynamicToolbarMaxHeight);
                 if (!mValid) {
                     GeckoView.this.setActive(true);
                 }
             }
             mValid = true;
         }
 
-        @Override // SurfaceHolder.Callback
-        public void surfaceDestroyed(final SurfaceHolder holder) {
+        @Override // SurfaceListener
+        public void onSurfaceDestroyed() {
             if (mDisplay != null) {
                 mDisplay.surfaceDestroyed();
                 GeckoView.this.setActive(false);
             }
             mValid = false;
         }
 
         public void onGlobalLayout() {
             if (mDisplay == null) {
                 return;
             }
-            if (GeckoView.this.mSurfaceView != null) {
-                GeckoView.this.mSurfaceView.getLocationOnScreen(mOrigin);
+            if (GeckoView.this.mSurfaceWrapper != null) {
+                GeckoView.this.mSurfaceWrapper.getView().getLocationOnScreen(mOrigin);
                 mDisplay.screenOriginChanged(mOrigin[0], mOrigin[1]);
             }
         }
 
         public boolean shouldPinOnScreen() {
             return mDisplay != null ? mDisplay.shouldPinOnScreen() : false;
         }
 
@@ -234,23 +236,23 @@ public class GeckoView extends FrameLayo
         // descendants to affect the way LayerView retains its focus.
         setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
 
         // This will stop PropertyAnimator from creating a drawing cache (i.e. a
         // bitmap) from a SurfaceView, which is just not possible (the bitmap will be
         // transparent).
         setWillNotCacheDrawing(false);
 
-        mSurfaceView = new SurfaceView(getContext());
-        mSurfaceView.setBackgroundColor(Color.WHITE);
-        addView(mSurfaceView,
+        mSurfaceWrapper = new SurfaceViewWrapper(getContext());
+        mSurfaceWrapper.setBackgroundColor(Color.WHITE);
+        addView(mSurfaceWrapper.getView(),
                 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                                            ViewGroup.LayoutParams.MATCH_PARENT));
 
-        mSurfaceView.getHolder().addCallback(mDisplay);
+        mSurfaceWrapper.setListener(mDisplay);
 
         final Activity activity = ActivityUtils.getActivityFromContext(getContext());
         if (activity != null) {
             mSelectionActionDelegate = new BasicSelectionActionDelegate(activity);
         }
 
         mAutofillDelegate = new AndroidAutofillDelegate();
     }
@@ -260,18 +262,52 @@ public class GeckoView extends FrameLayo
      * is automatically cleared once the new document starts painting. Set to
      * Color.TRANSPARENT to undo the cover.
      *
      * @param color Cover color.
      */
     public void coverUntilFirstPaint(final int color) {
         ThreadUtils.assertOnUiThread();
 
-        if (mSurfaceView != null) {
-            mSurfaceView.setBackgroundColor(color);
+        if (mSurfaceWrapper != null) {
+            mSurfaceWrapper.setBackgroundColor(color);
+        }
+    }
+
+    /**
+     * This GeckoView instance will be backed by a {@link SurfaceView}.
+     *
+     * This option offers the best performance at the price of not being
+     * able to animate GeckoView.
+     */
+    public static final int BACKEND_SURFACE_VIEW = 1;
+    /**
+     * This GeckoView instance will be backed by a {@link TextureView}.
+     *
+     * This option offers worse performance compared to {@link #BACKEND_SURFACE_VIEW}
+     * but allows you to animate GeckoView or to paint a GeckoView on top of another GeckoView.
+     */
+    public static final int BACKEND_TEXTURE_VIEW = 2;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({BACKEND_SURFACE_VIEW, BACKEND_TEXTURE_VIEW})
+    /* protected */ @interface ViewBackend {}
+
+    /**
+     * Set which view should be used by this GeckoView instance to display content.
+     *
+     * By default, GeckoView will use a {@link SurfaceView}.
+     *
+     * @param backend Any of {@link #BACKEND_SURFACE_VIEW BACKEND_*}.
+     */
+    public void setViewBackend(final @ViewBackend int backend) {
+        if (backend == BACKEND_SURFACE_VIEW) {
+            mSurfaceWrapper.useSurfaceView(getContext());
+        } else if (backend == BACKEND_TEXTURE_VIEW) {
+            mSurfaceWrapper.useTextureView(getContext());
         }
     }
 
     /**
      * Return whether the view should be pinned on the screen. When pinned, the view
      * should not be moved on the screen due to animation, scrolling, etc. A common reason
      * for the view being pinned is when the user is dragging a selection caret inside
      * the view; normal user interaction would be disrupted in that case if the view
@@ -505,17 +541,17 @@ public class GeckoView extends FrameLayo
         }
     }
 
     @Override
     public boolean gatherTransparentRegion(final Region region) {
         // For detecting changes in SurfaceView layout, we take a shortcut here and
         // override gatherTransparentRegion, instead of registering a layout listener,
         // which is more expensive.
-        if (mSurfaceView != null) {
+        if (mSurfaceWrapper != null) {
             mDisplay.onGlobalLayout();
         }
         return super.gatherTransparentRegion(region);
     }
 
     @Override
     protected Parcelable onSaveInstanceState() {
         mStateSaved = true;
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
@@ -32,27 +32,33 @@ exclude: true
 - Added text selection action methods to [`SelectionActionDelegate.Selection`][72.8]
   ([bug 1581161]({{bugzilla}}1581161))
 - Added [`BasicSelectionActionDelegate.getSelection`][72.9]
   ([bug 1581161]({{bugzilla}}1581161))
 - Changed [`BasicSelectionActionDelegate.clearSelection`][72.10] to public.
   ([bug 1581161]({{bugzilla}}1581161))
 - Added `Autofill` commit support.
   ([bug 1577005]({{bugzilla}}1577005))
+- Added [`GeckoView.setViewBackend`][72.11] to set whether GeckoView should be
+  backed by a [`TextureView`][72.12] or a [`SurfaceView`][72.13].
+  ([bug 1530402]({{bugzilla}}1530402))
 
 [72.1]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest#hasUserGesture-
 [72.2]: {{javadoc_uri}}/Autofill.html
 [72.3]: {{javadoc_uri}}/WebResponse.html#body
 [72.4]: {{javadoc_uri}}/WebResponse.html#setReadTimeoutMillis-long-
 [72.5]: {{javadoc_uri}}/WebResponse.html#DEFAULT_READ_TIMEOUT_MS
 [72.6]: {{javadoc_uri}}/GeckoSession.SelectionActionDelegate.html#onShowActionRequest-org.mozilla.geckoview.GeckoSession-org.mozilla.geckoview.GeckoSession.SelectionActionDelegate.Selection-
 [72.7]: {{javadoc_uri}}/BasicSelectionActionDelegate.html#onShowActionRequest-org.mozilla.geckoview.GeckoSession-org.mozilla.geckoview.GeckoSession.SelectionActionDelegate.Selection-
 [72.8]: {{javadoc_uri}}/GeckoSession.SelectionActionDelegate.Selection.html
 [72.9]: {{javadoc_uri}}/BasicSelectionActionDelegate.html#getSelection-
 [72.10]: {{javadoc_uri}}/BasicSelectionActionDelegate.html#clearSelection-
+[72.11]: {{javadoc_uri}}/GeckoView.html#setViewBackend-int-
+[72.12]: https://developer.android.com/reference/android/view/TextureView
+[72.13]: https://developer.android.com/reference/android/view/SurfaceView
 
 ## v71
 - Added a content blocking flag for blocked social cookies to [`ContentBlocking`][70.17].
   ([bug 1584479]({{bugzilla}}1584479))
 - Added [`onBooleanScalar`][71.1], [`onLongScalar`][71.2],
   [`onStringScalar`][71.3] to [`RuntimeTelemetry.Delegate`][70.12] to support
   scalars in streaming telemetry. ⚠️  As part of this change,
   `onTelemetryReceived` has been renamed to [`onHistogram`][71.4], and
@@ -440,9 +446,9 @@ exclude: true
 [65.19]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest.html#isRedirect
 [65.20]: {{javadoc_uri}}/GeckoSession.html#LOAD_FLAGS_BYPASS_CLASSIFIER    
 [65.21]: {{javadoc_uri}}/GeckoSession.ContentDelegate.ContextElement.html
 [65.22]: {{javadoc_uri}}/GeckoSession.ContentDelegate.html#onContextMenu-org.mozilla.geckoview.GeckoSession-int-int-org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement-
 [65.23]: {{javadoc_uri}}/GeckoSession.FinderResult.html
 [65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
 [65.25]: {{javadoc_uri}}/GeckoResult.html
 
-[api-version]: 8d6a09b6a33550dffb6303dc01c5e6ff2d3cc499
+[api-version]: cff8d49f3436c4b3b5ae91f96f333b8a5d55ab96