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