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.graphics.Bitmap; 8 import android.graphics.Canvas; 9 import android.graphics.Paint; 10 import android.graphics.Rect; 11 import android.os.Handler; 12 import android.os.Looper; 13 import android.view.PixelCopy; 14 import android.view.PixelCopy.OnPixelCopyFinishedListener; 15 import android.view.Surface; 16 import android.view.SurfaceView; 17 import android.view.View; 18 import android.view.Window; 19 import android.view.WindowManagerGlobal; 20 import androidx.annotation.NonNull; 21 import androidx.annotation.Nullable; 22 import org.robolectric.annotation.Implementation; 23 import org.robolectric.annotation.Implements; 24 import org.robolectric.shadow.api.Shadow; 25 import org.robolectric.shadows.ShadowWindowManagerGlobal.WindowManagerGlobalReflector; 26 27 /** 28 * Shadow for PixelCopy that uses View.draw to create screenshots. The real PixelCopy performs a 29 * full hardware capture of the screen at the given location, which is impossible in Robolectric. 30 * 31 * <p>If listenerThread is backed by a paused looper, make sure to call ShadowLooper.idle() to 32 * ensure the screenshot finishes. 33 */ 34 @Implements(value = PixelCopy.class, minSdk = O) 35 public class ShadowPixelCopy { 36 37 @Implementation request( SurfaceView source, @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread)38 protected static void request( 39 SurfaceView source, 40 @NonNull Bitmap dest, 41 @NonNull OnPixelCopyFinishedListener listener, 42 @NonNull Handler listenerThread) { 43 takeScreenshot(source, dest, null); 44 alertFinished(listener, listenerThread, PixelCopy.SUCCESS); 45 } 46 47 @Implementation request( @onNull SurfaceView source, @Nullable Rect srcRect, @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread)48 protected static void request( 49 @NonNull SurfaceView source, 50 @Nullable Rect srcRect, 51 @NonNull Bitmap dest, 52 @NonNull OnPixelCopyFinishedListener listener, 53 @NonNull Handler listenerThread) { 54 if (srcRect != null && srcRect.isEmpty()) { 55 throw new IllegalArgumentException("sourceRect is empty"); 56 } 57 takeScreenshot(source, dest, srcRect); 58 alertFinished(listener, listenerThread, PixelCopy.SUCCESS); 59 } 60 61 @Implementation request( @onNull Window source, @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread)62 protected static void request( 63 @NonNull Window source, 64 @NonNull Bitmap dest, 65 @NonNull OnPixelCopyFinishedListener listener, 66 @NonNull Handler listenerThread) { 67 request(source, null, dest, listener, listenerThread); 68 } 69 70 @Implementation request( @onNull Window source, @Nullable Rect srcRect, @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread)71 protected static void request( 72 @NonNull Window source, 73 @Nullable Rect srcRect, 74 @NonNull Bitmap dest, 75 @NonNull OnPixelCopyFinishedListener listener, 76 @NonNull Handler listenerThread) { 77 if (srcRect != null && srcRect.isEmpty()) { 78 throw new IllegalArgumentException("sourceRect is empty"); 79 } 80 View view = source.getDecorView(); 81 Rect adjustedSrcRect = null; 82 if (srcRect != null) { 83 adjustedSrcRect = new Rect(srcRect); 84 int[] locationInWindow = new int[2]; 85 view.getLocationInWindow(locationInWindow); 86 // offset the srcRect by the decor view's location in the window 87 adjustedSrcRect.offset(-locationInWindow[0], -locationInWindow[1]); 88 } 89 takeScreenshot(view, dest, adjustedSrcRect); 90 alertFinished(listener, listenerThread, PixelCopy.SUCCESS); 91 } 92 93 @Implementation request( @onNull Surface source, @Nullable Rect srcRect, @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread)94 protected static void request( 95 @NonNull Surface source, 96 @Nullable Rect srcRect, 97 @NonNull Bitmap dest, 98 @NonNull OnPixelCopyFinishedListener listener, 99 @NonNull Handler listenerThread) { 100 if (srcRect != null && srcRect.isEmpty()) { 101 throw new IllegalArgumentException("sourceRect is empty"); 102 } 103 104 View view = findViewForSurface(checkNotNull(source)); 105 Rect adjustedSrcRect = null; 106 if (srcRect != null) { 107 adjustedSrcRect = new Rect(srcRect); 108 int[] locationInSurface = ShadowView.getLocationInSurfaceCompat(view); 109 // offset the srcRect by the decor view's location in the surface 110 adjustedSrcRect.offset(-locationInSurface[0], -locationInSurface[1]); 111 } 112 takeScreenshot(view, dest, adjustedSrcRect); 113 alertFinished(listener, listenerThread, PixelCopy.SUCCESS); 114 } 115 findViewForSurface(Surface source)116 private static View findViewForSurface(Surface source) { 117 for (View windowView : 118 reflector(WindowManagerGlobalReflector.class, WindowManagerGlobal.getInstance()) 119 .getWindowViews()) { 120 ShadowViewRootImpl shadowViewRoot = Shadow.extract(windowView.getViewRootImpl()); 121 if (source.equals(shadowViewRoot.getSurface())) { 122 return windowView; 123 } 124 } 125 126 throw new IllegalArgumentException( 127 "Could not find view for surface. Is it attached to a window?"); 128 } 129 takeScreenshot(View view, Bitmap screenshot, @Nullable Rect srcRect)130 private static void takeScreenshot(View view, Bitmap screenshot, @Nullable Rect srcRect) { 131 validateBitmap(screenshot); 132 Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); 133 Canvas screenshotCanvas = new Canvas(bitmap); 134 view.draw(screenshotCanvas); 135 136 Rect dst = new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight()); 137 138 Canvas resizingCanvas = new Canvas(screenshot); 139 Paint paint = new Paint(); 140 resizingCanvas.drawBitmap(bitmap, srcRect, dst, paint); 141 } 142 alertFinished( OnPixelCopyFinishedListener listener, Handler listenerThread, int result)143 private static void alertFinished( 144 OnPixelCopyFinishedListener listener, Handler listenerThread, int result) { 145 if (listenerThread.getLooper() == Looper.getMainLooper()) { 146 listener.onPixelCopyFinished(result); 147 return; 148 } 149 listenerThread.post(() -> listener.onPixelCopyFinished(result)); 150 } 151 validateBitmap(Bitmap bitmap)152 private static Bitmap validateBitmap(Bitmap bitmap) { 153 if (bitmap == null) { 154 throw new IllegalArgumentException("Bitmap cannot be null"); 155 } 156 if (bitmap.isRecycled()) { 157 throw new IllegalArgumentException("Bitmap is recycled"); 158 } 159 if (!bitmap.isMutable()) { 160 throw new IllegalArgumentException("Bitmap is immutable"); 161 } 162 return bitmap; 163 } 164 } 165