1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.P; 4 import static android.os.Build.VERSION_CODES.Q; 5 6 import android.content.Context; 7 import android.content.res.Resources; 8 import android.content.res.TypedArray; 9 import android.graphics.Bitmap; 10 import android.graphics.Canvas; 11 import android.graphics.HardwareRenderer; 12 import android.graphics.PixelFormat; 13 import android.graphics.RenderNode; 14 import android.media.Image; 15 import android.media.Image.Plane; 16 import android.media.ImageReader; 17 import android.util.DisplayMetrics; 18 import android.view.Surface; 19 import android.view.View; 20 import android.view.ViewRootImpl; 21 import com.android.internal.R; 22 import com.google.common.base.Preconditions; 23 import java.util.WeakHashMap; 24 import org.robolectric.RuntimeEnvironment; 25 import org.robolectric.annotation.GraphicsMode; 26 import org.robolectric.util.ReflectionHelpers; 27 28 /** 29 * Helper class to provide hardware rendering-based screenshot to {@link ShadowPixelCopy} and {@link 30 * ShadowUiAutomation}. 31 */ 32 public final class HardwareRenderingScreenshot { 33 34 // It is important to reuse HardwareRenderer objects, and ensure that after a HardwareRenderer is 35 // collected, no associated views in the same View hierarchy will be rendered as well. 36 private static final WeakHashMap<ViewRootImpl, HardwareRenderer> hardwareRenderers = 37 new WeakHashMap<>(); 38 39 static final String PIXEL_COPY_RENDER_MODE = "robolectric.pixelCopyRenderMode"; 40 HardwareRenderingScreenshot()41 private HardwareRenderingScreenshot() {} 42 43 /** 44 * Indicates whether {@link #takeScreenshot(View, Bitmap)} can run, by validating the API level, 45 * the value of the {@link #PIXEL_COPY_RENDER_MODE} property, and the {@link GraphicsMode}. 46 */ canTakeScreenshot(View view)47 static boolean canTakeScreenshot(View view) { 48 return RuntimeEnvironment.getApiLevel() >= P 49 && "hardware".equalsIgnoreCase(System.getProperty(PIXEL_COPY_RENDER_MODE, "hardware")) 50 && ShadowView.useRealGraphics() 51 && view.canHaveDisplayList(); 52 } 53 54 /** 55 * Generates a bitmap given the current view using hardware accelerated canvases with native 56 * graphics calls. Requires API 28+ (S). 57 * 58 * <p>This code mirrors the behavior of LayoutLib's RenderSessionImpl.renderAndBuildResult(); see 59 * https://googleplex-android.googlesource.com/platform/frameworks/layoutlib/+/refs/heads/master-layoutlib-native/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java#573 60 */ takeScreenshot(View view, Bitmap destBitmap)61 static void takeScreenshot(View view, Bitmap destBitmap) { 62 int width = view.getWidth(); 63 int height = view.getHeight(); 64 65 try (ImageReader imageReader = 66 ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)) { 67 ViewRootImpl viewRootImpl = view.getViewRootImpl(); 68 Preconditions.checkNotNull(viewRootImpl, "View not attached"); 69 Surface surface = imageReader.getSurface(); 70 71 if (RuntimeEnvironment.getApiLevel() >= Q) { 72 // HardwareRenderer is only available on API 29+ (Q). 73 HardwareRenderer renderer = 74 hardwareRenderers.computeIfAbsent(viewRootImpl, k -> new HardwareRenderer()); 75 renderer.setSurface(surface); 76 setupRendererShadowProperties(renderer, view); 77 RenderNode node = getRenderNode(view); 78 renderer.setContentRoot(node); 79 renderer.createRenderRequest().syncAndDraw(); 80 } else { 81 // Note this API does not set any light source properties, so it will not render 82 // drop shadows. 83 Canvas canvas = surface.lockHardwareCanvas(); 84 view.draw(canvas); 85 surface.unlockCanvasAndPost(canvas); 86 } 87 Image nativeImage = imageReader.acquireNextImage(); 88 Plane[] planes = nativeImage.getPlanes(); 89 destBitmap.copyPixelsFromBuffer(planes[0].getBuffer()); 90 surface.release(); 91 } 92 } 93 getRenderNode(View view)94 private static RenderNode getRenderNode(View view) { 95 return ReflectionHelpers.callInstanceMethod(view, "updateDisplayListIfDirty"); 96 } 97 setupRendererShadowProperties(HardwareRenderer renderer, View view)98 private static void setupRendererShadowProperties(HardwareRenderer renderer, View view) { 99 Context context = view.getContext(); 100 Resources resources = context.getResources(); 101 DisplayMetrics displayMetrics = resources.getDisplayMetrics(); 102 103 // Get the LightSourceGeometry and LightSourceAlpha from resources. 104 // The default values are the ones recommended by the getLightSourceGeometry() and 105 // getLightSourceAlpha() documentation. 106 // This matches LayoutLib's RenderSessionImpl#renderAndBuildResult() implementation. 107 108 TypedArray a = context.obtainStyledAttributes(null, R.styleable.Lighting, 0, 0); 109 float lightX = displayMetrics.widthPixels / 2f; 110 float lightY = a.getDimension(R.styleable.Lighting_lightY, 0f); 111 float lightZ = a.getDimension(R.styleable.Lighting_lightZ, 600f * displayMetrics.density); 112 float lightRadius = 113 a.getDimension(R.styleable.Lighting_lightRadius, 800f * displayMetrics.density); 114 float ambientShadowAlpha = a.getFloat(R.styleable.Lighting_ambientShadowAlpha, 0.039f); 115 float spotShadowAlpha = a.getFloat(R.styleable.Lighting_spotShadowAlpha, 0.19f); 116 a.recycle(); 117 118 renderer.setLightSourceGeometry(lightX, lightY, lightZ, lightRadius); 119 renderer.setLightSourceAlpha(ambientShadowAlpha, spotShadowAlpha); 120 } 121 } 122