1 /* 2 * Copyright (C) 2021 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.systemui.screenshot; 18 19 import static com.android.systemui.screenshot.LogConfig.DEBUG_SCROLL; 20 21 import static java.lang.Math.min; 22 import static java.util.Objects.requireNonNull; 23 24 import android.annotation.BinderThread; 25 import android.annotation.UiContext; 26 import android.app.ActivityTaskManager; 27 import android.content.Context; 28 import android.graphics.PixelFormat; 29 import android.graphics.Rect; 30 import android.hardware.HardwareBuffer; 31 import android.media.Image; 32 import android.media.ImageReader; 33 import android.os.DeadObjectException; 34 import android.os.IBinder; 35 import android.os.ICancellationSignal; 36 import android.os.RemoteException; 37 import android.util.Log; 38 import android.view.IScrollCaptureCallbacks; 39 import android.view.IScrollCaptureConnection; 40 import android.view.IScrollCaptureResponseListener; 41 import android.view.IWindowManager; 42 import android.view.ScrollCaptureResponse; 43 44 import androidx.concurrent.futures.CallbackToFutureAdapter; 45 import androidx.concurrent.futures.CallbackToFutureAdapter.Completer; 46 47 import com.android.internal.annotations.VisibleForTesting; 48 import com.android.systemui.dagger.qualifiers.Background; 49 50 import com.google.common.util.concurrent.ListenableFuture; 51 52 import java.util.concurrent.Executor; 53 54 import javax.inject.Inject; 55 56 /** 57 * High(er) level interface to scroll capture API. 58 */ 59 public class ScrollCaptureClient { 60 private static final int TILE_SIZE_PX_MAX = 4 * (1024 * 1024); 61 private static final int TILES_PER_PAGE = 2; // increase once b/174571735 is addressed 62 private static final int MAX_TILES = 30; 63 64 @VisibleForTesting 65 static final int MATCH_ANY_TASK = ActivityTaskManager.INVALID_TASK_ID; 66 67 private static final String TAG = LogConfig.logTag(ScrollCaptureClient.class); 68 69 private final Executor mBgExecutor; 70 71 /** 72 * Represents the connection to a target window and provides a mechanism for requesting tiles. 73 */ 74 interface Session { 75 /** 76 * Request an image tile at the given position, from top, to top + {@link #getTileHeight()}, 77 * and from left 0, to {@link #getPageWidth()} 78 * 79 * @param top the top (y) position of the tile to capture, in content rect space 80 */ requestTile(int top)81 ListenableFuture<CaptureResult> requestTile(int top); 82 83 /** 84 * Returns the maximum number of tiles which may be requested and retained without 85 * being {@link Image#close() closed}. 86 * 87 * @return the maximum number of open tiles allowed 88 */ getMaxTiles()89 int getMaxTiles(); 90 91 /** 92 * Target pixel height for acquisition this session. Session may yield more or less data 93 * than this, but acquiring this height is considered sufficient for completion. 94 * 95 * @return target height in pixels. 96 */ getTargetHeight()97 int getTargetHeight(); 98 99 /** 100 * @return the height of each image tile 101 */ getTileHeight()102 int getTileHeight(); 103 104 105 /** 106 * @return the height of scrollable content being captured 107 */ getPageHeight()108 int getPageHeight(); 109 110 /** 111 * @return the width of the scrollable page 112 */ getPageWidth()113 int getPageWidth(); 114 115 /** 116 * @return the bounds on screen of the window being captured. 117 */ getWindowBounds()118 Rect getWindowBounds(); 119 120 /** 121 * End the capture session, return the target app to original state. The returned Future 122 * will complete once the target app is ready to become visible and interactive. 123 */ end()124 ListenableFuture<Void> end(); 125 release()126 void release(); 127 } 128 129 static class CaptureResult { 130 public final Image image; 131 /** 132 * The area requested, in content rect space, relative to scroll-bounds. 133 */ 134 public final Rect requested; 135 /** 136 * The actual area captured, in content rect space, relative to scroll-bounds. This may be 137 * cropped or empty depending on available content. 138 */ 139 public final Rect captured; 140 CaptureResult(Image image, Rect request, Rect captured)141 CaptureResult(Image image, Rect request, Rect captured) { 142 this.image = image; 143 this.requested = request; 144 this.captured = captured; 145 } 146 147 @Override toString()148 public String toString() { 149 return "CaptureResult{" 150 + "requested=" + requested 151 + " (" + requested.width() + "x" + requested.height() + ")" 152 + ", captured=" + captured 153 + " (" + captured.width() + "x" + captured.height() + ")" 154 + ", image=" + image 155 + '}'; 156 } 157 } 158 159 private final IWindowManager mWindowManagerService; 160 private IBinder mHostWindowToken; 161 162 @Inject ScrollCaptureClient(IWindowManager windowManagerService, @Background Executor bgExecutor, @UiContext Context context)163 public ScrollCaptureClient(IWindowManager windowManagerService, 164 @Background Executor bgExecutor, @UiContext Context context) { 165 requireNonNull(context.getDisplay(), "context must be associated with a Display!"); 166 mBgExecutor = bgExecutor; 167 mWindowManagerService = windowManagerService; 168 } 169 170 /** 171 * Set the window token for the screenshot window/ This is required to avoid targeting our 172 * window or any above it. 173 * 174 * @param token the windowToken of the screenshot window 175 */ setHostWindowToken(IBinder token)176 public void setHostWindowToken(IBinder token) { 177 mHostWindowToken = token; 178 } 179 180 /** 181 * Check for scroll capture support. 182 * 183 * @param displayId id for the display containing the target window 184 */ request(int displayId)185 public ListenableFuture<ScrollCaptureResponse> request(int displayId) { 186 return request(displayId, MATCH_ANY_TASK); 187 } 188 189 /** 190 * Check for scroll capture support. 191 * 192 * @param displayId id for the display containing the target window 193 * @param taskId id for the task containing the target window or {@link #MATCH_ANY_TASK}. 194 * @return a listenable future providing the response 195 */ request(int displayId, int taskId)196 public ListenableFuture<ScrollCaptureResponse> request(int displayId, int taskId) { 197 return CallbackToFutureAdapter.getFuture((completer) -> { 198 try { 199 mWindowManagerService.requestScrollCapture(displayId, mHostWindowToken, taskId, 200 new IScrollCaptureResponseListener.Stub() { 201 @Override 202 public void onScrollCaptureResponse(ScrollCaptureResponse response) { 203 completer.set(response); 204 } 205 }); 206 207 } catch (RemoteException e) { 208 completer.setException(e); 209 } 210 return "ScrollCaptureClient#request" 211 + "(displayId=" + displayId + ", taskId=" + taskId + ")"; 212 }); 213 } 214 215 /** 216 * Start a scroll capture session. 217 * 218 * @param response a response provided from a request containing a connection 219 * @param maxPages the capture buffer size expressed as a multiple of the content height 220 * @return a listenable future providing the session 221 */ 222 public ListenableFuture<Session> start(ScrollCaptureResponse response, float maxPages) { 223 IScrollCaptureConnection connection = response.getConnection(); 224 return CallbackToFutureAdapter.getFuture((completer) -> { 225 if (connection == null || !connection.asBinder().isBinderAlive()) { 226 completer.setException(new DeadObjectException("No active connection!")); 227 return ""; 228 } 229 SessionWrapper session = new SessionWrapper(connection, response.getWindowBounds(), 230 response.getBoundsInWindow(), maxPages, mBgExecutor); 231 session.start(completer); 232 return "IScrollCaptureCallbacks#onCaptureStarted"; 233 }); 234 } 235 236 private static class SessionWrapper extends IScrollCaptureCallbacks.Stub implements Session, 237 IBinder.DeathRecipient, ImageReader.OnImageAvailableListener { 238 239 private IScrollCaptureConnection mConnection; 240 private final Executor mBgExecutor; 241 private final Object mLock = new Object(); 242 243 private ImageReader mReader; 244 private final int mTileHeight; 245 private final int mTileWidth; 246 private Rect mRequestRect; 247 private Rect mCapturedArea; 248 private Image mCapturedImage; 249 private boolean mStarted; 250 private final int mTargetHeight; 251 252 private ICancellationSignal mCancellationSignal; 253 private final Rect mWindowBounds; 254 private final Rect mBoundsInWindow; 255 256 private Completer<Session> mStartCompleter; 257 private Completer<CaptureResult> mTileRequestCompleter; 258 private Completer<Void> mEndCompleter; 259 260 private SessionWrapper(IScrollCaptureConnection connection, Rect windowBounds, 261 Rect boundsInWindow, float maxPages, Executor bgExecutor) 262 throws RemoteException { 263 mConnection = requireNonNull(connection); 264 mConnection.asBinder().linkToDeath(SessionWrapper.this, 0); 265 mWindowBounds = requireNonNull(windowBounds); 266 mBoundsInWindow = requireNonNull(boundsInWindow); 267 268 int pxPerPage = mBoundsInWindow.width() * mBoundsInWindow.height(); 269 int pxPerTile = min(TILE_SIZE_PX_MAX, (pxPerPage / TILES_PER_PAGE)); 270 271 mTileWidth = mBoundsInWindow.width(); 272 mTileHeight = pxPerTile / mBoundsInWindow.width(); 273 mTargetHeight = (int) (mBoundsInWindow.height() * maxPages); 274 mBgExecutor = bgExecutor; 275 if (DEBUG_SCROLL) { 276 Log.d(TAG, "boundsInWindow: " + mBoundsInWindow); 277 Log.d(TAG, "tile size: " + mTileWidth + "x" + mTileHeight); 278 } 279 } 280 281 @Override 282 public void binderDied() { 283 Log.d(TAG, "binderDied! The target process just crashed :-("); 284 // Clean up 285 mConnection = null; 286 287 // Pass along the bad news. 288 if (mStartCompleter != null) { 289 mStartCompleter.setException(new DeadObjectException("The remote process died")); 290 } 291 if (mTileRequestCompleter != null) { 292 mTileRequestCompleter.setException( 293 new DeadObjectException("The remote process died")); 294 } 295 if (mEndCompleter != null) { 296 mEndCompleter.setException(new DeadObjectException("The remote process died")); 297 } 298 } 299 300 private void start(Completer<Session> completer) { 301 mReader = ImageReader.newInstance(mTileWidth, mTileHeight, PixelFormat.RGBA_8888, 302 MAX_TILES, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE); 303 mStartCompleter = completer; 304 mReader.setOnImageAvailableListenerWithExecutor(this, mBgExecutor); 305 try { 306 mCancellationSignal = mConnection.startCapture(mReader.getSurface(), this); 307 completer.addCancellationListener(() -> { 308 try { 309 mCancellationSignal.cancel(); 310 } catch (RemoteException e) { 311 // Ignore 312 } 313 }, Runnable::run); 314 mStarted = true; 315 } catch (RemoteException e) { 316 mReader.close(); 317 completer.setException(e); 318 } 319 } 320 321 @BinderThread 322 @Override 323 public void onCaptureStarted() { 324 Log.d(TAG, "onCaptureStarted"); 325 mStartCompleter.set(this); 326 } 327 328 @Override 329 public ListenableFuture<CaptureResult> requestTile(int top) { 330 mRequestRect = new Rect(0, top, mTileWidth, top + mTileHeight); 331 return CallbackToFutureAdapter.getFuture((completer -> { 332 if (mConnection == null || !mConnection.asBinder().isBinderAlive()) { 333 completer.setException(new DeadObjectException("Connection is closed!")); 334 return ""; 335 } 336 try { 337 mTileRequestCompleter = completer; 338 mCancellationSignal = mConnection.requestImage(mRequestRect); 339 completer.addCancellationListener(() -> { 340 try { 341 mCancellationSignal.cancel(); 342 } catch (RemoteException e) { 343 // Ignore 344 } 345 }, Runnable::run); 346 } catch (RemoteException e) { 347 completer.setException(e); 348 } 349 return "IScrollCaptureCallbacks#onImageRequestCompleted"; 350 })); 351 } 352 353 @BinderThread 354 @Override 355 public void onImageRequestCompleted(int flagsUnused, Rect contentArea) { 356 synchronized (mLock) { 357 mCapturedArea = contentArea; 358 if (mCapturedImage != null || (mCapturedArea == null || mCapturedArea.isEmpty())) { 359 completeCaptureRequest(); 360 } 361 } 362 } 363 364 /** @see ImageReader.OnImageAvailableListener */ 365 @Override 366 public void onImageAvailable(ImageReader reader) { 367 synchronized (mLock) { 368 mCapturedImage = mReader.acquireLatestImage(); 369 if (mCapturedArea != null) { 370 completeCaptureRequest(); 371 } 372 } 373 } 374 375 /** Produces a result for the caller as soon as both asynchronous results are received. */ 376 private void completeCaptureRequest() { 377 CaptureResult result = 378 new CaptureResult(mCapturedImage, mRequestRect, mCapturedArea); 379 mCapturedImage = null; 380 mRequestRect = null; 381 mCapturedArea = null; 382 mTileRequestCompleter.set(result); 383 } 384 385 @Override 386 public ListenableFuture<Void> end() { 387 Log.d(TAG, "end()"); 388 return CallbackToFutureAdapter.getFuture(completer -> { 389 if (!mStarted) { 390 try { 391 mConnection.asBinder().unlinkToDeath(SessionWrapper.this, 0); 392 mConnection.close(); 393 } catch (RemoteException e) { 394 /* ignore */ 395 } 396 mConnection = null; 397 completer.set(null); 398 return ""; 399 } 400 401 mEndCompleter = completer; 402 try { 403 mConnection.endCapture(); 404 } catch (RemoteException e) { 405 completer.setException(e); 406 } 407 return "IScrollCaptureCallbacks#onCaptureEnded"; 408 }); 409 } 410 411 public void release() { 412 mReader.close(); 413 } 414 415 @BinderThread 416 @Override 417 public void onCaptureEnded() { 418 try { 419 mConnection.close(); 420 } catch (RemoteException e) { 421 /* ignore */ 422 } 423 mConnection = null; 424 mEndCompleter.set(null); 425 } 426 427 // Misc 428 429 @Override 430 public int getPageHeight() { 431 return mBoundsInWindow.height(); 432 } 433 434 @Override 435 public int getPageWidth() { 436 return mBoundsInWindow.width(); 437 } 438 439 @Override 440 public int getTileHeight() { 441 return mTileHeight; 442 } 443 444 public Rect getWindowBounds() { 445 return new Rect(mWindowBounds); 446 } 447 448 public Rect getBoundsInWindow() { 449 return new Rect(mBoundsInWindow); 450 } 451 452 @Override 453 public int getTargetHeight() { 454 return mTargetHeight; 455 } 456 457 @Override 458 public int getMaxTiles() { 459 return MAX_TILES; 460 } 461 } 462 } 463