1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.O; 4 import static com.google.common.base.Preconditions.checkNotNull; 5 import static org.robolectric.util.reflector.Reflector.reflector; 6 7 import android.annotation.NonNull; 8 import android.annotation.Nullable; 9 import android.graphics.Bitmap; 10 import android.graphics.Canvas; 11 import android.graphics.Paint; 12 import android.graphics.Rect; 13 import android.os.Handler; 14 import android.os.Looper; 15 import android.view.PixelCopy; 16 import android.view.PixelCopy.OnPixelCopyFinishedListener; 17 import android.view.Surface; 18 import android.view.SurfaceView; 19 import android.view.View; 20 import android.view.ViewRootImpl; 21 import android.view.Window; 22 import android.view.WindowManagerGlobal; 23 import java.util.concurrent.Executor; 24 import java.util.function.Consumer; 25 import org.robolectric.annotation.ClassName; 26 import org.robolectric.annotation.Implementation; 27 import org.robolectric.annotation.Implements; 28 import org.robolectric.shadow.api.Shadow; 29 import org.robolectric.shadows.ShadowWindowManagerGlobal.WindowManagerGlobalReflector; 30 import org.robolectric.util.PerfStatsCollector; 31 import org.robolectric.util.reflector.Accessor; 32 import org.robolectric.util.reflector.Constructor; 33 import org.robolectric.util.reflector.ForType; 34 import org.robolectric.util.reflector.Static; 35 import org.robolectric.versioning.AndroidVersions.U; 36 37 /** 38 * Shadow for PixelCopy that uses View.draw to create screenshots. The real PixelCopy performs a 39 * full hardware capture of the screen at the given location, which is impossible in Robolectric. 40 * 41 * <p>If listenerThread is backed by a paused looper, make sure to call ShadowLooper.idle() to 42 * ensure the screenshot finishes. 43 */ 44 @Implements(value = PixelCopy.class, minSdk = O) 45 public class ShadowPixelCopy { 46 47 @Implementation request( SurfaceView source, @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread)48 protected static void request( 49 SurfaceView source, 50 @NonNull Bitmap dest, 51 @NonNull OnPixelCopyFinishedListener listener, 52 @NonNull Handler listenerThread) { 53 takeScreenshot(source, dest, null); 54 alertFinished(listener, listenerThread, PixelCopy.SUCCESS); 55 } 56 57 @Implementation request( @onNull SurfaceView source, @Nullable Rect srcRect, @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread)58 protected static void request( 59 @NonNull SurfaceView source, 60 @Nullable Rect srcRect, 61 @NonNull Bitmap dest, 62 @NonNull OnPixelCopyFinishedListener listener, 63 @NonNull Handler listenerThread) { 64 if (srcRect != null && srcRect.isEmpty()) { 65 throw new IllegalArgumentException("sourceRect is empty"); 66 } 67 takeScreenshot(source, dest, srcRect); 68 alertFinished(listener, listenerThread, PixelCopy.SUCCESS); 69 } 70 71 @Implementation request( @onNull Window source, @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread)72 protected static void request( 73 @NonNull Window source, 74 @NonNull Bitmap dest, 75 @NonNull OnPixelCopyFinishedListener listener, 76 @NonNull Handler listenerThread) { 77 request(source, null, dest, listener, listenerThread); 78 } 79 80 @Implementation request( @onNull Window source, @Nullable Rect srcRect, @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread)81 protected static void request( 82 @NonNull Window source, 83 @Nullable Rect srcRect, 84 @NonNull Bitmap dest, 85 @NonNull OnPixelCopyFinishedListener listener, 86 @NonNull Handler listenerThread) { 87 if (srcRect != null && srcRect.isEmpty()) { 88 throw new IllegalArgumentException("sourceRect is empty"); 89 } 90 takeScreenshot(source.getDecorView(), dest, srcRect); 91 alertFinished(listener, listenerThread, PixelCopy.SUCCESS); 92 } 93 94 @Implementation request( @onNull Surface source, @Nullable Rect srcRect, @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread)95 protected static void request( 96 @NonNull Surface source, 97 @Nullable Rect srcRect, 98 @NonNull Bitmap dest, 99 @NonNull OnPixelCopyFinishedListener listener, 100 @NonNull Handler listenerThread) { 101 if (srcRect != null && srcRect.isEmpty()) { 102 throw new IllegalArgumentException("sourceRect is empty"); 103 } 104 105 View view = findViewForSurface(checkNotNull(source)); 106 Rect adjustedSrcRect = null; 107 if (srcRect != null) { 108 adjustedSrcRect = new Rect(srcRect); 109 int[] locationInSurface = ShadowView.getLocationInSurfaceCompat(view); 110 // offset the srcRect by the decor view's location in the surface 111 adjustedSrcRect.offset(-locationInSurface[0], -locationInSurface[1]); 112 } 113 takeScreenshot(view, dest, adjustedSrcRect); 114 alertFinished(listener, listenerThread, PixelCopy.SUCCESS); 115 } 116 117 @Implementation(minSdk = U.SDK_INT) request( @lassName"android.view.PixelCopy$Request") Object requestObject, Executor callbackExecutor, Consumer< ?> listener)118 protected static void request( 119 @ClassName("android.view.PixelCopy$Request") Object requestObject, 120 Executor callbackExecutor, 121 Consumer</*android.view.PixelCopy$Result*/ ?> listener) { 122 PixelCopy.Request request = (PixelCopy.Request) requestObject; 123 RequestReflector requestReflector = reflector(RequestReflector.class, request); 124 OnPixelCopyFinishedListener legacyListener = 125 new OnPixelCopyFinishedListener() { 126 @Override 127 public void onPixelCopyFinished(int copyResult) { 128 ((Consumer<PixelCopy.Result>) listener) 129 .accept( 130 reflector(ResultReflector.class) 131 .newResult(copyResult, request.getDestinationBitmap())); 132 } 133 }; 134 Rect adjustedSrcRect = 135 reflector(PixelCopyReflector.class) 136 .adjustSourceRectForInsets(requestReflector.getSourceInsets(), request.getSourceRect()); 137 PixelCopy.request( 138 requestReflector.getSource(), 139 adjustedSrcRect, 140 request.getDestinationBitmap(), 141 legacyListener, 142 new Handler(Looper.getMainLooper())); 143 } 144 findViewForSurface(Surface source)145 private static View findViewForSurface(Surface source) { 146 for (View windowView : 147 reflector(WindowManagerGlobalReflector.class, WindowManagerGlobal.getInstance()) 148 .getWindowViews()) { 149 ShadowViewRootImpl shadowViewRoot = Shadow.extract(windowView.getViewRootImpl()); 150 if (source.equals(shadowViewRoot.getSurface())) { 151 return windowView; 152 } 153 } 154 155 throw new IllegalArgumentException( 156 "Could not find view for surface. Is it attached to a window?"); 157 } 158 takeScreenshot(View view, Bitmap screenshot, @Nullable Rect srcRect)159 private static void takeScreenshot(View view, Bitmap screenshot, @Nullable Rect srcRect) { 160 validateBitmap(screenshot); 161 162 Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); 163 164 if (HardwareRenderingScreenshot.canTakeScreenshot(view)) { 165 PerfStatsCollector.getInstance() 166 .measure( 167 "ShadowPixelCopy-Hardware", 168 () -> HardwareRenderingScreenshot.takeScreenshot(view, bitmap)); 169 } else { 170 PerfStatsCollector.getInstance() 171 .measure( 172 "ShadowPixelCopy-Software", 173 () -> { 174 Canvas screenshotCanvas = new Canvas(bitmap); 175 view.draw(screenshotCanvas); 176 }); 177 } 178 179 Rect dst = new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight()); 180 181 Canvas resizingCanvas = new Canvas(screenshot); 182 Paint paint = new Paint(); 183 resizingCanvas.drawBitmap(bitmap, srcRect, dst, paint); 184 } 185 alertFinished( OnPixelCopyFinishedListener listener, Handler listenerThread, int result)186 private static void alertFinished( 187 OnPixelCopyFinishedListener listener, Handler listenerThread, int result) { 188 if (listenerThread.getLooper() == Looper.getMainLooper()) { 189 listener.onPixelCopyFinished(result); 190 return; 191 } 192 listenerThread.post(() -> listener.onPixelCopyFinished(result)); 193 } 194 validateBitmap(Bitmap bitmap)195 private static Bitmap validateBitmap(Bitmap bitmap) { 196 if (bitmap == null) { 197 throw new IllegalArgumentException("Bitmap cannot be null"); 198 } 199 if (bitmap.isRecycled()) { 200 throw new IllegalArgumentException("Bitmap is recycled"); 201 } 202 if (!bitmap.isMutable()) { 203 throw new IllegalArgumentException("Bitmap is immutable"); 204 } 205 return bitmap; 206 } 207 208 @Implements(value = PixelCopy.Request.Builder.class, minSdk = U.SDK_INT, isInAndroidSdk = false) 209 public static class ShadowPixelCopyRequestBuilder { 210 211 // TODO(brettchabot): remove once robolectric has proper support for initializing a Surface 212 // for now, this copies Android implementation and just omits the valid surface check 213 @Implementation ofWindow(View source)214 protected static PixelCopy.Request.Builder ofWindow(View source) { 215 if (source == null || !source.isAttachedToWindow()) { 216 throw new IllegalArgumentException("View must not be null & must be attached to window"); 217 } 218 final Rect insets = new Rect(); 219 Surface surface = null; 220 final ViewRootImpl root = source.getViewRootImpl(); 221 if (root != null) { 222 surface = root.mSurface; 223 insets.set(root.mWindowAttributes.surfaceInsets); 224 } 225 PixelCopy.Request request = reflector(RequestReflector.class).newRequest(surface, insets); 226 return reflector(BuilderReflector.class).newBuilder(request); 227 } 228 } 229 230 @ForType(PixelCopy.class) 231 private interface PixelCopyReflector { 232 @Static adjustSourceRectForInsets(Rect insets, Rect srcRect)233 Rect adjustSourceRectForInsets(Rect insets, Rect srcRect); 234 } 235 236 @ForType(PixelCopy.Request.Builder.class) 237 private interface BuilderReflector { 238 @Constructor newBuilder(PixelCopy.Request request)239 PixelCopy.Request.Builder newBuilder(PixelCopy.Request request); 240 } 241 242 @ForType(PixelCopy.Request.class) 243 private interface RequestReflector { 244 @Constructor newRequest(Surface surface, Rect insets)245 PixelCopy.Request newRequest(Surface surface, Rect insets); 246 247 @Accessor("mSource") getSource()248 Surface getSource(); 249 250 @Accessor("mSourceInsets") getSourceInsets()251 Rect getSourceInsets(); 252 } 253 254 @ForType(PixelCopy.Result.class) 255 private interface ResultReflector { 256 @Constructor newResult(int copyResult, Bitmap bitmap)257 PixelCopy.Result newResult(int copyResult, Bitmap bitmap); 258 } 259 } 260