1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.view;
18 
19 import android.annotation.UiThread;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.pm.ActivityInfo;
23 import android.graphics.HardwareRenderer;
24 import android.graphics.HardwareRenderer.SyncAndDrawResult;
25 import android.graphics.RecordingCanvas;
26 import android.graphics.Rect;
27 import android.graphics.RenderNode;
28 import android.os.CancellationSignal;
29 import android.os.SystemClock;
30 import android.provider.Settings;
31 import android.util.DisplayMetrics;
32 import android.util.Log;
33 import android.view.Display.ColorMode;
34 import android.view.ScrollCaptureCallback;
35 import android.view.ScrollCaptureSession;
36 import android.view.Surface;
37 import android.view.View;
38 import android.view.ViewGroup;
39 
40 import com.android.internal.view.ScrollCaptureViewHelper.ScrollResult;
41 
42 import java.lang.ref.WeakReference;
43 import java.util.function.Consumer;
44 
45 /**
46  * Provides a base ScrollCaptureCallback implementation to handle arbitrary View-based scrolling
47  * containers. This class handles the bookkeeping aspects of {@link ScrollCaptureCallback}
48  * including rendering output using HWUI. Adaptable to any {@link View} using
49  * {@link ScrollCaptureViewHelper}.
50  *
51  * @param <V> the specific View subclass handled
52  * @see ScrollCaptureViewHelper
53  */
54 @UiThread
55 public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCallback {
56 
57     private static final String TAG = "SCViewSupport";
58 
59     private static final String SETTING_CAPTURE_DELAY = "screenshot.scroll_capture_delay";
60     private static final long SETTING_CAPTURE_DELAY_DEFAULT = 60L; // millis
61 
62     private final WeakReference<V> mWeakView;
63     private final ScrollCaptureViewHelper<V> mViewHelper;
64     private final ViewRenderer mRenderer;
65     private final long mPostScrollDelayMillis;
66 
67     private boolean mStarted;
68     private boolean mEnded;
69 
ScrollCaptureViewSupport(V containingView, ScrollCaptureViewHelper<V> viewHelper)70     ScrollCaptureViewSupport(V containingView, ScrollCaptureViewHelper<V> viewHelper) {
71         mWeakView = new WeakReference<>(containingView);
72         mRenderer = new ViewRenderer();
73         // TODO(b/177649144): provide access to color space from android.media.Image
74         mViewHelper = viewHelper;
75         Context context = containingView.getContext();
76         ContentResolver contentResolver = context.getContentResolver();
77         mPostScrollDelayMillis = Settings.Global.getLong(contentResolver,
78                 SETTING_CAPTURE_DELAY, SETTING_CAPTURE_DELAY_DEFAULT);
79         Log.d(TAG, "screenshot.scroll_capture_delay = " + mPostScrollDelayMillis);
80     }
81 
82     /** Based on ViewRootImpl#updateColorModeIfNeeded */
83     @ColorMode
getColorMode(View containingView)84     private static int getColorMode(View containingView) {
85         Context context = containingView.getContext();
86         int colorMode = containingView.getViewRootImpl().mWindowAttributes.getColorMode();
87         if (!context.getResources().getConfiguration().isScreenWideColorGamut()) {
88             colorMode = ActivityInfo.COLOR_MODE_DEFAULT;
89         }
90         return colorMode;
91     }
92 
93     /**
94      * Maps a rect in request bounds relative space  (relative to requestBounds) to container-local
95      * space, accounting for the provided value of scrollY.
96      *
97      * @param scrollY the current scroll offset to apply to rect
98      * @param requestBounds defines the local coordinate space of rect, within the container
99      * @param requestRect the rectangle to transform to container-local coordinates
100      * @return the same rectangle mapped to container bounds
101      */
transformFromRequestToContainer(int scrollY, Rect requestBounds, Rect requestRect)102     public static Rect transformFromRequestToContainer(int scrollY, Rect requestBounds,
103             Rect requestRect) {
104         Rect requestedContainerBounds = new Rect(requestRect);
105         requestedContainerBounds.offset(0, -scrollY);
106         requestedContainerBounds.offset(requestBounds.left, requestBounds.top);
107         return requestedContainerBounds;
108     }
109 
110     /**
111      * Maps a rect in container-local coordinate space to request space (relative to
112      * requestBounds), accounting for the provided value of scrollY.
113      *
114      * @param scrollY the current scroll offset of the container
115      * @param requestBounds defines the local coordinate space of rect, within the container
116      * @param containerRect the rectangle within the container local coordinate space
117      * @return the same rectangle mapped to within request bounds
118      */
transformFromContainerToRequest(int scrollY, Rect requestBounds, Rect containerRect)119     public static Rect transformFromContainerToRequest(int scrollY, Rect requestBounds,
120             Rect containerRect) {
121         Rect requestRect = new Rect(containerRect);
122         requestRect.offset(-requestBounds.left, -requestBounds.top);
123         requestRect.offset(0, scrollY);
124         return requestRect;
125     }
126 
127     /**
128      * Implements the core contract of requestRectangleOnScreen. Given a bounding rect and
129      * another rectangle, return the minimum scroll distance that will maximize the visible area
130      * of the requested rectangle.
131      *
132      * @param parentVisibleBounds the visible area
133      * @param requested the requested area
134      */
computeScrollAmount(Rect parentVisibleBounds, Rect requested)135     public static int computeScrollAmount(Rect parentVisibleBounds, Rect requested) {
136         final int height = parentVisibleBounds.height();
137         final int top = parentVisibleBounds.top;
138         final int bottom = parentVisibleBounds.bottom;
139         int scrollYDelta = 0;
140 
141         if (requested.bottom > bottom && requested.top > top) {
142             // need to scroll DOWN (move views up) to get it in view:
143             // move just enough so that the entire rectangle is in view
144             // (or at least the first screen size chunk).
145 
146             if (requested.height() > height) {
147                 // just enough to get screen size chunk on
148                 scrollYDelta += (requested.top - top);
149             } else {
150                 // entire rect at bottom
151                 scrollYDelta += (requested.bottom - bottom);
152             }
153         } else if (requested.top < top && requested.bottom < bottom) {
154             // need to scroll UP (move views down) to get it in view:
155             // move just enough so that entire rectangle is in view
156             // (or at least the first screen size chunk of it).
157 
158             if (requested.height() > height) {
159                 // screen size chunk
160                 scrollYDelta -= (bottom - requested.bottom);
161             } else {
162                 // entire rect at top
163                 scrollYDelta -= (top - requested.top);
164             }
165         }
166         return scrollYDelta;
167     }
168 
169     /**
170      * Locate a view to use as a reference, given an anticipated scrolling movement.
171      * <p>
172      * This view will be used to measure the actual movement of child views after scrolling.
173      * When scrolling down, the last (max(y)) view is used, otherwise the first (min(y)
174      * view. This helps to avoid recycling the reference view as a side effect of scrolling.
175      *
176      * @param parent the scrolling container
177      * @param expectedScrollDistance the amount of scrolling to perform
178      */
findScrollingReferenceView(ViewGroup parent, int expectedScrollDistance)179     public static View findScrollingReferenceView(ViewGroup parent, int expectedScrollDistance) {
180         View selected = null;
181         Rect parentLocalVisible = new Rect();
182         parent.getLocalVisibleRect(parentLocalVisible);
183 
184         final int childCount = parent.getChildCount();
185         for (int i = 0; i < childCount; i++) {
186             View child = parent.getChildAt(i);
187             if (selected == null) {
188                 selected = child;
189             } else if (expectedScrollDistance < 0) {
190                 if (child.getTop() < selected.getTop()) {
191                     selected = child;
192                 }
193             } else if (child.getBottom() > selected.getBottom()) {
194                 selected = child;
195             }
196         }
197         return selected;
198     }
199 
200     @Override
onScrollCaptureSearch(CancellationSignal signal, Consumer<Rect> onReady)201     public final void onScrollCaptureSearch(CancellationSignal signal, Consumer<Rect> onReady) {
202         if (signal.isCanceled()) {
203             return;
204         }
205         V view = mWeakView.get();
206         mStarted = false;
207         mEnded = false;
208 
209         if (view != null && view.isVisibleToUser() && mViewHelper.onAcceptSession(view)) {
210             onReady.accept(mViewHelper.onComputeScrollBounds(view));
211             return;
212         }
213         onReady.accept(null);
214     }
215 
216     @Override
onScrollCaptureStart(ScrollCaptureSession session, CancellationSignal signal, Runnable onReady)217     public final void onScrollCaptureStart(ScrollCaptureSession session, CancellationSignal signal,
218             Runnable onReady) {
219         if (signal.isCanceled()) {
220             return;
221         }
222         V view = mWeakView.get();
223 
224         mEnded = false;
225         mStarted = true;
226 
227         // Note: If somehow the view is already gone or detached, the first call to
228         // {@code onScrollCaptureImageRequest} will return an error and request the session to
229         // end.
230         if (view != null && view.isVisibleToUser()) {
231             mRenderer.setSurface(session.getSurface());
232             mViewHelper.onPrepareForStart(view, session.getScrollBounds());
233         }
234         onReady.run();
235     }
236 
237     @Override
onScrollCaptureImageRequest(ScrollCaptureSession session, CancellationSignal signal, Rect requestRect, Consumer<Rect> onComplete)238     public final void onScrollCaptureImageRequest(ScrollCaptureSession session,
239             CancellationSignal signal, Rect requestRect, Consumer<Rect> onComplete) {
240         if (signal.isCanceled()) {
241             Log.w(TAG, "onScrollCaptureImageRequest: cancelled!");
242             return;
243         }
244 
245         V view = mWeakView.get();
246         if (view == null || !view.isVisibleToUser()) {
247             // Signal to the controller that we have a problem and can't continue.
248             onComplete.accept(new Rect());
249             return;
250         }
251 
252         // Ask the view to scroll as needed to bring this area into view.
253         mViewHelper.onScrollRequested(view, session.getScrollBounds(), requestRect, signal,
254                 (result) -> onScrollResult(result, view, signal, onComplete));
255     }
256 
onScrollResult(ScrollResult scrollResult, V view, CancellationSignal signal, Consumer<Rect> onComplete)257     private void onScrollResult(ScrollResult scrollResult, V view, CancellationSignal signal,
258             Consumer<Rect> onComplete) {
259 
260         if (signal.isCanceled()) {
261             Log.w(TAG, "onScrollCaptureImageRequest: cancelled! skipping render.");
262             return;
263         }
264 
265         if (scrollResult.availableArea.isEmpty()) {
266             onComplete.accept(scrollResult.availableArea);
267             return;
268         }
269 
270         // For image capture, shift back by scrollDelta to arrive at the location
271         // within the view where the requested content will be drawn
272         Rect viewCaptureArea = new Rect(scrollResult.availableArea);
273         viewCaptureArea.offset(0, -scrollResult.scrollDelta);
274 
275         view.postOnAnimationDelayed(
276                 () -> doCapture(scrollResult, view, viewCaptureArea, onComplete),
277                 mPostScrollDelayMillis);
278     }
279 
doCapture(ScrollResult scrollResult, V view, Rect viewCaptureArea, Consumer<Rect> onComplete)280     private void doCapture(ScrollResult scrollResult, V view, Rect viewCaptureArea,
281             Consumer<Rect> onComplete) {
282         int result = mRenderer.renderView(view, viewCaptureArea);
283         if (result == HardwareRenderer.SYNC_OK
284                 || result == HardwareRenderer.SYNC_REDRAW_REQUESTED) {
285             /* Frame synced, buffer will be produced... notify client. */
286             onComplete.accept(new Rect(scrollResult.availableArea));
287         } else {
288             // No buffer will be produced.
289             Log.e(TAG, "syncAndDraw(): SyncAndDrawResult = " + result);
290             onComplete.accept(new Rect(/* empty */));
291         }
292     }
293 
294     @Override
onScrollCaptureEnd(Runnable onReady)295     public final void onScrollCaptureEnd(Runnable onReady) {
296         V view = mWeakView.get();
297         if (mStarted && !mEnded) {
298             if (view != null) {
299                 mViewHelper.onPrepareForEnd(view);
300                 view.invalidate();
301             }
302             mEnded = true;
303             mRenderer.destroy();
304         }
305         onReady.run();
306     }
307 
308     /**
309      * Internal helper class which assists in rendering sections of the view hierarchy relative to a
310      * given view.
311      */
312     static final class ViewRenderer {
313         // alpha, "reasonable default" from Javadoc
314         private static final float AMBIENT_SHADOW_ALPHA = 0.039f;
315         private static final float SPOT_SHADOW_ALPHA = 0.039f;
316 
317         // Default values:
318         //    lightX = (screen.width() / 2) - windowLeft
319         //    lightY = 0 - windowTop
320         //    lightZ = 600dp
321         //    lightRadius = 800dp
322         private static final float LIGHT_Z_DP = 400;
323         private static final float LIGHT_RADIUS_DP = 800;
324         private static final String TAG = "ViewRenderer";
325 
326         private final HardwareRenderer mRenderer;
327         private final RenderNode mCaptureRenderNode;
328         private final Rect mTempRect = new Rect();
329         private final int[] mTempLocation = new int[2];
330         private long mLastRenderedSourceDrawingId = -1;
331         private Surface mSurface;
332 
ViewRenderer()333         ViewRenderer() {
334             mRenderer = new HardwareRenderer();
335             mRenderer.setName("ScrollCapture");
336             mCaptureRenderNode = new RenderNode("ScrollCaptureRoot");
337             mRenderer.setContentRoot(mCaptureRenderNode);
338 
339             // TODO: Figure out a way to flip this on when we are sure the source window is opaque
340             mRenderer.setOpaque(false);
341         }
342 
setSurface(Surface surface)343         public void setSurface(Surface surface) {
344             mSurface = surface;
345             mRenderer.setSurface(surface);
346         }
347 
348         /**
349          * Cache invalidation check. If the source view is the same as the previous call (which is
350          * mostly always the case, then we can skip setting up lighting on each call (for now)
351          *
352          * @return true if the view changed, false if the view was previously rendered by this class
353          */
updateForView(View source)354         private boolean updateForView(View source) {
355             if (mLastRenderedSourceDrawingId == source.getUniqueDrawingId()) {
356                 return false;
357             }
358             mLastRenderedSourceDrawingId = source.getUniqueDrawingId();
359             return true;
360         }
361 
362         // TODO: may need to adjust lightY based on the virtual canvas position to get
363         //       consistent shadow positions across the whole capture. Or possibly just
364         //       pull lightZ way back to make shadows more uniform.
setupLighting(View mSource)365         private void setupLighting(View mSource) {
366             mLastRenderedSourceDrawingId = mSource.getUniqueDrawingId();
367             DisplayMetrics metrics = mSource.getResources().getDisplayMetrics();
368             mSource.getLocationOnScreen(mTempLocation);
369             final float lightX = metrics.widthPixels / 2f - mTempLocation[0];
370             final float lightY = metrics.heightPixels - mTempLocation[1];
371             final int lightZ = (int) (LIGHT_Z_DP * metrics.density);
372             final int lightRadius = (int) (LIGHT_RADIUS_DP * metrics.density);
373 
374             // Enable shadows for elevation/Z
375             mRenderer.setLightSourceGeometry(lightX, lightY, lightZ, lightRadius);
376             mRenderer.setLightSourceAlpha(AMBIENT_SHADOW_ALPHA, SPOT_SHADOW_ALPHA);
377         }
378 
updateRootNode(View source, Rect localSourceRect)379         private void updateRootNode(View source, Rect localSourceRect) {
380             final View rootView = source.getRootView();
381             transformToRoot(source, localSourceRect, mTempRect);
382 
383             mCaptureRenderNode.setPosition(0, 0, mTempRect.width(), mTempRect.height());
384             RecordingCanvas canvas = mCaptureRenderNode.beginRecording();
385             canvas.enableZ();
386             canvas.translate(-mTempRect.left, -mTempRect.top);
387 
388             RenderNode rootViewRenderNode = rootView.updateDisplayListIfDirty();
389             if (rootViewRenderNode.hasDisplayList()) {
390                 canvas.drawRenderNode(rootViewRenderNode);
391             }
392             mCaptureRenderNode.endRecording();
393         }
394 
395         @SyncAndDrawResult
renderView(View view, Rect sourceRect)396         public int renderView(View view, Rect sourceRect) {
397             HardwareRenderer.FrameRenderRequest request = mRenderer.createRenderRequest();
398             request.setVsyncTime(System.nanoTime());
399             if (updateForView(view)) {
400                 setupLighting(view);
401             }
402             view.invalidate();
403             updateRootNode(view, sourceRect);
404             return request.syncAndDraw();
405         }
406 
trimMemory()407         public void trimMemory() {
408             mRenderer.clearContent();
409         }
410 
destroy()411         public void destroy() {
412             mSurface = null;
413             mRenderer.destroy();
414         }
415 
transformToRoot(View local, Rect localRect, Rect outRect)416         private void transformToRoot(View local, Rect localRect, Rect outRect) {
417             local.getLocationInWindow(mTempLocation);
418             outRect.set(localRect);
419             outRect.offset(mTempLocation[0], mTempLocation[1]);
420         }
421 
setColorMode(@olorMode int colorMode)422         public void setColorMode(@ColorMode int colorMode) {
423             mRenderer.setColorMode(colorMode);
424         }
425     }
426 
427     @Override
toString()428     public String toString() {
429         return "ScrollCaptureViewSupport{"
430                 + "view=" + mWeakView.get()
431                 + ", helper=" + mViewHelper
432                 + '}';
433     }
434 }
435