• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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