From 63fe0c779edbba466c0b108268dbd7ca48e96ae9 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Fri, 18 Apr 2025 13:09:30 -0700 Subject: [PATCH] Adding support for generating a preview bitmap with an optional delay to allow widgets to be rendered > Adding a completionSignal when preview is generated so that callers can wait on it > Also adding support for proving an optional screen-id for preview Bug: 400403115 Test: atest GridPreviewTest Flag: EXEMPT refactor and unused new API Change-Id: I569e0ab318ad101e4f6dd808f2e1cfc39ba50bda --- .../launcher3/uioverrides/SystemApiWrapper.kt | 15 ++++ .../launcher3/WorkspaceLayoutManager.java | 2 - .../graphics/GridCustomizationsProxy.java | 50 ++++++++++-- .../graphics/LauncherPreviewRenderer.java | 35 ++++++--- .../graphics/PreviewSurfaceRenderer.java | 77 +++++++++++-------- .../android/launcher3/util/ApiWrapper.java | 8 ++ 6 files changed, 135 insertions(+), 52 deletions(-) diff --git a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt index 8bdf4943b9..8ca603b11a 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt +++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt @@ -26,14 +26,18 @@ import android.content.pm.ActivityInfo import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherApps import android.content.pm.ShortcutInfo +import android.graphics.Bitmap +import android.graphics.Rect import android.os.Bundle import android.os.Flags.allowPrivateProfile import android.os.IBinder import android.os.UserHandle import android.os.UserManager import android.util.ArrayMap +import android.view.SurfaceControlViewHost import android.widget.Toast import android.window.RemoteTransition +import android.window.ScreenCapture import com.android.launcher3.Flags.enablePrivateSpace import com.android.launcher3.Flags.privateSpaceSysAppsSeparation import com.android.launcher3.R @@ -193,4 +197,15 @@ open class SystemApiWrapper @Inject constructor(@ApplicationContext context: Con override fun isFileDrawable(shortcutInfo: ShortcutInfo) = shortcutInfo.hasIconFile() || shortcutInfo.hasIconUri() + + override fun captureSnapshot(host: SurfaceControlViewHost, width: Int, height: Int): Bitmap = + ScreenCapture.captureLayers( + ScreenCapture.LayerCaptureArgs.Builder(host.surfacePackage!!.surfaceControl) + .setSourceCrop(Rect(0, 0, width, height)) + .setAllowProtected(true) + .setHintForSeamlessTransition(true) + .build() + ) + .asBitmap() + .copy(Bitmap.Config.ARGB_8888, true) } diff --git a/src/com/android/launcher3/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java index f11a88fa10..9f57eb38e5 100644 --- a/src/com/android/launcher3/WorkspaceLayoutManager.java +++ b/src/com/android/launcher3/WorkspaceLayoutManager.java @@ -42,8 +42,6 @@ public interface WorkspaceLayoutManager { // The is the first screen. It is always present, even if its empty. int FIRST_SCREEN_ID = 0; - // This is the second page. On two panel home it is always present, even if its empty. - int SECOND_SCREEN_ID = 1; /** * At bind time, we use the rank (screenId) to compute x and y for hotseat items. diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProxy.java b/src/com/android/launcher3/graphics/GridCustomizationsProxy.java index f66601807c..0069d5fbf5 100644 --- a/src/com/android/launcher3/graphics/GridCustomizationsProxy.java +++ b/src/com/android/launcher3/graphics/GridCustomizationsProxy.java @@ -15,19 +15,25 @@ */ package com.android.launcher3.graphics; - import static com.android.launcher3.BuildConfig.IS_DEBUG_DEVICE; import static com.android.launcher3.Flags.enableLauncherIconShapes; +import static com.android.launcher3.graphics.PreviewSurfaceRenderer.KEY_BITMAP_GENERATION_DELAY_MS; +import static com.android.launcher3.graphics.PreviewSurfaceRenderer.KEY_VIEW_HEIGHT; +import static com.android.launcher3.graphics.PreviewSurfaceRenderer.KEY_VIEW_WIDTH; +import static com.android.launcher3.graphics.PreviewSurfaceRenderer.MIN_BITMAP_GENERATION_DELAY_MS; import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static java.util.Objects.requireNonNullElse; +import static java.util.concurrent.CompletableFuture.delayedExecutor; +import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Binder; import android.os.Bundle; @@ -50,6 +56,7 @@ import com.android.launcher3.dagger.LauncherAppSingleton; import com.android.launcher3.model.BgDataModel; import com.android.launcher3.shapes.IconShapeModel; import com.android.launcher3.shapes.ShapesProvider; +import com.android.launcher3.util.ApiWrapper; import com.android.launcher3.util.ContentProviderProxy.ProxyProvider; import com.android.launcher3.util.DaggerSingletonTracker; import com.android.launcher3.util.Executors; @@ -118,6 +125,7 @@ public class GridCustomizationsProxy implements ProxyProvider { private static final String SET_SHAPE = "/shape"; private static final String METHOD_GET_PREVIEW = "get_preview"; + public static final String METHOD_GET_PREVIEW_BITMAP = "get_preview_bitmap"; private static final String GET_ICON_THEMED = "/get_icon_themed"; private static final String SET_ICON_THEMED = "/set_icon_themed"; @@ -128,6 +136,7 @@ public class GridCustomizationsProxy implements ProxyProvider { private static final String KEY_CALLBACK = "callback"; public static final String KEY_HIDE_BOTTOM_ROW = "hide_bottom_row"; public static final String KEY_GRID_NAME = "grid_name"; + public static final String KEY_IMAGE = "image"; private static final int MESSAGE_ID_UPDATE_PREVIEW = 1337; private static final int MESSAGE_ID_UPDATE_SHAPE = 2586; @@ -309,18 +318,45 @@ public class GridCustomizationsProxy implements ProxyProvider { @Override public Bundle call(@NonNull String method, String arg, Bundle extras) { - if (METHOD_GET_PREVIEW.equals(method)) { - return getPreview(extras); - } else { - return null; + return switch (method) { + case METHOD_GET_PREVIEW -> getPreview(extras); + case METHOD_GET_PREVIEW_BITMAP -> getPreviewBitmap(extras); + default -> null; + }; + } + + private Bundle getPreviewBitmap(Bundle request) { + RunnableList lifeCycleTracker = new RunnableList(); + try { + int width = request.getInt(KEY_VIEW_WIDTH); + int height = request.getInt(KEY_VIEW_HEIGHT); + long previewDelay = Math.max(request.getLong(KEY_BITMAP_GENERATION_DELAY_MS, 0), + MIN_BITMAP_GENERATION_DELAY_MS); + + PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer( + mContext, lifeCycleTracker, request, Binder.getCallingPid(), + true /* skip animations */); + renderer.loadAsync().thenRunAsync( + () -> { }, delayedExecutor(previewDelay, MILLISECONDS, MAIN_EXECUTOR)).get(); + Bitmap previewBitmap = ApiWrapper.INSTANCE.get(mContext) + .captureSnapshot(renderer.getHost(), width, height); + + Bundle result = new Bundle(); + result.putParcelable(KEY_IMAGE, previewBitmap); + return result; + } catch (Exception e) { + Log.e(TAG, "Unable to generate preview", e); } + MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy); + return null; } private synchronized Bundle getPreview(Bundle request) { RunnableList lifeCycleTracker = new RunnableList(); try { PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer( - mContext, lifeCycleTracker, request, Binder.getCallingPid()); + mContext, lifeCycleTracker, request, Binder.getCallingPid(), + false /* skip animations */); PreviewLifecycleObserver observer = new PreviewLifecycleObserver(lifeCycleTracker, renderer); @@ -333,7 +369,7 @@ public class GridCustomizationsProxy implements ProxyProvider { renderer.getHostToken().linkToDeath(observer, 0); Bundle result = new Bundle(); - result.putParcelable(KEY_SURFACE_PACKAGE, renderer.getSurfacePackage()); + result.putParcelable(KEY_SURFACE_PACKAGE, renderer.getHost().getSurfacePackage()); mActivePreviews.add(observer); lifeCycleTracker.add(() -> mActivePreviews.remove(observer)); diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java index cf98a92bbe..74749f02a6 100644 --- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java @@ -68,7 +68,6 @@ import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.ProxyPrefs; import com.android.launcher3.R; -import com.android.launcher3.Workspace; import com.android.launcher3.WorkspaceLayoutManager; import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.celllayout.CellPosMapper; @@ -165,7 +164,11 @@ public class LauncherPreviewRenderer extends BaseContext emptyDbDir(); mDbDir.mkdirs(); builder.bindParserFactory(new XmlLayoutParserFactory(this, layoutXml)) - .bindWidgetsFactory(c -> new LauncherWidgetHolder(c, widgetHostId)); + .bindWidgetsFactory(c -> { + LauncherWidgetHolder holder = new LauncherWidgetHolder(c, widgetHostId); + holder.startListening(); + return holder; + }); } initDaggerComponent(builder); @@ -212,14 +215,17 @@ public class LauncherPreviewRenderer extends BaseContext public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp, WallpaperColors wallpaperColorsOverride, + int workspaceScreenId, @Nullable final SparseArray launcherWidgetSpanInfo) { - this(context, idp, null, wallpaperColorsOverride, launcherWidgetSpanInfo); + this(context, idp, null, wallpaperColorsOverride, workspaceScreenId, + launcherWidgetSpanInfo); } public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp, SparseIntArray previewColorOverride, WallpaperColors wallpaperColorsOverride, + int workspaceScreenId, @Nullable final SparseArray launcherWidgetSpanInfo) { super(context, Themes.getActivityThemeRes(context)); @@ -262,7 +268,6 @@ public class LauncherPreviewRenderer extends BaseContext : (mDp.workspacePadding.right + mDp.cellLayoutPaddingPx.right), mDp.workspacePadding.bottom + mDp.cellLayoutPaddingPx.bottom ); - mWorkspaceScreens.put(FIRST_SCREEN_ID, firstScreen); if (mDp.isTwoPanels) { CellLayout rightPanel = mRootView.findViewById(R.id.workspace_right); @@ -272,7 +277,12 @@ public class LauncherPreviewRenderer extends BaseContext mDp.workspacePadding.right + mDp.cellLayoutPaddingPx.right, mDp.workspacePadding.bottom + mDp.cellLayoutPaddingPx.bottom ); - mWorkspaceScreens.put(Workspace.SECOND_SCREEN_ID, rightPanel); + + int closestEvenPageId = workspaceScreenId - (workspaceScreenId % 2); + mWorkspaceScreens.put(closestEvenPageId, firstScreen); + mWorkspaceScreens.put(closestEvenPageId + 1, rightPanel); + } else { + mWorkspaceScreens.put(workspaceScreenId, firstScreen); } SparseIntArray wallpaperColorResources; @@ -462,12 +472,15 @@ public class LauncherPreviewRenderer extends BaseContext // Add first page QSB if (BuildConfig.QSB_ON_FIRST_SCREEN) { CellLayout firstScreen = mWorkspaceScreens.get(FIRST_SCREEN_ID); - View qsb = mHomeElementInflater.inflate(R.layout.qsb_preview, firstScreen, false); - // TODO: set bgHandler on qsb when it is BaseTemplateCard, which requires API changes. - CellLayoutLayoutParams lp = new CellLayoutLayoutParams( - 0, 0, firstScreen.getCountX(), 1); - lp.canReorder = false; - firstScreen.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true); + if (firstScreen != null) { + View qsb = mHomeElementInflater.inflate(R.layout.qsb_preview, firstScreen, false); + // TODO: set bgHandler on qsb when it is BaseTemplateCard, which requires API + // changes. + CellLayoutLayoutParams lp = new CellLayoutLayoutParams( + 0, 0, firstScreen.getCountX(), 1); + lp.canReorder = false; + firstScreen.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true); + } } measureView(mRootView, mDp.widthPx, mDp.heightPx); diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java index 59a3ec0d85..9dbbb6d4e2 100644 --- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java +++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java @@ -24,7 +24,6 @@ import static com.android.launcher3.Flags.extendibleThemeManager; import static com.android.launcher3.LauncherPrefs.GRID_NAME; import static com.android.launcher3.LauncherPrefs.NON_FIXED_LANDSCAPE_GRID_NAME; import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID; -import static com.android.launcher3.WorkspaceLayoutManager.SECOND_SCREEN_ID; import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE; import static com.android.launcher3.graphics.ThemeManager.THEMED_ICONS; import static com.android.launcher3.provider.LauncherDbUtils.selectionForWorkspaceScreen; @@ -48,7 +47,6 @@ import android.util.SparseIntArray; import android.view.ContextThemeWrapper; import android.view.Display; import android.view.SurfaceControlViewHost; -import android.view.SurfaceControlViewHost.SurfacePackage; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; @@ -77,6 +75,7 @@ import com.android.launcher3.widget.LocalColorExtractor; import com.android.systemui.shared.Flags; import java.util.Objects; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; /** Render preview using surface view. */ @@ -84,17 +83,21 @@ import java.util.concurrent.TimeUnit; public class PreviewSurfaceRenderer { private static final String TAG = "PreviewSurfaceRenderer"; - private static final int FADE_IN_ANIMATION_DURATION = 200; - private static final String KEY_HOST_TOKEN = "host_token"; - private static final String KEY_VIEW_WIDTH = "width"; - private static final String KEY_VIEW_HEIGHT = "height"; - private static final String KEY_DISPLAY_ID = "display_id"; - private static final String KEY_COLORS = "wallpaper_colors"; - private static final String KEY_COLOR_RESOURCE_IDS = "color_resource_ids"; - private static final String KEY_COLOR_VALUES = "color_values"; - private static final String KEY_DARK_MODE = "use_dark_mode"; - private static final String KEY_LAYOUT_XML = "layout_xml"; - public static final String KEY_SKIP_ANIMATIONS = "skip_animations"; + public static final int FADE_IN_ANIMATION_DURATION = 200; + public static final String KEY_HOST_TOKEN = "host_token"; + public static final String KEY_VIEW_WIDTH = "width"; + public static final String KEY_VIEW_HEIGHT = "height"; + public static final String KEY_DISPLAY_ID = "display_id"; + public static final String KEY_COLORS = "wallpaper_colors"; + public static final String KEY_COLOR_RESOURCE_IDS = "color_resource_ids"; + public static final String KEY_COLOR_VALUES = "color_values"; + public static final String KEY_DARK_MODE = "use_dark_mode"; + public static final String KEY_LAYOUT_XML = "layout_xml"; + public static final String KEY_BITMAP_GENERATION_DELAY_MS = "bitmap_delay_ms"; + // Wait for some time before capturing screenshot to allow the surface to be laid out + public static final long MIN_BITMAP_GENERATION_DELAY_MS = 100L; + + public static final String KEY_WORKSPACE_PAGE_ID = "workspace_page_id"; public static final String FIXED_LANDSCAPE_GRID = "fixed_landscape_mode"; private final Context mContext; @@ -103,6 +106,7 @@ public class PreviewSurfaceRenderer { private String mLastSelectedGridName; private String mShapeKey; private String mLayoutXml; + private int mWorkspacePageId; private boolean mIsMonoThemeEnabled; @Nullable private Boolean mDarkMode; @@ -127,7 +131,7 @@ public class PreviewSurfaceRenderer { @Nullable private SurfaceControlViewHost.SurfacePackage mSurfacePackage; public PreviewSurfaceRenderer(Context context, RunnableList lifecycleTracker, Bundle bundle, - int callingPid) throws Exception { + int callingPid, boolean skipAnimations) throws Exception { mContext = context; mLifeCycleTracker = lifecycleTracker; mCallingPid = callingPid; @@ -151,11 +155,12 @@ public class PreviewSurfaceRenderer { mHostToken = bundle.getBinder(KEY_HOST_TOKEN); mWidth = bundle.getInt(KEY_VIEW_WIDTH); mHeight = bundle.getInt(KEY_VIEW_HEIGHT); - mSkipAnimations = bundle.getBoolean(KEY_SKIP_ANIMATIONS, false); + mSkipAnimations = skipAnimations; mDisplayId = bundle.getInt(KEY_DISPLAY_ID); mDisplay = context.getSystemService(DisplayManager.class) .getDisplay(mDisplayId); mLayoutXml = bundle.getString(KEY_LAYOUT_XML); + mWorkspacePageId = bundle.getInt(KEY_WORKSPACE_PAGE_ID, FIRST_SCREEN_ID); if (mDisplay == null) { throw new IllegalArgumentException("Display ID does not match any displays."); } @@ -177,11 +182,8 @@ public class PreviewSurfaceRenderer { return mHostToken; } - public SurfacePackage getSurfacePackage() { - if (mSurfacePackage == null) { - mSurfacePackage = mSurfaceControlViewHost.getSurfacePackage(); - } - return mSurfacePackage; + public SurfaceControlViewHost getHost() { + return mSurfaceControlViewHost; } private void setCurrentRenderer(LauncherPreviewRenderer renderer) { @@ -241,10 +243,12 @@ public class PreviewSurfaceRenderer { } /** - * Generates the preview in background + * Generates the preview in background and returns the generated view */ - public void loadAsync() { - MODEL_EXECUTOR.execute(this::loadModelData); + public CompletableFuture loadAsync() { + CompletableFuture result = new CompletableFuture<>(); + MODEL_EXECUTOR.execute(() -> loadModelData(result)); + return result; } /** @@ -379,7 +383,7 @@ public class PreviewSurfaceRenderer { } @WorkerThread - private void loadModelData() { + private void loadModelData(CompletableFuture onCompleteCallback) { final Context inflationContext = getPreviewContext(); if (shouldReloadModelData()) { boolean isCustomLayout = extendibleThemeManager() && !TextUtils.isEmpty(mLayoutXml); @@ -409,21 +413,26 @@ public class PreviewSurfaceRenderer { InvariantDeviceProfile idp = appComponent.getIDP(); DeviceProfile deviceProfile = idp.getDeviceProfile(previewContext); + + int closestEvenPageId = mWorkspacePageId - (mWorkspacePageId % 2); String query = deviceProfile.isTwoPanels - ? selectionForWorkspaceScreen(FIRST_SCREEN_ID, SECOND_SCREEN_ID) - : selectionForWorkspaceScreen(FIRST_SCREEN_ID); + ? selectionForWorkspaceScreen(closestEvenPageId, closestEvenPageId + 1) + : selectionForWorkspaceScreen(mWorkspacePageId); task.loadWorkspaceForPreview(query); final SparseArray spanInfo = getLoadedLauncherWidgetInfo(); MAIN_EXECUTOR.execute(() -> { - renderView(previewContext, appComponent.getDataModel(), spanInfo, idp); + renderView(previewContext, appComponent.getDataModel(), spanInfo, idp, + onCompleteCallback); mLifeCycleTracker.add(previewContext::onDestroy); }); } else { LauncherAppState.getInstance(inflationContext).getModel().loadAsync(dataModel -> { if (dataModel != null) { MAIN_EXECUTOR.execute(() -> renderView(inflationContext, dataModel, - null, LauncherAppState.getIDP(inflationContext))); + null, LauncherAppState.getIDP(inflationContext), onCompleteCallback)); } else { + onCompleteCallback.completeExceptionally( + new RuntimeException("Model loading failed")); Log.e(TAG, "Model loading failed"); } }); @@ -433,17 +442,19 @@ public class PreviewSurfaceRenderer { @UiThread private void renderView(Context inflationContext, BgDataModel dataModel, - @Nullable final SparseArray launcherWidgetSpanInfo, InvariantDeviceProfile idp) { + @Nullable final SparseArray launcherWidgetSpanInfo, InvariantDeviceProfile idp, + CompletableFuture onCompleteCallback) { if (mDestroyed) { + onCompleteCallback.completeExceptionally(new RuntimeException("Renderer destroyed")); return; } LauncherPreviewRenderer renderer; if (Flags.newCustomizationPickerUi()) { - renderer = new LauncherPreviewRenderer(inflationContext, idp, - mPreviewColorOverride, mWallpaperColors, launcherWidgetSpanInfo); + renderer = new LauncherPreviewRenderer(inflationContext, idp, mPreviewColorOverride, + mWallpaperColors, mWorkspacePageId, launcherWidgetSpanInfo); } else { renderer = new LauncherPreviewRenderer(inflationContext, idp, - mWallpaperColors, launcherWidgetSpanInfo); + mWallpaperColors, mWorkspacePageId, launcherWidgetSpanInfo); } renderer.hideBottomRow(mHideQsb); renderer.populate(dataModel); @@ -472,6 +483,7 @@ public class PreviewSurfaceRenderer { view.getMeasuredWidth(), view.getMeasuredHeight() ); + onCompleteCallback.complete(view); return; } @@ -497,6 +509,7 @@ public class PreviewSurfaceRenderer { mViewRoot.removeAllViews(); mViewRoot.addView(view); } + onCompleteCallback.complete(view); } private static class MySurfaceControlViewHost extends SurfaceControlViewHost { diff --git a/src/com/android/launcher3/util/ApiWrapper.java b/src/com/android/launcher3/util/ApiWrapper.java index 9d7dfdeb7b..53cae67a29 100644 --- a/src/com/android/launcher3/util/ApiWrapper.java +++ b/src/com/android/launcher3/util/ApiWrapper.java @@ -25,12 +25,14 @@ import android.content.Context; import android.content.Intent; import android.content.pm.LauncherActivityInfo; import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.util.ArrayMap; +import android.view.SurfaceControlViewHost; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -41,6 +43,7 @@ import com.android.launcher3.Utilities; import com.android.launcher3.dagger.ApplicationContext; import com.android.launcher3.dagger.LauncherAppComponent; import com.android.launcher3.dagger.LauncherAppSingleton; +import com.android.launcher3.icons.BitmapRenderer; import java.util.Collections; import java.util.List; @@ -207,6 +210,11 @@ public class ApiWrapper { return false; } + /** Captures a snapshot of the host content as a bitmap */ + public Bitmap captureSnapshot(SurfaceControlViewHost host, int width, int height) { + return BitmapRenderer.createHardwareBitmap(width, height, host.getView()::draw); + } + private static class NoopDrawable extends ColorDrawable { @Override public int getIntrinsicHeight() {