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 android.util.MathUtils.constrain; 20 21 import static com.google.common.util.concurrent.Futures.immediateFuture; 22 23 import static org.mockito.Mockito.mock; 24 import static org.mockito.Mockito.when; 25 26 import static java.lang.Math.abs; 27 import static java.lang.Math.max; 28 import static java.lang.Math.min; 29 30 import android.graphics.Rect; 31 import android.hardware.HardwareBuffer; 32 import android.media.Image; 33 import android.util.Log; 34 35 import com.google.common.util.concurrent.Futures; 36 import com.google.common.util.concurrent.ListenableFuture; 37 38 /** 39 * A flexible test double for {@link ScrollCaptureClient.Session}. 40 * <p> 41 * FakeSession provides the ability to emulate both the available scrollable content range as well 42 * as the current visible bounds. Visible bounds may vary because the target view itself may be 43 * slid vertically during capture, with portions may become clipped by parent views. This scenario 44 * frequently occurs with UIs constructed from nested scrolling views or collapsing headers. 45 */ 46 class FakeSession implements ScrollCaptureClient.Session { 47 private static final String TAG = "FakeSession"; 48 // Available range of content 49 private final Rect mAvailable; 50 51 /** bounds for scrollDelta (y), range with bottom adjusted to account for page height. */ 52 private final Rect mAvailableTop; 53 54 private final Rect mVisiblePage; 55 private final int mTileHeight; 56 private final int mMaxTiles; 57 58 private int mScrollDelta; 59 private int mPageHeight; 60 private int mTargetHeight; 61 FakeSession(int pageHeight, float maxPages, int tileHeight, int visiblePageTop, int visiblePageBottom, int availableTop, int availableBottom, int maxTiles)62 FakeSession(int pageHeight, float maxPages, int tileHeight, int visiblePageTop, 63 int visiblePageBottom, int availableTop, int availableBottom, 64 int maxTiles) { 65 mPageHeight = pageHeight; 66 mTileHeight = tileHeight; 67 mAvailable = new Rect(0, availableTop, getPageWidth(), availableBottom); 68 mAvailableTop = new Rect(mAvailable); 69 mAvailableTop.inset(0, 0, 0, pageHeight); 70 mVisiblePage = new Rect(0, visiblePageTop, getPageWidth(), visiblePageBottom); 71 mTargetHeight = (int) (pageHeight * maxPages); 72 mMaxTiles = maxTiles; 73 } 74 mockImage()75 private static Image mockImage() { 76 Image image = mock(Image.class); 77 when(image.getHardwareBuffer()).thenReturn(mock(HardwareBuffer.class)); 78 return image; 79 } 80 getScrollDelta()81 public int getScrollDelta() { 82 return mScrollDelta; 83 } 84 85 @Override requestTile(int requestedTop)86 public ListenableFuture<ScrollCaptureClient.CaptureResult> requestTile(int requestedTop) { 87 Rect requested = new Rect(0, requestedTop, getPageWidth(), requestedTop + getTileHeight()); 88 Log.d(TAG, "requested: " + requested); 89 Rect page = new Rect(0, 0, getPageWidth(), mPageHeight); 90 page.offset(0, mScrollDelta); 91 Log.d(TAG, "page: " + page); 92 // Simulate behavior from lower levels by replicating 'requestChildRectangleOnScreen' 93 if (!page.contains(requested)) { 94 Log.d(TAG, "requested not within page, scrolling"); 95 // distance+direction needed to scroll to align each edge of request with 96 // corresponding edge of the page 97 int distTop = requested.top - page.top; // positive means already visible 98 int distBottom = requested.bottom - page.bottom; // negative means already visible 99 Log.d(TAG, "distTop = " + distTop); 100 Log.d(TAG, "distBottom = " + distBottom); 101 102 boolean scrollUp = false; 103 if (distTop < 0 && distBottom > 0) { 104 scrollUp = abs(distTop) < distBottom; 105 } else if (distTop < 0) { 106 scrollUp = true; 107 } 108 109 // determine which edges are currently clipped 110 if (scrollUp) { 111 Log.d(TAG, "trying to scroll up by " + -distTop + " px"); 112 // need to scroll up to align top edge to visible-top 113 mScrollDelta += distTop; 114 Log.d(TAG, "new scrollDelta = " + mScrollDelta); 115 } else { 116 Log.d(TAG, "trying to scroll down by " + distBottom + " px"); 117 // scroll down to align bottom edge with visible bottom, but keep top visible 118 int topEdgeDistance = max(0, requestedTop - page.top); 119 mScrollDelta += min(distBottom, topEdgeDistance); 120 Log.d(TAG, "new scrollDelta = " + mScrollDelta); 121 } 122 123 // Clamp to available content 124 mScrollDelta = constrain(mScrollDelta, mAvailableTop.top, mAvailableTop.bottom); 125 Log.d(TAG, "scrollDelta, adjusted to available range = " + mScrollDelta); 126 127 // Reset to apply a changed scroll delta possibly. 128 page.offsetTo(0, 0); 129 page.offset(0, mScrollDelta); 130 131 Log.d(TAG, "page (after scroll): " + page); 132 Log.d(TAG, "requested (after scroll): " + requested); 133 } 134 Log.d(TAG, "mVisiblePage = " + mVisiblePage); 135 Log.d(TAG, "scrollDelta = " + mScrollDelta); 136 137 Rect target = new Rect(requested); 138 Rect visible = new Rect(mVisiblePage); 139 visible.offset(0, mScrollDelta); 140 141 Log.d(TAG, "target: " + target); 142 Log.d(TAG, "visible: " + visible); 143 144 // if any of the requested rect is available to scroll into the view: 145 if (target.intersect(page) && target.intersect(visible)) { 146 Log.d(TAG, "returning captured = " + target); 147 ScrollCaptureClient.CaptureResult result = 148 new ScrollCaptureClient.CaptureResult(mockImage(), requested, target); 149 return immediateFuture(result); 150 } 151 Log.d(TAG, "no part of requested rect is within page, returning empty"); 152 ScrollCaptureClient.CaptureResult result = 153 new ScrollCaptureClient.CaptureResult(null, requested, new Rect()); 154 return immediateFuture(result); 155 } 156 157 158 @Override 159 public int getMaxTiles() { 160 return mMaxTiles; 161 } 162 163 @Override 164 public int getTargetHeight() { 165 return mTargetHeight; 166 } 167 168 @Override 169 public int getTileHeight() { 170 return mTileHeight; 171 } 172 173 @Override 174 public int getPageWidth() { 175 return 100; 176 } 177 178 @Override 179 public int getPageHeight() { 180 return mPageHeight; 181 } 182 183 @Override 184 public Rect getWindowBounds() { 185 throw new IllegalStateException("Not implemented"); 186 } 187 188 @Override 189 public ListenableFuture<Void> end() { 190 return Futures.immediateVoidFuture(); 191 } 192 193 @Override 194 public void release() { 195 } 196 } 197