• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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