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.systemui.screenshot; 18 19 import android.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.Rect; 22 import android.graphics.drawable.Drawable; 23 import android.provider.Settings; 24 import android.util.Log; 25 import android.view.ScrollCaptureResponse; 26 27 import androidx.concurrent.futures.CallbackToFutureAdapter; 28 import androidx.concurrent.futures.CallbackToFutureAdapter.Completer; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.internal.logging.UiEventLogger; 32 import com.android.systemui.dagger.qualifiers.Background; 33 import com.android.systemui.screenshot.ScrollCaptureClient.CaptureResult; 34 import com.android.systemui.screenshot.ScrollCaptureClient.Session; 35 36 import com.google.common.util.concurrent.ListenableFuture; 37 38 import java.util.concurrent.ExecutionException; 39 import java.util.concurrent.Executor; 40 41 import javax.inject.Inject; 42 43 /** 44 * Interaction controller between the UI and ScrollCaptureClient. 45 */ 46 public class ScrollCaptureController { 47 private static final String TAG = LogConfig.logTag(ScrollCaptureController.class); 48 private static final float MAX_PAGES_DEFAULT = 3f; 49 50 private static final String SETTING_KEY_MAX_PAGES = "screenshot.scroll_max_pages"; 51 // Portion of the tiles to be acquired above the starting position in infinite scroll 52 // situations. 1.0 means maximize the area above, 0 means just go down. 53 private static final float IDEAL_PORTION_ABOVE = 0.4f; 54 55 private boolean mScrollingUp = true; 56 // If true, stop acquiring images when no more bitmap data is available in the current direction 57 // or if the desired bitmap size is reached. 58 private boolean mFinishOnBoundary; 59 60 public static final int MAX_HEIGHT = 12000; 61 62 private final Context mContext; 63 private final Executor mBgExecutor; 64 private final ImageTileSet mImageTileSet; 65 private final UiEventLogger mEventLogger; 66 private final ScrollCaptureClient mClient; 67 68 private Completer<LongScreenshot> mCaptureCompleter; 69 70 private ListenableFuture<Session> mSessionFuture; 71 private Session mSession; 72 private ListenableFuture<CaptureResult> mTileFuture; 73 private ListenableFuture<Void> mEndFuture; 74 private String mWindowOwner; 75 76 static class LongScreenshot { 77 private final ImageTileSet mImageTileSet; 78 private final Session mSession; 79 LongScreenshot(Session session, ImageTileSet imageTileSet)80 LongScreenshot(Session session, ImageTileSet imageTileSet) { 81 mSession = session; 82 mImageTileSet = imageTileSet; 83 } 84 85 /** Returns a bitmap containing the combinded result. */ toBitmap()86 public Bitmap toBitmap() { 87 return mImageTileSet.toBitmap(); 88 } 89 toBitmap(Rect bounds)90 public Bitmap toBitmap(Rect bounds) { 91 return mImageTileSet.toBitmap(bounds); 92 } 93 94 /** Releases image resources from the screenshot. */ release()95 public void release() { 96 if (LogConfig.DEBUG_SCROLL) { 97 Log.d(TAG, "LongScreenshot :: release()"); 98 } 99 mImageTileSet.clear(); 100 mSession.release(); 101 } 102 getLeft()103 public int getLeft() { 104 return mImageTileSet.getLeft(); 105 } 106 getTop()107 public int getTop() { 108 return mImageTileSet.getTop(); 109 } 110 getBottom()111 public int getBottom() { 112 return mImageTileSet.getBottom(); 113 } 114 getWidth()115 public int getWidth() { 116 return mImageTileSet.getWidth(); 117 } 118 getHeight()119 public int getHeight() { 120 return mImageTileSet.getHeight(); 121 } 122 123 /** @return the height of the visible area of the scrolling page, in pixels */ getPageHeight()124 public int getPageHeight() { 125 return mSession.getPageHeight(); 126 } 127 128 @Override toString()129 public String toString() { 130 return "LongScreenshot{w=" + mImageTileSet.getWidth() 131 + ", h=" + mImageTileSet.getHeight() + "}"; 132 } 133 getDrawable()134 public Drawable getDrawable() { 135 return mImageTileSet.getDrawable(); 136 } 137 } 138 139 @Inject ScrollCaptureController(Context context, @Background Executor bgExecutor, ScrollCaptureClient client, ImageTileSet imageTileSet, UiEventLogger logger)140 ScrollCaptureController(Context context, @Background Executor bgExecutor, 141 ScrollCaptureClient client, ImageTileSet imageTileSet, UiEventLogger logger) { 142 mContext = context; 143 mBgExecutor = bgExecutor; 144 mClient = client; 145 mImageTileSet = imageTileSet; 146 mEventLogger = logger; 147 } 148 149 @VisibleForTesting getTargetTopSizeRatio()150 float getTargetTopSizeRatio() { 151 return IDEAL_PORTION_ABOVE; 152 } 153 154 /** 155 * Run scroll capture. Performs a batch capture, collecting image tiles. 156 * 157 * @param response a scroll capture response from a previous request which is 158 * {@link ScrollCaptureResponse#isConnected() connected}. 159 * @return a future ImageTile set containing the result 160 */ run(ScrollCaptureResponse response)161 ListenableFuture<LongScreenshot> run(ScrollCaptureResponse response) { 162 return CallbackToFutureAdapter.getFuture(completer -> { 163 mCaptureCompleter = completer; 164 mWindowOwner = response.getPackageName(); 165 mBgExecutor.execute(() -> { 166 float maxPages = Settings.Secure.getFloat(mContext.getContentResolver(), 167 SETTING_KEY_MAX_PAGES, MAX_PAGES_DEFAULT); 168 mSessionFuture = mClient.start(response, maxPages); 169 mSessionFuture.addListener(this::onStartComplete, mContext.getMainExecutor()); 170 }); 171 return "<batch scroll capture>"; 172 }); 173 } 174 onStartComplete()175 private void onStartComplete() { 176 try { 177 mSession = mSessionFuture.get(); 178 if (LogConfig.DEBUG_SCROLL) { 179 Log.d(TAG, "got session " + mSession); 180 } 181 mEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_STARTED, 0, mWindowOwner); 182 requestNextTile(0); 183 } catch (InterruptedException | ExecutionException e) { 184 // Failure to start, propagate to caller 185 Log.e(TAG, "session start failed!"); 186 mCaptureCompleter.setException(e); 187 mEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_FAILURE, 0, mWindowOwner); 188 } 189 } 190 requestNextTile(int topPx)191 private void requestNextTile(int topPx) { 192 if (LogConfig.DEBUG_SCROLL) { 193 Log.d(TAG, "requestNextTile: " + topPx); 194 } 195 mTileFuture = mSession.requestTile(topPx); 196 mTileFuture.addListener(() -> { 197 try { 198 if (LogConfig.DEBUG_SCROLL) { 199 Log.d(TAG, "onCaptureResult"); 200 } 201 onCaptureResult(mTileFuture.get()); 202 } catch (InterruptedException | ExecutionException e) { 203 Log.e(TAG, "requestTile failed!", e); 204 mCaptureCompleter.setException(e); 205 } 206 }, mContext.getMainExecutor()); 207 } 208 onCaptureResult(CaptureResult result)209 private void onCaptureResult(CaptureResult result) { 210 if (LogConfig.DEBUG_SCROLL) { 211 Log.d(TAG, "onCaptureResult: " + result + " scrolling " + (mScrollingUp ? "UP" : "DOWN") 212 + " finish on boundary: " + mFinishOnBoundary); 213 } 214 boolean emptyResult = result.captured.height() == 0; 215 216 if (emptyResult) { 217 // Potentially reached a vertical boundary. Extend in the other direction. 218 if (mFinishOnBoundary) { 219 if (LogConfig.DEBUG_SCROLL) { 220 Log.d(TAG, "Empty: finished!"); 221 } 222 finishCapture(); 223 return; 224 } else { 225 // We hit a boundary, clear the tiles, capture everything in the opposite direction, 226 // then finish. 227 mImageTileSet.clear(); 228 mFinishOnBoundary = true; 229 mScrollingUp = !mScrollingUp; 230 if (LogConfig.DEBUG_SCROLL) { 231 Log.d(TAG, "Empty: cleared, switch direction to finish"); 232 } 233 } 234 } else { 235 // Got a non-empty result, but may already have enough bitmap data now 236 int expectedTiles = mImageTileSet.size() + 1; 237 if (expectedTiles >= mSession.getMaxTiles()) { 238 if (LogConfig.DEBUG_SCROLL) { 239 Log.d(TAG, "Hit max tiles: finished"); 240 } 241 // If we ever hit the max tiles, we've got enough bitmap data to finish 242 // (even if we weren't sure we'd finish on this pass). 243 finishCapture(); 244 return; 245 } else { 246 if (mScrollingUp && !mFinishOnBoundary) { 247 // During the initial scroll up, we only want to acquire the portion described 248 // by IDEAL_PORTION_ABOVE. 249 if (mImageTileSet.getHeight() + result.captured.height() 250 >= mSession.getTargetHeight() * IDEAL_PORTION_ABOVE) { 251 if (LogConfig.DEBUG_SCROLL) { 252 Log.d(TAG, "Hit ideal portion above: clear and switch direction"); 253 } 254 // We got enough above the start point, now see how far down it can go. 255 mImageTileSet.clear(); 256 mScrollingUp = false; 257 } 258 } 259 } 260 } 261 262 if (!emptyResult) { 263 mImageTileSet.addTile(new ImageTile(result.image, result.captured)); 264 } 265 if (LogConfig.DEBUG_SCROLL) { 266 Log.d(TAG, "bounds: " + mImageTileSet.getLeft() + "," + mImageTileSet.getTop() 267 + " - " + mImageTileSet.getRight() + "," + mImageTileSet.getBottom() 268 + " (" + mImageTileSet.getWidth() + "x" + mImageTileSet.getHeight() + ")"); 269 } 270 271 Rect gapBounds = mImageTileSet.getGaps(); 272 if (!gapBounds.isEmpty()) { 273 if (LogConfig.DEBUG_SCROLL) { 274 Log.d(TAG, "Found gaps in tileset: " + gapBounds + ", requesting " + gapBounds.top); 275 } 276 requestNextTile(gapBounds.top); 277 return; 278 } 279 280 if (mImageTileSet.getHeight() >= mSession.getTargetHeight()) { 281 if (LogConfig.DEBUG_SCROLL) { 282 Log.d(TAG, "Target height reached."); 283 } 284 finishCapture(); 285 return; 286 } 287 288 int nextTop; 289 if (emptyResult) { 290 // An empty result caused the direction the flip, 291 // so use the requested edges to determine the next top. 292 nextTop = (mScrollingUp) 293 ? result.requested.top - mSession.getTileHeight() 294 : result.requested.bottom; 295 } else { 296 nextTop = (mScrollingUp) 297 ? mImageTileSet.getTop() - mSession.getTileHeight() 298 : mImageTileSet.getBottom(); 299 } 300 requestNextTile(nextTop); 301 } 302 finishCapture()303 private void finishCapture() { 304 if (LogConfig.DEBUG_SCROLL) { 305 Log.d(TAG, "finishCapture()"); 306 } 307 if (mImageTileSet.getHeight() > 0) { 308 mEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_COMPLETED, 0, mWindowOwner); 309 } else { 310 mEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_FAILURE, 0, mWindowOwner); 311 } 312 mEndFuture = mSession.end(); 313 mEndFuture.addListener(() -> { 314 if (LogConfig.DEBUG_SCROLL) { 315 Log.d(TAG, "endCapture completed"); 316 } 317 // Provide result to caller and complete the top-level future 318 // Caller is responsible for releasing this resource (ImageReader/HardwareBuffers) 319 mCaptureCompleter.set(new LongScreenshot(mSession, mImageTileSet)); 320 }, mContext.getMainExecutor()); 321 } 322 } 323