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