xref: /aosp_15_r20/external/robolectric/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java (revision e6ba16074e6af37d123cb567d575f496bf0a58ee)
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