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