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