From 0e66bb7f30562c5e0c8114d5257ce03bfd87e955 Mon Sep 17 00:00:00 2001 From: Kyujung Youn Date: Fri, 24 Jul 2026 09:00:59 -0700 Subject: [PATCH 1/2] cobalt: Use activity Window surface for UI rendering Introduces support for rendering UI directly into the Activity's Window surface via Window.takeSurface(), as an alternative to an embedded child SurfaceView. This behavior is abstracted behind WindowSurfaceBridge and is controlled by the `--use-window-surface-for-ui` command-line flag (disabled by default). Issue: 494590075 --- .../java/dev/cobalt/coat/CobaltActivity.java | 4 +- .../cobalt/shell/ContentViewRenderView.java | 190 +++++++++++++++--- 2 files changed, 160 insertions(+), 34 deletions(-) diff --git a/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java index 89f25246679a..3b06b14ea636 100644 --- a/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java +++ b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java @@ -237,9 +237,9 @@ protected void createContent(final Bundle savedInstanceState) { // Set up the animation placeholder to be the SurfaceView. This disables the // SurfaceView's 'hole' clipping during animations that are notified to the window. mWindowAndroid.setAnimationPlaceholderView( - mShellManager.getContentViewRenderView().getSurfaceView()); + mShellManager.getContentViewRenderView().getAnchorView()); mA11yHelper = - new CobaltA11yHelper(this, mShellManager.getContentViewRenderView().getSurfaceView()); + new CobaltA11yHelper(this, mShellManager.getContentViewRenderView().getAnchorView()); if (mStartupUrl == null || mStartupUrl.isEmpty()) { String[] args = getStarboardBridge().getArgs(); diff --git a/cobalt/shell/android/java/src/dev/cobalt/shell/ContentViewRenderView.java b/cobalt/shell/android/java/src/dev/cobalt/shell/ContentViewRenderView.java index 60667cc77476..6f8d5f5dd276 100644 --- a/cobalt/shell/android/java/src/dev/cobalt/shell/ContentViewRenderView.java +++ b/cobalt/shell/android/java/src/dev/cobalt/shell/ContentViewRenderView.java @@ -4,6 +4,7 @@ package dev.cobalt.shell; +import android.app.Activity; import android.content.Context; import android.graphics.PixelFormat; import android.view.Surface; @@ -11,8 +12,11 @@ import android.view.MotionEvent; import android.view.SurfaceView; import android.view.View; +import android.view.Window; import android.widget.FrameLayout; +import org.chromium.base.CommandLine; +import org.chromium.base.Log; import org.chromium.content_public.browser.WebContents; import org.chromium.ui.base.EventForwarder; import org.chromium.ui.base.WindowAndroid; @@ -28,6 +32,8 @@ */ @JNINamespace("cobalt") public class ContentViewRenderView extends FrameLayout { + private static final String TAG = "cobalt"; + // The native side of this object. private long mNativeContentViewRenderView; private WindowAndroid mWindowAndroid; @@ -53,7 +59,12 @@ public ContentViewRenderView(Context context) { } protected SurfaceBridge createSurfaceBridge() { - return new SurfaceBridge(); + if (CommandLine.getInstance().hasSwitch("use-window-surface-for-ui")) { + Log.i(TAG, "ContentViewRenderView: created using WindowSurfaceBridge"); + return new WindowSurfaceBridge(); + } + Log.i(TAG, "ContentViewRenderView: created with SurfaceView"); + return new SurfaceViewBridge(); } /** @@ -62,7 +73,8 @@ protected SurfaceBridge createSurfaceBridge() { * @param rootWindow The {@link WindowAndroid} this render view should be linked to. */ public void onNativeLibraryLoaded(WindowAndroid rootWindow) { - assert !getSurfaceView().getHolder().getSurface().isValid() + assert mSurfaceBridge.getSurfaceView() == null + || !mSurfaceBridge.getSurfaceView().getHolder().getSurface().isValid() : "Surface created before native library loaded."; assert rootWindow != null; mNativeContentViewRenderView = @@ -96,7 +108,10 @@ public void surfaceCreated(SurfaceHolder holder) { // devices, where a relayout never happens. This bug is out of Chromium's // control, but can be worked around by forcibly re-setting the visibility of // the surface view. Otherwise, the screen stays black, and some tests fail. - getSurfaceView().setVisibility(getSurfaceView().getVisibility()); + SurfaceView surfaceView = mSurfaceBridge.getSurfaceView(); + if (surfaceView != null) { + surfaceView.setVisibility(surfaceView.getVisibility()); + } onReadyToRender(); } @@ -108,7 +123,7 @@ public void surfaceDestroyed(SurfaceHolder holder) { mNativeContentViewRenderView, ContentViewRenderView.this); } }; - mSurfaceBridge.connect(surfaceCallback); + mSurfaceBridge.connect(surfaceCallback, rootWindow); } @Override @@ -135,22 +150,12 @@ protected void onWindowVisibilityChanged(int visibility) { } /** - * Sets the background color of the surface view. This method is necessary because the - * background color of ContentViewRenderView itself is covered by the background of - * SurfaceView. - * @param color The color of the background. - */ - public void setSurfaceViewBackgroundColor(int color) { - if (getSurfaceView() != null) { - getSurfaceView().setBackgroundColor(color); - } - } - - /** - * Gets the SurfaceView for this ContentViewRenderView + * Gets the View used for layout anchoring, animation placeholder, or accessibility (child + * SurfaceView in SurfaceView mode, or this host View in Window Surface mode). */ - public SurfaceView getSurfaceView() { - return mSurfaceBridge.getSurfaceView(); + public View getAnchorView() { + SurfaceView surfaceView = mSurfaceBridge.getSurfaceView(); + return surfaceView != null ? surfaceView : this; } /** @@ -195,47 +200,72 @@ protected SurfaceView createSurfaceView(Context context) { return new SurfaceView(context); } - /** - * @return whether the surface view is initialized and ready to render. - */ - public boolean isInitialized() { - return getSurfaceView().getHolder().getSurface() != null; - } - /** * Enter or leave overlay video mode. * @param enabled Whether overlay mode is enabled. */ public void setOverlayVideoMode(boolean enabled) { int format = enabled ? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; - getSurfaceView().getHolder().setFormat(format); + mSurfaceBridge.setFormat(format); ContentViewRenderViewJni.get().setOverlayVideoMode( mNativeContentViewRenderView, ContentViewRenderView.this, enabled); } @CalledByNative private void didSwapFrame() { - if (getSurfaceView().getBackground() != null) { + SurfaceView surfaceView = mSurfaceBridge.getSurfaceView(); + if (surfaceView == null) { + // In Window Surface mode, no child SurfaceView background to clear. + return; + } + + if (surfaceView.getBackground() != null) { post(new Runnable() { @Override public void run() { - getSurfaceView().setBackgroundResource(0); + surfaceView.setBackgroundResource(0); } }); } } /** - * Connecting class to hold a SurfaceView. + * Connecting class to hold a surface management strategy. */ - protected static class SurfaceBridge { + protected abstract static class SurfaceBridge { + protected abstract void initialize(ContentViewRenderView renderView); + protected abstract void connect(SurfaceHolder.Callback surfaceCallback, WindowAndroid windowAndroid); + protected abstract void disconnect(); + protected abstract SurfaceView getSurfaceView(); + protected abstract void setFormat(int format); + } + + /** + * SurfaceBridge implementation that uses a standard SurfaceView. + * This is used for the default rendering path where a child SurfaceView is embedded + * within the ContentViewRenderView. + * + * Lifetime: Bound to the lifetime of the outer ContentViewRenderView. + * Threading: Must be called on the UI thread. + */ + protected static class SurfaceViewBridge extends SurfaceBridge { private SurfaceView mSurfaceView; private SurfaceHolder.Callback mSurfaceCallback; + @Override protected SurfaceView getSurfaceView() { return mSurfaceView; } + @Override + protected void setFormat(int format) { + if (mSurfaceView == null) { + return; + } + mSurfaceView.getHolder().setFormat(format); + } + + @Override protected void initialize(ContentViewRenderView renderView) { mSurfaceView = renderView.createSurfaceView(renderView.getContext()); mSurfaceView.setZOrderMediaOverlay(true); @@ -246,17 +276,113 @@ protected void initialize(ContentViewRenderView renderView) { mSurfaceView.setVisibility(GONE); } - protected void connect(SurfaceHolder.Callback surfaceCallback) { + @Override + protected void connect(SurfaceHolder.Callback surfaceCallback, WindowAndroid windowAndroid) { mSurfaceCallback = surfaceCallback; mSurfaceView.getHolder().addCallback(mSurfaceCallback); mSurfaceView.setVisibility(VISIBLE); } + @Override protected void disconnect() { mSurfaceView.getHolder().removeCallback(mSurfaceCallback); } } + /** + * SurfaceBridge implementation that takes ownership of the Activity's Window surface. + * This allows direct rendering to the window surface instead of a child SurfaceView. + * + * Lifetime: Bound to the lifetime of the outer ContentViewRenderView and the associated Activity. + * Threading: Must be called on the UI thread. + */ + protected static class WindowSurfaceBridge extends SurfaceBridge { + private Window mWindow; + private SurfaceHolder mWindowSurfaceHolder; + private Integer mSurfaceFormat; + + private static Window getWindow(WindowAndroid windowAndroid) { + if (windowAndroid == null || windowAndroid.getActivity() == null) { + return null; + } + Activity activity = windowAndroid.getActivity().get(); + return activity != null ? activity.getWindow() : null; + } + + @Override + protected void initialize(ContentViewRenderView renderView) {} + + @Override + protected void connect( + SurfaceHolder.Callback surfaceCallback, WindowAndroid windowAndroid) { + mWindow = getWindow(windowAndroid); + if (mWindow == null) { + Log.w(TAG, "ContentViewRenderView: WindowSurfaceBridge connect failed: Activity or Window is null."); + return; + } + + mWindow.takeSurface(new SurfaceHolder.Callback2() { + @Override + public void surfaceCreated(SurfaceHolder holder) { + mWindowSurfaceHolder = holder; + if (mSurfaceFormat != null) { + Log.i(TAG, "ContentViewRenderView: Applying pending format"); + mWindowSurfaceHolder.setFormat(mSurfaceFormat); + } + surfaceCallback.surfaceCreated(holder); + } + + @Override + public void surfaceChanged( + SurfaceHolder holder, int format, int width, int height) { + mWindowSurfaceHolder = holder; + surfaceCallback.surfaceChanged(holder, format, width, height); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mWindowSurfaceHolder = null; + surfaceCallback.surfaceDestroyed(holder); + } + + @Override + public void surfaceRedrawNeeded(SurfaceHolder holder) { + if (!(surfaceCallback instanceof SurfaceHolder.Callback2)) { + return; + } + ((SurfaceHolder.Callback2) surfaceCallback).surfaceRedrawNeeded(holder); + } + }); + } + + @Override + protected void disconnect() { + if (mWindow != null) { + mWindow.takeSurface(null); + } else { + Log.w(TAG, "ContentViewRenderView: disconnect() is called w/o connect()."); + } + mWindow = null; + mWindowSurfaceHolder = null; + mSurfaceFormat = null; + } + + @Override + protected SurfaceView getSurfaceView() { + return null; + } + + @Override + protected void setFormat(int format) { + mSurfaceFormat = format; + if (mWindowSurfaceHolder == null) { + Log.i(TAG, "ContentViewRenderView: surface is not ready yet. Will apply format later"); + return; + } + mWindowSurfaceHolder.setFormat(format); + } + } + private EventForwarder getEventForwarder() { if (mWebContents == null || mWebContents.isDestroyed()) { return null; From 46306c023032e082eaf6fcb2125f61117490bb38 Mon Sep 17 00:00:00 2001 From: Kyujung Youn Date: Tue, 28 Jul 2026 09:15:45 -0700 Subject: [PATCH 2/2] Address comments --- .../dev/cobalt/shell/ContentViewRenderView.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cobalt/shell/android/java/src/dev/cobalt/shell/ContentViewRenderView.java b/cobalt/shell/android/java/src/dev/cobalt/shell/ContentViewRenderView.java index 6f8d5f5dd276..86696c62a216 100644 --- a/cobalt/shell/android/java/src/dev/cobalt/shell/ContentViewRenderView.java +++ b/cobalt/shell/android/java/src/dev/cobalt/shell/ContentViewRenderView.java @@ -299,7 +299,13 @@ protected void disconnect() { protected static class WindowSurfaceBridge extends SurfaceBridge { private Window mWindow; private SurfaceHolder mWindowSurfaceHolder; - private Integer mSurfaceFormat; + + /** + * The last requested PixelFormat (e.g. TRANSLUCENT for overlay video mode, OPAQUE for normal). + * Preserved across surface destruction and recreation so newly created SurfaceHolders + * automatically inherit the desired format. + */ + private Integer mRequestedSurfaceFormat; private static Window getWindow(WindowAndroid windowAndroid) { if (windowAndroid == null || windowAndroid.getActivity() == null) { @@ -325,9 +331,9 @@ protected void connect( @Override public void surfaceCreated(SurfaceHolder holder) { mWindowSurfaceHolder = holder; - if (mSurfaceFormat != null) { + if (mRequestedSurfaceFormat != null) { Log.i(TAG, "ContentViewRenderView: Applying pending format"); - mWindowSurfaceHolder.setFormat(mSurfaceFormat); + mWindowSurfaceHolder.setFormat(mRequestedSurfaceFormat); } surfaceCallback.surfaceCreated(holder); } @@ -364,7 +370,7 @@ protected void disconnect() { } mWindow = null; mWindowSurfaceHolder = null; - mSurfaceFormat = null; + mRequestedSurfaceFormat = null; } @Override @@ -374,7 +380,7 @@ protected SurfaceView getSurfaceView() { @Override protected void setFormat(int format) { - mSurfaceFormat = format; + mRequestedSurfaceFormat = format; if (mWindowSurfaceHolder == null) { Log.i(TAG, "ContentViewRenderView: surface is not ready yet. Will apply format later"); return;