From ae633054622949be14a5e92dbcdce3ea100ce007 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Sun, 10 Jul 2022 17:51:13 +1000 Subject: [PATCH] add additional Java source files needed for Android build --- .../google/android/filament/SwapChain.java | 144 +++++ .../filament/android/DisplayHelper.java | 202 ++++++ .../android/filament/android/UiHelper.java | 581 ++++++++++++++++++ 3 files changed, 927 insertions(+) create mode 100644 android/src/main/java/com/google/android/filament/SwapChain.java create mode 100644 android/src/main/java/com/google/android/filament/android/DisplayHelper.java create mode 100644 android/src/main/java/com/google/android/filament/android/UiHelper.java diff --git a/android/src/main/java/com/google/android/filament/SwapChain.java b/android/src/main/java/com/google/android/filament/SwapChain.java new file mode 100644 index 00000000..391f1f6e --- /dev/null +++ b/android/src/main/java/com/google/android/filament/SwapChain.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.filament; + +import androidx.annotation.NonNull; + +/** + * A SwapChain represents an Operating System's native renderable surface. + * + *

Typically it's a native window or a view. Because a SwapChain is initialized + * from a native object, it is given to filament as an Object, which must be of the + * proper type for each platform filament is running on.

+ * + * + * SwapChain swapChain = engine.createSwapChain(nativeWindow); + * + * + *

The nativeWindow parameter above must be of type:

+ * + *
+ * + * + * + *
Platform nativeWindow type
Android {@link android.view.Surface Surface}
+ *
+ *

+ * + *

Examples

+ * + *

Android

+ * + * + *

A {@link android.view.Surface Surface} can be retrieved from a + * {@link android.view.SurfaceView SurfaceView} or {@link android.view.SurfaceHolder SurfaceHolder} + * easily using {@link android.view.SurfaceHolder#getSurface SurfaceHolder.getSurface()} and/or + * {@link android.view.SurfaceView#getHolder SurfaceView.getHolder()}.

+ * + *

To use a {@link android.view.TextureView Textureview} as a SwapChain, it is + * necessary to first get its {@link android.graphics.SurfaceTexture SurfaceTexture}, + * for instance using {@link android.view.TextureView.SurfaceTextureListener SurfaceTextureListener} + * and then create a {@link android.view.Surface Surface}:

+ * + *
+ *  // using a TextureView.SurfaceTextureListener:
+ *  public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
+ *      mSurface = new Surface(surfaceTexture);
+ *      // mSurface can now be used with Engine.createSwapChain()
+ *  }
+ * 
+ * + * @see Engine + */ +public class SwapChain { + private final Object mSurface; + private long mNativeObject; + + public static final long CONFIG_DEFAULT = 0x0; + + /** + * This flag indicates that the SwapChain must be allocated with an + * alpha-channel. + */ + public static final long CONFIG_TRANSPARENT = 0x1; + + /** + * This flag indicates that the SwapChain may be used as a source surface + * for reading back render results. This config must be set when creating + * any SwapChain that will be used as the source for a blit operation. + * + * @see Renderer#copyFrame + */ + public static final long CONFIG_READABLE = 0x2; + + /** + * Indicates that the native X11 window is an XCB window rather than an XLIB window. + * This is ignored on non-Linux platforms and in builds that support only one X11 API. + */ + public static final long CONFIG_ENABLE_XCB = 0x4; + + SwapChain(long nativeSwapChain, Object surface) { + mNativeObject = nativeSwapChain; + mSurface = surface; + } + + /** + * @return the native Object this SwapChain was created from or null + * for a headless SwapChain. + */ + public Object getNativeWindow() { + return mSurface; + } + + /** + * FrameCompletedCallback is a callback function that notifies an application when a frame's + * contents have completed rendering on the GPU. + * + *

+ * Use setFrameCompletedCallback to set a callback on an individual SwapChain. Each time a frame + * completes GPU rendering, the callback will be called. + *

+ * + *

+ * The FrameCompletedCallback is guaranteed to be called on the main Filament thread. + *

+ * + *

+ * Warning: Only Filament's Metal backend supports frame callbacks. Other backends ignore the + * callback (which will never be called) and proceed normally. + *

+ * + * @param handler A {@link java.util.concurrent.Executor Executor}. + * @param callback The Runnable callback to invoke. + */ + public void setFrameCompletedCallback(@NonNull Object handler, @NonNull Runnable callback) { + nSetFrameCompletedCallback(getNativeObject(), handler, callback); + } + + public long getNativeObject() { + if (mNativeObject == 0) { + throw new IllegalStateException("Calling method on destroyed SwapChain"); + } + return mNativeObject; + } + + void clearNativeObject() { + mNativeObject = 0; + } + + private static native void nSetFrameCompletedCallback(long nativeSwapChain, Object handler, Runnable callback); +} diff --git a/android/src/main/java/com/google/android/filament/android/DisplayHelper.java b/android/src/main/java/com/google/android/filament/android/DisplayHelper.java new file mode 100644 index 00000000..3fc1f5e3 --- /dev/null +++ b/android/src/main/java/com/google/android/filament/android/DisplayHelper.java @@ -0,0 +1,202 @@ +// /* +// * Copyright (C) 2020 The Android Open Source Project +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ + +// package com.google.android.filament.android; + +// import android.content.Context; +// import android.hardware.display.DisplayManager; +// import android.os.Build; +// import android.os.Handler; +// import android.view.Display; + +// import com.google.android.filament.Renderer; + +// import androidx.annotation.NonNull; +// import androidx.annotation.Nullable; + +// /** +// * DisplayHelper is here to help managing a Display, for instance being notified when its +// * resolution or refresh rate changes. +// */ +// public class DisplayHelper { + +// private Handler mHandler = null; +// private DisplayManager mDisplayManager; +// private Display mDisplay; +// private Renderer mRenderer; +// private DisplayManager.DisplayListener mListener; + +// /** +// * Creates a DisplayHelper which helps managing a {@link Display}. +// * +// * The {@link Display} to manage is specified with {@link #attach} +// * +// * @param context a {@link Context} to used to retrieve the {@link DisplayManager} +// */ +// public DisplayHelper(@NonNull Context context) { +// mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); +// } + +// /** +// * Creates a DisplayHelper which helps manage a {@link Display} and provides a Handler +// * where callbacks can execute filament code. Use this method if filament is executing +// * on another thread. +// * +// * @param context a {@link Context} to used to retrieve teh {@link DisplayManager} +// * @param handler a {@link Handler} used to run callbacks accessing filament +// */ +// public DisplayHelper(@NonNull Context context, @NonNull Handler handler) { +// this(context); +// mHandler = handler; +// } + +// @Override +// protected void finalize() throws Throwable { +// try { +// // just for safety +// detach(); +// } finally { +// super.finalize(); +// } +// } + +// /** +// * Sets the filament {@link Renderer} associated to the {@link Display}, from this point +// * on, {@link Renderer.DisplayInfo} will be automatically updated when the {@link Display} +// * properties change. +// * +// * This is typically called from {@link UiHelper.RendererCallback#onNativeWindowChanged}. +// * +// * @param renderer a filament {@link Renderer} instance +// * @param display a {@link Display} to be associated with the {@link Renderer} +// */ +// public void attach(@NonNull Renderer renderer, @NonNull Display display) { +// if (renderer == mRenderer && display == mDisplay) { +// return; +// } +// mRenderer = renderer; +// mDisplay = display; +// mListener = new DisplayManager.DisplayListener() { +// @Override +// public void onDisplayAdded(int displayId) { +// } +// @Override +// public void onDisplayRemoved(int displayId) { +// } +// @Override +// public void onDisplayChanged(int displayId) { +// if (displayId == display.getDisplayId()) { +// updateDisplayInfo(); +// } +// } +// }; +// mDisplayManager.registerDisplayListener(mListener, mHandler); + +// // always invoke the callback when it's registered +// if (mHandler != null) { +// mHandler.post(new Runnable() { +// @Override +// public void run() { +// updateDisplayInfo(); +// } +// }); +// } else { +// updateDisplayInfo(); +// } +// } + +// /** +// * Disconnect the previously set {@link Renderer} from {@link Display} +// * This is typically called from {@link UiHelper.RendererCallback#onDetachedFromSurface}. +// */ +// public void detach() { +// if (mListener != null) { +// mDisplayManager.unregisterDisplayListener(mListener); +// mListener = null; +// mDisplay = null; +// mRenderer = null; +// } +// } + +// private void updateDisplayInfo() { +// mRenderer.setDisplayInfo( +// DisplayHelper.getDisplayInfo(mDisplay, mRenderer.getDisplayInfo())); +// } + +// /** +// * Returns the {@link Display} currently monitored +// * @return the {@link Display} set in {@link #attach} or null +// */ +// public Display getDisplay() { +// return mDisplay; +// } + +// /** +// * Populate a {@link Renderer.DisplayInfo} with properties from the given {@link Display} +// * +// * @param display {@link Display} to get {@link Renderer.DisplayInfo} from +// * @param info an instance of {@link Renderer.DisplayInfo} or null +// * @return an populated instance of {@link Renderer.DisplayInfo} +// */ +// @NonNull +// public static Renderer.DisplayInfo getDisplayInfo(@NonNull Display display, @Nullable Renderer.DisplayInfo info) { +// if (info == null) { +// info = new Renderer.DisplayInfo(); +// } +// info.refreshRate = DisplayHelper.getRefreshRate(display); +// return info; +// } + +// /** +// * @return the {@link Display} application vsync offset 0 if not supported +// * @see Display#getAppVsyncOffsetNanos +// */ +// public static long getAppVsyncOffsetNanos(@NonNull Display display) { +// if (Build.VERSION.SDK_INT >= 29) { +// return display.getAppVsyncOffsetNanos(); +// } +// return 0; +// } + +// /** +// * @return the {@link Display} presentation deadline before the h/w vsync event in nanoseconds +// * @see Display#getPresentationDeadlineNanos +// */ +// public static long getPresentationDeadlineNanos(@NonNull Display display) { +// if (Build.VERSION.SDK_INT >= 29) { +// return display.getPresentationDeadlineNanos(); +// } +// // not supported, pick something reasonable +// return 11_600_000; +// } + +// /** +// * @return the {@link Display} refresh rate in Hz +// * @see Display#getRefreshRate +// */ +// public static float getRefreshRate(@NonNull Display display) { +// return display.getRefreshRate(); +// } + +// /** +// * Returns a {@link Display}'s refresh period in nanoseconds +// * @param display the {@link Display} to get the refresh period from +// * @return the {@link Display} refresh period in nanoseconds +// */ +// public static long getRefreshPeriodNanos(@NonNull Display display) { +// return (long) (1_000_000_000.0 / display.getRefreshRate()); +// } +// } diff --git a/android/src/main/java/com/google/android/filament/android/UiHelper.java b/android/src/main/java/com/google/android/filament/android/UiHelper.java new file mode 100644 index 00000000..ae1bdcd7 --- /dev/null +++ b/android/src/main/java/com/google/android/filament/android/UiHelper.java @@ -0,0 +1,581 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.filament.android; + +import android.graphics.PixelFormat; +import android.graphics.SurfaceTexture; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; + +import com.google.android.filament.SwapChain; + +/** + * UiHelper is a simple class that can manage either a SurfaceView, TextureView, or a SurfaceHolder + * so it can be used to render into with Filament. + * + * Here is a simple example with a SurfaceView. The code would be exactly the same with a + * TextureView: + * + *
+ * public class FilamentActivity extends Activity {
+ *     private UiHelper mUiHelper;
+ *     private SurfaceView mSurfaceView;
+ *
+ *     // Filament specific APIs
+ *     private Engine mEngine;
+ *     private Renderer mRenderer;
+ *     private View mView; // com.google.android.filament.View, not android.view.View
+ *     private SwapChain mSwapChain;
+ *
+ *     public void onCreate(Bundle savedInstanceState) {
+ *         super.onCreate(savedInstanceState);
+ *
+ *         // Create a SurfaceView and add it to the activity
+ *         mSurfaceView = new SurfaceView(this);
+ *         setContentView(mSurfaceView);
+ *
+ *         // Create the Filament UI helper
+ *         mUiHelper = new UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK);
+ *
+ *         // Attach the SurfaceView to the helper, you could do the same with a TextureView
+ *         mUiHelper.attachTo(mSurfaceView);
+ *
+ *         // Set a rendering callback that we will use to invoke Filament
+ *         mUiHelper.setRenderCallback(new UiHelper.RendererCallback() {
+ *             public void onNativeWindowChanged(Surface surface) {
+ *                 if (mSwapChain != null) mEngine.destroySwapChain(mSwapChain);
+ *                 mSwapChain = mEngine.createSwapChain(surface, mUiHelper.getSwapChainFlags());
+ *             }
+ *
+ *             // The native surface went away, we must stop rendering.
+ *             public void onDetachedFromSurface() {
+ *                 if (mSwapChain != null) {
+ *                     mEngine.destroySwapChain(mSwapChain);
+ *
+ *                     // Required to ensure we don't return before Filament is done executing the
+ *                     // destroySwapChain command, otherwise Android might destroy the Surface
+ *                     // too early
+ *                     mEngine.flushAndWait();
+ *
+ *                     mSwapChain = null;
+ *                 }
+ *             }
+ *
+ *             // The native surface has changed size. This is always called at least once
+ *             // after the surface is created (after onNativeWindowChanged() is invoked).
+ *             public void onResized(int width, int height) {
+ *                 // Compute camera projection and set the viewport on the view
+ *             }
+ *         });
+ *
+ *         mEngine = Engine.create();
+ *         mRenderer = mEngine.createRenderer();
+ *         mView = mEngine.createView();
+ *         // Create scene, camera, etc.
+ *     }
+ *
+ *     public void onDestroy() {
+ *         super.onDestroy();
+ *         // Always detach the surface before destroying the engine
+ *         mUiHelper.detach();
+ *
+ *         mEngine.destroy();
+ *     }
+ *
+ *     // This is an example of a render function. You will most likely invoke this from
+ *     // a Choreographer callback to trigger rendering at vsync.
+ *     public void render() {
+ *         if (mUiHelper.isReadyToRender) {
+ *             // If beginFrame() returns false you should skip the frame
+ *             // This means you are sending frames too quickly to the GPU
+ *             if (mRenderer.beginFrame(swapChain)) {
+ *                 mRenderer.render(mView);
+ *                 mRenderer.endFrame();
+ *             }
+ *         }
+ *     }
+ * }
+ * 
+ */ +public class UiHelper { + private static final String LOG_TAG = "UiHelper"; + private static final boolean LOGGING = false; + + private int mDesiredWidth; + private int mDesiredHeight; + private Object mNativeWindow; + + private RendererCallback mRenderCallback; + private boolean mHasSwapChain; + + private RenderSurface mRenderSurface; + + private boolean mOpaque = true; + private boolean mOverlay = false; + + /** + * Enum used to decide whether UiHelper should perform extra error checking. + * + * @see UiHelper#UiHelper(ContextErrorPolicy) + */ + public enum ContextErrorPolicy { + /** Check for extra errors. */ + CHECK, + /** Do not check for extra errors. */ + DONT_CHECK + } + + /** + * Interface used to know when the native surface is created, destroyed or resized. + * + * @see #setRenderCallback(RendererCallback) + */ + public interface RendererCallback { + /** + * Called when the underlying native window has changed. + */ + void onNativeWindowChanged(Surface surface); + + /** + * Called when the surface is going away. After this call isReadyToRender() + * returns false. You MUST have stopped drawing when returning. + * This is called from detach() or if the surface disappears on its own. + */ + void onDetachedFromSurface(); + + /** + * Called when the underlying native window has been resized. + */ + void onResized(int width, int height); + } + + private interface RenderSurface { + void resize(int width, int height); + void detach(); + } + + private static class SurfaceViewHandler implements RenderSurface { + private SurfaceView mSurfaceView; + + SurfaceViewHandler(SurfaceView surface) { + mSurfaceView = surface; + } + + @Override + public void resize(int width, int height) { + mSurfaceView.getHolder().setFixedSize(width, height); + } + + @Override + public void detach() { + } + } + + private static class SurfaceHolderHandler implements RenderSurface { + private SurfaceHolder mSurfaceHolder; + + SurfaceHolderHandler(SurfaceHolder surface) { + mSurfaceHolder = surface; + } + + @Override + public void resize(int width, int height) { + mSurfaceHolder.setFixedSize(width, height); + } + + @Override + public void detach() { + } + } + + private class TextureViewHandler implements RenderSurface { + private TextureView mTextureView; + private Surface mSurface; + + TextureViewHandler(TextureView surface) { mTextureView = surface; } + + @Override + public void resize(int width, int height) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + mTextureView.getSurfaceTexture().setDefaultBufferSize(width, height); + } + // the call above won't cause TextureView.onSurfaceTextureSizeChanged() + mRenderCallback.onResized(width, height); + } + + @Override + public void detach() { + setSurface(null); + } + + void setSurface(Surface surface) { + if (surface == null) { + if (mSurface != null) { + mSurface.release(); + } + } + mSurface = surface; + } + } + + /** + * Creates a UiHelper which will help manage the native surface provided by a + * SurfaceView or a TextureView. + */ + public UiHelper() { + this(ContextErrorPolicy.CHECK); + } + + /** + * Creates a UiHelper which will help manage the native surface provided by a + * SurfaceView or a TextureView. + * + * @param policy The error checking policy to use. + */ + public UiHelper(ContextErrorPolicy policy) { + // TODO: do something with policy + } + + /** + * Sets the renderer callback that will be notified when the native surface is + * created, destroyed or resized. + * + * @param renderCallback The callback to register. + */ + public void setRenderCallback(@Nullable RendererCallback renderCallback) { + mRenderCallback = renderCallback; + } + + /** + * Returns the current render callback associated with this UiHelper. + */ + @Nullable + public RendererCallback getRenderCallback() { + return mRenderCallback; + } + + /** + * Free resources associated to the native window specified in {@link #attachTo(SurfaceView)}, + * {@link #attachTo(TextureView)}, or {@link #attachTo(SurfaceHolder)}. + */ + public void detach() { + destroySwapChain(); + mNativeWindow = null; + mRenderSurface = null; + } + + /** + * Checks whether we are ready to render into the attached surface. + * + * Using OpenGL ES when this returns true, will result in drawing commands being lost, + * HOWEVER, GLES state will be preserved. This is useful to initialize the engine. + * + * @return true: rendering is possible, false: rendering is not possible. + */ + public boolean isReadyToRender() { + return mHasSwapChain; + } + + /** + * Set the size of the render target buffers of the native surface. + */ + public void setDesiredSize(int width, int height) { + mDesiredWidth = width; + mDesiredHeight = height; + if (mRenderSurface != null) { + mRenderSurface.resize(width, height); + } + } + + /** + * Returns the requested width for the native surface. + */ + public int getDesiredWidth() { + return mDesiredWidth; + } + + /** + * Returns the requested height for the native surface. + */ + public int getDesiredHeight() { + return mDesiredHeight; + } + + /** + * Returns true if the render target is opaque. + */ + public boolean isOpaque() { + return mOpaque; + } + + /** + * Controls whether the render target (SurfaceView or TextureView) is opaque or not. + * The render target is considered opaque by default. + * + * Must be called before calling {@link #attachTo(SurfaceView)}, {@link #attachTo(TextureView)}, + * or {@link #attachTo(SurfaceHolder)}. + * + * @param opaque Indicates whether the render target should be opaque. True by default. + */ + public void setOpaque(boolean opaque) { + mOpaque = opaque; + } + + /** + * Returns true if the SurfaceView used as a render target should be positioned above + * other surfaces but below the activity's surface. False by default. + */ + public boolean isMediaOverlay() { + return mOverlay; + } + + /** + * Controls whether the surface of the SurfaceView used as a render target should be + * positioned above other surfaces but below the activity's surface. This property + * only has an effect when used in combination with {@link #setOpaque(boolean) setOpaque(false)} + * and does not affect TextureView targets. + * + * Must be called before calling {@link #attachTo(SurfaceView)} + * or {@link #attachTo(TextureView)}. + * + * Has no effect when using {@link #attachTo(SurfaceHolder)}. + * + * @param overlay Indicates whether the render target should be rendered below the activity's + * surface when transparent. + */ + public void setMediaOverlay(boolean overlay) { + mOverlay = overlay; + } + + /** + * Returns the flags to pass to + * {@link com.google.android.filament.Engine#createSwapChain(Object, long)} to honor all + * the options set on this UiHelper. + */ + public long getSwapChainFlags() { + return isOpaque() ? SwapChain.CONFIG_DEFAULT : SwapChain.CONFIG_TRANSPARENT; + } + + /** + * Associate UiHelper with a SurfaceView. + * + * As soon as SurfaceView is ready (i.e. has a Surface), we'll create the + * EGL resources needed, and call user callbacks if needed. + */ + public void attachTo(@NonNull SurfaceView view) { + if (attach(view)) { + boolean translucent = !isOpaque(); + // setZOrderOnTop() and setZOrderMediaOverlay() override each other, + // we must only call one of them + if (isMediaOverlay()) { + view.setZOrderMediaOverlay(translucent); + } else { + view.setZOrderOnTop(translucent); + } + + int format = isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT; + view.getHolder().setFormat(format); + + mRenderSurface = new SurfaceViewHandler(view); + + final SurfaceHolder.Callback callback = new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder holder) { + if (LOGGING) Log.d(LOG_TAG, "surfaceCreated()"); + createSwapChain(holder.getSurface()); + } + + @Override + public void surfaceChanged( + SurfaceHolder holder, int format, int width, int height) { + // Note: this is always called at least once after surfaceCreated() + if (LOGGING) Log.d(LOG_TAG, "surfaceChanged(" + width + ", " + height + ")"); + mRenderCallback.onResized(width, height); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (LOGGING) Log.d(LOG_TAG, "surfaceDestroyed()"); + destroySwapChain(); + } + }; + + SurfaceHolder holder = view.getHolder(); + holder.addCallback(callback); + if (mDesiredWidth > 0 && mDesiredHeight > 0) { + holder.setFixedSize(mDesiredWidth, mDesiredHeight); + } + + // in case the SurfaceView's surface already existed + final Surface surface = holder.getSurface(); + if (surface != null && surface.isValid()) { + callback.surfaceCreated(holder); + callback.surfaceChanged(holder, format, + holder.getSurfaceFrame().width(), holder.getSurfaceFrame().height()); + } + } + } + + /** + * Associate UiHelper with a TextureView. + * + * As soon as TextureView is ready (i.e. has a buffer), we'll create the + * EGL resources needed, and call user callbacks if needed. + */ + public void attachTo(@NonNull TextureView view) { + if (attach(view)) { + view.setOpaque(isOpaque()); + + mRenderSurface = new TextureViewHandler(view); + + TextureView.SurfaceTextureListener listener = new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable( + SurfaceTexture surfaceTexture, int width, int height) { + if (LOGGING) Log.d(LOG_TAG, "onSurfaceTextureAvailable()"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + if (mDesiredWidth > 0 && mDesiredHeight > 0) { + surfaceTexture.setDefaultBufferSize(mDesiredWidth, mDesiredHeight); + } + } + + Surface surface = new Surface(surfaceTexture); + TextureViewHandler textureViewHandler = (TextureViewHandler) mRenderSurface; + textureViewHandler.setSurface(surface); + + createSwapChain(surface); + + // Call this the first time because onSurfaceTextureSizeChanged() + // isn't called at initialization time + mRenderCallback.onResized(width, height); + } + + @Override + public void onSurfaceTextureSizeChanged( + SurfaceTexture surfaceTexture, int width, int height) { + if (LOGGING) Log.d(LOG_TAG, "onSurfaceTextureSizeChanged()"); + if (mDesiredWidth > 0 && mDesiredHeight > 0) { + surfaceTexture.setDefaultBufferSize(mDesiredWidth, mDesiredHeight); + mRenderCallback.onResized(mDesiredWidth, mDesiredHeight); + } else { + mRenderCallback.onResized(width, height); + } + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + if (LOGGING) Log.d(LOG_TAG, "onSurfaceTextureDestroyed()"); + destroySwapChain(); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { } + }; + + view.setSurfaceTextureListener(listener); + + // in case the View's SurfaceTexture already existed + if (view.isAvailable()) { + SurfaceTexture surfaceTexture = view.getSurfaceTexture(); + listener.onSurfaceTextureAvailable(surfaceTexture, mDesiredWidth, mDesiredHeight); + } + } + } + + /** + * Associate UiHelper with a SurfaceHolder. + * + * As soon as a Surface is created, we'll create the + * EGL resources needed, and call user callbacks if needed. + */ + public void attachTo(@NonNull SurfaceHolder holder) { + if (attach(holder)) { + int format = isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT; + holder.setFormat(format); + + mRenderSurface = new SurfaceHolderHandler(holder); + + final SurfaceHolder.Callback callback = new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder surfaceHolder) { + if (LOGGING) Log.d(LOG_TAG, "surfaceCreated()"); + createSwapChain(holder.getSurface()); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + // Note: this is always called at least once after surfaceCreated() + if (LOGGING) Log.d(LOG_TAG, "surfaceChanged(" + width + ", " + height + ")"); + mRenderCallback.onResized(width, height); + } + + @Override + public void surfaceDestroyed(SurfaceHolder surfaceHolder) { + if (LOGGING) Log.d(LOG_TAG, "surfaceDestroyed()"); + destroySwapChain(); + } + }; + + holder.addCallback(callback); + if (mDesiredWidth > 0 && mDesiredHeight > 0) { + holder.setFixedSize(mDesiredWidth, mDesiredHeight); + } + + // in case the SurfaceHolder's surface already existed + final Surface surface = holder.getSurface(); + if (surface != null && surface.isValid()) { + callback.surfaceCreated(holder); + callback.surfaceChanged(holder, format, + holder.getSurfaceFrame().width(), holder.getSurfaceFrame().height()); + } + } + } + + private boolean attach(@NonNull Object nativeWindow) { + if (mNativeWindow != null) { + // we are already attached to a native window + if (mNativeWindow == nativeWindow) { + // nothing to do + return false; + } + destroySwapChain(); + } + mNativeWindow = nativeWindow; + return true; + } + + private void createSwapChain(@NonNull Surface surface) { + mRenderCallback.onNativeWindowChanged(surface); + mHasSwapChain = true; + } + + private void destroySwapChain() { + if (mRenderSurface != null) { + mRenderSurface.detach(); + } + mRenderCallback.onDetachedFromSurface(); + mHasSwapChain = false; + } +}