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